|  | /* | 
|  | * ALSA SoC Synopsys I2S Audio Layer | 
|  | * | 
|  | * sound/soc/dwc/designware_i2s.c | 
|  | * | 
|  | * Copyright (C) 2010 ST Microelectronics | 
|  | * Rajeev Kumar <rajeevkumar.linux@gmail.com> | 
|  | * | 
|  | * This file is licensed under the terms of the GNU General Public | 
|  | * License version 2. This program is licensed "as is" without any | 
|  | * warranty of any kind, whether express or implied. | 
|  | */ | 
|  |  | 
|  | #include <linux/clk.h> | 
|  | #include <linux/device.h> | 
|  | #include <linux/init.h> | 
|  | #include <linux/io.h> | 
|  | #include <linux/interrupt.h> | 
|  | #include <linux/module.h> | 
|  | #include <linux/slab.h> | 
|  | #include <sound/designware_i2s.h> | 
|  | #include <sound/pcm.h> | 
|  | #include <sound/pcm_params.h> | 
|  | #include <sound/soc.h> | 
|  |  | 
|  | /* common register for all channel */ | 
|  | #define IER		0x000 | 
|  | #define IRER		0x004 | 
|  | #define ITER		0x008 | 
|  | #define CER		0x00C | 
|  | #define CCR		0x010 | 
|  | #define RXFFR		0x014 | 
|  | #define TXFFR		0x018 | 
|  |  | 
|  | /* I2STxRxRegisters for all channels */ | 
|  | #define LRBR_LTHR(x)	(0x40 * x + 0x020) | 
|  | #define RRBR_RTHR(x)	(0x40 * x + 0x024) | 
|  | #define RER(x)		(0x40 * x + 0x028) | 
|  | #define TER(x)		(0x40 * x + 0x02C) | 
|  | #define RCR(x)		(0x40 * x + 0x030) | 
|  | #define TCR(x)		(0x40 * x + 0x034) | 
|  | #define ISR(x)		(0x40 * x + 0x038) | 
|  | #define IMR(x)		(0x40 * x + 0x03C) | 
|  | #define ROR(x)		(0x40 * x + 0x040) | 
|  | #define TOR(x)		(0x40 * x + 0x044) | 
|  | #define RFCR(x)		(0x40 * x + 0x048) | 
|  | #define TFCR(x)		(0x40 * x + 0x04C) | 
|  | #define RFF(x)		(0x40 * x + 0x050) | 
|  | #define TFF(x)		(0x40 * x + 0x054) | 
|  |  | 
|  | /* I2SCOMPRegisters */ | 
|  | #define I2S_COMP_PARAM_2	0x01F0 | 
|  | #define I2S_COMP_PARAM_1	0x01F4 | 
|  | #define I2S_COMP_VERSION	0x01F8 | 
|  | #define I2S_COMP_TYPE		0x01FC | 
|  |  | 
|  | #define MAX_CHANNEL_NUM		8 | 
|  | #define MIN_CHANNEL_NUM		2 | 
|  |  | 
|  | struct dw_i2s_dev { | 
|  | void __iomem *i2s_base; | 
|  | struct clk *clk; | 
|  | int active; | 
|  | unsigned int capability; | 
|  | struct device *dev; | 
|  |  | 
|  | /* data related to DMA transfers b/w i2s and DMAC */ | 
|  | struct i2s_dma_data play_dma_data; | 
|  | struct i2s_dma_data capture_dma_data; | 
|  | struct i2s_clk_config_data config; | 
|  | int (*i2s_clk_cfg)(struct i2s_clk_config_data *config); | 
|  | }; | 
|  |  | 
|  | static inline void i2s_write_reg(void __iomem *io_base, int reg, u32 val) | 
|  | { | 
|  | writel(val, io_base + reg); | 
|  | } | 
|  |  | 
|  | static inline u32 i2s_read_reg(void __iomem *io_base, int reg) | 
|  | { | 
|  | return readl(io_base + reg); | 
|  | } | 
|  |  | 
|  | static inline void i2s_disable_channels(struct dw_i2s_dev *dev, u32 stream) | 
|  | { | 
|  | u32 i = 0; | 
|  |  | 
|  | if (stream == SNDRV_PCM_STREAM_PLAYBACK) { | 
|  | for (i = 0; i < 4; i++) | 
|  | i2s_write_reg(dev->i2s_base, TER(i), 0); | 
|  | } else { | 
|  | for (i = 0; i < 4; i++) | 
|  | i2s_write_reg(dev->i2s_base, RER(i), 0); | 
|  | } | 
|  | } | 
|  |  | 
|  | static inline void i2s_clear_irqs(struct dw_i2s_dev *dev, u32 stream) | 
|  | { | 
|  | u32 i = 0; | 
|  |  | 
|  | if (stream == SNDRV_PCM_STREAM_PLAYBACK) { | 
|  | for (i = 0; i < 4; i++) | 
|  | i2s_write_reg(dev->i2s_base, TOR(i), 0); | 
|  | } else { | 
|  | for (i = 0; i < 4; i++) | 
|  | i2s_write_reg(dev->i2s_base, ROR(i), 0); | 
|  | } | 
|  | } | 
|  |  | 
|  | static void i2s_start(struct dw_i2s_dev *dev, | 
|  | struct snd_pcm_substream *substream) | 
|  | { | 
|  |  | 
|  | i2s_write_reg(dev->i2s_base, IER, 1); | 
|  |  | 
|  | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) | 
|  | i2s_write_reg(dev->i2s_base, ITER, 1); | 
|  | else | 
|  | i2s_write_reg(dev->i2s_base, IRER, 1); | 
|  |  | 
|  | i2s_write_reg(dev->i2s_base, CER, 1); | 
|  | } | 
|  |  | 
|  | static void i2s_stop(struct dw_i2s_dev *dev, | 
|  | struct snd_pcm_substream *substream) | 
|  | { | 
|  | u32 i = 0, irq; | 
|  |  | 
|  | i2s_clear_irqs(dev, substream->stream); | 
|  | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { | 
|  | i2s_write_reg(dev->i2s_base, ITER, 0); | 
|  |  | 
|  | for (i = 0; i < 4; i++) { | 
|  | irq = i2s_read_reg(dev->i2s_base, IMR(i)); | 
|  | i2s_write_reg(dev->i2s_base, IMR(i), irq | 0x30); | 
|  | } | 
|  | } else { | 
|  | i2s_write_reg(dev->i2s_base, IRER, 0); | 
|  |  | 
|  | for (i = 0; i < 4; i++) { | 
|  | irq = i2s_read_reg(dev->i2s_base, IMR(i)); | 
|  | i2s_write_reg(dev->i2s_base, IMR(i), irq | 0x03); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (!dev->active) { | 
|  | i2s_write_reg(dev->i2s_base, CER, 0); | 
|  | i2s_write_reg(dev->i2s_base, IER, 0); | 
|  | } | 
|  | } | 
|  |  | 
|  | static int dw_i2s_startup(struct snd_pcm_substream *substream, | 
|  | struct snd_soc_dai *cpu_dai) | 
|  | { | 
|  | struct dw_i2s_dev *dev = snd_soc_dai_get_drvdata(cpu_dai); | 
|  | struct i2s_dma_data *dma_data = NULL; | 
|  |  | 
|  | if (!(dev->capability & DWC_I2S_RECORD) && | 
|  | (substream->stream == SNDRV_PCM_STREAM_CAPTURE)) | 
|  | return -EINVAL; | 
|  |  | 
|  | if (!(dev->capability & DWC_I2S_PLAY) && | 
|  | (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)) | 
|  | return -EINVAL; | 
|  |  | 
|  | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) | 
|  | dma_data = &dev->play_dma_data; | 
|  | else if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) | 
|  | dma_data = &dev->capture_dma_data; | 
|  |  | 
|  | snd_soc_dai_set_dma_data(cpu_dai, substream, (void *)dma_data); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int dw_i2s_hw_params(struct snd_pcm_substream *substream, | 
|  | struct snd_pcm_hw_params *params, struct snd_soc_dai *dai) | 
|  | { | 
|  | struct dw_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); | 
|  | struct i2s_clk_config_data *config = &dev->config; | 
|  | u32 ccr, xfer_resolution, ch_reg, irq; | 
|  | int ret; | 
|  |  | 
|  | switch (params_format(params)) { | 
|  | case SNDRV_PCM_FORMAT_S16_LE: | 
|  | config->data_width = 16; | 
|  | ccr = 0x00; | 
|  | xfer_resolution = 0x02; | 
|  | break; | 
|  |  | 
|  | case SNDRV_PCM_FORMAT_S24_LE: | 
|  | config->data_width = 24; | 
|  | ccr = 0x08; | 
|  | xfer_resolution = 0x04; | 
|  | break; | 
|  |  | 
|  | case SNDRV_PCM_FORMAT_S32_LE: | 
|  | config->data_width = 32; | 
|  | ccr = 0x10; | 
|  | xfer_resolution = 0x05; | 
|  | break; | 
|  |  | 
|  | default: | 
|  | dev_err(dev->dev, "designware-i2s: unsuppted PCM fmt"); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | config->chan_nr = params_channels(params); | 
|  |  | 
|  | switch (config->chan_nr) { | 
|  | case EIGHT_CHANNEL_SUPPORT: | 
|  | case SIX_CHANNEL_SUPPORT: | 
|  | case FOUR_CHANNEL_SUPPORT: | 
|  | case TWO_CHANNEL_SUPPORT: | 
|  | break; | 
|  | default: | 
|  | dev_err(dev->dev, "channel not supported\n"); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | i2s_disable_channels(dev, substream->stream); | 
|  |  | 
|  | for (ch_reg = 0; ch_reg < (config->chan_nr / 2); ch_reg++) { | 
|  | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { | 
|  | i2s_write_reg(dev->i2s_base, TCR(ch_reg), | 
|  | xfer_resolution); | 
|  | i2s_write_reg(dev->i2s_base, TFCR(ch_reg), 0x02); | 
|  | irq = i2s_read_reg(dev->i2s_base, IMR(ch_reg)); | 
|  | i2s_write_reg(dev->i2s_base, IMR(ch_reg), irq & ~0x30); | 
|  | i2s_write_reg(dev->i2s_base, TER(ch_reg), 1); | 
|  | } else { | 
|  | i2s_write_reg(dev->i2s_base, RCR(ch_reg), | 
|  | xfer_resolution); | 
|  | i2s_write_reg(dev->i2s_base, RFCR(ch_reg), 0x07); | 
|  | irq = i2s_read_reg(dev->i2s_base, IMR(ch_reg)); | 
|  | i2s_write_reg(dev->i2s_base, IMR(ch_reg), irq & ~0x03); | 
|  | i2s_write_reg(dev->i2s_base, RER(ch_reg), 1); | 
|  | } | 
|  | } | 
|  |  | 
|  | i2s_write_reg(dev->i2s_base, CCR, ccr); | 
|  |  | 
|  | config->sample_rate = params_rate(params); | 
|  |  | 
|  | if (!dev->i2s_clk_cfg) | 
|  | return -EINVAL; | 
|  |  | 
|  | ret = dev->i2s_clk_cfg(config); | 
|  | if (ret < 0) { | 
|  | dev_err(dev->dev, "runtime audio clk config fail\n"); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static void dw_i2s_shutdown(struct snd_pcm_substream *substream, | 
|  | struct snd_soc_dai *dai) | 
|  | { | 
|  | snd_soc_dai_set_dma_data(dai, substream, NULL); | 
|  | } | 
|  |  | 
|  | static int dw_i2s_prepare(struct snd_pcm_substream *substream, | 
|  | struct snd_soc_dai *dai) | 
|  | { | 
|  | struct dw_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); | 
|  |  | 
|  | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) | 
|  | i2s_write_reg(dev->i2s_base, TXFFR, 1); | 
|  | else | 
|  | i2s_write_reg(dev->i2s_base, RXFFR, 1); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int dw_i2s_trigger(struct snd_pcm_substream *substream, | 
|  | int cmd, struct snd_soc_dai *dai) | 
|  | { | 
|  | struct dw_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); | 
|  | int ret = 0; | 
|  |  | 
|  | switch (cmd) { | 
|  | case SNDRV_PCM_TRIGGER_START: | 
|  | case SNDRV_PCM_TRIGGER_RESUME: | 
|  | case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: | 
|  | dev->active++; | 
|  | i2s_start(dev, substream); | 
|  | break; | 
|  |  | 
|  | case SNDRV_PCM_TRIGGER_STOP: | 
|  | case SNDRV_PCM_TRIGGER_SUSPEND: | 
|  | case SNDRV_PCM_TRIGGER_PAUSE_PUSH: | 
|  | dev->active--; | 
|  | i2s_stop(dev, substream); | 
|  | break; | 
|  | default: | 
|  | ret = -EINVAL; | 
|  | break; | 
|  | } | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | static struct snd_soc_dai_ops dw_i2s_dai_ops = { | 
|  | .startup	= dw_i2s_startup, | 
|  | .shutdown	= dw_i2s_shutdown, | 
|  | .hw_params	= dw_i2s_hw_params, | 
|  | .prepare	= dw_i2s_prepare, | 
|  | .trigger	= dw_i2s_trigger, | 
|  | }; | 
|  |  | 
|  | static const struct snd_soc_component_driver dw_i2s_component = { | 
|  | .name		= "dw-i2s", | 
|  | }; | 
|  |  | 
|  | #ifdef CONFIG_PM | 
|  |  | 
|  | static int dw_i2s_suspend(struct snd_soc_dai *dai) | 
|  | { | 
|  | struct dw_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); | 
|  |  | 
|  | clk_disable(dev->clk); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int dw_i2s_resume(struct snd_soc_dai *dai) | 
|  | { | 
|  | struct dw_i2s_dev *dev = snd_soc_dai_get_drvdata(dai); | 
|  |  | 
|  | clk_enable(dev->clk); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | #else | 
|  | #define dw_i2s_suspend	NULL | 
|  | #define dw_i2s_resume	NULL | 
|  | #endif | 
|  |  | 
|  | static void dw_configure_dai_by_pd(struct dw_i2s_dev *dev, | 
|  | struct snd_soc_dai_driver *dw_i2s_dai, | 
|  | struct resource *res, | 
|  | const struct i2s_platform_data *pdata) | 
|  | { | 
|  | /* Set DMA slaves info */ | 
|  |  | 
|  | dev->play_dma_data.data = pdata->play_dma_data; | 
|  | dev->capture_dma_data.data = pdata->capture_dma_data; | 
|  | dev->play_dma_data.addr = res->start + I2S_TXDMA; | 
|  | dev->capture_dma_data.addr = res->start + I2S_RXDMA; | 
|  | dev->play_dma_data.max_burst = 16; | 
|  | dev->capture_dma_data.max_burst = 16; | 
|  | dev->play_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; | 
|  | dev->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; | 
|  | dev->play_dma_data.filter = pdata->filter; | 
|  | dev->capture_dma_data.filter = pdata->filter; | 
|  |  | 
|  | if (pdata->cap & DWC_I2S_PLAY) { | 
|  | dev_dbg(dev->dev, " designware: play supported\n"); | 
|  | dw_i2s_dai->playback.channels_min = MIN_CHANNEL_NUM; | 
|  | dw_i2s_dai->playback.channels_max = pdata->channel; | 
|  | dw_i2s_dai->playback.formats = pdata->snd_fmts; | 
|  | dw_i2s_dai->playback.rates = pdata->snd_rates; | 
|  | } | 
|  |  | 
|  | if (pdata->cap & DWC_I2S_RECORD) { | 
|  | dev_dbg(dev->dev, "designware: record supported\n"); | 
|  | dw_i2s_dai->capture.channels_min = MIN_CHANNEL_NUM; | 
|  | dw_i2s_dai->capture.channels_max = pdata->channel; | 
|  | dw_i2s_dai->capture.formats = pdata->snd_fmts; | 
|  | dw_i2s_dai->capture.rates = pdata->snd_rates; | 
|  | } | 
|  | } | 
|  |  | 
|  | static int dw_i2s_probe(struct platform_device *pdev) | 
|  | { | 
|  | const struct i2s_platform_data *pdata = pdev->dev.platform_data; | 
|  | struct dw_i2s_dev *dev; | 
|  | struct resource *res; | 
|  | int ret; | 
|  | struct snd_soc_dai_driver *dw_i2s_dai; | 
|  |  | 
|  | if (!pdata) { | 
|  | dev_err(&pdev->dev, "Invalid platform data\n"); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); | 
|  | if (!dev) { | 
|  | dev_warn(&pdev->dev, "kzalloc fail\n"); | 
|  | return -ENOMEM; | 
|  | } | 
|  |  | 
|  | dw_i2s_dai = devm_kzalloc(&pdev->dev, sizeof(*dw_i2s_dai), GFP_KERNEL); | 
|  | if (!dw_i2s_dai) | 
|  | return -ENOMEM; | 
|  |  | 
|  | dw_i2s_dai->ops = &dw_i2s_dai_ops; | 
|  | dw_i2s_dai->suspend = dw_i2s_suspend; | 
|  | dw_i2s_dai->resume = dw_i2s_resume; | 
|  |  | 
|  | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | 
|  | dev->i2s_base = devm_ioremap_resource(&pdev->dev, res); | 
|  | if (IS_ERR(dev->i2s_base)) | 
|  | return PTR_ERR(dev->i2s_base); | 
|  |  | 
|  | dev->dev = &pdev->dev; | 
|  | dw_configure_dai_by_pd(dev, dw_i2s_dai, res, pdata); | 
|  |  | 
|  | dev->capability = pdata->cap; | 
|  | dev->i2s_clk_cfg = pdata->i2s_clk_cfg; | 
|  | dev->clk = clk_get(&pdev->dev, NULL); | 
|  | if (IS_ERR(dev->clk)) | 
|  | return  PTR_ERR(dev->clk); | 
|  |  | 
|  | ret = clk_enable(dev->clk); | 
|  | if (ret < 0) | 
|  | goto err_clk_put; | 
|  |  | 
|  | dev_set_drvdata(&pdev->dev, dev); | 
|  | ret = snd_soc_register_component(&pdev->dev, &dw_i2s_component, | 
|  | dw_i2s_dai, 1); | 
|  | if (ret != 0) { | 
|  | dev_err(&pdev->dev, "not able to register dai\n"); | 
|  | goto err_clk_disable; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  |  | 
|  | err_clk_disable: | 
|  | clk_disable(dev->clk); | 
|  | err_clk_put: | 
|  | clk_put(dev->clk); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | static int dw_i2s_remove(struct platform_device *pdev) | 
|  | { | 
|  | struct dw_i2s_dev *dev = dev_get_drvdata(&pdev->dev); | 
|  |  | 
|  | snd_soc_unregister_component(&pdev->dev); | 
|  |  | 
|  | clk_put(dev->clk); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static struct platform_driver dw_i2s_driver = { | 
|  | .probe		= dw_i2s_probe, | 
|  | .remove		= dw_i2s_remove, | 
|  | .driver		= { | 
|  | .name	= "designware-i2s", | 
|  | }, | 
|  | }; | 
|  |  | 
|  | module_platform_driver(dw_i2s_driver); | 
|  |  | 
|  | MODULE_AUTHOR("Rajeev Kumar <rajeevkumar.linux@gmail.com>"); | 
|  | MODULE_DESCRIPTION("DESIGNWARE I2S SoC Interface"); | 
|  | MODULE_LICENSE("GPL"); | 
|  | MODULE_ALIAS("platform:designware_i2s"); |