root/sound/soc/spacemit/k1_i2s.c
// 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");