| // SPDX-License-Identifier: GPL-2.0 |
| /* Copyright (c) 2025 Troy Mitchell <troy.mitchell@linux.spacemit.com> */ |
| |
| #include <linux/bitfield.h> |
| #include <linux/clk.h> |
| #include <linux/reset.h> |
| #include <sound/dmaengine_pcm.h> |
| #include <sound/pcm.h> |
| #include <sound/pcm_params.h> |
| |
| #define SSCR 0x00 /* SPI/I2S top control register */ |
| #define SSFCR 0x04 /* SPI/I2S FIFO control register */ |
| #define SSINTEN 0x08 /* SPI/I2S interrupt enable register */ |
| #define SSDATR 0x10 /* SPI/I2S data register */ |
| #define SSPSP 0x18 /* SPI/I2S programmable serial protocol control register */ |
| #define SSRWT 0x24 /* SPI/I2S root control register */ |
| |
| /* SPI/I2S Work data size, register bits value 0~31 indicated data size 1~32 bits */ |
| #define SSCR_FIELD_DSS GENMASK(9, 5) |
| #define SSCR_DW_8BYTE FIELD_PREP(SSCR_FIELD_DSS, 0x7) |
| #define SSCR_DW_16BYTE FIELD_PREP(SSCR_FIELD_DSS, 0xf) |
| #define SSCR_DW_18BYTE FIELD_PREP(SSCR_FIELD_DSS, 0x11) |
| #define SSCR_DW_32BYTE FIELD_PREP(SSCR_FIELD_DSS, 0x1f) |
| |
| #define SSCR_SSE BIT(0) /* SPI/I2S Enable */ |
| #define SSCR_FRF_PSP GENMASK(2, 1) /* Frame Format*/ |
| #define SSCR_TRAIL BIT(13) /* Trailing Byte */ |
| |
| #define SSFCR_FIELD_TFT GENMASK(3, 0) /* TXFIFO Trigger Threshold */ |
| #define SSFCR_FIELD_RFT GENMASK(8, 5) /* RXFIFO Trigger Threshold */ |
| #define SSFCR_TSRE BIT(10) /* Transmit Service Request Enable */ |
| #define SSFCR_RSRE BIT(11) /* Receive Service Request Enable */ |
| |
| #define SSPSP_FSRT BIT(3) /* Frame Sync Relative Timing Bit */ |
| #define SSPSP_SFRMP BIT(4) /* Serial Frame Polarity */ |
| #define SSPSP_FIELD_SFRMWDTH GENMASK(17, 12) /* Serial Frame Width field */ |
| |
| #define SSRWT_RWOT BIT(0) /* Receive Without Transmit */ |
| |
| #define SPACEMIT_PCM_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000 | \ |
| SNDRV_PCM_RATE_48000) |
| #define SPACEMIT_PCM_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S32_LE) |
| |
| #define SPACEMIT_I2S_PERIOD_SIZE 1024 |
| |
| struct spacemit_i2s_dev { |
| struct device *dev; |
| |
| void __iomem *base; |
| |
| struct reset_control *reset; |
| |
| struct clk *sysclk; |
| struct clk *bclk; |
| struct clk *sspa_clk; |
| |
| struct snd_dmaengine_dai_dma_data capture_dma_data; |
| struct snd_dmaengine_dai_dma_data playback_dma_data; |
| |
| bool has_capture; |
| bool has_playback; |
| |
| int dai_fmt; |
| |
| int started_count; |
| }; |
| |
| static const struct snd_pcm_hardware spacemit_pcm_hardware = { |
| .info = SNDRV_PCM_INFO_INTERLEAVED | |
| SNDRV_PCM_INFO_BATCH, |
| .formats = SPACEMIT_PCM_FORMATS, |
| .rates = SPACEMIT_PCM_RATES, |
| .rate_min = SNDRV_PCM_RATE_8000, |
| .rate_max = SNDRV_PCM_RATE_192000, |
| .channels_min = 1, |
| .channels_max = 2, |
| .buffer_bytes_max = SPACEMIT_I2S_PERIOD_SIZE * 4 * 4, |
| .period_bytes_min = SPACEMIT_I2S_PERIOD_SIZE * 2, |
| .period_bytes_max = SPACEMIT_I2S_PERIOD_SIZE * 4, |
| .periods_min = 2, |
| .periods_max = 4, |
| }; |
| |
| static const struct snd_dmaengine_pcm_config spacemit_dmaengine_pcm_config = { |
| .pcm_hardware = &spacemit_pcm_hardware, |
| .prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config, |
| .chan_names = {"tx", "rx"}, |
| .prealloc_buffer_size = 32 * 1024, |
| }; |
| |
| static void spacemit_i2s_init(struct spacemit_i2s_dev *i2s) |
| { |
| u32 sscr_val, sspsp_val, ssfcr_val, ssrwt_val; |
| |
| sscr_val = SSCR_TRAIL | SSCR_FRF_PSP; |
| ssfcr_val = FIELD_PREP(SSFCR_FIELD_TFT, 5) | |
| FIELD_PREP(SSFCR_FIELD_RFT, 5) | |
| SSFCR_RSRE | SSFCR_TSRE; |
| ssrwt_val = SSRWT_RWOT; |
| sspsp_val = SSPSP_SFRMP; |
| |
| writel(sscr_val, i2s->base + SSCR); |
| writel(ssfcr_val, i2s->base + SSFCR); |
| writel(sspsp_val, i2s->base + SSPSP); |
| writel(ssrwt_val, i2s->base + SSRWT); |
| writel(0, i2s->base + SSINTEN); |
| } |
| |
| static int spacemit_i2s_hw_params(struct snd_pcm_substream *substream, |
| struct snd_pcm_hw_params *params, |
| struct snd_soc_dai *dai) |
| { |
| struct spacemit_i2s_dev *i2s = snd_soc_dai_get_drvdata(dai); |
| struct snd_dmaengine_dai_dma_data *dma_data; |
| u32 data_width, data_bits; |
| unsigned long bclk_rate; |
| u32 val; |
| int ret; |
| |
| val = readl(i2s->base + SSCR); |
| if (val & SSCR_SSE) |
| return 0; |
| |
| dma_data = &i2s->playback_dma_data; |
| |
| if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) |
| dma_data = &i2s->capture_dma_data; |
| |
| switch (params_format(params)) { |
| case SNDRV_PCM_FORMAT_S8: |
| data_bits = 8; |
| data_width = SSCR_DW_8BYTE; |
| dma_data->maxburst = 8; |
| dma_data->addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE; |
| break; |
| case SNDRV_PCM_FORMAT_S16_LE: |
| data_bits = 16; |
| data_width = SSCR_DW_16BYTE; |
| dma_data->maxburst = 16; |
| dma_data->addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; |
| break; |
| case SNDRV_PCM_FORMAT_S32_LE: |
| data_bits = 32; |
| data_width = SSCR_DW_32BYTE; |
| dma_data->maxburst = 32; |
| dma_data->addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; |
| break; |
| default: |
| dev_dbg(i2s->dev, "unexpected data width type"); |
| return -EINVAL; |
| } |
| |
| switch (i2s->dai_fmt & SND_SOC_DAIFMT_FORMAT_MASK) { |
| case SND_SOC_DAIFMT_I2S: |
| if (data_bits == 16) { |
| data_width = SSCR_DW_32BYTE; |
| dma_data->maxburst = 32; |
| dma_data->addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; |
| } |
| |
| snd_pcm_hw_constraint_minmax(substream->runtime, |
| SNDRV_PCM_HW_PARAM_CHANNELS, |
| 1, 2); |
| snd_pcm_hw_constraint_mask64(substream->runtime, |
| SNDRV_PCM_HW_PARAM_FORMAT, |
| SNDRV_PCM_FMTBIT_S16_LE); |
| break; |
| case SND_SOC_DAIFMT_DSP_A: |
| case SND_SOC_DAIFMT_DSP_B: |
| snd_pcm_hw_constraint_minmax(substream->runtime, |
| SNDRV_PCM_HW_PARAM_CHANNELS, |
| 1, 1); |
| snd_pcm_hw_constraint_mask64(substream->runtime, |
| SNDRV_PCM_HW_PARAM_FORMAT, |
| SNDRV_PCM_FMTBIT_S32_LE); |
| break; |
| default: |
| dev_dbg(i2s->dev, "unexpected format type"); |
| return -EINVAL; |
| |
| } |
| |
| val = readl(i2s->base + SSCR); |
| val &= ~SSCR_DW_32BYTE; |
| val |= data_width; |
| writel(val, i2s->base + SSCR); |
| |
| bclk_rate = params_channels(params) * |
| params_rate(params) * |
| data_bits; |
| |
| ret = clk_set_rate(i2s->bclk, bclk_rate); |
| if (ret) |
| return ret; |
| |
| return clk_set_rate(i2s->sspa_clk, bclk_rate); |
| } |
| |
| static int spacemit_i2s_set_sysclk(struct snd_soc_dai *cpu_dai, int clk_id, |
| unsigned int freq, int dir) |
| { |
| struct spacemit_i2s_dev *i2s = dev_get_drvdata(cpu_dai->dev); |
| |
| if (freq == 0) |
| return 0; |
| |
| return clk_set_rate(i2s->sysclk, freq); |
| } |
| |
| static int spacemit_i2s_set_fmt(struct snd_soc_dai *cpu_dai, |
| unsigned int fmt) |
| { |
| struct spacemit_i2s_dev *i2s = dev_get_drvdata(cpu_dai->dev); |
| u32 sspsp_val; |
| |
| sspsp_val = readl(i2s->base + SSPSP); |
| sspsp_val &= ~SSPSP_FIELD_SFRMWDTH; |
| sspsp_val |= SSPSP_FSRT; |
| |
| i2s->dai_fmt = fmt; |
| |
| switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { |
| case SND_SOC_DAIFMT_I2S: |
| sspsp_val |= FIELD_PREP(SSPSP_FIELD_SFRMWDTH, 0x10); |
| break; |
| case SND_SOC_DAIFMT_DSP_B: |
| /* DSP_B: next frame asserted after previous frame end, so clear FSRT */ |
| sspsp_val &= ~SSPSP_FSRT; |
| fallthrough; |
| case SND_SOC_DAIFMT_DSP_A: |
| sspsp_val |= FIELD_PREP(SSPSP_FIELD_SFRMWDTH, 0x1); |
| break; |
| default: |
| dev_dbg(i2s->dev, "unexpected format type"); |
| return -EINVAL; |
| } |
| |
| writel(sspsp_val, i2s->base + SSPSP); |
| |
| return 0; |
| } |
| |
| static int spacemit_i2s_trigger(struct snd_pcm_substream *substream, |
| int cmd, struct snd_soc_dai *dai) |
| { |
| struct spacemit_i2s_dev *i2s = snd_soc_dai_get_drvdata(dai); |
| u32 val; |
| |
| switch (cmd) { |
| case SNDRV_PCM_TRIGGER_START: |
| case SNDRV_PCM_TRIGGER_RESUME: |
| case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: |
| if (!i2s->started_count) { |
| val = readl(i2s->base + SSCR); |
| val |= SSCR_SSE; |
| writel(val, i2s->base + SSCR); |
| } |
| i2s->started_count++; |
| break; |
| case SNDRV_PCM_TRIGGER_STOP: |
| case SNDRV_PCM_TRIGGER_SUSPEND: |
| case SNDRV_PCM_TRIGGER_PAUSE_PUSH: |
| if (i2s->started_count) |
| i2s->started_count--; |
| |
| if (!i2s->started_count) { |
| val = readl(i2s->base + SSCR); |
| val &= ~SSCR_SSE; |
| writel(val, i2s->base + SSCR); |
| } |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int spacemit_i2s_dai_probe(struct snd_soc_dai *dai) |
| { |
| struct spacemit_i2s_dev *i2s = snd_soc_dai_get_drvdata(dai); |
| |
| snd_soc_dai_init_dma_data(dai, |
| i2s->has_playback ? &i2s->playback_dma_data : NULL, |
| i2s->has_capture ? &i2s->capture_dma_data : NULL); |
| |
| reset_control_deassert(i2s->reset); |
| |
| spacemit_i2s_init(i2s); |
| |
| return 0; |
| } |
| |
| static int spacemit_i2s_dai_remove(struct snd_soc_dai *dai) |
| { |
| struct spacemit_i2s_dev *i2s = snd_soc_dai_get_drvdata(dai); |
| |
| reset_control_assert(i2s->reset); |
| |
| return 0; |
| } |
| |
| static const struct snd_soc_dai_ops spacemit_i2s_dai_ops = { |
| .probe = spacemit_i2s_dai_probe, |
| .remove = spacemit_i2s_dai_remove, |
| .hw_params = spacemit_i2s_hw_params, |
| .set_sysclk = spacemit_i2s_set_sysclk, |
| .set_fmt = spacemit_i2s_set_fmt, |
| .trigger = spacemit_i2s_trigger, |
| }; |
| |
| static struct snd_soc_dai_driver spacemit_i2s_dai = { |
| .ops = &spacemit_i2s_dai_ops, |
| .playback = { |
| .channels_min = 1, |
| .channels_max = 2, |
| .rates = SPACEMIT_PCM_RATES, |
| .rate_min = SNDRV_PCM_RATE_8000, |
| .rate_max = SNDRV_PCM_RATE_48000, |
| .formats = SPACEMIT_PCM_FORMATS, |
| }, |
| .capture = { |
| .channels_min = 1, |
| .channels_max = 2, |
| .rates = SPACEMIT_PCM_RATES, |
| .rate_min = SNDRV_PCM_RATE_8000, |
| .rate_max = SNDRV_PCM_RATE_48000, |
| .formats = SPACEMIT_PCM_FORMATS, |
| }, |
| .symmetric_rate = 1, |
| }; |
| |
| static int spacemit_i2s_init_dai(struct spacemit_i2s_dev *i2s, |
| struct snd_soc_dai_driver **dp, |
| dma_addr_t addr) |
| { |
| struct device_node *node = i2s->dev->of_node; |
| struct snd_soc_dai_driver *dai; |
| struct property *dma_names; |
| const char *dma_name; |
| |
| of_property_for_each_string(node, "dma-names", dma_names, dma_name) { |
| if (!strcmp(dma_name, "tx")) |
| i2s->has_playback = true; |
| if (!strcmp(dma_name, "rx")) |
| i2s->has_capture = true; |
| } |
| |
| dai = devm_kmemdup(i2s->dev, &spacemit_i2s_dai, |
| sizeof(*dai), GFP_KERNEL); |
| if (!dai) |
| return -ENOMEM; |
| |
| if (i2s->has_playback) { |
| dai->playback.stream_name = "Playback"; |
| dai->playback.channels_min = 1; |
| dai->playback.channels_max = 2; |
| dai->playback.rates = SPACEMIT_PCM_RATES; |
| dai->playback.formats = SPACEMIT_PCM_FORMATS; |
| |
| i2s->playback_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; |
| i2s->playback_dma_data.maxburst = 32; |
| i2s->playback_dma_data.addr = addr; |
| } |
| |
| if (i2s->has_capture) { |
| dai->capture.stream_name = "Capture"; |
| dai->capture.channels_min = 1; |
| dai->capture.channels_max = 2; |
| dai->capture.rates = SPACEMIT_PCM_RATES; |
| dai->capture.formats = SPACEMIT_PCM_FORMATS; |
| |
| i2s->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; |
| i2s->capture_dma_data.maxburst = 32; |
| i2s->capture_dma_data.addr = addr; |
| } |
| |
| if (dp) |
| *dp = dai; |
| |
| return 0; |
| } |
| |
| static const struct snd_soc_component_driver spacemit_i2s_component = { |
| .name = "i2s-k1", |
| .legacy_dai_naming = 1, |
| }; |
| |
| static int spacemit_i2s_probe(struct platform_device *pdev) |
| { |
| struct snd_soc_dai_driver *dai; |
| struct spacemit_i2s_dev *i2s; |
| struct resource *res; |
| struct clk *clk; |
| int ret; |
| |
| i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL); |
| if (!i2s) |
| return -ENOMEM; |
| |
| i2s->dev = &pdev->dev; |
| |
| i2s->sysclk = devm_clk_get_enabled(i2s->dev, "sysclk"); |
| if (IS_ERR(i2s->sysclk)) |
| return dev_err_probe(i2s->dev, PTR_ERR(i2s->sysclk), |
| "failed to enable sysbase clock\n"); |
| |
| i2s->bclk = devm_clk_get_enabled(i2s->dev, "bclk"); |
| if (IS_ERR(i2s->bclk)) |
| return dev_err_probe(i2s->dev, PTR_ERR(i2s->bclk), "failed to enable bit clock\n"); |
| |
| clk = devm_clk_get_enabled(i2s->dev, "sspa_bus"); |
| if (IS_ERR(clk)) |
| return dev_err_probe(i2s->dev, PTR_ERR(clk), "failed to enable sspa_bus clock\n"); |
| |
| i2s->sspa_clk = devm_clk_get_enabled(i2s->dev, "sspa"); |
| if (IS_ERR(i2s->sspa_clk)) |
| return dev_err_probe(i2s->dev, PTR_ERR(i2s->sspa_clk), |
| "failed to enable sspa clock\n"); |
| |
| i2s->base = devm_platform_get_and_ioremap_resource(pdev, 0, &res); |
| if (IS_ERR(i2s->base)) |
| return dev_err_probe(i2s->dev, PTR_ERR(i2s->base), "failed to map registers\n"); |
| |
| i2s->reset = devm_reset_control_get_exclusive(&pdev->dev, NULL); |
| if (IS_ERR(i2s->reset)) |
| return dev_err_probe(i2s->dev, PTR_ERR(i2s->reset), |
| "failed to get reset control"); |
| |
| dev_set_drvdata(i2s->dev, i2s); |
| |
| ret = spacemit_i2s_init_dai(i2s, &dai, res->start + SSDATR); |
| if (ret) |
| return ret; |
| |
| ret = devm_snd_soc_register_component(i2s->dev, |
| &spacemit_i2s_component, |
| dai, 1); |
| if (ret) |
| return dev_err_probe(i2s->dev, ret, "failed to register component"); |
| |
| return devm_snd_dmaengine_pcm_register(&pdev->dev, &spacemit_dmaengine_pcm_config, 0); |
| } |
| |
| static const struct of_device_id spacemit_i2s_of_match[] = { |
| { .compatible = "spacemit,k1-i2s", }, |
| { /* sentinel */ } |
| }; |
| MODULE_DEVICE_TABLE(of, spacemit_i2s_of_match); |
| |
| static struct platform_driver spacemit_i2s_driver = { |
| .probe = spacemit_i2s_probe, |
| .driver = { |
| .name = "i2s-k1", |
| .of_match_table = spacemit_i2s_of_match, |
| }, |
| }; |
| module_platform_driver(spacemit_i2s_driver); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_DESCRIPTION("I2S bus driver for SpacemiT K1 SoC"); |