| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Au1000/Au1500/Au1100 I2S controller driver for ASoC |
| * |
| * (c) 2011 Manuel Lauss <manuel.lauss@googlemail.com> |
| * |
| * Note: clock supplied to the I2S controller must be 256x samplerate. |
| */ |
| |
| #include <linux/init.h> |
| #include <linux/module.h> |
| #include <linux/slab.h> |
| #include <linux/suspend.h> |
| #include <sound/core.h> |
| #include <sound/pcm.h> |
| #include <sound/initval.h> |
| #include <sound/soc.h> |
| #include <asm/mach-au1x00/au1000.h> |
| |
| #include "psc.h" |
| |
| #define I2S_RXTX 0x00 |
| #define I2S_CFG 0x04 |
| #define I2S_ENABLE 0x08 |
| |
| #define CFG_XU (1 << 25) /* tx underflow */ |
| #define CFG_XO (1 << 24) |
| #define CFG_RU (1 << 23) |
| #define CFG_RO (1 << 22) |
| #define CFG_TR (1 << 21) |
| #define CFG_TE (1 << 20) |
| #define CFG_TF (1 << 19) |
| #define CFG_RR (1 << 18) |
| #define CFG_RF (1 << 17) |
| #define CFG_ICK (1 << 12) /* clock invert */ |
| #define CFG_PD (1 << 11) /* set to make I2SDIO INPUT */ |
| #define CFG_LB (1 << 10) /* loopback */ |
| #define CFG_IC (1 << 9) /* word select invert */ |
| #define CFG_FM_I2S (0 << 7) /* I2S format */ |
| #define CFG_FM_LJ (1 << 7) /* left-justified */ |
| #define CFG_FM_RJ (2 << 7) /* right-justified */ |
| #define CFG_FM_MASK (3 << 7) |
| #define CFG_TN (1 << 6) /* tx fifo en */ |
| #define CFG_RN (1 << 5) /* rx fifo en */ |
| #define CFG_SZ_8 (0x08) |
| #define CFG_SZ_16 (0x10) |
| #define CFG_SZ_18 (0x12) |
| #define CFG_SZ_20 (0x14) |
| #define CFG_SZ_24 (0x18) |
| #define CFG_SZ_MASK (0x1f) |
| #define EN_D (1 << 1) /* DISable */ |
| #define EN_CE (1 << 0) /* clock enable */ |
| |
| /* only limited by clock generator and board design */ |
| #define AU1XI2SC_RATES \ |
| SNDRV_PCM_RATE_CONTINUOUS |
| |
| #define AU1XI2SC_FMTS \ |
| (SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_U8 | \ |
| SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S16_BE | \ |
| SNDRV_PCM_FMTBIT_U16_LE | SNDRV_PCM_FMTBIT_U16_BE | \ |
| SNDRV_PCM_FMTBIT_S18_3LE | SNDRV_PCM_FMTBIT_U18_3LE | \ |
| SNDRV_PCM_FMTBIT_S18_3BE | SNDRV_PCM_FMTBIT_U18_3BE | \ |
| SNDRV_PCM_FMTBIT_S20_3LE | SNDRV_PCM_FMTBIT_U20_3LE | \ |
| SNDRV_PCM_FMTBIT_S20_3BE | SNDRV_PCM_FMTBIT_U20_3BE | \ |
| SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S24_BE | \ |
| SNDRV_PCM_FMTBIT_U24_LE | SNDRV_PCM_FMTBIT_U24_BE | \ |
| 0) |
| |
| static inline unsigned long RD(struct au1xpsc_audio_data *ctx, int reg) |
| { |
| return __raw_readl(ctx->mmio + reg); |
| } |
| |
| static inline void WR(struct au1xpsc_audio_data *ctx, int reg, unsigned long v) |
| { |
| __raw_writel(v, ctx->mmio + reg); |
| wmb(); |
| } |
| |
| static int au1xi2s_set_fmt(struct snd_soc_dai *cpu_dai, unsigned int fmt) |
| { |
| struct au1xpsc_audio_data *ctx = snd_soc_dai_get_drvdata(cpu_dai); |
| unsigned long c; |
| int ret; |
| |
| ret = -EINVAL; |
| c = ctx->cfg; |
| |
| c &= ~CFG_FM_MASK; |
| switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { |
| case SND_SOC_DAIFMT_I2S: |
| c |= CFG_FM_I2S; |
| break; |
| case SND_SOC_DAIFMT_MSB: |
| c |= CFG_FM_RJ; |
| break; |
| case SND_SOC_DAIFMT_LSB: |
| c |= CFG_FM_LJ; |
| break; |
| default: |
| goto out; |
| } |
| |
| c &= ~(CFG_IC | CFG_ICK); /* IB-IF */ |
| switch (fmt & SND_SOC_DAIFMT_INV_MASK) { |
| case SND_SOC_DAIFMT_NB_NF: |
| c |= CFG_IC | CFG_ICK; |
| break; |
| case SND_SOC_DAIFMT_NB_IF: |
| c |= CFG_IC; |
| break; |
| case SND_SOC_DAIFMT_IB_NF: |
| c |= CFG_ICK; |
| break; |
| case SND_SOC_DAIFMT_IB_IF: |
| break; |
| default: |
| goto out; |
| } |
| |
| /* I2S controller only supports provider */ |
| switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) { |
| case SND_SOC_DAIFMT_BP_FP: /* CODEC consumer */ |
| break; |
| default: |
| goto out; |
| } |
| |
| ret = 0; |
| ctx->cfg = c; |
| out: |
| return ret; |
| } |
| |
| static int au1xi2s_trigger(struct snd_pcm_substream *substream, |
| int cmd, struct snd_soc_dai *dai) |
| { |
| struct au1xpsc_audio_data *ctx = snd_soc_dai_get_drvdata(dai); |
| int stype = SUBSTREAM_TYPE(substream); |
| |
| switch (cmd) { |
| case SNDRV_PCM_TRIGGER_START: |
| case SNDRV_PCM_TRIGGER_RESUME: |
| /* power up */ |
| WR(ctx, I2S_ENABLE, EN_D | EN_CE); |
| WR(ctx, I2S_ENABLE, EN_CE); |
| ctx->cfg |= (stype == PCM_TX) ? CFG_TN : CFG_RN; |
| WR(ctx, I2S_CFG, ctx->cfg); |
| break; |
| case SNDRV_PCM_TRIGGER_STOP: |
| case SNDRV_PCM_TRIGGER_SUSPEND: |
| ctx->cfg &= ~((stype == PCM_TX) ? CFG_TN : CFG_RN); |
| WR(ctx, I2S_CFG, ctx->cfg); |
| WR(ctx, I2S_ENABLE, EN_D); /* power off */ |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static unsigned long msbits_to_reg(int msbits) |
| { |
| switch (msbits) { |
| case 8: |
| return CFG_SZ_8; |
| case 16: |
| return CFG_SZ_16; |
| case 18: |
| return CFG_SZ_18; |
| case 20: |
| return CFG_SZ_20; |
| case 24: |
| return CFG_SZ_24; |
| } |
| return 0; |
| } |
| |
| static int au1xi2s_hw_params(struct snd_pcm_substream *substream, |
| struct snd_pcm_hw_params *params, |
| struct snd_soc_dai *dai) |
| { |
| struct au1xpsc_audio_data *ctx = snd_soc_dai_get_drvdata(dai); |
| unsigned long v; |
| |
| v = msbits_to_reg(params->msbits); |
| if (!v) |
| return -EINVAL; |
| |
| ctx->cfg &= ~CFG_SZ_MASK; |
| ctx->cfg |= v; |
| return 0; |
| } |
| |
| static int au1xi2s_startup(struct snd_pcm_substream *substream, |
| struct snd_soc_dai *dai) |
| { |
| struct au1xpsc_audio_data *ctx = snd_soc_dai_get_drvdata(dai); |
| snd_soc_dai_set_dma_data(dai, substream, &ctx->dmaids[0]); |
| return 0; |
| } |
| |
| static const struct snd_soc_dai_ops au1xi2s_dai_ops = { |
| .startup = au1xi2s_startup, |
| .trigger = au1xi2s_trigger, |
| .hw_params = au1xi2s_hw_params, |
| .set_fmt = au1xi2s_set_fmt, |
| }; |
| |
| static struct snd_soc_dai_driver au1xi2s_dai_driver = { |
| .symmetric_rate = 1, |
| .playback = { |
| .rates = AU1XI2SC_RATES, |
| .formats = AU1XI2SC_FMTS, |
| .channels_min = 2, |
| .channels_max = 2, |
| }, |
| .capture = { |
| .rates = AU1XI2SC_RATES, |
| .formats = AU1XI2SC_FMTS, |
| .channels_min = 2, |
| .channels_max = 2, |
| }, |
| .ops = &au1xi2s_dai_ops, |
| }; |
| |
| static const struct snd_soc_component_driver au1xi2s_component = { |
| .name = "au1xi2s", |
| .legacy_dai_naming = 1, |
| }; |
| |
| static int au1xi2s_drvprobe(struct platform_device *pdev) |
| { |
| struct resource *iores, *dmares; |
| struct au1xpsc_audio_data *ctx; |
| |
| ctx = devm_kzalloc(&pdev->dev, sizeof(*ctx), GFP_KERNEL); |
| if (!ctx) |
| return -ENOMEM; |
| |
| iores = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| if (!iores) |
| return -ENODEV; |
| |
| if (!devm_request_mem_region(&pdev->dev, iores->start, |
| resource_size(iores), |
| pdev->name)) |
| return -EBUSY; |
| |
| ctx->mmio = devm_ioremap(&pdev->dev, iores->start, |
| resource_size(iores)); |
| if (!ctx->mmio) |
| return -EBUSY; |
| |
| dmares = platform_get_resource(pdev, IORESOURCE_DMA, 0); |
| if (!dmares) |
| return -EBUSY; |
| ctx->dmaids[SNDRV_PCM_STREAM_PLAYBACK] = dmares->start; |
| |
| dmares = platform_get_resource(pdev, IORESOURCE_DMA, 1); |
| if (!dmares) |
| return -EBUSY; |
| ctx->dmaids[SNDRV_PCM_STREAM_CAPTURE] = dmares->start; |
| |
| platform_set_drvdata(pdev, ctx); |
| |
| return snd_soc_register_component(&pdev->dev, &au1xi2s_component, |
| &au1xi2s_dai_driver, 1); |
| } |
| |
| static void au1xi2s_drvremove(struct platform_device *pdev) |
| { |
| struct au1xpsc_audio_data *ctx = platform_get_drvdata(pdev); |
| |
| snd_soc_unregister_component(&pdev->dev); |
| |
| WR(ctx, I2S_ENABLE, EN_D); /* clock off, disable */ |
| } |
| |
| #ifdef CONFIG_PM |
| static int au1xi2s_drvsuspend(struct device *dev) |
| { |
| struct au1xpsc_audio_data *ctx = dev_get_drvdata(dev); |
| |
| WR(ctx, I2S_ENABLE, EN_D); /* clock off, disable */ |
| |
| return 0; |
| } |
| |
| static int au1xi2s_drvresume(struct device *dev) |
| { |
| return 0; |
| } |
| |
| static const struct dev_pm_ops au1xi2sc_pmops = { |
| .suspend = au1xi2s_drvsuspend, |
| .resume = au1xi2s_drvresume, |
| }; |
| |
| #define AU1XI2SC_PMOPS (&au1xi2sc_pmops) |
| |
| #else |
| |
| #define AU1XI2SC_PMOPS NULL |
| |
| #endif |
| |
| static struct platform_driver au1xi2s_driver = { |
| .driver = { |
| .name = "alchemy-i2sc", |
| .pm = AU1XI2SC_PMOPS, |
| }, |
| .probe = au1xi2s_drvprobe, |
| .remove = au1xi2s_drvremove, |
| }; |
| |
| module_platform_driver(au1xi2s_driver); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_DESCRIPTION("Au1000/1500/1100 I2S ASoC driver"); |
| MODULE_AUTHOR("Manuel Lauss"); |