From ad50e1f63873e5d1f2f421bbd11387a0a1d0ca54 Mon Sep 17 00:00:00 2001 From: "Anton D. Stavinskii" Date: Tue, 20 Jan 2026 23:06:03 +0400 Subject: [PATCH 1/7] ASoC: dt-bindings: sophgo,cv1800b: add I2S/TDM controller There are 4 TDM controllers on the SoC. Each controller can receive or transmit data over DMA. The dma it self has 8 channels. Each channel can be connected only to a specific i2s node. But each of dma channel can have multiple purposes so in order to save dma channels the configurations allows to use tx and rx, only rx, only tx or none channels. I2S controller without channels can be useful in configuration where I2S is used as clock source only and doesn't produce any data. Signed-off-by: Anton D. Stavinskii Reviewed-by: Krzysztof Kozlowski Link: https://patch.msgid.link/20260120-cv1800b-i2s-driver-v4-1-6ef787dc6426@gmail.com Signed-off-by: Mark Brown --- .../bindings/sound/sophgo,cv1800b-i2s.yaml | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/sophgo,cv1800b-i2s.yaml diff --git a/Documentation/devicetree/bindings/sound/sophgo,cv1800b-i2s.yaml b/Documentation/devicetree/bindings/sound/sophgo,cv1800b-i2s.yaml new file mode 100644 index 000000000000..f08362b0ca5e --- /dev/null +++ b/Documentation/devicetree/bindings/sound/sophgo,cv1800b-i2s.yaml @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/sound/sophgo,cv1800b-i2s.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Sophgo CV1800B I2S/TDM controller + +maintainers: + - Anton D. Stavinskii + +description: I2S/TDM controller found in CV1800B / Sophgo SG2002/SG2000 SoCs. + +allOf: + - $ref: dai-common.yaml# + +properties: + compatible: + const: sophgo,cv1800b-i2s + + reg: + maxItems: 1 + + "#sound-dai-cells": + const: 0 + + clocks: + maxItems: 2 + + clock-names: + items: + - const: i2s + - const: mclk + + dmas: + minItems: 1 + maxItems: 2 + + dma-names: + minItems: 1 + items: + - enum: [rx, tx] + - const: tx + +required: + - compatible + - reg + - clocks + - clock-names + - "#sound-dai-cells" + +unevaluatedProperties: false + +examples: + - | + #include + + i2s@4110000 { + compatible = "sophgo,cv1800b-i2s"; + reg = <0x04110000 0x10000>; + clocks = <&clk CLK_APB_I2S1>, <&clk CLK_SDMA_AUD1>; + clock-names = "i2s", "mclk"; + dmas = <&dmamux 2 1>, <&dmamux 3 1>; + dma-names = "rx", "tx"; + #sound-dai-cells = <0>; + }; +... From ea0fb91c02c14748ae525dd547ede7b4a6535d09 Mon Sep 17 00:00:00 2001 From: "Anton D. Stavinskii" Date: Tue, 20 Jan 2026 23:06:04 +0400 Subject: [PATCH 2/7] ASoC: sophgo: add CV1800B I2S/TDM controller driver The actual CPU DAI controller. The driver can be used with simple-audio-card. It respects fixed clock configuration from simple-audio-card. The card driver can request direction out, this will be interpreted as mclk out, the clock which can be used in other CPU or codecs. For example I2S3 generates clock for ADC. I2S was tested in S24_32 and S16 dual channel formats. Signed-off-by: Anton D. Stavinskii Link: https://patch.msgid.link/20260120-cv1800b-i2s-driver-v4-2-6ef787dc6426@gmail.com Signed-off-by: Mark Brown --- sound/soc/Kconfig | 1 + sound/soc/Makefile | 1 + sound/soc/sophgo/Kconfig | 25 ++ sound/soc/sophgo/Makefile | 3 + sound/soc/sophgo/cv1800b-tdm.c | 716 +++++++++++++++++++++++++++++++++ 5 files changed, 746 insertions(+) create mode 100644 sound/soc/sophgo/Kconfig create mode 100644 sound/soc/sophgo/Makefile create mode 100644 sound/soc/sophgo/cv1800b-tdm.c diff --git a/sound/soc/Kconfig b/sound/soc/Kconfig index 36e0d443ba0e..edfdcbf734fe 100644 --- a/sound/soc/Kconfig +++ b/sound/soc/Kconfig @@ -127,6 +127,7 @@ source "sound/soc/renesas/Kconfig" source "sound/soc/rockchip/Kconfig" source "sound/soc/samsung/Kconfig" source "sound/soc/sdca/Kconfig" +source "sound/soc/sophgo/Kconfig" source "sound/soc/spacemit/Kconfig" source "sound/soc/spear/Kconfig" source "sound/soc/sprd/Kconfig" diff --git a/sound/soc/Makefile b/sound/soc/Makefile index 8c0480e6484e..21d8406767fc 100644 --- a/sound/soc/Makefile +++ b/sound/soc/Makefile @@ -70,6 +70,7 @@ obj-$(CONFIG_SND_SOC) += rockchip/ obj-$(CONFIG_SND_SOC) += samsung/ obj-$(CONFIG_SND_SOC) += sdca/ obj-$(CONFIG_SND_SOC) += sof/ +obj-$(CONFIG_SND_SOC) += sophgo/ obj-$(CONFIG_SND_SOC) += spacemit/ obj-$(CONFIG_SND_SOC) += spear/ obj-$(CONFIG_SND_SOC) += sprd/ diff --git a/sound/soc/sophgo/Kconfig b/sound/soc/sophgo/Kconfig new file mode 100644 index 000000000000..9495ab49f042 --- /dev/null +++ b/sound/soc/sophgo/Kconfig @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# SoC audio configuration for cv1800b +# + +menu "Sophgo" + depends on COMPILE_TEST || ARCH_SOPHGO + +config SND_SOC_CV1800B_TDM + tristate "Sophgo CV1800B I2S/TDM support" + depends on SND_SOC && OF + select SND_SOC_GENERIC_DMAENGINE_PCM + help + This option enables the I2S/TDM audio controller found in Sophgo + CV1800B / SG2002 SoCs. The controller supports standard I2S + audio modes for playback and capture. + + The driver integrates with the ASoC framework and uses the DMA + engine for audio data transfer. It is intended to be configured + via Device Tree along with simple-audio-card module. + + To compile the driver as a module, choose M here: the module will + be called cv1800b_tdm. + +endmenu diff --git a/sound/soc/sophgo/Makefile b/sound/soc/sophgo/Makefile new file mode 100644 index 000000000000..3f9f1d07227a --- /dev/null +++ b/sound/soc/sophgo/Makefile @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0 +# Sophgo Platform Support +obj-$(CONFIG_SND_SOC_CV1800B_TDM) += cv1800b-tdm.o diff --git a/sound/soc/sophgo/cv1800b-tdm.c b/sound/soc/sophgo/cv1800b-tdm.c new file mode 100644 index 000000000000..4cbac8c1160f --- /dev/null +++ b/sound/soc/sophgo/cv1800b-tdm.c @@ -0,0 +1,716 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define TX_FIFO_SIZE (1024) +#define RX_FIFO_SIZE (1024) +#define TX_MAX_BURST (8) +#define RX_MAX_BURST (8) + +#define CV1800B_DEF_FREQ 24576000 +#define CV1800B_DEF_MCLK_FS_RATIO 256 + +/* tdm registers */ +#define CV1800B_BLK_MODE_SETTING 0x000 +#define CV1800B_FRAME_SETTING 0x004 +#define CV1800B_SLOT_SETTING1 0x008 +#define CV1800B_SLOT_SETTING2 0x00C +#define CV1800B_DATA_FORMAT 0x010 +#define CV1800B_BLK_CFG 0x014 +#define CV1800B_I2S_ENABLE 0x018 +#define CV1800B_I2S_RESET 0x01C +#define CV1800B_I2S_INT_EN 0x020 +#define CV1800B_I2S_INT 0x024 +#define CV1800B_FIFO_THRESHOLD 0x028 +#define CV1800B_LRCK_MASTER 0x02C /* special clock only mode */ +#define CV1800B_FIFO_RESET 0x030 +#define CV1800B_RX_STATUS 0x040 +#define CV1800B_TX_STATUS 0x048 +#define CV1800B_CLK_CTRL0 0x060 +#define CV1800B_CLK_CTRL1 0x064 +#define CV1800B_PCM_SYNTH 0x068 +#define CV1800B_RX_RD_PORT 0x080 +#define CV1800B_TX_WR_PORT 0x0C0 + +/* CV1800B_BLK_MODE_SETTING (0x000) */ +#define BLK_TX_MODE_MASK GENMASK(0, 0) +#define BLK_MASTER_MODE_MASK GENMASK(1, 1) +#define BLK_DMA_MODE_MASK GENMASK(7, 7) + +/* CV1800B_CLK_CTRL1 (0x064) */ +#define CLK_MCLK_DIV_MASK GENMASK(15, 0) +#define CLK_BCLK_DIV_MASK GENMASK(31, 16) + +/* CV1800B_CLK_CTRL0 (0x060) */ +#define CLK_AUD_CLK_SEL_MASK GENMASK(0, 0) +#define CLK_BCLK_OUT_CLK_FORCE_EN_MASK GENMASK(6, 6) +#define CLK_MCLK_OUT_EN_MASK GENMASK(7, 7) +#define CLK_AUD_EN_MASK GENMASK(8, 8) + +/* CV1800B_I2S_RESET (0x01C) */ +#define RST_I2S_RESET_RX_MASK GENMASK(0, 0) +#define RST_I2S_RESET_TX_MASK GENMASK(1, 1) + +/* CV1800B_FIFO_RESET (0x030) */ +#define FIFO_RX_RESET_MASK GENMASK(0, 0) +#define FIFO_TX_RESET_MASK GENMASK(16, 16) + +/* CV1800B_I2S_ENABLE (0x018) */ +#define I2S_ENABLE_MASK GENMASK(0, 0) + +/* CV1800B_BLK_CFG (0x014) */ +#define BLK_AUTO_DISABLE_WITH_CH_EN_MASK GENMASK(4, 4) +#define BLK_RX_BLK_CLK_FORCE_EN_MASK GENMASK(8, 8) +#define BLK_RX_FIFO_DMA_CLK_FORCE_EN_MASK GENMASK(9, 9) +#define BLK_TX_BLK_CLK_FORCE_EN_MASK GENMASK(16, 16) +#define BLK_TX_FIFO_DMA_CLK_FORCE_EN_MASK GENMASK(17, 17) + +/* CV1800B_FRAME_SETTING (0x004) */ +#define FRAME_LENGTH_MASK GENMASK(8, 0) +#define FS_ACTIVE_LENGTH_MASK GENMASK(23, 16) + +/* CV1800B_I2S_INT_EN (0x020) */ +#define INT_I2S_INT_EN_MASK GENMASK(8, 8) + +/* CV1800B_SLOT_SETTING2 (0x00C) */ +#define SLOT_EN_MASK GENMASK(15, 0) + +/* CV1800B_LRCK_MASTER (0x02C) */ +#define LRCK_MASTER_ENABLE_MASK GENMASK(0, 0) + +/* CV1800B_DATA_FORMAT (0x010) */ +#define DF_WORD_LENGTH_MASK GENMASK(2, 1) +#define DF_TX_SOURCE_LEFT_ALIGN_MASK GENMASK(6, 6) + +/* CV1800B_FIFO_THRESHOLD (0x028) */ +#define FIFO_RX_THRESHOLD_MASK GENMASK(4, 0) +#define FIFO_TX_THRESHOLD_MASK GENMASK(20, 16) +#define FIFO_TX_HIGH_THRESHOLD_MASK GENMASK(28, 24) + +/* CV1800B_SLOT_SETTING1 (0x008) */ +#define SLOT_NUM_MASK GENMASK(3, 0) +#define SLOT_SIZE_MASK GENMASK(13, 8) +#define DATA_SIZE_MASK GENMASK(20, 16) +#define FB_OFFSET_MASK GENMASK(28, 24) + +enum cv1800b_tdm_word_length { + CV1800B_WORD_LENGTH_8_BIT = 0, + CV1800B_WORD_LENGTH_16_BIT = 1, + CV1800B_WORD_LENGTH_32_BIT = 2, +}; + +struct cv1800b_i2s { + void __iomem *base; + struct clk *clk; + struct clk *sysclk; + struct device *dev; + struct snd_dmaengine_dai_dma_data playback_dma; + struct snd_dmaengine_dai_dma_data capture_dma; + u32 mclk_rate; + bool bclk_ratio_fixed; + u32 bclk_ratio; + +}; + +static void cv1800b_setup_dma_struct(struct cv1800b_i2s *i2s, + phys_addr_t phys_base) +{ + i2s->playback_dma.addr = phys_base + CV1800B_TX_WR_PORT; + i2s->playback_dma.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; + i2s->playback_dma.fifo_size = TX_FIFO_SIZE; + i2s->playback_dma.maxburst = TX_MAX_BURST; + + i2s->capture_dma.addr = phys_base + CV1800B_RX_RD_PORT; + i2s->capture_dma.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; + i2s->capture_dma.fifo_size = RX_FIFO_SIZE; + i2s->capture_dma.maxburst = RX_MAX_BURST; +} + +static const struct snd_dmaengine_pcm_config cv1800b_i2s_pcm_config = { + .prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config, +}; + +static void cv1800b_reset_fifo(struct cv1800b_i2s *i2s) +{ + u32 val; + + val = readl(i2s->base + CV1800B_FIFO_RESET); + val = u32_replace_bits(val, 1, FIFO_RX_RESET_MASK); + val = u32_replace_bits(val, 1, FIFO_TX_RESET_MASK); + writel(val, i2s->base + CV1800B_FIFO_RESET); + + usleep_range(10, 20); + + val = readl(i2s->base + CV1800B_FIFO_RESET); + val = u32_replace_bits(val, 0, FIFO_RX_RESET_MASK); + val = u32_replace_bits(val, 0, FIFO_TX_RESET_MASK); + writel(val, i2s->base + CV1800B_FIFO_RESET); +} + +static void cv1800b_reset_i2s(struct cv1800b_i2s *i2s) +{ + u32 val; + + val = readl(i2s->base + CV1800B_I2S_RESET); + val = u32_replace_bits(val, 1, RST_I2S_RESET_RX_MASK); + val = u32_replace_bits(val, 1, RST_I2S_RESET_TX_MASK); + writel(val, i2s->base + CV1800B_I2S_RESET); + + usleep_range(10, 20); + + val = readl(i2s->base + CV1800B_I2S_RESET); + val = u32_replace_bits(val, 0, RST_I2S_RESET_RX_MASK); + val = u32_replace_bits(val, 0, RST_I2S_RESET_TX_MASK); + writel(val, i2s->base + CV1800B_I2S_RESET); +} + +static void cv1800b_set_mclk_div(struct cv1800b_i2s *i2s, u32 mclk_div) +{ + u32 val; + + val = readl(i2s->base + CV1800B_CLK_CTRL1); + val = u32_replace_bits(val, mclk_div, CLK_MCLK_DIV_MASK); + writel(val, i2s->base + CV1800B_CLK_CTRL1); + dev_dbg(i2s->dev, "mclk_div is set to %u\n", mclk_div); +} + +static void cv1800b_set_tx_mode(struct cv1800b_i2s *i2s, bool is_tx) +{ + u32 val; + + val = readl(i2s->base + CV1800B_BLK_MODE_SETTING); + val = u32_replace_bits(val, is_tx, BLK_TX_MODE_MASK); + writel(val, i2s->base + CV1800B_BLK_MODE_SETTING); + dev_dbg(i2s->dev, "tx_mode is set to %u\n", is_tx); +} + +static int cv1800b_set_bclk_div(struct cv1800b_i2s *i2s, u32 bclk_div) +{ + u32 val; + + if (bclk_div == 0 || bclk_div > 0xFFFF) + return -EINVAL; + + val = readl(i2s->base + CV1800B_CLK_CTRL1); + val = u32_replace_bits(val, bclk_div, CLK_BCLK_DIV_MASK); + writel(val, i2s->base + CV1800B_CLK_CTRL1); + dev_dbg(i2s->dev, "bclk_div is set to %u\n", bclk_div); + return 0; +} + +/* set memory width of audio data , reg word_length */ +static int cv1800b_set_word_length(struct cv1800b_i2s *i2s, + unsigned int physical_width) +{ + u8 word_length_val; + u32 val; + + switch (physical_width) { + case 8: + word_length_val = CV1800B_WORD_LENGTH_8_BIT; + break; + case 16: + word_length_val = CV1800B_WORD_LENGTH_16_BIT; + break; + case 32: + word_length_val = CV1800B_WORD_LENGTH_32_BIT; + break; + default: + dev_dbg(i2s->dev, "can't set word_length field\n"); + return -EINVAL; + } + + val = readl(i2s->base + CV1800B_DATA_FORMAT); + val = u32_replace_bits(val, word_length_val, DF_WORD_LENGTH_MASK); + writel(val, i2s->base + CV1800B_DATA_FORMAT); + return 0; +} + +static void cv1800b_enable_clocks(struct cv1800b_i2s *i2s, bool enabled) +{ + u32 val; + + val = readl(i2s->base + CV1800B_CLK_CTRL0); + val = u32_replace_bits(val, enabled, CLK_AUD_EN_MASK); + writel(val, i2s->base + CV1800B_CLK_CTRL0); +} + +static int cv1800b_set_slot_settings(struct cv1800b_i2s *i2s, u32 slots, + u32 physical_width, u32 data_size) +{ + u32 slot_num; + u32 slot_size; + u32 frame_length; + u32 frame_active_length; + u32 val; + + if (!slots || !physical_width || !data_size) { + dev_err(i2s->dev, "frame or slot settings are not valid\n"); + return -EINVAL; + } + if (slots > 16 || physical_width > 64 || data_size > 32) { + dev_err(i2s->dev, "frame or slot settings are not valid\n"); + return -EINVAL; + } + + slot_num = slots - 1; + slot_size = physical_width - 1; + frame_length = (physical_width * slots) - 1; + frame_active_length = physical_width - 1; + + if (frame_length > 511 || frame_active_length > 255) { + dev_err(i2s->dev, "frame or slot settings are not valid\n"); + return -EINVAL; + } + + val = readl(i2s->base + CV1800B_SLOT_SETTING1); + val = u32_replace_bits(val, slot_size, SLOT_SIZE_MASK); + val = u32_replace_bits(val, data_size - 1, DATA_SIZE_MASK); + val = u32_replace_bits(val, slot_num, SLOT_NUM_MASK); + writel(val, i2s->base + CV1800B_SLOT_SETTING1); + + val = readl(i2s->base + CV1800B_FRAME_SETTING); + val = u32_replace_bits(val, frame_length, FRAME_LENGTH_MASK); + val = u32_replace_bits(val, frame_active_length, FS_ACTIVE_LENGTH_MASK); + writel(val, i2s->base + CV1800B_FRAME_SETTING); + + dev_dbg(i2s->dev, "slot settings num: %u width: %u\n", slots, physical_width); + return 0; +} + +/* + * calculate mclk_div. + * if requested value is bigger than optimal + * leave mclk_div as 1. cff clock is capable + * to handle it + */ +static int cv1800b_calc_mclk_div(unsigned int target_mclk, u32 *mclk_div) +{ + *mclk_div = 1; + + if (target_mclk == 0) + return -EINVAL; + + /* optimal parent frequency is close to CV1800B_DEF_FREQ */ + if (target_mclk < CV1800B_DEF_FREQ) { + *mclk_div = DIV_ROUND_CLOSEST(CV1800B_DEF_FREQ, target_mclk); + if (!*mclk_div || *mclk_div > 0xFFFF) + return -EINVAL; + } + return 0; +} + +/* + * set CCF clock and divider for this clock + * mclk_clock = ccf_clock / mclk_div + */ +static int cv1800b_i2s_set_rate_for_mclk(struct cv1800b_i2s *i2s, + unsigned int target_mclk) +{ + u32 mclk_div = 1; + u64 tmp; + int ret; + unsigned long clk_rate; + unsigned long actual; + + ret = cv1800b_calc_mclk_div(target_mclk, &mclk_div); + if (ret) { + dev_dbg(i2s->dev, "can't calc mclk_div for freq %u\n", + target_mclk); + return ret; + } + + tmp = (u64)target_mclk * mclk_div; + if (tmp > ULONG_MAX) { + dev_err(i2s->dev, "clk_rate overflow: freq=%u div=%u\n", + target_mclk, mclk_div); + return -ERANGE; + } + + clk_rate = (unsigned long)tmp; + + cv1800b_enable_clocks(i2s, false); + + ret = clk_set_rate(i2s->sysclk, clk_rate); + if (ret) + return ret; + + actual = clk_get_rate(i2s->sysclk); + if (clk_rate != actual) { + dev_err_ratelimited(i2s->dev, + "clk_set_rate failed %lu, actual is %lu\n", + clk_rate, actual); + } + + cv1800b_set_mclk_div(i2s, mclk_div); + cv1800b_enable_clocks(i2s, true); + + return 0; +} + +static int cv1800b_i2s_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct cv1800b_i2s *i2s = snd_soc_dai_get_drvdata(dai); + unsigned int rate = params_rate(params); + unsigned int channels = params_channels(params); + unsigned int physical_width = params_physical_width(params); + int data_width = params_width(params); + bool tx_mode = (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) ? 1 : 0; + int ret; + u32 bclk_div; + u32 bclk_ratio; + u32 mclk_rate; + u32 tmp; + + if (data_width < 0) + return data_width; + + if (!channels || !rate || !physical_width) + return -EINVAL; + + ret = cv1800b_set_slot_settings(i2s, channels, physical_width, data_width); + if (ret) + return ret; + + if (i2s->mclk_rate) { + mclk_rate = i2s->mclk_rate; + } else { + dev_dbg(i2s->dev, "mclk is not set by machine driver\n"); + ret = cv1800b_i2s_set_rate_for_mclk(i2s, + rate * CV1800B_DEF_MCLK_FS_RATIO); + if (ret) + return ret; + mclk_rate = rate * CV1800B_DEF_MCLK_FS_RATIO; + } + + bclk_ratio = (i2s->bclk_ratio_fixed) ? i2s->bclk_ratio : + (physical_width * channels); + + if (check_mul_overflow(rate, bclk_ratio, &tmp)) + return -EOVERFLOW; + + if (!tmp) + return -EINVAL; + if (mclk_rate % tmp) + dev_warn(i2s->dev, "mclk rate is not aligned to bclk or rate\n"); + + bclk_div = DIV_ROUND_CLOSEST(mclk_rate, tmp); + + ret = cv1800b_set_bclk_div(i2s, bclk_div); + if (ret) + return ret; + + ret = cv1800b_set_word_length(i2s, physical_width); + if (ret) + return ret; + + cv1800b_set_tx_mode(i2s, tx_mode); + + cv1800b_reset_fifo(i2s); + cv1800b_reset_i2s(i2s); + return 0; +} + +static int cv1800b_i2s_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct cv1800b_i2s *i2s = snd_soc_dai_get_drvdata(dai); + u32 val; + + val = readl(i2s->base + CV1800B_I2S_ENABLE); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + val = u32_replace_bits(val, 1, I2S_ENABLE_MASK); + break; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + val = u32_replace_bits(val, 0, I2S_ENABLE_MASK); + break; + default: + return -EINVAL; + } + writel(val, i2s->base + CV1800B_I2S_ENABLE); + return 0; +} + +static int cv1800b_i2s_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); + struct cv1800b_i2s *i2s = snd_soc_dai_get_drvdata(dai); + struct snd_soc_dai_link *dai_link = rtd->dai_link; + + dev_dbg(i2s->dev, "%s: dai=%s substream=%d\n", __func__, dai->name, + substream->stream); + /** + * Ensure DMA is stopped before DAI + * shutdown (prevents DW AXI DMAC stop/busy on next open). + */ + dai_link->trigger_stop = SND_SOC_TRIGGER_ORDER_LDC; + return 0; +} + +static int cv1800b_i2s_dai_probe(struct snd_soc_dai *dai) +{ + struct cv1800b_i2s *i2s = snd_soc_dai_get_drvdata(dai); + + if (!i2s) { + dev_err(dai->dev, "no drvdata in DAI probe\n"); + return -ENODEV; + } + + snd_soc_dai_init_dma_data(dai, &i2s->playback_dma, &i2s->capture_dma); + return 0; +} + +static int cv1800b_i2s_dai_set_fmt(struct snd_soc_dai *dai, unsigned int fmt) +{ + struct cv1800b_i2s *i2s = snd_soc_dai_get_drvdata(dai); + u32 val; + u32 master; + + /* only i2s format is supported */ + if ((fmt & SND_SOC_DAIFMT_FORMAT_MASK) != SND_SOC_DAIFMT_I2S) + return -EINVAL; + + switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { + case SND_SOC_DAIFMT_CBP_CFP: + dev_dbg(i2s->dev, "set to master mode\n"); + master = 1; + break; + + case SND_SOC_DAIFMT_CBC_CFC: + dev_dbg(i2s->dev, "set to slave mode\n"); + master = 0; + break; + default: + return -EINVAL; + } + + val = readl(i2s->base + CV1800B_BLK_MODE_SETTING); + val = u32_replace_bits(val, master, BLK_MASTER_MODE_MASK); + writel(val, i2s->base + CV1800B_BLK_MODE_SETTING); + return 0; +} + +static int cv1800b_i2s_dai_set_bclk_ratio(struct snd_soc_dai *dai, + unsigned int ratio) +{ + struct cv1800b_i2s *i2s = snd_soc_dai_get_drvdata(dai); + + if (ratio == 0) + return -EINVAL; + i2s->bclk_ratio = ratio; + i2s->bclk_ratio_fixed = true; + return 0; +} + +static int cv1800b_i2s_dai_set_sysclk(struct snd_soc_dai *dai, int clk_id, + unsigned int freq, int dir) +{ + struct cv1800b_i2s *i2s = snd_soc_dai_get_drvdata(dai); + int ret; + u32 val; + bool output_enable = (dir == SND_SOC_CLOCK_OUT) ? true : false; + + dev_dbg(i2s->dev, "%s called with %u\n", __func__, freq); + ret = cv1800b_i2s_set_rate_for_mclk(i2s, freq); + if (ret) + return ret; + + val = readl(i2s->base + CV1800B_CLK_CTRL0); + val = u32_replace_bits(val, output_enable, CLK_MCLK_OUT_EN_MASK); + writel(val, i2s->base + CV1800B_CLK_CTRL0); + + i2s->mclk_rate = freq; + return 0; +} + +static const struct snd_soc_dai_ops cv1800b_i2s_dai_ops = { + .probe = cv1800b_i2s_dai_probe, + .startup = cv1800b_i2s_startup, + .hw_params = cv1800b_i2s_hw_params, + .trigger = cv1800b_i2s_trigger, + .set_fmt = cv1800b_i2s_dai_set_fmt, + .set_bclk_ratio = cv1800b_i2s_dai_set_bclk_ratio, + .set_sysclk = cv1800b_i2s_dai_set_sysclk, +}; + +static const struct snd_soc_dai_driver cv1800b_i2s_dai_template = { + .name = "cv1800b-i2s", + .playback = { + .stream_name = "Playback", + .channels_min = 1, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_8000_192000, + .formats = SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S16_LE, + }, + .capture = { + .stream_name = "Capture", + .channels_min = 1, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_8000_192000, + .formats = SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S16_LE, + }, + .ops = &cv1800b_i2s_dai_ops, +}; + +static const struct snd_soc_component_driver cv1800b_i2s_component = { + .name = "cv1800b-i2s", +}; + +static void cv1800b_i2s_hw_disable(struct cv1800b_i2s *i2s) +{ + u32 val; + + val = readl(i2s->base + CV1800B_I2S_ENABLE); + val = u32_replace_bits(val, 0, I2S_ENABLE_MASK); + writel(val, i2s->base + CV1800B_I2S_ENABLE); + + val = readl(i2s->base + CV1800B_CLK_CTRL0); + val = u32_replace_bits(val, 0, CLK_AUD_EN_MASK); + val = u32_replace_bits(val, 0, CLK_MCLK_OUT_EN_MASK); + writel(val, i2s->base + CV1800B_CLK_CTRL0); + + val = readl(i2s->base + CV1800B_I2S_RESET); + val = u32_replace_bits(val, 1, RST_I2S_RESET_RX_MASK); + val = u32_replace_bits(val, 1, RST_I2S_RESET_TX_MASK); + writel(val, i2s->base + CV1800B_I2S_RESET); + + val = readl(i2s->base + CV1800B_FIFO_RESET); + val = u32_replace_bits(val, 1, FIFO_RX_RESET_MASK); + val = u32_replace_bits(val, 1, FIFO_TX_RESET_MASK); + writel(val, i2s->base + CV1800B_FIFO_RESET); +} + +static void cv1800b_i2s_setup_tdm(struct cv1800b_i2s *i2s) +{ + u32 val; + + val = readl(i2s->base + CV1800B_BLK_MODE_SETTING); + val = u32_replace_bits(val, 1, BLK_DMA_MODE_MASK); + writel(val, i2s->base + CV1800B_BLK_MODE_SETTING); + + val = readl(i2s->base + CV1800B_CLK_CTRL0); + val = u32_replace_bits(val, 0, CLK_AUD_CLK_SEL_MASK); + val = u32_replace_bits(val, 0, CLK_MCLK_OUT_EN_MASK); + val = u32_replace_bits(val, 0, CLK_AUD_EN_MASK); + writel(val, i2s->base + CV1800B_CLK_CTRL0); + + val = readl(i2s->base + CV1800B_FIFO_THRESHOLD); + val = u32_replace_bits(val, 4, FIFO_RX_THRESHOLD_MASK); + val = u32_replace_bits(val, 4, FIFO_TX_THRESHOLD_MASK); + val = u32_replace_bits(val, 4, FIFO_TX_HIGH_THRESHOLD_MASK); + writel(val, i2s->base + CV1800B_FIFO_THRESHOLD); + + val = readl(i2s->base + CV1800B_I2S_ENABLE); + val = u32_replace_bits(val, 0, I2S_ENABLE_MASK); + writel(val, i2s->base + CV1800B_I2S_ENABLE); +} + +static int cv1800b_i2s_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct cv1800b_i2s *i2s; + struct resource *res; + void __iomem *regs; + struct snd_soc_dai_driver *dai; + int ret; + + i2s = devm_kzalloc(dev, sizeof(*i2s), GFP_KERNEL); + if (!i2s) + return -ENOMEM; + + regs = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(regs)) + return PTR_ERR(regs); + i2s->dev = &pdev->dev; + i2s->base = regs; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -ENODEV; + cv1800b_setup_dma_struct(i2s, res->start); + + i2s->clk = devm_clk_get_enabled(dev, "i2s"); + if (IS_ERR(i2s->clk)) + return dev_err_probe(dev, PTR_ERR(i2s->clk), + "failed to get+enable i2s\n"); + i2s->sysclk = devm_clk_get_enabled(dev, "mclk"); + if (IS_ERR(i2s->sysclk)) + return dev_err_probe(dev, PTR_ERR(i2s->sysclk), + "failed to get+enable mclk\n"); + + platform_set_drvdata(pdev, i2s); + cv1800b_i2s_setup_tdm(i2s); + + dai = devm_kmemdup(dev, &cv1800b_i2s_dai_template, sizeof(*dai), + GFP_KERNEL); + if (!dai) + return -ENOMEM; + + ret = devm_snd_soc_register_component(dev, &cv1800b_i2s_component, dai, + 1); + if (ret) + return ret; + + ret = devm_snd_dmaengine_pcm_register(dev, &cv1800b_i2s_pcm_config, 0); + if (ret) { + dev_err(dev, "dmaengine_pcm_register failed: %d\n", ret); + return ret; + } + + return 0; +} + +static void cv1800b_i2s_remove(struct platform_device *pdev) +{ + struct cv1800b_i2s *i2s = platform_get_drvdata(pdev); + + if (!i2s) + return; + cv1800b_i2s_hw_disable(i2s); +} + +static const struct of_device_id cv1800b_i2s_of_match[] = { + { .compatible = "sophgo,cv1800b-i2s" }, + { /* sentinel */ } +}; + +MODULE_DEVICE_TABLE(of, cv1800b_i2s_of_match); + +static struct platform_driver cv1800b_i2s_driver = { + .probe = cv1800b_i2s_probe, + .remove = cv1800b_i2s_remove, + .driver = { + .name = "cv1800b-i2s", + .of_match_table = cv1800b_i2s_of_match, + }, +}; +module_platform_driver(cv1800b_i2s_driver); + +MODULE_DESCRIPTION("Sophgo cv1800b I2S/TDM driver"); +MODULE_AUTHOR("Anton D. Stavinsky "); +MODULE_LICENSE("GPL"); From c294aafe474bbbd7a7476773f56f6191742a39e1 Mon Sep 17 00:00:00 2001 From: "Anton D. Stavinskii" Date: Tue, 20 Jan 2026 23:06:05 +0400 Subject: [PATCH 3/7] ASoC: dt-bindings: sophgo,cv1800b: add ADC/DAC codec Document the internal ADC and DAC audio codecs integrated in the Sophgo CV1800B SoC. Signed-off-by: Anton D. Stavinskii Reviewed-by: Rob Herring (Arm) Link: https://patch.msgid.link/20260120-cv1800b-i2s-driver-v4-3-6ef787dc6426@gmail.com Signed-off-by: Mark Brown --- .../bindings/sound/sophgo,cv1800b-codecs.yaml | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/sophgo,cv1800b-codecs.yaml diff --git a/Documentation/devicetree/bindings/sound/sophgo,cv1800b-codecs.yaml b/Documentation/devicetree/bindings/sound/sophgo,cv1800b-codecs.yaml new file mode 100644 index 000000000000..7293a98e98c5 --- /dev/null +++ b/Documentation/devicetree/bindings/sound/sophgo,cv1800b-codecs.yaml @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/sound/sophgo,cv1800b-codecs.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Sophgo CV1800B Internal ADC/DAC Codec + +maintainers: + - Anton D. Stavinskii + +description: + Internal ADC and DAC audio codecs integrated in the Sophgo CV1800B SoC. + Codecs expose a single DAI and are intended to be connected + to an I2S/TDM controller via an ASoC machine driver. + +allOf: + - $ref: dai-common.yaml# + +properties: + compatible: + enum: + - sophgo,cv1800b-sound-adc + - sophgo,cv1800b-sound-dac + + reg: + maxItems: 1 + + "#sound-dai-cells": + const: 0 + +required: + - compatible + - reg + - "#sound-dai-cells" + +unevaluatedProperties: false + +examples: + - | + audio-codec@300a100 { + compatible = "sophgo,cv1800b-sound-adc"; + reg = <0x0300a100 0x100>; + #sound-dai-cells = <0>; + }; +... From 4cf8752a03e67b2927d137a47c4eca4d516b4838 Mon Sep 17 00:00:00 2001 From: "Anton D. Stavinskii" Date: Tue, 20 Jan 2026 23:06:06 +0400 Subject: [PATCH 4/7] ASoC: sophgo: add CV1800B internal ADC codec driver Codec DAI endpoint for RXADC + basic controls. THe codec have basic volume control. Which is imlemented by lookup table for simplicity. The codec expects set_sysclk callback to adjust internal mclk divider. Signed-off-by: Anton D. Stavinskii Link: https://patch.msgid.link/20260120-cv1800b-i2s-driver-v4-4-6ef787dc6426@gmail.com Signed-off-by: Mark Brown --- sound/soc/sophgo/Kconfig | 12 + sound/soc/sophgo/Makefile | 1 + sound/soc/sophgo/cv1800b-sound-adc.c | 322 +++++++++++++++++++++++++++ 3 files changed, 335 insertions(+) create mode 100644 sound/soc/sophgo/cv1800b-sound-adc.c diff --git a/sound/soc/sophgo/Kconfig b/sound/soc/sophgo/Kconfig index 9495ab49f042..12d1a57ea308 100644 --- a/sound/soc/sophgo/Kconfig +++ b/sound/soc/sophgo/Kconfig @@ -22,4 +22,16 @@ config SND_SOC_CV1800B_TDM To compile the driver as a module, choose M here: the module will be called cv1800b_tdm. +config SND_SOC_CV1800B_ADC_CODEC + tristate "Sophgo CV1800B/SG2002 internal ADC codec" + depends on SND_SOC + help + This driver provides an ASoC codec DAI for capture and basic + control of the RXADC registers. + + Say Y or M to build support for the Sophgo CV1800B + internal analog ADC codec block (RXADC). + The module will be called cv1800b-sound-adc + + endmenu diff --git a/sound/soc/sophgo/Makefile b/sound/soc/sophgo/Makefile index 3f9f1d07227a..c654d6059cbd 100644 --- a/sound/soc/sophgo/Makefile +++ b/sound/soc/sophgo/Makefile @@ -1,3 +1,4 @@ # SPDX-License-Identifier: GPL-2.0 # Sophgo Platform Support obj-$(CONFIG_SND_SOC_CV1800B_TDM) += cv1800b-tdm.o +obj-$(CONFIG_SND_SOC_CV1800B_ADC_CODEC) += cv1800b-sound-adc.o diff --git a/sound/soc/sophgo/cv1800b-sound-adc.c b/sound/soc/sophgo/cv1800b-sound-adc.c new file mode 100644 index 000000000000..794030b713e9 --- /dev/null +++ b/sound/soc/sophgo/cv1800b-sound-adc.c @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Internal adc codec for cv1800b compatible SoC + * + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CV1800B_RXADC_WORD_LEN 16 +#define CV1800B_RXADC_CHANNELS 2 + +#define CV1800B_RXADC_CTRL0 0x00 +#define CV1800B_RXADCC_CTRL1 0x04 +#define CV1800B_RXADC_STATUS 0x08 +#define CV1800B_RXADC_CLK 0x0c +#define CV1800B_RXADC_ANA0 0x10 +#define CV1800B_RXADC_ANA1 0x14 +#define CV1800B_RXADC_ANA2 0x18 +#define CV1800B_RXADC_ANA3 0x1c +#define CV1800B_RXADC_ANA4 0x20 + +/* CV1800B_RXADC_CTRL0 */ +#define REG_RXADC_EN GENMASK(0, 0) +#define REG_I2S_TX_EN GENMASK(1, 1) + +/* CV1800B_RXADCC_CTRL1 */ +#define REG_RXADC_CIC_OPT GENMASK(1, 0) +#define REG_RXADC_IGR_INIT GENMASK(8, 8) + +/* CV1800B_RXADC_ANA0 */ +#define REG_GSTEPL_RXPGA GENMASK(12, 0) +#define REG_G6DBL_RXPGA GENMASK(13, 13) +#define REG_GAINL_RXADC GENMASK(15, 14) +#define REG_GSTEPR_RXPGA GENMASK(28, 16) +#define REG_G6DBR_RXPGA GENMASK(29, 29) +#define REG_GAINR_RXADC GENMASK(31, 30) +#define REG_COMB_LEFT_VOLUME GENMASK(15, 0) +#define REG_COMB_RIGHT_VOLUME GENMASK(31, 16) + +/* CV1800B_RXADC_ANA2 */ +#define REG_MUTEL_RXPGA GENMASK(0, 0) +#define REG_MUTER_RXPGA GENMASK(1, 1) + +/* CV1800B_RXADC_CLK */ +#define REG_RXADC_CLK_INV GENMASK(0, 0) +#define REG_RXADC_SCK_DIV GENMASK(15, 8) +#define REG_RXADC_DLYEN GENMASK(23, 16) + +enum decimation_values { + DECIMATION_64 = 0, + DECIMATION_128, + DECIMATION_256, + DECIMATION_512, +}; + +static const u32 cv1800b_gains[] = { + 0x0001, /* 0dB */ + 0x0002, /* 2dB */ + 0x0004, /* 4dB */ + 0x0008, /* 6dB */ + 0x0010, /* 8dB */ + 0x0020, /* 10dB */ + 0x0040, /* 12dB */ + 0x0080, /* 14dB */ + 0x0100, /* 16dB */ + 0x0200, /* 18dB */ + 0x0400, /* 20dB */ + 0x0800, /* 22dB */ + 0x1000, /* 24dB */ + 0x2400, /* 26dB */ + 0x2800, /* 28dB */ + 0x3000, /* 30dB */ + 0x6400, /* 32dB */ + 0x6800, /* 34dB */ + 0x7000, /* 36dB */ + 0xA400, /* 38dB */ + 0xA800, /* 40dB */ + 0xB000, /* 42dB */ + 0xE400, /* 44dB */ + 0xE800, /* 46dB */ + 0xF000, /* 48dB */ +}; + +struct cv1800b_priv { + void __iomem *regs; + struct device *dev; + unsigned int mclk_rate; +}; + +static int cv1800b_adc_setbclk_div(struct cv1800b_priv *priv, unsigned int rate) +{ + u32 val; + u32 bclk_div; + u64 tmp; + + if (!priv->mclk_rate || !rate) + return -EINVAL; + + tmp = priv->mclk_rate; + tmp /= CV1800B_RXADC_WORD_LEN; + tmp /= CV1800B_RXADC_CHANNELS; + tmp /= rate; + tmp /= 2; + + if (!tmp) { + dev_err(priv->dev, "computed BCLK divider is zero\n"); + return -EINVAL; + } + + if (tmp > 256) { + dev_err(priv->dev, "BCLK divider %llu out of range\n", tmp); + return -EINVAL; + } + + bclk_div = tmp - 1; + val = readl(priv->regs + CV1800B_RXADC_CLK); + val = u32_replace_bits(val, bclk_div, REG_RXADC_SCK_DIV); + /* Vendor value for 48kHz, tested on SG2000/SG2002 */ + val = u32_replace_bits(val, 0x19, REG_RXADC_DLYEN); + writel(val, priv->regs + CV1800B_RXADC_CLK); + + return 0; +} + +static void cv1800b_adc_enable(struct cv1800b_priv *priv, bool enable) +{ + u32 val; + + val = readl(priv->regs + CV1800B_RXADC_CTRL0); + val = u32_replace_bits(val, enable, REG_RXADC_EN); + val = u32_replace_bits(val, enable, REG_I2S_TX_EN); + writel(val, priv->regs + CV1800B_RXADC_CTRL0); +} + +static unsigned int cv1800b_adc_calc_db(u32 ana0, bool right) +{ + u32 step_mask = right ? FIELD_GET(REG_GSTEPR_RXPGA, ana0) : + FIELD_GET(REG_GSTEPL_RXPGA, ana0); + u32 coarse = right ? FIELD_GET(REG_GAINR_RXADC, ana0) : + FIELD_GET(REG_GAINL_RXADC, ana0); + bool g6db = right ? FIELD_GET(REG_G6DBR_RXPGA, ana0) : + FIELD_GET(REG_G6DBL_RXPGA, ana0); + + u32 step = step_mask ? __ffs(step_mask) : 0; + + step = min(step, 12U); + coarse = min(coarse, 3U); + + return 2 * step + 6 * coarse + (g6db ? 6 : 0); +} + +static int cv1800b_adc_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct cv1800b_priv *priv = snd_soc_dai_get_drvdata(dai); + unsigned int rate = params_rate(params); + u32 val; + int ret; + + ret = cv1800b_adc_setbclk_div(priv, rate); + if (ret) { + dev_err(priv->dev, + "could not set rate, check DT node for fixed clock\n"); + return ret; + } + + /* init adc */ + val = readl(priv->regs + CV1800B_RXADCC_CTRL1); + val = u32_replace_bits(val, 1, REG_RXADC_IGR_INIT); + val = u32_replace_bits(val, DECIMATION_64, REG_RXADC_CIC_OPT); + writel(val, priv->regs + CV1800B_RXADCC_CTRL1); + return 0; +} + +static int cv1800b_adc_dai_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct cv1800b_priv *priv = snd_soc_dai_get_drvdata(dai); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + cv1800b_adc_enable(priv, true); + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + cv1800b_adc_enable(priv, false); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int cv1800b_adc_dai_set_sysclk(struct snd_soc_dai *dai, int clk_id, + unsigned int freq, int dir) +{ + struct cv1800b_priv *priv = snd_soc_dai_get_drvdata(dai); + + priv->mclk_rate = freq; + dev_dbg(priv->dev, "mclk is set to %u\n", freq); + return 0; +} + +static const struct snd_soc_dai_ops cv1800b_adc_dai_ops = { + .hw_params = cv1800b_adc_hw_params, + .set_sysclk = cv1800b_adc_dai_set_sysclk, + .trigger = cv1800b_adc_dai_trigger, +}; + +static struct snd_soc_dai_driver cv1800b_adc_dai = { + .name = "adc-hifi", + .capture = { .stream_name = "ADC Capture", + .channels_min = 1, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_48000, + .formats = SNDRV_PCM_FMTBIT_S16_LE }, + .ops = &cv1800b_adc_dai_ops, +}; + +static int cv1800b_adc_volume_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); + struct cv1800b_priv *priv = snd_soc_component_get_drvdata(component); + u32 ana0 = readl(priv->regs + CV1800B_RXADC_ANA0); + + unsigned int left = cv1800b_adc_calc_db(ana0, false); + unsigned int right = cv1800b_adc_calc_db(ana0, true); + + ucontrol->value.integer.value[0] = min(left / 2, 24U); + ucontrol->value.integer.value[1] = min(right / 2, 24U); + return 0; +} + +static int cv1800b_adc_volume_set(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); + struct cv1800b_priv *priv = snd_soc_component_get_drvdata(component); + + u32 v_left = clamp_t(u32, ucontrol->value.integer.value[0], 0, 24); + u32 v_right = clamp_t(u32, ucontrol->value.integer.value[1], 0, 24); + u32 val; + + val = readl(priv->regs + CV1800B_RXADC_ANA0); + val = u32_replace_bits(val, cv1800b_gains[v_left], + REG_COMB_LEFT_VOLUME); + val = u32_replace_bits(val, cv1800b_gains[v_right], + REG_COMB_RIGHT_VOLUME); + writel(val, priv->regs + CV1800B_RXADC_ANA0); + + return 0; +} + +static DECLARE_TLV_DB_SCALE(cv1800b_volume_tlv, 0, 200, 0); + +static const struct snd_kcontrol_new cv1800b_adc_controls[] = { + SOC_DOUBLE_EXT_TLV("Internal I2S Capture Volume", SND_SOC_NOPM, 0, 16, 24, false, + cv1800b_adc_volume_get, cv1800b_adc_volume_set, + cv1800b_volume_tlv), +}; + +static const struct snd_soc_component_driver cv1800b_adc_component = { + .name = "cv1800b-adc-codec", + .controls = cv1800b_adc_controls, + .num_controls = ARRAY_SIZE(cv1800b_adc_controls), +}; + +static int cv1800b_adc_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct cv1800b_priv *priv; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->dev = dev; + priv->regs = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(priv->regs)) + return PTR_ERR(priv->regs); + + platform_set_drvdata(pdev, priv); + return devm_snd_soc_register_component(&pdev->dev, + &cv1800b_adc_component, + &cv1800b_adc_dai, 1); +} + +static const struct of_device_id cv1800b_adc_of_match[] = { + { .compatible = "sophgo,cv1800b-sound-adc" }, + { /* sentinel */ } +}; + +MODULE_DEVICE_TABLE(of, cv1800b_adc_of_match); + +static struct platform_driver cv1800b_adc_driver = { + .probe = cv1800b_adc_probe, + .driver = { + .name = "cv1800b-sound-adc", + .of_match_table = cv1800b_adc_of_match, + }, +}; + +module_platform_driver(cv1800b_adc_driver); + +MODULE_DESCRIPTION("ADC codec for CV1800B"); +MODULE_AUTHOR("Anton D. Stavinskii "); +MODULE_LICENSE("GPL"); From b3eb755e2db07d85c30e8ff4043ffb9a14b4ece7 Mon Sep 17 00:00:00 2001 From: "Anton D. Stavinskii" Date: Tue, 20 Jan 2026 23:06:07 +0400 Subject: [PATCH 5/7] ASoC: sophgo: add CV1800B internal DAC codec driver Codec DAI endpoint for TXDAC. The codec does only a few things - set up decimation - enable codec and I2S output - ensures the driver doesn't have dac overwrite enabled. (unmute the output) Signed-off-by: Anton D. Stavinskii Link: https://patch.msgid.link/20260120-cv1800b-i2s-driver-v4-5-6ef787dc6426@gmail.com Signed-off-by: Mark Brown --- sound/soc/sophgo/Kconfig | 11 +- sound/soc/sophgo/Makefile | 1 + sound/soc/sophgo/cv1800b-sound-dac.c | 204 +++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 sound/soc/sophgo/cv1800b-sound-dac.c diff --git a/sound/soc/sophgo/Kconfig b/sound/soc/sophgo/Kconfig index 12d1a57ea308..e4786f087589 100644 --- a/sound/soc/sophgo/Kconfig +++ b/sound/soc/sophgo/Kconfig @@ -28,10 +28,19 @@ config SND_SOC_CV1800B_ADC_CODEC help This driver provides an ASoC codec DAI for capture and basic control of the RXADC registers. - Say Y or M to build support for the Sophgo CV1800B internal analog ADC codec block (RXADC). The module will be called cv1800b-sound-adc +config SND_SOC_CV1800B_DAC_CODEC + tristate "Sophgo CV1800B/SG2002 internal DAC codec" + depends on SND_SOC + help + This driver provides an ASoC codec DAI for playback and basic + control of the TXDAC registers. + + Say Y or M to build support for the Sophgo CV1800B + internal analog DAC codec block (TXDAC). + The module will be called cv1800b-sound-dac endmenu diff --git a/sound/soc/sophgo/Makefile b/sound/soc/sophgo/Makefile index c654d6059cbd..ec8dd31efddd 100644 --- a/sound/soc/sophgo/Makefile +++ b/sound/soc/sophgo/Makefile @@ -2,3 +2,4 @@ # Sophgo Platform Support obj-$(CONFIG_SND_SOC_CV1800B_TDM) += cv1800b-tdm.o obj-$(CONFIG_SND_SOC_CV1800B_ADC_CODEC) += cv1800b-sound-adc.o +obj-$(CONFIG_SND_SOC_CV1800B_DAC_CODEC) += cv1800b-sound-dac.o diff --git a/sound/soc/sophgo/cv1800b-sound-dac.c b/sound/soc/sophgo/cv1800b-sound-dac.c new file mode 100644 index 000000000000..ccf386174639 --- /dev/null +++ b/sound/soc/sophgo/cv1800b-sound-dac.c @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Internal DAC codec for cv1800b based CPUs + */ + +#include +#include +#include +#include +#include +#include + +#define CV1800B_TXDAC_CTRL0 0x00 +#define CV1800B_TXDAC_CTRL1 0x04 +#define CV1800B_TXDAC_STATUS 0x08 +#define CV1800B_TXDAC_AFE0 0x0c +#define CV1800B_TXDAC_AFE1 0x10 +#define CV1800B_TXDAC_ANA0 0x20 +#define CV1800B_TXDAC_ANA1 0x24 +#define CV1800B_TXDAC_ANA2 0x28 + +/* cv1800b_TXDAC_CTRL0 */ +#define REG_TXDAC_EN GENMASK(0, 0) +#define REG_I2S_RX_EN GENMASK(1, 1) + +/* cv1800b_TXDAC_CTRL1 */ +#define REG_TXDAC_CIC_OPT GENMASK(1, 0) + +/* cv1800b_TXDAC_AFE0 */ +#define REG_TXDAC_INIT_DLY_CNT GENMASK(5, 0) + +/* cv1800b_TXDAC_ANA2 */ +#define TXDAC_OW_VAL_L_MASK GENMASK(7, 0) +#define TXDAC_OW_VAL_R_MASK GENMASK(15, 8) +#define TXDAC_OW_EN_L_MASK GENMASK(16, 16) +#define TXDAC_OW_EN_R_MASK GENMASK(17, 17) + +struct cv1800b_priv { + void __iomem *regs; + struct device *dev; +}; + +enum decimation_values { + DECIMATION_64 = 0, + DECIMATION_128, + DECIMATION_256, + DECIMATION_512, +}; + +static void cv1800b_dac_enable(struct cv1800b_priv *priv, bool enable) +{ + u32 val; + + val = readl(priv->regs + CV1800B_TXDAC_CTRL0); + val = u32_replace_bits(val, enable, REG_TXDAC_EN); + val = u32_replace_bits(val, enable, REG_I2S_RX_EN); + writel(val, priv->regs + CV1800B_TXDAC_CTRL0); +} + +static void cv1800b_dac_mute(struct cv1800b_priv *priv, bool enable) +{ + u32 val; + + val = readl(priv->regs + CV1800B_TXDAC_ANA2); + val = u32_replace_bits(val, enable, TXDAC_OW_EN_L_MASK); + val = u32_replace_bits(val, enable, TXDAC_OW_EN_R_MASK); + writel(val, priv->regs + CV1800B_TXDAC_ANA2); +} + +static int cv1800b_dac_decimation(struct cv1800b_priv *priv, u8 dec) +{ + u32 val; + + if (dec > 3) + return -EINVAL; + + val = readl(priv->regs + CV1800B_TXDAC_CTRL1); + val = u32_replace_bits(val, dec, REG_TXDAC_CIC_OPT); + writel(val, priv->regs + CV1800B_TXDAC_CTRL1); + return 0; +} + +static int cv1800b_dac_dly(struct cv1800b_priv *priv, u32 dly) +{ + u32 val; + + if (dly > 63) + return -EINVAL; + + val = readl(priv->regs + CV1800B_TXDAC_AFE0); + val = u32_replace_bits(val, dly, REG_TXDAC_INIT_DLY_CNT); + writel(val, priv->regs + CV1800B_TXDAC_AFE0); + return 0; +} + +static int cv1800b_dac_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct cv1800b_priv *priv = snd_soc_dai_get_drvdata(dai); + int ret; + unsigned int rate = params_rate(params); + + if (rate != 48000) { + dev_err(priv->dev, "rate %u is not supported\n", rate); + return -EINVAL; + } + + cv1800b_dac_mute(priv, false); + /* minimal decimation for 48kHz is 64*/ + ret = cv1800b_dac_decimation(priv, DECIMATION_64); + if (ret) + return ret; + + /* value is taken from vendors driver 48kHz + * tested on sg2000 and sg2002. + */ + ret = cv1800b_dac_dly(priv, 0x19); + if (ret) + return ret; + + return 0; +} + +static int cv1800b_dac_dai_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct cv1800b_priv *priv = snd_soc_dai_get_drvdata(dai); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + cv1800b_dac_enable(priv, true); + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + cv1800b_dac_enable(priv, false); + break; + default: + return -EINVAL; + } + + return 0; +} + +static const struct snd_soc_dai_ops cv1800b_dac_dai_ops = { + .hw_params = cv1800b_dac_hw_params, + .trigger = cv1800b_dac_dai_trigger, +}; + +static struct snd_soc_dai_driver cv1800b_dac_dai = { + .name = "dac-hifi", + .playback = { .stream_name = "DAC Playback", + .channels_min = 2, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_48000, + .formats = SNDRV_PCM_FMTBIT_S16_LE }, + .ops = &cv1800b_dac_dai_ops, +}; + +static const struct snd_soc_component_driver cv1800b_dac_component = { + .name = "cv1800b-dac-codec", +}; + +static int cv1800b_dac_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct cv1800b_priv *priv; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->dev = dev; + priv->regs = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(priv->regs)) + return PTR_ERR(priv->regs); + + platform_set_drvdata(pdev, priv); + return devm_snd_soc_register_component(&pdev->dev, + &cv1800b_dac_component, + &cv1800b_dac_dai, 1); +} + +static const struct of_device_id cv1800b_dac_of_match[] = { + { .compatible = "sophgo,cv1800b-sound-dac" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, cv1800b_dac_of_match); + +static struct platform_driver cv1800b_dac_driver = { + .probe = cv1800b_dac_probe, + .driver = { + .name = "cv1800b-dac-codec", + .of_match_table = cv1800b_dac_of_match, + }, +}; +module_platform_driver(cv1800b_dac_driver); + +MODULE_DESCRIPTION("DAC codec for CV1800B"); +MODULE_AUTHOR("Anton D. Stavinskii "); +MODULE_LICENSE("GPL"); From a8e3e488293118b8fa5d5e7ca786ca25b954ce12 Mon Sep 17 00:00:00 2001 From: "Anton D. Stavinskii" Date: Tue, 27 Jan 2026 23:08:19 +0400 Subject: [PATCH 6/7] ASoC: sophgo: cv1800b: document DAC overwrite handling Add comments to cv1800b_dac_mute() and its caller to explain how the overwrite mechanism works and why we force it off before playback. Signed-off-by: Anton D. Stavinskii Link: https://patch.msgid.link/20260127-incremental-for-i2s-dvier-v2-1-5f66b841f63d@gmail.com Signed-off-by: Mark Brown --- sound/soc/sophgo/cv1800b-sound-dac.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sound/soc/sophgo/cv1800b-sound-dac.c b/sound/soc/sophgo/cv1800b-sound-dac.c index ccf386174639..135322bcf6ad 100644 --- a/sound/soc/sophgo/cv1800b-sound-dac.c +++ b/sound/soc/sophgo/cv1800b-sound-dac.c @@ -57,6 +57,10 @@ static void cv1800b_dac_enable(struct cv1800b_priv *priv, bool enable) writel(val, priv->regs + CV1800B_TXDAC_CTRL0); } +/* + * Control the DAC overwrite bits. When enabled, the DAC outputs the fixed + * overwrite value instead of samples from the I2S input. + */ static void cv1800b_dac_mute(struct cv1800b_priv *priv, bool enable) { u32 val; @@ -105,7 +109,7 @@ static int cv1800b_dac_hw_params(struct snd_pcm_substream *substream, dev_err(priv->dev, "rate %u is not supported\n", rate); return -EINVAL; } - + /* Clear DAC overwrite so playback uses I2S data. */ cv1800b_dac_mute(priv, false); /* minimal decimation for 48kHz is 64*/ ret = cv1800b_dac_decimation(priv, DECIMATION_64); From 8cf19b19dba8814ccc8b1179dabf28b7f8eefc22 Mon Sep 17 00:00:00 2001 From: "Anton D. Stavinskii" Date: Tue, 27 Jan 2026 23:08:20 +0400 Subject: [PATCH 7/7] ASoC: sophgo: cv1800b: tidy Kconfig spacing Restore the empty line that was accidentally removed earlier Signed-off-by: Anton D. Stavinskii Link: https://patch.msgid.link/20260127-incremental-for-i2s-dvier-v2-2-5f66b841f63d@gmail.com Signed-off-by: Mark Brown --- sound/soc/sophgo/Kconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/sound/soc/sophgo/Kconfig b/sound/soc/sophgo/Kconfig index e4786f087589..9b454261bcfd 100644 --- a/sound/soc/sophgo/Kconfig +++ b/sound/soc/sophgo/Kconfig @@ -28,6 +28,7 @@ config SND_SOC_CV1800B_ADC_CODEC help This driver provides an ASoC codec DAI for capture and basic control of the RXADC registers. + Say Y or M to build support for the Sophgo CV1800B internal analog ADC codec block (RXADC). The module will be called cv1800b-sound-adc