|  | // 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"); |