|  | // SPDX-License-Identifier: GPL-2.0-or-later | 
|  | // | 
|  | // Author: Kevin Wells <kevin.wells@nxp.com> | 
|  | // | 
|  | // Copyright (C) 2008 NXP Semiconductors | 
|  | // Copyright 2023 Timesys Corporation <piotr.wojtaszczyk@timesys.com> | 
|  |  | 
|  | #include <linux/init.h> | 
|  | #include <linux/module.h> | 
|  | #include <linux/interrupt.h> | 
|  | #include <linux/device.h> | 
|  | #include <linux/delay.h> | 
|  | #include <linux/clk.h> | 
|  | #include <linux/io.h> | 
|  |  | 
|  | #include <sound/core.h> | 
|  | #include <sound/pcm.h> | 
|  | #include <sound/pcm_params.h> | 
|  | #include <sound/dmaengine_pcm.h> | 
|  | #include <sound/initval.h> | 
|  | #include <sound/soc.h> | 
|  |  | 
|  | #include "lpc3xxx-i2s.h" | 
|  |  | 
|  | #define I2S_PLAYBACK_FLAG 0x1 | 
|  | #define I2S_CAPTURE_FLAG 0x2 | 
|  |  | 
|  | #define LPC3XXX_I2S_RATES ( \ | 
|  | SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_22050 | \ | 
|  | SNDRV_PCM_RATE_32000 | SNDRV_PCM_RATE_44100 | \ | 
|  | SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_96000) | 
|  |  | 
|  | #define LPC3XXX_I2S_FORMATS ( \ | 
|  | SNDRV_PCM_FMTBIT_S8 | \ | 
|  | SNDRV_PCM_FMTBIT_S16_LE | \ | 
|  | SNDRV_PCM_FMTBIT_S32_LE) | 
|  |  | 
|  | static void __lpc3xxx_find_clkdiv(u32 *clkx, u32 *clky, int freq, int xbytes, u32 clkrate) | 
|  | { | 
|  | u32 i2srate; | 
|  | u32 idxx, idyy; | 
|  | u32 diff, trate, baseclk; | 
|  |  | 
|  | /* Adjust rate for sample size (bits) and 2 channels and offset for | 
|  | * divider in clock output | 
|  | */ | 
|  | i2srate = (freq / 100) * 2 * (8 * xbytes); | 
|  | i2srate = i2srate << 1; | 
|  | clkrate = clkrate / 100; | 
|  | baseclk = clkrate; | 
|  | *clkx = 1; | 
|  | *clky = 1; | 
|  |  | 
|  | /* Find the best divider */ | 
|  | *clkx = *clky = 0; | 
|  | diff = ~0; | 
|  | for (idxx = 1; idxx < 0xFF; idxx++) { | 
|  | for (idyy = 1; idyy < 0xFF; idyy++) { | 
|  | trate = (baseclk * idxx) / idyy; | 
|  | if (abs(trate - i2srate) < diff) { | 
|  | diff = abs(trate - i2srate); | 
|  | *clkx = idxx; | 
|  | *clky = idyy; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | static int lpc3xxx_i2s_startup(struct snd_pcm_substream *substream, struct snd_soc_dai *cpu_dai) | 
|  | { | 
|  | struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai); | 
|  | struct device *dev = i2s_info_p->dev; | 
|  | u32 flag; | 
|  | int ret = 0; | 
|  |  | 
|  | guard(mutex)(&i2s_info_p->lock); | 
|  |  | 
|  | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) | 
|  | flag = I2S_PLAYBACK_FLAG; | 
|  | else | 
|  | flag = I2S_CAPTURE_FLAG; | 
|  |  | 
|  | if (flag & i2s_info_p->streams_in_use) { | 
|  | dev_warn(dev, "I2S channel is busy\n"); | 
|  | ret = -EBUSY; | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | if (i2s_info_p->streams_in_use == 0) { | 
|  | ret = clk_prepare_enable(i2s_info_p->clk); | 
|  | if (ret) { | 
|  | dev_err(dev, "Can't enable clock, err=%d\n", ret); | 
|  | return ret; | 
|  | } | 
|  | } | 
|  |  | 
|  | i2s_info_p->streams_in_use |= flag; | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static void lpc3xxx_i2s_shutdown(struct snd_pcm_substream *substream, struct snd_soc_dai *cpu_dai) | 
|  | { | 
|  | struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai); | 
|  | struct regmap *regs = i2s_info_p->regs; | 
|  | const u32 stop_bits = (LPC3XXX_I2S_RESET | LPC3XXX_I2S_STOP); | 
|  | u32 flag; | 
|  |  | 
|  | guard(mutex)(&i2s_info_p->lock); | 
|  |  | 
|  | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { | 
|  | flag = I2S_PLAYBACK_FLAG; | 
|  | regmap_write(regs, LPC3XXX_REG_I2S_TX_RATE, 0); | 
|  | regmap_update_bits(regs, LPC3XXX_REG_I2S_DAO, stop_bits, stop_bits); | 
|  | } else { | 
|  | flag = I2S_CAPTURE_FLAG; | 
|  | regmap_write(regs, LPC3XXX_REG_I2S_RX_RATE, 0); | 
|  | regmap_update_bits(regs, LPC3XXX_REG_I2S_DAI, stop_bits, stop_bits); | 
|  | } | 
|  | i2s_info_p->streams_in_use &= ~flag; | 
|  |  | 
|  | if (i2s_info_p->streams_in_use == 0) | 
|  | clk_disable_unprepare(i2s_info_p->clk); | 
|  | } | 
|  |  | 
|  | static int lpc3xxx_i2s_set_dai_sysclk(struct snd_soc_dai *cpu_dai, | 
|  | int clk_id, unsigned int freq, int dir) | 
|  | { | 
|  | struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai); | 
|  |  | 
|  | /* Will use in HW params later */ | 
|  | i2s_info_p->freq = freq; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int lpc3xxx_i2s_set_dai_fmt(struct snd_soc_dai *cpu_dai, unsigned int fmt) | 
|  | { | 
|  | struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai); | 
|  | struct device *dev = i2s_info_p->dev; | 
|  |  | 
|  | if ((fmt & SND_SOC_DAIFMT_FORMAT_MASK) != SND_SOC_DAIFMT_I2S) { | 
|  | dev_warn(dev, "unsupported bus format %d\n", fmt); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | if ((fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) != SND_SOC_DAIFMT_BP_FP) { | 
|  | dev_warn(dev, "unsupported clock provider %d\n", fmt); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int lpc3xxx_i2s_hw_params(struct snd_pcm_substream *substream, | 
|  | struct snd_pcm_hw_params *params, | 
|  | struct snd_soc_dai *cpu_dai) | 
|  | { | 
|  | struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai); | 
|  | struct device *dev = i2s_info_p->dev; | 
|  | struct regmap *regs = i2s_info_p->regs; | 
|  | int xfersize; | 
|  | u32 tmp, clkx, clky; | 
|  |  | 
|  | tmp = LPC3XXX_I2S_RESET | LPC3XXX_I2S_STOP; | 
|  | switch (params_format(params)) { | 
|  | case SNDRV_PCM_FORMAT_S8: | 
|  | tmp |= LPC3XXX_I2S_WW8 | LPC3XXX_I2S_WS_HP(LPC3XXX_I2S_WW8_HP); | 
|  | xfersize = 1; | 
|  | break; | 
|  |  | 
|  | case SNDRV_PCM_FORMAT_S16_LE: | 
|  | tmp |= LPC3XXX_I2S_WW16 | LPC3XXX_I2S_WS_HP(LPC3XXX_I2S_WW16_HP); | 
|  | xfersize = 2; | 
|  | break; | 
|  |  | 
|  | case SNDRV_PCM_FORMAT_S32_LE: | 
|  | tmp |= LPC3XXX_I2S_WW32 | LPC3XXX_I2S_WS_HP(LPC3XXX_I2S_WW32_HP); | 
|  | xfersize = 4; | 
|  | break; | 
|  |  | 
|  | default: | 
|  | dev_warn(dev, "Unsupported audio data format %d\n", params_format(params)); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | if (params_channels(params) == 1) | 
|  | tmp |= LPC3XXX_I2S_MONO; | 
|  |  | 
|  | __lpc3xxx_find_clkdiv(&clkx, &clky, i2s_info_p->freq, xfersize, i2s_info_p->clkrate); | 
|  |  | 
|  | dev_dbg(dev, "Stream                : %s\n", snd_pcm_direction_name(substream->stream)); | 
|  | dev_dbg(dev, "Desired clock rate    : %d\n", i2s_info_p->freq); | 
|  | dev_dbg(dev, "Base clock rate       : %d\n", i2s_info_p->clkrate); | 
|  | dev_dbg(dev, "Transfer size (bytes) : %d\n", xfersize); | 
|  | dev_dbg(dev, "Clock divider (x)     : %d\n", clkx); | 
|  | dev_dbg(dev, "Clock divider (y)     : %d\n", clky); | 
|  | dev_dbg(dev, "Channels              : %d\n", params_channels(params)); | 
|  | dev_dbg(dev, "Data format           : %s\n", "I2S"); | 
|  |  | 
|  | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { | 
|  | regmap_write(regs, LPC3XXX_REG_I2S_DMA1, | 
|  | LPC3XXX_I2S_DMA1_TX_EN | LPC3XXX_I2S_DMA0_TX_DEPTH(4)); | 
|  | regmap_write(regs, LPC3XXX_REG_I2S_TX_RATE, (clkx << 8) | clky); | 
|  | regmap_write(regs, LPC3XXX_REG_I2S_DAO, tmp); | 
|  | } else { | 
|  | regmap_write(regs, LPC3XXX_REG_I2S_DMA0, | 
|  | LPC3XXX_I2S_DMA0_RX_EN | LPC3XXX_I2S_DMA1_RX_DEPTH(4)); | 
|  | regmap_write(regs, LPC3XXX_REG_I2S_RX_RATE, (clkx << 8) | clky); | 
|  | regmap_write(regs, LPC3XXX_REG_I2S_DAI, tmp); | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int lpc3xxx_i2s_trigger(struct snd_pcm_substream *substream, int cmd, | 
|  | struct snd_soc_dai *cpu_dai) | 
|  | { | 
|  | struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(cpu_dai); | 
|  | struct regmap *regs = i2s_info_p->regs; | 
|  | int ret = 0; | 
|  |  | 
|  | switch (cmd) { | 
|  | case SNDRV_PCM_TRIGGER_STOP: | 
|  | case SNDRV_PCM_TRIGGER_PAUSE_PUSH: | 
|  | case SNDRV_PCM_TRIGGER_SUSPEND: | 
|  | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) | 
|  | regmap_update_bits(regs, LPC3XXX_REG_I2S_DAO, | 
|  | LPC3XXX_I2S_STOP, LPC3XXX_I2S_STOP); | 
|  | else | 
|  | regmap_update_bits(regs, LPC3XXX_REG_I2S_DAI, | 
|  | LPC3XXX_I2S_STOP, LPC3XXX_I2S_STOP); | 
|  | break; | 
|  |  | 
|  | case SNDRV_PCM_TRIGGER_START: | 
|  | case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: | 
|  | case SNDRV_PCM_TRIGGER_RESUME: | 
|  | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) | 
|  | regmap_update_bits(regs, LPC3XXX_REG_I2S_DAO, | 
|  | (LPC3XXX_I2S_RESET | LPC3XXX_I2S_STOP), 0); | 
|  | else | 
|  | regmap_update_bits(regs, LPC3XXX_REG_I2S_DAI, | 
|  | (LPC3XXX_I2S_RESET | LPC3XXX_I2S_STOP), 0); | 
|  | break; | 
|  | default: | 
|  | ret = -EINVAL; | 
|  | } | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | static int lpc3xxx_i2s_dai_probe(struct snd_soc_dai *dai) | 
|  | { | 
|  | struct lpc3xxx_i2s_info *i2s_info_p = snd_soc_dai_get_drvdata(dai); | 
|  |  | 
|  | snd_soc_dai_init_dma_data(dai, &i2s_info_p->playback_dma_config, | 
|  | &i2s_info_p->capture_dma_config); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const struct snd_soc_dai_ops lpc3xxx_i2s_dai_ops = { | 
|  | .probe	= lpc3xxx_i2s_dai_probe, | 
|  | .startup = lpc3xxx_i2s_startup, | 
|  | .shutdown = lpc3xxx_i2s_shutdown, | 
|  | .trigger = lpc3xxx_i2s_trigger, | 
|  | .hw_params = lpc3xxx_i2s_hw_params, | 
|  | .set_sysclk = lpc3xxx_i2s_set_dai_sysclk, | 
|  | .set_fmt = lpc3xxx_i2s_set_dai_fmt, | 
|  | }; | 
|  |  | 
|  | static struct snd_soc_dai_driver lpc3xxx_i2s_dai_driver = { | 
|  | .playback = { | 
|  | .channels_min = 1, | 
|  | .channels_max = 2, | 
|  | .rates = LPC3XXX_I2S_RATES, | 
|  | .formats = LPC3XXX_I2S_FORMATS, | 
|  | }, | 
|  | .capture = { | 
|  | .channels_min = 1, | 
|  | .channels_max = 2, | 
|  | .rates = LPC3XXX_I2S_RATES, | 
|  | .formats = LPC3XXX_I2S_FORMATS, | 
|  | }, | 
|  | .ops = &lpc3xxx_i2s_dai_ops, | 
|  | .symmetric_rate = 1, | 
|  | .symmetric_channels = 1, | 
|  | .symmetric_sample_bits = 1, | 
|  | }; | 
|  |  | 
|  | static const struct snd_soc_component_driver lpc32xx_i2s_component = { | 
|  | .name = "lpc32xx-i2s", | 
|  | .legacy_dai_naming = 1, | 
|  | }; | 
|  |  | 
|  | static const struct regmap_config lpc32xx_i2s_regconfig = { | 
|  | .reg_bits = 32, | 
|  | .reg_stride = 4, | 
|  | .val_bits = 32, | 
|  | .max_register = LPC3XXX_REG_I2S_RX_RATE, | 
|  | }; | 
|  |  | 
|  | static int lpc32xx_i2s_probe(struct platform_device *pdev) | 
|  | { | 
|  | struct device *dev = &pdev->dev; | 
|  | struct lpc3xxx_i2s_info *i2s_info_p; | 
|  | struct resource *res; | 
|  | void __iomem *iomem; | 
|  | int ret; | 
|  |  | 
|  | i2s_info_p = devm_kzalloc(dev, sizeof(*i2s_info_p), GFP_KERNEL); | 
|  | if (!i2s_info_p) | 
|  | return -ENOMEM; | 
|  |  | 
|  | platform_set_drvdata(pdev, i2s_info_p); | 
|  | i2s_info_p->dev = dev; | 
|  |  | 
|  | iomem = devm_platform_get_and_ioremap_resource(pdev, 0, &res); | 
|  | if (IS_ERR(iomem)) | 
|  | return dev_err_probe(dev, PTR_ERR(iomem), "Can't map registers\n"); | 
|  |  | 
|  | i2s_info_p->regs = devm_regmap_init_mmio(dev, iomem, &lpc32xx_i2s_regconfig); | 
|  | if (IS_ERR(i2s_info_p->regs)) | 
|  | return dev_err_probe(dev, PTR_ERR(i2s_info_p->regs), | 
|  | "failed to init register map: %pe\n", i2s_info_p->regs); | 
|  |  | 
|  | i2s_info_p->clk = devm_clk_get(dev, NULL); | 
|  | if (IS_ERR(i2s_info_p->clk)) | 
|  | return dev_err_probe(dev, PTR_ERR(i2s_info_p->clk), "Can't get clock\n"); | 
|  |  | 
|  | i2s_info_p->clkrate = clk_get_rate(i2s_info_p->clk); | 
|  | if (i2s_info_p->clkrate == 0) | 
|  | return dev_err_probe(dev, -EINVAL, "Invalid returned clock rate\n"); | 
|  |  | 
|  | mutex_init(&i2s_info_p->lock); | 
|  |  | 
|  | ret = devm_snd_soc_register_component(dev, &lpc32xx_i2s_component, | 
|  | &lpc3xxx_i2s_dai_driver, 1); | 
|  | if (ret) | 
|  | return dev_err_probe(dev, ret, "Can't register cpu_dai component\n"); | 
|  |  | 
|  | i2s_info_p->playback_dma_config.addr = (dma_addr_t)(res->start + LPC3XXX_REG_I2S_TX_FIFO); | 
|  | i2s_info_p->playback_dma_config.maxburst = 4; | 
|  |  | 
|  | i2s_info_p->capture_dma_config.addr = (dma_addr_t)(res->start + LPC3XXX_REG_I2S_RX_FIFO); | 
|  | i2s_info_p->capture_dma_config.maxburst = 4; | 
|  |  | 
|  | ret = lpc3xxx_pcm_register(pdev); | 
|  | if (ret) | 
|  | return dev_err_probe(dev, ret, "Can't register pcm component\n"); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const struct of_device_id lpc32xx_i2s_match[] = { | 
|  | { .compatible = "nxp,lpc3220-i2s" }, | 
|  | {}, | 
|  | }; | 
|  | MODULE_DEVICE_TABLE(of, lpc32xx_i2s_match); | 
|  |  | 
|  | static struct platform_driver lpc32xx_i2s_driver = { | 
|  | .probe = lpc32xx_i2s_probe, | 
|  | .driver		= { | 
|  | .name	= "lpc3xxx-i2s", | 
|  | .of_match_table = lpc32xx_i2s_match, | 
|  | }, | 
|  | }; | 
|  |  | 
|  | module_platform_driver(lpc32xx_i2s_driver); | 
|  |  | 
|  | MODULE_AUTHOR("Kevin Wells <kevin.wells@nxp.com>"); | 
|  | MODULE_AUTHOR("Piotr Wojtaszczyk <piotr.wojtaszczyk@timesys.com>"); | 
|  | MODULE_DESCRIPTION("ASoC LPC3XXX I2S interface"); | 
|  | MODULE_LICENSE("GPL"); |