Commit 52e8a94b authored by Sven Van Asbroeck's avatar Sven Van Asbroeck Committed by Mark Brown

ASoC: Add initial ZL38060 driver

The ZL38060 is a "Connected Home Audio Processor" from Microsemi,
which consists of a Digital Signal Processor (DSP), several Digital
Audio Interfaces (DAIs), analog outputs, and a block of 14 GPIOs.

This driver supports only a tiny subset of the chip's functionality
and possible configurations:
- bypass mode (whole dsp block is bypassed)
- chip's I2S DAI routed directly to/from DACs and microphone
- chip's internal clock is driven by a 12 MHz external crystal
- chip's DAI connected to CPU is I2S, and bit + frame clock master
- chip must be strapped for "host boot": in this mode, firmware will
  be provided by this driver.
Signed-off-by: default avatarSven Van Asbroeck <TheSven73@gmail.com>
Link: https://lore.kernel.org/r/20200417221341.31428-2-TheSven73@gmail.comSigned-off-by: default avatarMark Brown <broonie@kernel.org>
parent 19f303ca
...@@ -272,6 +272,7 @@ config SND_SOC_ALL_CODECS ...@@ -272,6 +272,7 @@ config SND_SOC_ALL_CODECS
imply SND_SOC_WM9712 imply SND_SOC_WM9712
imply SND_SOC_WM9713 imply SND_SOC_WM9713
imply SND_SOC_WSA881X imply SND_SOC_WSA881X
imply SND_SOC_ZL38060
help help
Normally ASoC codec drivers are only built if a machine driver which Normally ASoC codec drivers are only built if a machine driver which
uses them is also built since they are only usable with a machine uses them is also built since they are only usable with a machine
...@@ -1645,6 +1646,16 @@ config SND_SOC_WSA881X ...@@ -1645,6 +1646,16 @@ config SND_SOC_WSA881X
This enables support for Qualcomm WSA8810/WSA8815 Class-D This enables support for Qualcomm WSA8810/WSA8815 Class-D
Smart Speaker Amplifier. Smart Speaker Amplifier.
config SND_SOC_ZL38060
tristate "Microsemi ZL38060 Connected Home Audio Processor"
depends on SPI_MASTER
select GPIOLIB
select REGMAP
help
Support for ZL38060 Connected Home Audio Processor from Microsemi,
which consists of a Digital Signal Processor (DSP), several Digital
Audio Interfaces (DAIs), analog outputs, and a block of 14 GPIOs.
config SND_SOC_ZX_AUD96P22 config SND_SOC_ZX_AUD96P22
tristate "ZTE ZX AUD96P22 CODEC" tristate "ZTE ZX AUD96P22 CODEC"
depends on I2C depends on I2C
......
...@@ -288,6 +288,7 @@ snd-soc-wm9712-objs := wm9712.o ...@@ -288,6 +288,7 @@ snd-soc-wm9712-objs := wm9712.o
snd-soc-wm9713-objs := wm9713.o snd-soc-wm9713-objs := wm9713.o
snd-soc-wm-hubs-objs := wm_hubs.o snd-soc-wm-hubs-objs := wm_hubs.o
snd-soc-wsa881x-objs := wsa881x.o snd-soc-wsa881x-objs := wsa881x.o
snd-soc-zl38060-objs := zl38060.o
snd-soc-zx-aud96p22-objs := zx_aud96p22.o snd-soc-zx-aud96p22-objs := zx_aud96p22.o
# Amp # Amp
snd-soc-max9877-objs := max9877.o snd-soc-max9877-objs := max9877.o
...@@ -588,6 +589,7 @@ obj-$(CONFIG_SND_SOC_WM9713) += snd-soc-wm9713.o ...@@ -588,6 +589,7 @@ obj-$(CONFIG_SND_SOC_WM9713) += snd-soc-wm9713.o
obj-$(CONFIG_SND_SOC_WM_ADSP) += snd-soc-wm-adsp.o obj-$(CONFIG_SND_SOC_WM_ADSP) += snd-soc-wm-adsp.o
obj-$(CONFIG_SND_SOC_WM_HUBS) += snd-soc-wm-hubs.o obj-$(CONFIG_SND_SOC_WM_HUBS) += snd-soc-wm-hubs.o
obj-$(CONFIG_SND_SOC_WSA881X) += snd-soc-wsa881x.o obj-$(CONFIG_SND_SOC_WSA881X) += snd-soc-wsa881x.o
obj-$(CONFIG_SND_SOC_ZL38060) += snd-soc-zl38060.o
obj-$(CONFIG_SND_SOC_ZX_AUD96P22) += snd-soc-zx-aud96p22.o obj-$(CONFIG_SND_SOC_ZX_AUD96P22) += snd-soc-zx-aud96p22.o
# Amp # Amp
......
// SPDX-License-Identifier: GPL-2.0-only
//
// Codec driver for Microsemi ZL38060 Connected Home Audio Processor.
//
// Copyright(c) 2020 Sven Van Asbroeck
// The ZL38060 is very flexible and configurable. This driver implements only a
// tiny subset of the chip's possible configurations:
//
// - DSP block bypassed: DAI routed straight to DACs
// microphone routed straight to DAI
// - chip's internal clock is driven by a 12 MHz external crystal
// - chip's DAI connected to CPU is I2S, and bit + frame clock master
// - chip must be strapped for "host boot": in this mode, firmware will be
// provided by this driver.
#include <linux/gpio/consumer.h>
#include <linux/gpio/driver.h>
#include <linux/property.h>
#include <linux/spi/spi.h>
#include <linux/regmap.h>
#include <linux/module.h>
#include <linux/ihex.h>
#include <sound/pcm_params.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/soc.h>
#define DRV_NAME "zl38060"
#define ZL38_RATES (SNDRV_PCM_RATE_8000 |\
SNDRV_PCM_RATE_16000 |\
SNDRV_PCM_RATE_48000)
#define ZL38_FORMATS SNDRV_PCM_FMTBIT_S16_LE
#define HBI_FIRMWARE_PAGE 0xFF
#define ZL38_MAX_RAW_XFER 0x100
#define REG_TDMA_CFG_CLK 0x0262
#define CFG_CLK_PCLK_SHIFT 4
#define CFG_CLK_PCLK_MASK (0x7ff << CFG_CLK_PCLK_SHIFT)
#define CFG_CLK_PCLK(bits) ((bits - 1) << CFG_CLK_PCLK_SHIFT)
#define CFG_CLK_MASTER BIT(15)
#define CFG_CLK_FSRATE_MASK 0x7
#define CFG_CLK_FSRATE_8KHZ 0x1
#define CFG_CLK_FSRATE_16KHZ 0x2
#define CFG_CLK_FSRATE_48KHZ 0x6
#define REG_CLK_CFG 0x0016
#define CLK_CFG_SOURCE_XTAL BIT(15)
#define REG_CLK_STATUS 0x0014
#define CLK_STATUS_HWRST BIT(0)
#define REG_PARAM_RESULT 0x0034
#define PARAM_RESULT_READY 0xD3D3
#define REG_PG255_BASE_HI 0x000C
#define REG_PG255_OFFS(addr) ((HBI_FIRMWARE_PAGE << 8) | (addr & 0xFF))
#define REG_FWR_EXEC 0x012C
#define REG_CMD 0x0032
#define REG_HW_REV 0x0020
#define REG_FW_PROD 0x0022
#define REG_FW_REV 0x0024
#define REG_SEMA_FLAGS 0x0006
#define SEMA_FLAGS_BOOT_CMD BIT(0)
#define SEMA_FLAGS_APP_REBOOT BIT(1)
#define REG_HW_REV 0x0020
#define REG_FW_PROD 0x0022
#define REG_FW_REV 0x0024
#define REG_GPIO_DIR 0x02DC
#define REG_GPIO_DAT 0x02DA
#define BOOTCMD_LOAD_COMPLETE 0x000D
#define BOOTCMD_FW_GO 0x0008
#define FIRMWARE_MAJOR 2
#define FIRMWARE_MINOR 2
struct zl38_codec_priv {
struct device *dev;
struct regmap *regmap;
bool is_stream_in_use[2];
struct gpio_chip *gpio_chip;
};
static int zl38_fw_issue_command(struct regmap *regmap, u16 cmd)
{
unsigned int val;
int err;
err = regmap_read_poll_timeout(regmap, REG_SEMA_FLAGS, val,
!(val & SEMA_FLAGS_BOOT_CMD), 10000,
10000 * 100);
if (err)
return err;
err = regmap_write(regmap, REG_CMD, cmd);
if (err)
return err;
err = regmap_update_bits(regmap, REG_SEMA_FLAGS, SEMA_FLAGS_BOOT_CMD,
SEMA_FLAGS_BOOT_CMD);
if (err)
return err;
return regmap_read_poll_timeout(regmap, REG_CMD, val, !val, 10000,
10000 * 100);
}
static int zl38_fw_go(struct regmap *regmap)
{
int err;
err = zl38_fw_issue_command(regmap, BOOTCMD_LOAD_COMPLETE);
if (err)
return err;
return zl38_fw_issue_command(regmap, BOOTCMD_FW_GO);
}
static int zl38_fw_enter_boot_mode(struct regmap *regmap)
{
unsigned int val;
int err;
err = regmap_update_bits(regmap, REG_CLK_STATUS, CLK_STATUS_HWRST,
CLK_STATUS_HWRST);
if (err)
return err;
return regmap_read_poll_timeout(regmap, REG_PARAM_RESULT, val,
val == PARAM_RESULT_READY, 1000, 50000);
}
static int
zl38_fw_send_data(struct regmap *regmap, u32 addr, const void *data, u16 len)
{
__be32 addr_base = cpu_to_be32(addr & ~0xFF);
int err;
err = regmap_raw_write(regmap, REG_PG255_BASE_HI, &addr_base,
sizeof(addr_base));
if (err)
return err;
return regmap_raw_write(regmap, REG_PG255_OFFS(addr), data, len);
}
static int zl38_fw_send_xaddr(struct regmap *regmap, const void *data)
{
/* execution address from ihex: 32-bit little endian.
* device register expects 32-bit big endian.
*/
u32 addr = le32_to_cpup(data);
__be32 baddr = cpu_to_be32(addr);
return regmap_raw_write(regmap, REG_FWR_EXEC, &baddr, sizeof(baddr));
}
static int zl38_load_firmware(struct device *dev, struct regmap *regmap)
{
const struct ihex_binrec *rec;
const struct firmware *fw;
u32 addr;
u16 len;
int err;
/* how to get this firmware:
* 1. request and download chip firmware from Microsemi
* (provided by Microsemi in srec format)
* 2. convert downloaded firmware from srec to ihex. Simple tool:
* https://gitlab.com/TheSven73/s3-to-irec
* 3. convert ihex to binary (.fw) using ihex2fw tool which is included
* with the Linux kernel sources
*/
err = request_ihex_firmware(&fw, "zl38060.fw", dev);
if (err)
return err;
err = zl38_fw_enter_boot_mode(regmap);
if (err)
goto out;
rec = (const struct ihex_binrec *)fw->data;
while (rec) {
addr = be32_to_cpu(rec->addr);
len = be16_to_cpu(rec->len);
if (addr) {
/* regular data ihex record */
err = zl38_fw_send_data(regmap, addr, rec->data, len);
} else if (len == 4) {
/* execution address ihex record */
err = zl38_fw_send_xaddr(regmap, rec->data);
} else {
err = -EINVAL;
}
if (err)
goto out;
/* next ! */
rec = ihex_next_binrec(rec);
}
err = zl38_fw_go(regmap);
out:
release_firmware(fw);
return err;
}
static int zl38_software_reset(struct regmap *regmap)
{
unsigned int val;
int err;
err = regmap_update_bits(regmap, REG_SEMA_FLAGS, SEMA_FLAGS_APP_REBOOT,
SEMA_FLAGS_APP_REBOOT);
if (err)
return err;
/* wait for host bus interface to settle.
* Not sure if this is required: Microsemi's vendor driver does this,
* but the firmware manual does not mention it. Leave it in, there's
* little downside, apart from a slower reset.
*/
msleep(50);
return regmap_read_poll_timeout(regmap, REG_SEMA_FLAGS, val,
!(val & SEMA_FLAGS_APP_REBOOT), 10000,
10000 * 100);
}
static int zl38_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
{
struct zl38_codec_priv *priv = snd_soc_dai_get_drvdata(dai);
int err;
switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
case SND_SOC_DAIFMT_I2S:
/* firmware default is normal i2s */
break;
default:
return -EINVAL;
}
switch (fmt & SND_SOC_DAIFMT_INV_MASK) {
case SND_SOC_DAIFMT_NB_NF:
/* firmware default is normal bitclock and frame */
break;
default:
return -EINVAL;
}
switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
case SND_SOC_DAIFMT_CBM_CFM:
/* always 32 bits per frame (= 16 bits/channel, 2 channels) */
err = regmap_update_bits(priv->regmap, REG_TDMA_CFG_CLK,
CFG_CLK_MASTER | CFG_CLK_PCLK_MASK,
CFG_CLK_MASTER | CFG_CLK_PCLK(32));
if (err)
return err;
break;
default:
return -EINVAL;
}
return 0;
}
static int zl38_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
{
struct zl38_codec_priv *priv = snd_soc_dai_get_drvdata(dai);
bool tx = substream->stream == SNDRV_PCM_STREAM_PLAYBACK;
unsigned int fsrate;
int err;
/* We cannot change hw_params while the dai is already in use - the
* software reset will corrupt the audio. However, this is not required,
* as the chip's TDM buses are fully symmetric, which mandates identical
* rates, channels, and samplebits for record and playback.
*/
if (priv->is_stream_in_use[!tx])
goto skip_setup;
switch (params_rate(params)) {
case 8000:
fsrate = CFG_CLK_FSRATE_8KHZ;
break;
case 16000:
fsrate = CFG_CLK_FSRATE_16KHZ;
break;
case 48000:
fsrate = CFG_CLK_FSRATE_48KHZ;
break;
default:
return -EINVAL;
};
err = regmap_update_bits(priv->regmap, REG_TDMA_CFG_CLK,
CFG_CLK_FSRATE_MASK, fsrate);
if (err)
return err;
/* chip requires a software reset to apply audio register changes */
err = zl38_software_reset(priv->regmap);
if (err)
return err;
skip_setup:
priv->is_stream_in_use[tx] = true;
return 0;
}
static int zl38_hw_free(struct snd_pcm_substream *substream,
struct snd_soc_dai *dai)
{
struct zl38_codec_priv *priv = snd_soc_dai_get_drvdata(dai);
bool tx = substream->stream == SNDRV_PCM_STREAM_PLAYBACK;
priv->is_stream_in_use[tx] = false;
return 0;
}
/* stereo bypass with no AEC */
static const struct reg_sequence cp_config_stereo_bypass[] = {
/* interconnects must be programmed first */
{ 0x0210, 0x0005 }, /* DAC1 in <= I2S1-L */
{ 0x0212, 0x0006 }, /* DAC2 in <= I2S1-R */
{ 0x0214, 0x0001 }, /* I2S1-L in <= MIC1 */
{ 0x0216, 0x0001 }, /* I2S1-R in <= MIC1 */
{ 0x0224, 0x0000 }, /* AEC-S in <= n/a */
{ 0x0226, 0x0000 }, /* AEC-R in <= n/a */
/* output enables must be programmed next */
{ 0x0202, 0x000F }, /* enable I2S1 + DAC */
};
static const struct snd_soc_dai_ops zl38_dai_ops = {
.set_fmt = zl38_set_fmt,
.hw_params = zl38_hw_params,
.hw_free = zl38_hw_free,
};
static struct snd_soc_dai_driver zl38_dai = {
.name = "zl38060-tdma",
.playback = {
.stream_name = "Playback",
.channels_min = 2,
.channels_max = 2,
.rates = ZL38_RATES,
.formats = ZL38_FORMATS,
},
.capture = {
.stream_name = "Capture",
.channels_min = 2,
.channels_max = 2,
.rates = ZL38_RATES,
.formats = ZL38_FORMATS,
},
.ops = &zl38_dai_ops,
.symmetric_rates = 1,
.symmetric_samplebits = 1,
.symmetric_channels = 1,
};
static const struct snd_soc_dapm_widget zl38_dapm_widgets[] = {
SND_SOC_DAPM_OUTPUT("DAC1"),
SND_SOC_DAPM_OUTPUT("DAC2"),
SND_SOC_DAPM_INPUT("DMICL"),
};
static const struct snd_soc_dapm_route zl38_dapm_routes[] = {
{ "DAC1", NULL, "Playback" },
{ "DAC2", NULL, "Playback" },
{ "Capture", NULL, "DMICL" },
};
static const struct snd_soc_component_driver zl38_component_dev = {
.dapm_widgets = zl38_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(zl38_dapm_widgets),
.dapm_routes = zl38_dapm_routes,
.num_dapm_routes = ARRAY_SIZE(zl38_dapm_routes),
.endianness = 1,
.non_legacy_dai_naming = 1,
};
static void chip_gpio_set(struct gpio_chip *c, unsigned int offset, int val)
{
struct regmap *regmap = gpiochip_get_data(c);
unsigned int mask = BIT(offset);
regmap_update_bits(regmap, REG_GPIO_DAT, mask, val ? mask : 0);
}
static int chip_gpio_get(struct gpio_chip *c, unsigned int offset)
{
struct regmap *regmap = gpiochip_get_data(c);
unsigned int mask = BIT(offset);
unsigned int val;
int err;
err = regmap_read(regmap, REG_GPIO_DAT, &val);
if (err)
return err;
return !!(val & mask);
}
static int chip_direction_input(struct gpio_chip *c, unsigned int offset)
{
struct regmap *regmap = gpiochip_get_data(c);
unsigned int mask = BIT(offset);
return regmap_update_bits(regmap, REG_GPIO_DIR, mask, 0);
}
static int
chip_direction_output(struct gpio_chip *c, unsigned int offset, int val)
{
struct regmap *regmap = gpiochip_get_data(c);
unsigned int mask = BIT(offset);
chip_gpio_set(c, offset, val);
return regmap_update_bits(regmap, REG_GPIO_DIR, mask, mask);
}
static const struct gpio_chip template_chip = {
.owner = THIS_MODULE,
.label = DRV_NAME,
.base = -1,
.ngpio = 14,
.direction_input = chip_direction_input,
.direction_output = chip_direction_output,
.get = chip_gpio_get,
.set = chip_gpio_set,
.can_sleep = true,
};
static int zl38_check_revision(struct device *dev, struct regmap *regmap)
{
unsigned int hwrev, fwprod, fwrev;
int fw_major, fw_minor, fw_micro;
int err;
err = regmap_read(regmap, REG_HW_REV, &hwrev);
if (err)
return err;
err = regmap_read(regmap, REG_FW_PROD, &fwprod);
if (err)
return err;
err = regmap_read(regmap, REG_FW_REV, &fwrev);
if (err)
return err;
fw_major = (fwrev >> 12) & 0xF;
fw_minor = (fwrev >> 8) & 0xF;
fw_micro = fwrev & 0xFF;
dev_info(dev, "hw rev 0x%x, fw product code %d, firmware rev %d.%d.%d",
hwrev & 0x1F, fwprod, fw_major, fw_minor, fw_micro);
if (fw_major != FIRMWARE_MAJOR || fw_minor < FIRMWARE_MINOR) {
dev_err(dev, "unsupported firmware. driver supports %d.%d",
FIRMWARE_MAJOR, FIRMWARE_MINOR);
return -EINVAL;
}
return 0;
}
static int zl38_bus_read(void *context,
const void *reg_buf, size_t reg_size,
void *val_buf, size_t val_size)
{
struct spi_device *spi = context;
const u8 *reg_buf8 = reg_buf;
size_t len = 0;
u8 offs, page;
u8 txbuf[4];
if (reg_size != 2 || val_size > ZL38_MAX_RAW_XFER)
return -EINVAL;
offs = reg_buf8[1] >> 1;
page = reg_buf8[0];
if (page) {
txbuf[len++] = 0xFE;
txbuf[len++] = page == HBI_FIRMWARE_PAGE ? 0xFF : page - 1;
txbuf[len++] = offs;
txbuf[len++] = val_size / 2 - 1;
} else {
txbuf[len++] = offs | 0x80;
txbuf[len++] = val_size / 2 - 1;
}
return spi_write_then_read(spi, txbuf, len, val_buf, val_size);
}
static int zl38_bus_write(void *context, const void *data, size_t count)
{
struct spi_device *spi = context;
u8 buf[4 + ZL38_MAX_RAW_XFER];
size_t val_len, len = 0;
const u8 *data8 = data;
u8 offs, page;
if (count > (2 + ZL38_MAX_RAW_XFER) || count < 4)
return -EINVAL;
val_len = count - 2;
offs = data8[1] >> 1;
page = data8[0];
if (page) {
buf[len++] = 0xFE;
buf[len++] = page == HBI_FIRMWARE_PAGE ? 0xFF : page - 1;
buf[len++] = offs;
buf[len++] = (val_len / 2 - 1) | 0x80;
} else {
buf[len++] = offs | 0x80;
buf[len++] = (val_len / 2 - 1) | 0x80;
}
memcpy(buf + len, data8 + 2, val_len);
len += val_len;
return spi_write(spi, buf, len);
}
static const struct regmap_bus zl38_regmap_bus = {
.read = zl38_bus_read,
.write = zl38_bus_write,
.max_raw_write = ZL38_MAX_RAW_XFER,
.max_raw_read = ZL38_MAX_RAW_XFER,
};
static const struct regmap_config zl38_regmap_conf = {
.reg_bits = 16,
.val_bits = 16,
.reg_stride = 2,
.use_single_read = true,
.use_single_write = true,
};
static int zl38_spi_probe(struct spi_device *spi)
{
struct device *dev = &spi->dev;
struct zl38_codec_priv *priv;
struct gpio_desc *reset_gpio;
int err;
/* get the chip to a known state by putting it in reset */
reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(reset_gpio))
return PTR_ERR(reset_gpio);
if (reset_gpio) {
/* datasheet: need > 10us for a digital + analog reset */
usleep_range(15, 50);
/* take the chip out of reset */
gpiod_set_value_cansleep(reset_gpio, 0);
/* datasheet: need > 3ms for digital section to become stable */
usleep_range(3000, 10000);
}
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->dev = dev;
dev_set_drvdata(dev, priv);
priv->regmap = devm_regmap_init(dev, &zl38_regmap_bus, spi,
&zl38_regmap_conf);
if (IS_ERR(priv->regmap))
return PTR_ERR(priv->regmap);
err = zl38_load_firmware(dev, priv->regmap);
if (err)
return err;
err = zl38_check_revision(dev, priv->regmap);
if (err)
return err;
priv->gpio_chip = devm_kmemdup(dev, &template_chip,
sizeof(template_chip), GFP_KERNEL);
if (!priv->gpio_chip)
return -ENOMEM;
#ifdef CONFIG_OF_GPIO
priv->gpio_chip->of_node = dev->of_node;
#endif
err = devm_gpiochip_add_data(dev, priv->gpio_chip, priv->regmap);
if (err)
return err;
/* setup the cross-point switch for stereo bypass */
err = regmap_multi_reg_write(priv->regmap, cp_config_stereo_bypass,
ARRAY_SIZE(cp_config_stereo_bypass));
if (err)
return err;
/* setup for 12MHz crystal connected to the chip */
err = regmap_update_bits(priv->regmap, REG_CLK_CFG, CLK_CFG_SOURCE_XTAL,
CLK_CFG_SOURCE_XTAL);
if (err)
return err;
return devm_snd_soc_register_component(dev, &zl38_component_dev,
&zl38_dai, 1);
}
static const struct of_device_id zl38_dt_ids[] = {
{ .compatible = "mscc,zl38060", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, zl38_dt_ids);
static const struct spi_device_id zl38_spi_ids[] = {
{ "zl38060", 0 },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(spi, zl38_spi_ids);
static struct spi_driver zl38060_spi_driver = {
.driver = {
.name = DRV_NAME,
.of_match_table = of_match_ptr(zl38_dt_ids),
},
.probe = zl38_spi_probe,
.id_table = zl38_spi_ids,
};
module_spi_driver(zl38060_spi_driver);
MODULE_DESCRIPTION("ASoC ZL38060 driver");
MODULE_AUTHOR("Sven Van Asbroeck <TheSven73@gmail.com>");
MODULE_LICENSE("GPL v2");
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment