mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
638 lines
16 KiB
638 lines
16 KiB
// 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_rate = 1, |
|
.symmetric_sample_bits = 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 <[email protected]>"); |
|
MODULE_LICENSE("GPL v2");
|
|
|