* [PATCH v3 0/4] ASoC: cdns: Add Cadence I2S-SC and I2S-MC controller support
@ 2026-10-09 9:25 joakim.zhang
2026-10-09 9:25 ` [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller joakim.zhang
` (3 more replies)
0 siblings, 4 replies; 11+ messages in thread
From: joakim.zhang @ 2026-10-09 9:25 UTC (permalink / raw)
To: lgirdwood, broonie, robh, krzk+dt, conor+dt, perex, tiwai,
p.zabel
Cc: cix-kernel-upstream, linux-sound, devicetree, linux-kernel,
linux-arm-kernel, Joakim Zhang
From: Joakim Zhang <joakim.zhang@cixtech.com>
This series adds support for the Cadence I2S-SC and I2S-MC controllers
integrated in the CIX SKY1 audio subsystem, together with their DT
bindings.
I2S-SC provides full-duplex stereo playback and capture in standard
I2S, left/right-justified and DSP modes, along with TDM operation
supporting up to 16 slots. I2S-MC provides multi-channel playback and
capture through a configurable set of its 8 data pins, described by
cix,pin-out-num, cix,pin-rx-mask and cix,pin-tx-mask, and exposes a
transmit DAI and a receive DAI.
Both drivers register the dmaengine PCM helper, select the audio
reference clock parent for the 8 kHz or 11.025 kHz sample rate family
and keep the minimum functional-clock to bit-clock ratio required for
safe clock domain crossing.
Changes in v3:
- Add the explicit <linux/bitfield.h> include, which a W=1 build of the
FIELD_PREP() sites needs (both drivers).
- Fix the probe error paths: disable the manually enabled clocks and
drop the runtime PM usage count on every failure path, unwinding
through a single goto label (both drivers).
- Return dev_err_probe() directly instead of logging with it and then
returning the error separately (both drivers).
Changes in v2:
- Move the IRQ request after the clocks are enabled and the reset is
released, so the handler never runs against unclocked hardware.
- Disable the IRQ before switching the regcache to cache-only mode on
suspend, and re-enable it only after the cache is synced on resume;
check the status read in the ISR (both drivers).
- Replace the manual runtime PM bookkeeping with devm_pm_runtime_enable()
and drop the remove() callbacks (both drivers).
- Switch to RUNTIME_PM_OPS()/SYSTEM_SLEEP_PM_OPS() wrapped in pm_ptr()
and drop the __maybe_unused annotations (both drivers).
- Serialize the shared transceiver clock state in prepare() against the
trigger() state machine with the existing spinlock (I2S-SC).
- Disable clocks in the reverse order of enablement (both drivers).
- Sort the cdns/ entry into the vendor list in sound/soc/Makefile and
switch sound/soc/cdns/Makefile to the '-y' object syntax.
- Clean up the includes and initializer style (both drivers).
- Add a NULL check for the match data in probe (both drivers).
- Factor the MC transmitter and receiver configuration into a shared
helper.
- Drop the compatible string from the DMAC stub node in the binding
examples.
Signed-off-by: Joakim Zhang <joakim.zhang@cixtech.com>
Joakim Zhang (4):
ASoC: dt-bindings: Add Cadence I2S-SC controller
ASoC: cdns: Add Cadence I2S-SC controller driver
ASoC: dt-bindings: Add Cadence I2S-MC controller
ASoC: cdns: Add Cadence I2S-MC controller driver
.../bindings/sound/cix,sky1-i2s-mc.yaml | 123 ++
.../bindings/sound/cix,sky1-i2s-sc.yaml | 93 ++
sound/soc/Kconfig | 1 +
sound/soc/Makefile | 1 +
sound/soc/cdns/Kconfig | 21 +
sound/soc/cdns/Makefile | 7 +
sound/soc/cdns/cdns-i2s-mc.c | 1127 ++++++++++++++++
sound/soc/cdns/cdns-i2s-sc.c | 1166 +++++++++++++++++
8 files changed, 2539 insertions(+)
create mode 100644 Documentation/devicetree/bindings/sound/cix,sky1-i2s-mc.yaml
create mode 100644 Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
create mode 100644 sound/soc/cdns/Kconfig
create mode 100644 sound/soc/cdns/Makefile
create mode 100644 sound/soc/cdns/cdns-i2s-mc.c
create mode 100644 sound/soc/cdns/cdns-i2s-sc.c
--
2.50.1
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller
2026-10-09 9:25 [PATCH v3 0/4] ASoC: cdns: Add Cadence I2S-SC and I2S-MC controller support joakim.zhang
@ 2026-10-09 9:25 ` joakim.zhang
2026-10-09 9:32 ` sashiko-bot
` (2 more replies)
2026-10-09 9:25 ` [PATCH v3 2/4] ASoC: cdns: Add Cadence I2S-SC controller driver joakim.zhang
` (2 subsequent siblings)
3 siblings, 3 replies; 11+ messages in thread
From: joakim.zhang @ 2026-10-09 9:25 UTC (permalink / raw)
To: lgirdwood, broonie, robh, krzk+dt, conor+dt, perex, tiwai,
p.zabel
Cc: cix-kernel-upstream, linux-sound, devicetree, linux-kernel,
linux-arm-kernel, Joakim Zhang
From: Joakim Zhang <joakim.zhang@cixtech.com>
Add the DT binding for the Cadence I2S-SC controller integrated in
the CIX SKY1 audio subsystem. I2S-SC is a full-duplex stereo
controller supporting standard I2S, left/right-justified and DSP
formats, plus TDM operation with up to 16 slots.
Signed-off-by: Joakim Zhang <joakim.zhang@cixtech.com>
---
.../bindings/sound/cix,sky1-i2s-sc.yaml | 93 +++++++++++++++++++
1 file changed, 93 insertions(+)
create mode 100644 Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
diff --git a/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
new file mode 100644
index 000000000000..3fdf2334c492
--- /dev/null
+++ b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
@@ -0,0 +1,93 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/sound/cix,sky1-i2s-sc.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Cadence I2S-SC controller
+
+maintainers:
+ - Joakim Zhang <joakim.zhang@cixtech.com>
+
+description: |
+ The Cadence I2S-SC controller integrated in the CIX SKY1 audio subsystem.
+ It supports full-duplex stereo playback and capture in standard I2S,
+ left/right-justified and DSP modes, as well as TDM operation with up to
+ 16 slots. The transmitter and receiver are started and stopped together
+ by the controller.
+
+allOf:
+ - $ref: dai-common.yaml#
+
+properties:
+ compatible:
+ const: cix,sky1-i2s-sc
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ clocks:
+ maxItems: 4
+
+ clock-names:
+ items:
+ - const: hst
+ - const: i2s
+ - const: audio-clk0
+ - const: audio-clk2
+
+ resets:
+ maxItems: 1
+
+ dmas:
+ minItems: 1
+ maxItems: 2
+ description:
+ One DMA channel for instances wired for a single direction, either
+ "tx" or "rx", or two channels for both transmit and receive.
+
+ dma-names:
+ minItems: 1
+ maxItems: 2
+ items:
+ enum: [ tx, rx ]
+
+ "#sound-dai-cells":
+ const: 0
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - clocks
+ - clock-names
+ - resets
+ - dmas
+ - dma-names
+ - "#sound-dai-cells"
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+ audss_dmac: dma-controller {
+ #dma-cells = <1>;
+ };
+
+ i2s@7020000 {
+ compatible = "cix,sky1-i2s-sc";
+ reg = <0x7020000 0x10000>;
+ interrupts = <GIC_SPI 219 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&audss_cru 16>, <&audss_cru 26>,
+ <&scmi_clk 100>, <&scmi_clk 101>;
+ clock-names = "hst", "i2s", "audio-clk0", "audio-clk2";
+ resets = <&audss_cru 0>;
+ dmas = <&audss_dmac 0>, <&audss_dmac 1>;
+ dma-names = "tx", "rx";
+ #sound-dai-cells = <0>;
+ };
--
2.50.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 2/4] ASoC: cdns: Add Cadence I2S-SC controller driver
2026-10-09 9:25 [PATCH v3 0/4] ASoC: cdns: Add Cadence I2S-SC and I2S-MC controller support joakim.zhang
2026-10-09 9:25 ` [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller joakim.zhang
@ 2026-10-09 9:25 ` joakim.zhang
2026-10-09 9:37 ` sashiko-bot
2026-10-09 9:25 ` [PATCH v3 3/4] ASoC: dt-bindings: Add Cadence I2S-MC controller joakim.zhang
2026-10-09 9:25 ` [PATCH v3 4/4] ASoC: cdns: Add Cadence I2S-MC controller driver joakim.zhang
3 siblings, 1 reply; 11+ messages in thread
From: joakim.zhang @ 2026-10-09 9:25 UTC (permalink / raw)
To: lgirdwood, broonie, robh, krzk+dt, conor+dt, perex, tiwai,
p.zabel
Cc: cix-kernel-upstream, linux-sound, devicetree, linux-kernel,
linux-arm-kernel, Joakim Zhang
From: Joakim Zhang <joakim.zhang@cixtech.com>
Add support for the Cadence I2S-SC controller found in the CIX SKY1
audio subsystem.
The controller provides full-duplex stereo playback and capture in
standard I2S, left/right-justified and DSP modes, with mono operation
mapped onto the left audio channel. It also supports TDM operation
with up to 16 slots per frame, where the PCM channels map onto the
active slots of the stream direction while the bit clock covers the
whole frame.
The transmitter and receiver share the transceiver enable sequence,
so the start/stop state machine is serialized with a spinlock against
independently triggered playback and capture PCMs. The driver
registers the dmaengine PCM helper, selects the audio reference clock
parent for the 8 kHz or 11.025 kHz sample rate family and keeps the
minimum functional-clock to bit-clock ratio required for safe clock
domain crossing. Runtime and system suspend/resume restore the
registers through the regcache.
Signed-off-by: Joakim Zhang <joakim.zhang@cixtech.com>
---
sound/soc/Kconfig | 1 +
sound/soc/Makefile | 1 +
sound/soc/cdns/Kconfig | 11 +
sound/soc/cdns/Makefile | 4 +
sound/soc/cdns/cdns-i2s-sc.c | 1166 ++++++++++++++++++++++++++++++++++
5 files changed, 1183 insertions(+)
create mode 100644 sound/soc/cdns/Kconfig
create mode 100644 sound/soc/cdns/Makefile
create mode 100644 sound/soc/cdns/cdns-i2s-sc.c
diff --git a/sound/soc/Kconfig b/sound/soc/Kconfig
index edfdcbf734fe..d9b746ffd701 100644
--- a/sound/soc/Kconfig
+++ b/sound/soc/Kconfig
@@ -108,6 +108,7 @@ source "sound/soc/apple/Kconfig"
source "sound/soc/atmel/Kconfig"
source "sound/soc/au1x/Kconfig"
source "sound/soc/bcm/Kconfig"
+source "sound/soc/cdns/Kconfig"
source "sound/soc/cirrus/Kconfig"
source "sound/soc/dwc/Kconfig"
source "sound/soc/fsl/Kconfig"
diff --git a/sound/soc/Makefile b/sound/soc/Makefile
index 21d8406767fc..ba3cf6eb937a 100644
--- a/sound/soc/Makefile
+++ b/sound/soc/Makefile
@@ -50,6 +50,7 @@ obj-$(CONFIG_SND_SOC) += amd/
obj-$(CONFIG_SND_SOC) += atmel/
obj-$(CONFIG_SND_SOC) += au1x/
obj-$(CONFIG_SND_SOC) += bcm/
+obj-$(CONFIG_SND_SOC) += cdns/
obj-$(CONFIG_SND_SOC) += cirrus/
obj-$(CONFIG_SND_SOC) += dwc/
obj-$(CONFIG_SND_SOC) += fsl/
diff --git a/sound/soc/cdns/Kconfig b/sound/soc/cdns/Kconfig
new file mode 100644
index 000000000000..631e2e0764cb
--- /dev/null
+++ b/sound/soc/cdns/Kconfig
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0
+
+config SND_SOC_CDNS_I2S_SC
+ tristate "Cadence I2S-SC controller support"
+ depends on ARCH_CIX || COMPILE_TEST
+ select SND_SOC_GENERIC_DMAENGINE_PCM
+ help
+ Say Y or M if you want to add support for the Cadence I2S-SC
+ controller found in the CIX SKY1 audio subsystem. It provides
+ full-duplex stereo playback and capture in standard I2S and
+ TDM modes with up to 16 slots.
diff --git a/sound/soc/cdns/Makefile b/sound/soc/cdns/Makefile
new file mode 100644
index 000000000000..6abfeef29e43
--- /dev/null
+++ b/sound/soc/cdns/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0
+
+snd-soc-cdns-i2s-sc-y := cdns-i2s-sc.o
+obj-$(CONFIG_SND_SOC_CDNS_I2S_SC) += snd-soc-cdns-i2s-sc.o
diff --git a/sound/soc/cdns/cdns-i2s-sc.c b/sound/soc/cdns/cdns-i2s-sc.c
new file mode 100644
index 000000000000..160a0ec573d7
--- /dev/null
+++ b/sound/soc/cdns/cdns-i2s-sc.c
@@ -0,0 +1,1166 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright 2026 Cix Technology Group Co., Ltd.
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/module.h>
+#include <linux/pinctrl/consumer.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/regmap.h>
+#include <linux/reset.h>
+#include <sound/dmaengine_pcm.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+
+/* Special Function Registers */
+/* Control Register */
+#define I2S_CTRL 0x00
+#define I2S_CTRL_LR_PACK BIT(31)
+#define I2S_CTRL_FIFO_AFULL_MASK BIT(30)
+#define I2S_CTRL_FIFO_FULL_MASK BIT(29)
+#define I2S_CTRL_FIFO_AEMPTY_MASK BIT(28)
+#define I2S_CTRL_FIFO_EMPTY_MASK BIT(27)
+#define I2S_CTRL_I2S_MASK BIT(26)
+#define I2S_CTRL_INTREQ_MASK BIT(25)
+#define I2S_CTRL_I2S_STB BIT(24)
+#define I2S_CTRL_HOST_DATA_ALIGN BIT(23)
+#define I2S_CTRL_DATA_ORDER BIT(22)
+#define I2S_CTRL_DATA_ALIGN BIT(21)
+#define I2S_CTRL_DATA_WS_DEL GENMASK(20, 16)
+#define I2S_CTRL_WS_POLAR BIT(15)
+#define I2S_CTRL_SCK_POLAR BIT(14)
+#define I2S_CTRL_AUDIO_MODE BIT(13)
+#define I2S_CTRL_MONO_MODE BIT(12)
+#define I2S_CTRL_WS_MODE GENMASK(11, 8)
+#define I2S_CTRL_CHN_WIDTH GENMASK(7, 5)
+#define I2S_CTRL_CHN_WIDTH_8 0
+#define I2S_CTRL_CHN_WIDTH_12 1
+#define I2S_CTRL_CHN_WIDTH_16 2
+#define I2S_CTRL_CHN_WIDTH_18 3
+#define I2S_CTRL_CHN_WIDTH_20 4
+#define I2S_CTRL_CHN_WIDTH_24 5
+#define I2S_CTRL_CHN_WIDTH_28 6
+#define I2S_CTRL_CHN_WIDTH_32 7
+#define I2S_CTRL_FIFO_RST BIT(4)
+#define I2S_CTRL_SFR_RST BIT(3)
+#define I2S_CTRL_MS_CFG BIT(2)
+#define I2S_CTRL_DIR_CFG BIT(1)
+#define I2S_CTRL_I2S_EN BIT(0)
+
+/* Full-Duplex Mode Control Register */
+#define I2S_CTRL_FDX 0x04
+#define I2S_CTRL_FDX_RFIFO_AFULL_MASK BIT(30)
+#define I2S_CTRL_FDX_RFIFO_FULL_MASK BIT(29)
+#define I2S_CTRL_FDX_RFIFO_AEMPTY_MASK BIT(28)
+#define I2S_CTRL_FDX_RFIFO_EMPTY_MASK BIT(27)
+#define I2S_CTRL_FDX_RI2S_MASK BIT(26)
+#define I2S_CTRL_FDX_RFIFO_RST BIT(4)
+#define I2S_CTRL_FDX_I2S_FRX_EN BIT(2)
+#define I2S_CTRL_FDX_I2S_FTX_EN BIT(1)
+#define I2S_CTRL_FDX_FULL_DUPLEX BIT(0)
+
+/* Sample Resolution Register */
+#define I2S_SRES 0x08
+#define I2S_SRES_RESOLUTION GENMASK(4, 0)
+
+/* Full-Duplex Mode Receive Sample Resolution Register */
+#define I2S_SRES_FDR 0x0c
+#define I2S_SRES_FDR_RRESOLUTION GENMASK(4, 0)
+
+/* Transceiver Sample Rate Register */
+#define I2S_SRATE 0x10
+#define I2S_SRATE_SAMPLE_RATE GENMASK(19, 0)
+
+/* Status Flags Register */
+#define I2S_STAT 0x14
+#define I2S_STAT_RFIFO_AFULL BIT(19)
+#define I2S_STAT_RFIFO_FULL BIT(18)
+#define I2S_STAT_RFIFO_AEMPTY BIT(17)
+#define I2S_STAT_RFIFO_EMPTY BIT(16)
+#define I2S_STAT_FIFO_AFULL BIT(5)
+#define I2S_STAT_FIFO_FULL BIT(4)
+#define I2S_STAT_FIFO_AEMPTY BIT(3)
+#define I2S_STAT_FIFO_EMPTY BIT(2)
+#define I2S_STAT_RDATA_OVERR BIT(1)
+#define I2S_STAT_TDATA_UNDERR BIT(0)
+
+/* FIFO Level Register (read only) */
+#define I2S_FIFO_LEVEL 0x18
+
+/* FIFO Almost Empty Level Register */
+#define I2S_FIFO_AEMPTY 0x1c
+
+/* FIFO Almost Full Level Register */
+#define I2S_FIFO_AFULL 0x20
+
+/* Full-Duplex Mode Receiver FIFO Level Register (read only) */
+#define I2S_FIFO_LEVEL_FDR 0x24
+
+/* Full-Duplex Mode Receiver FIFO Almost Empty Level Register */
+#define I2S_FIFO_AEMPTY_FDR 0x28
+
+/* Full-Duplex Mode Receiver FIFO Almost Full Level Register */
+#define I2S_FIFO_AFULL_FDR 0x2c
+
+/* Time Division Multiplexing Control Register */
+#define I2S_TDM_CTRL 0x30
+#define I2S_TDM_CTRL_CHN_EN GENMASK(31, 16)
+#define I2S_TDM_CTRL_CHN_NO GENMASK(4, 1)
+#define I2S_TDM_CTRL_TDM_EN BIT(0)
+
+/* Time Division Multiplexing Full-Duplex Mode Channels Direction Register */
+#define I2S_TDM_FD_DIR 0x34
+#define I2S_TDM_FD_DIR_CHN_RXEN GENMASK(31, 16)
+#define I2S_TDM_FD_DIR_CHN_TXEN GENMASK(15, 0)
+
+/* Transmit And Receive FIFOs Address */
+#define I2S_FIFO_ADDRESS 0x40
+
+/*
+ * The I2S functional clock must run at a minimum ratio above the audio
+ * bit clock to safely cross clock domains, regardless of whether the
+ * bit clock is generated internally or externally. See the controller
+ * reference manual, section 3.3 "Clocks and Clock Domain Crossing".
+ */
+#define I2S_CLK_RATIO_MIN 6
+
+#define DRV_NAME "cdns-i2s-sc"
+
+enum {
+ AUDIO_CLK0,
+ AUDIO_CLK2,
+ AUDIO_CLK_NUM,
+};
+
+static const char *cdns_i2s_sc_clk_pll_names[AUDIO_CLK_NUM] = {
+ [AUDIO_CLK0] = "audio-clk0",
+ [AUDIO_CLK2] = "audio-clk2",
+};
+
+struct cdns_i2s_sc_devtype_data {
+ u32 rx_fifo_aempty_threshold;
+ u32 rx_fifo_afull_threshold;
+ u32 tx_fifo_aempty_threshold;
+ u32 tx_fifo_afull_threshold;
+};
+
+struct cdns_i2s_sc_tdm_config {
+ unsigned int tx_mask;
+ unsigned int rx_mask;
+ int slots;
+ int slot_width;
+};
+
+struct cdns_i2s_sc_priv {
+ struct device *dev;
+ struct regmap *regmap;
+ struct reset_control *i2s_rst;
+
+ int irq;
+
+ struct clk *clk_hst;
+ struct clk *clk_i2s;
+ struct clk *clks[AUDIO_CLK_NUM];
+
+ struct snd_dmaengine_dai_dma_data capture_dma_data;
+ struct snd_dmaengine_dai_dma_data playback_dma_data;
+
+ const struct cdns_i2s_sc_devtype_data *devtype_data;
+ struct cdns_i2s_sc_tdm_config tdm_config;
+
+ bool is_master_mode;
+ bool is_tdm_mode;
+ bool is_mono_in_i2s_mode;
+
+ /* Serializes the transmitter/receiver start/stop state machine below */
+ spinlock_t lock;
+
+ /*
+ * NOTE:
+ * I2S_CTRL_FDX[I2S_FTX_EN] = 1 && I2S_CTRL[I2S_EN] = 1 to enable transmitter
+ * I2S_CTRL_FDX[I2S_FRX_EN] = 1 && I2S_CTRL[I2S_EN] = 1 to enable receiver
+ * We can dynamically switch of the transmitter and receiver enable separately,
+ * but need always enable I2S transceiver, so I2S-SC controller hopes to start
+ * the transmitter and receiver together, also to stop them when they are both
+ * try to stop.
+ *
+ * The playback and capture PCMs trigger independently (the PCM core holds
+ * only each substream's own lock), so tx_start/rx_start and the shared
+ * transceiver registers must be accessed under &lock.
+ */
+ bool rx_start;
+ bool tx_start;
+};
+
+static int cdns_i2s_sc_clks_enable(struct cdns_i2s_sc_priv *i2s_sc_priv)
+{
+ int ret;
+
+ ret = clk_prepare_enable(i2s_sc_priv->clk_hst);
+ if (ret)
+ return ret;
+
+ ret = clk_prepare_enable(i2s_sc_priv->clk_i2s);
+ if (ret)
+ clk_disable_unprepare(i2s_sc_priv->clk_hst);
+
+ return ret;
+}
+
+static void cdns_i2s_sc_clks_disable(struct cdns_i2s_sc_priv *i2s_sc_priv)
+{
+ clk_disable_unprepare(i2s_sc_priv->clk_i2s);
+ clk_disable_unprepare(i2s_sc_priv->clk_hst);
+}
+
+static void cdns_i2s_sc_rxtx_common_config(struct cdns_i2s_sc_priv *i2s_sc_priv, bool on)
+{
+ if (on) {
+ /* Full-duplex mode enable */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL_FDX,
+ I2S_CTRL_FDX_FULL_DUPLEX, I2S_CTRL_FDX_FULL_DUPLEX);
+
+ if (i2s_sc_priv->is_tdm_mode) {
+ /* TDM mode enable */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_TDM_CTRL,
+ I2S_TDM_CTRL_TDM_EN, I2S_TDM_CTRL_TDM_EN);
+
+ /* Number of supported audio channels in TDM mode */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_TDM_CTRL,
+ I2S_TDM_CTRL_CHN_NO,
+ FIELD_PREP(I2S_TDM_CTRL_CHN_NO,
+ i2s_sc_priv->tdm_config.slots - 1));
+
+ /* TDM mode channels enable */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_TDM_CTRL, I2S_TDM_CTRL_CHN_EN,
+ FIELD_PREP(I2S_TDM_CTRL_CHN_EN,
+ i2s_sc_priv->tdm_config.rx_mask |
+ i2s_sc_priv->tdm_config.tx_mask));
+ }
+
+ /* Transceiver clock enable */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_I2S_STB, 0);
+
+ /* All interrupt requests unmask */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_INTREQ_MASK, I2S_CTRL_INTREQ_MASK);
+ } else {
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_INTREQ_MASK, 0);
+
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_I2S_STB, I2S_CTRL_I2S_STB);
+
+ if (i2s_sc_priv->is_tdm_mode) {
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_TDM_CTRL,
+ I2S_TDM_CTRL_TDM_EN, 0);
+
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_TDM_CTRL,
+ I2S_TDM_CTRL_CHN_NO, 0);
+
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_TDM_CTRL,
+ I2S_TDM_CTRL_CHN_EN, 0);
+ }
+
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL_FDX,
+ I2S_CTRL_FDX_FULL_DUPLEX, 0);
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_I2S_EN, 0);
+ }
+}
+
+static void cdns_i2s_sc_tx_config(struct cdns_i2s_sc_priv *i2s_sc_priv, bool on)
+{
+ u32 irq_mask = 0;
+
+ irq_mask |= I2S_CTRL_I2S_MASK;
+
+ if (on) {
+ cdns_i2s_sc_rxtx_common_config(i2s_sc_priv, on);
+
+ /* Transmitter data underrun interrupt unmask */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL, irq_mask, irq_mask);
+
+ if (i2s_sc_priv->is_tdm_mode) {
+ /* TDM mode channels transmit enable */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_TDM_FD_DIR,
+ I2S_TDM_FD_DIR_CHN_TXEN,
+ FIELD_PREP(I2S_TDM_FD_DIR_CHN_TXEN,
+ i2s_sc_priv->tdm_config.tx_mask));
+ } else {
+ /*
+ * I2S mode mono/stereo selection
+ * audio_mode: HIGH = only one channel active
+ * mono_mode: LOW = left channel, HIGH = right channel
+ */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_AUDIO_MODE | I2S_CTRL_MONO_MODE,
+ i2s_sc_priv->is_mono_in_i2s_mode ?
+ I2S_CTRL_AUDIO_MODE : 0);
+ }
+
+ /* Full-duplex mode transmitter enable */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL_FDX,
+ I2S_CTRL_FDX_I2S_FTX_EN, I2S_CTRL_FDX_I2S_FTX_EN);
+
+ /* Transceiver enable */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_I2S_EN, I2S_CTRL_I2S_EN);
+
+ i2s_sc_priv->tx_start = true;
+ } else {
+ i2s_sc_priv->tx_start = false;
+
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL_FDX,
+ I2S_CTRL_FDX_I2S_FTX_EN, 0);
+
+ if (i2s_sc_priv->is_tdm_mode)
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_TDM_FD_DIR,
+ I2S_TDM_FD_DIR_CHN_TXEN, 0);
+
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL, irq_mask, 0);
+
+ if (!i2s_sc_priv->rx_start)
+ cdns_i2s_sc_rxtx_common_config(i2s_sc_priv, on);
+ }
+}
+
+static void cdns_i2s_sc_rx_config(struct cdns_i2s_sc_priv *i2s_sc_priv, bool on)
+{
+ u32 irq_mask = 0;
+
+ irq_mask |= I2S_CTRL_FDX_RI2S_MASK;
+
+ if (on) {
+ cdns_i2s_sc_rxtx_common_config(i2s_sc_priv, on);
+
+ /* Receiver data overrun interrupt unmask */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL_FDX, irq_mask, irq_mask);
+
+ if (i2s_sc_priv->is_tdm_mode) {
+ /* TDM mode channels receive enable */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_TDM_FD_DIR,
+ I2S_TDM_FD_DIR_CHN_RXEN,
+ FIELD_PREP(I2S_TDM_FD_DIR_CHN_RXEN,
+ i2s_sc_priv->tdm_config.rx_mask));
+ } else {
+ /*
+ * I2S mode mono/stereo selection
+ * audio_mode: HIGH = only one channel active
+ * mono_mode: LOW = left channel, HIGH = right channel
+ */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_AUDIO_MODE | I2S_CTRL_MONO_MODE,
+ i2s_sc_priv->is_mono_in_i2s_mode ?
+ I2S_CTRL_AUDIO_MODE : 0);
+ }
+
+ /* Full-duplex mode receiver enable */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL_FDX,
+ I2S_CTRL_FDX_I2S_FRX_EN, I2S_CTRL_FDX_I2S_FRX_EN);
+
+ /* Transceiver enable */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_I2S_EN, I2S_CTRL_I2S_EN);
+
+ i2s_sc_priv->rx_start = true;
+ } else {
+ i2s_sc_priv->rx_start = false;
+
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL_FDX,
+ I2S_CTRL_FDX_I2S_FRX_EN, 0);
+
+ if (i2s_sc_priv->is_tdm_mode)
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_TDM_FD_DIR,
+ I2S_TDM_FD_DIR_CHN_RXEN, 0);
+
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL_FDX, irq_mask, 0);
+
+ if (!i2s_sc_priv->tx_start)
+ cdns_i2s_sc_rxtx_common_config(i2s_sc_priv, on);
+ }
+}
+
+static irqreturn_t cdns_i2s_sc_isr(int irq, void *devid)
+{
+ struct cdns_i2s_sc_priv *i2s_sc_priv = (struct cdns_i2s_sc_priv *)devid;
+ struct device *dev = i2s_sc_priv->dev;
+ u32 stat;
+
+ if (regmap_read(i2s_sc_priv->regmap, I2S_STAT, &stat))
+ return IRQ_NONE;
+
+ stat &= I2S_STAT_TDATA_UNDERR | I2S_STAT_RDATA_OVERR;
+ if (!stat)
+ return IRQ_NONE;
+
+ /* Clear the status */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_STAT, stat, 0);
+
+ /* Transmitter status */
+ if (stat & I2S_STAT_TDATA_UNDERR)
+ dev_dbg(dev, "isr: tx data underrun\n");
+
+ /* Receiver status */
+ if (stat & I2S_STAT_RDATA_OVERR)
+ dev_dbg(dev, "isr: rx data overrun\n");
+
+ return IRQ_HANDLED;
+}
+
+static int cdns_i2s_sc_set_tdm_slot(struct snd_soc_dai *cpu_dai, unsigned int tx_mask,
+ unsigned int rx_mask, int slots, int slot_width)
+{
+ struct cdns_i2s_sc_priv *i2s_sc_priv = snd_soc_dai_get_drvdata(cpu_dai);
+ struct device *dev = i2s_sc_priv->dev;
+
+ dev_dbg(dev, "tx_mask = 0x%x, rx_mask = 0x%x, slots = %d, slot_width = %d\n",
+ tx_mask, rx_mask, slots, slot_width);
+
+ if (slots < 1 || slots > 16) {
+ dev_err(i2s_sc_priv->dev, "TDM mode supports 1 to 16 slots\n");
+ return -EINVAL;
+ }
+
+ if (slot_width < 8 || slot_width > 32) {
+ dev_err(i2s_sc_priv->dev, "TDM slot width must be 8 to 32 bits\n");
+ return -EINVAL;
+ }
+
+ if ((tx_mask >> slots) || (rx_mask >> slots)) {
+ dev_err(i2s_sc_priv->dev, "TDM slot mask has bits outside the slots\n");
+ return -EINVAL;
+ }
+
+ i2s_sc_priv->tdm_config.slots = slots;
+ i2s_sc_priv->tdm_config.slot_width = slot_width;
+ i2s_sc_priv->tdm_config.rx_mask = rx_mask;
+ i2s_sc_priv->tdm_config.tx_mask = tx_mask;
+
+ i2s_sc_priv->is_tdm_mode = true;
+
+ return 0;
+}
+
+static int cdns_i2s_sc_set_fmt(struct snd_soc_dai *cpu_dai, unsigned int fmt)
+{
+ struct cdns_i2s_sc_priv *i2s_sc_priv = snd_soc_dai_get_drvdata(cpu_dai);
+ struct device *dev = i2s_sc_priv->dev;
+ u32 ctrl = 0, ctrl_mask = 0;
+
+ dev_dbg(dev, "format = 0x%x\n", fmt);
+
+ /* DAI hardware audio formats */
+ switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+ case SND_SOC_DAIFMT_I2S:
+ ctrl |= FIELD_PREP(I2S_CTRL_WS_MODE, 1) |
+ FIELD_PREP(I2S_CTRL_DATA_WS_DEL, 1);
+ ctrl &= ~(I2S_CTRL_DATA_ALIGN | I2S_CTRL_DATA_ORDER);
+ break;
+ case SND_SOC_DAIFMT_RIGHT_J:
+ ctrl |= FIELD_PREP(I2S_CTRL_WS_MODE, 1) |
+ FIELD_PREP(I2S_CTRL_DATA_WS_DEL, 0) |
+ I2S_CTRL_DATA_ALIGN;
+ ctrl &= ~I2S_CTRL_DATA_ORDER;
+ break;
+ case SND_SOC_DAIFMT_LEFT_J:
+ ctrl |= FIELD_PREP(I2S_CTRL_WS_MODE, 1) |
+ FIELD_PREP(I2S_CTRL_DATA_WS_DEL, 0);
+ ctrl &= ~(I2S_CTRL_DATA_ALIGN | I2S_CTRL_DATA_ORDER);
+ break;
+ case SND_SOC_DAIFMT_DSP_A:
+ ctrl |= FIELD_PREP(I2S_CTRL_WS_MODE, 0) |
+ FIELD_PREP(I2S_CTRL_DATA_WS_DEL, 1);
+ ctrl &= ~(I2S_CTRL_DATA_ALIGN | I2S_CTRL_DATA_ORDER);
+ break;
+ case SND_SOC_DAIFMT_DSP_B:
+ ctrl |= FIELD_PREP(I2S_CTRL_WS_MODE, 0) |
+ FIELD_PREP(I2S_CTRL_DATA_WS_DEL, 0);
+ ctrl &= ~(I2S_CTRL_DATA_ALIGN | I2S_CTRL_DATA_ORDER);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* DAI hardware signal polarity */
+ switch (fmt & SND_SOC_DAIFMT_INV_MASK) {
+ case SND_SOC_DAIFMT_NB_NF:
+ /* both normal clocks */
+ ctrl |= I2S_CTRL_SCK_POLAR;
+ ctrl &= ~I2S_CTRL_WS_POLAR;
+ break;
+ case SND_SOC_DAIFMT_NB_IF:
+ /* invert frame clock */
+ ctrl |= I2S_CTRL_SCK_POLAR;
+ ctrl |= I2S_CTRL_WS_POLAR;
+ break;
+ case SND_SOC_DAIFMT_IB_NF:
+ /* invert bit clock */
+ ctrl &= ~I2S_CTRL_SCK_POLAR;
+ ctrl &= ~I2S_CTRL_WS_POLAR;
+ break;
+ case SND_SOC_DAIFMT_IB_IF:
+ /* invert both clocks */
+ ctrl &= ~I2S_CTRL_SCK_POLAR;
+ ctrl |= I2S_CTRL_WS_POLAR;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* DAI hardware clock masters */
+ switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) {
+ case SND_SOC_DAIFMT_CBC_CFC:
+ ctrl &= ~I2S_CTRL_MS_CFG;
+ i2s_sc_priv->is_master_mode = false;
+ break;
+ case SND_SOC_DAIFMT_CBP_CFP:
+ ctrl |= I2S_CTRL_MS_CFG;
+ i2s_sc_priv->is_master_mode = true;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ ctrl_mask |= I2S_CTRL_WS_MODE | I2S_CTRL_DATA_WS_DEL | I2S_CTRL_DATA_ALIGN |
+ I2S_CTRL_DATA_ORDER | I2S_CTRL_SCK_POLAR | I2S_CTRL_WS_POLAR |
+ I2S_CTRL_MS_CFG;
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL, ctrl_mask, ctrl);
+
+ return 0;
+}
+
+static int cdns_i2s_sc_prepare(struct snd_pcm_substream *substream,
+ struct snd_soc_dai *cpu_dai)
+{
+ struct cdns_i2s_sc_priv *i2s_sc_priv = snd_soc_dai_get_drvdata(cpu_dai);
+
+ /*
+ * The transceiver clock must stay on across the FIFO reset below,
+ * while the trigger() of the other stream may gate it through the
+ * shared start/stop state machine. Serialize against it with &lock.
+ */
+ scoped_guard(spinlock_irqsave, &i2s_sc_priv->lock) {
+ /* Transceiver clock enable */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL, I2S_CTRL_I2S_STB, 0);
+
+ /*
+ * FIFO reset should meet two requirements
+ * 1) after i2s_stb clock on
+ * 2) before dma ready to receive trigger
+ */
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
+ /*
+ * Transmitter FIFO reset
+ * When LOW, FIFO pointer is reset to zero. Threshold levels for
+ * FIFO are unchanged. This bit is automatically set to HIGH after
+ * one clock cycle.
+ * Deassert then assert this bit here, since I2S_CTRL register is not
+ * volatile, would not read from hardware any longer. If not, it would
+ * clear tx fifo every time when write this register.
+ */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL, I2S_CTRL_FIFO_RST, 0);
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_FIFO_RST, I2S_CTRL_FIFO_RST);
+
+ /* Transmitter FIFO threshold set */
+ regmap_write(i2s_sc_priv->regmap, I2S_FIFO_AEMPTY,
+ i2s_sc_priv->devtype_data->tx_fifo_aempty_threshold);
+ regmap_write(i2s_sc_priv->regmap, I2S_FIFO_AFULL,
+ i2s_sc_priv->devtype_data->tx_fifo_afull_threshold);
+ } else {
+ /*
+ * Receiver FIFO reset
+ * When '0', RFIFO pointer is reset to zero. Threshold levels for RFIFO
+ * are unchanged. The bit is automatically set to '1' after one clock cycle.
+ * Deassert then assert this bit here, since I2S_CTRL_FDX register is not
+ * volatile, would not read from hardware any longer. If not, it would
+ * clear rx fifo every time when write this register.
+ */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL_FDX,
+ I2S_CTRL_FDX_RFIFO_RST, 0);
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL_FDX,
+ I2S_CTRL_FDX_RFIFO_RST, I2S_CTRL_FDX_RFIFO_RST);
+
+ /* Receiver FIFO threshold set */
+ regmap_write(i2s_sc_priv->regmap, I2S_FIFO_AEMPTY_FDR,
+ i2s_sc_priv->devtype_data->rx_fifo_aempty_threshold);
+ regmap_write(i2s_sc_priv->regmap, I2S_FIFO_AFULL_FDR,
+ i2s_sc_priv->devtype_data->rx_fifo_afull_threshold);
+ }
+ }
+
+ return 0;
+}
+
+static int cdns_i2s_sc_hw_params(struct snd_pcm_substream *substream,
+ struct snd_pcm_hw_params *params,
+ struct snd_soc_dai *cpu_dai)
+{
+ struct cdns_i2s_sc_priv *i2s_sc_priv = snd_soc_dai_get_drvdata(cpu_dai);
+ struct device *dev = i2s_sc_priv->dev;
+ u32 slots, slot_width, resolution, val;
+ u32 rate, sample_rate, frame_slots;
+ unsigned long i2s_clk_rate;
+ struct clk *clk_parent;
+ int ret;
+
+ /* Set sample slots, slot width and resolution */
+ slots = params_channels(params);
+ resolution = params_width(params);
+ dev_dbg(dev, "slots = %d, resolution = %d\n", slots, resolution);
+
+ if (i2s_sc_priv->is_tdm_mode) {
+ unsigned int mask, nch;
+
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
+ mask = i2s_sc_priv->tdm_config.tx_mask;
+ else
+ mask = i2s_sc_priv->tdm_config.rx_mask;
+
+ nch = hweight32(mask);
+ if (slots != nch) {
+ dev_err(i2s_sc_priv->dev,
+ "Channel number does not match active tdm slots: %d\n", nch);
+ return -EINVAL;
+ }
+
+ if (resolution != i2s_sc_priv->tdm_config.slot_width) {
+ dev_err(i2s_sc_priv->dev,
+ "Sample width does not match with specified tdm slot width\n");
+ return -EINVAL;
+ }
+
+ slot_width = i2s_sc_priv->tdm_config.slot_width;
+
+ frame_slots = i2s_sc_priv->tdm_config.slots;
+ } else {
+ /*
+ * I2S mode: mono maps onto the left audio channel, while
+ * the frame still carries two slots.
+ */
+ if (slots == 1) {
+ i2s_sc_priv->is_mono_in_i2s_mode = true;
+ slots = 2;
+ } else if (slots == 2) {
+ i2s_sc_priv->is_mono_in_i2s_mode = false;
+ } else {
+ dev_warn(i2s_sc_priv->dev,
+ "I2S mode supports up to two channels\n");
+ return -EOPNOTSUPP;
+ }
+
+ slot_width = 32;
+
+ frame_slots = slots;
+ }
+
+ dev_dbg(dev, "slot_width = %d\n", slot_width);
+
+ if (slot_width == 8) {
+ val = I2S_CTRL_CHN_WIDTH_8;
+ } else if (slot_width == 12) {
+ val = I2S_CTRL_CHN_WIDTH_12;
+ } else if (slot_width == 16) {
+ val = I2S_CTRL_CHN_WIDTH_16;
+ } else if (slot_width == 18) {
+ val = I2S_CTRL_CHN_WIDTH_18;
+ } else if (slot_width == 20) {
+ val = I2S_CTRL_CHN_WIDTH_20;
+ } else if (slot_width == 24) {
+ val = I2S_CTRL_CHN_WIDTH_24;
+ } else if (slot_width == 28) {
+ val = I2S_CTRL_CHN_WIDTH_28;
+ } else if (slot_width == 32) {
+ val = I2S_CTRL_CHN_WIDTH_32;
+ } else {
+ dev_err(i2s_sc_priv->dev, "Slot width is invalid value\n");
+ return -EINVAL;
+ }
+
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL, I2S_CTRL_CHN_WIDTH,
+ FIELD_PREP(I2S_CTRL_CHN_WIDTH, val));
+
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
+ regmap_write(i2s_sc_priv->regmap, I2S_SRES,
+ FIELD_PREP(I2S_SRES_RESOLUTION, (resolution - 1)));
+ else
+ regmap_write(i2s_sc_priv->regmap, I2S_SRES_FDR,
+ FIELD_PREP(I2S_SRES_FDR_RRESOLUTION, (resolution - 1)));
+
+ rate = params_rate(params);
+ dev_dbg(dev, "rate = %d\n", rate);
+
+ /* switch clk mux to select the appropriate clk parent */
+ if (rate % 8000 == 0) {
+ /* Sampling rate is a multiple of 8KHz, select "audio-clk0" */
+ clk_parent = i2s_sc_priv->clks[AUDIO_CLK0];
+
+ ret = clk_set_parent(i2s_sc_priv->clk_i2s, clk_parent);
+ } else if (rate % 11025 == 0) {
+ /* Sampling rate is a multiple of 11.025KHz, select "audio-clk2" */
+ clk_parent = i2s_sc_priv->clks[AUDIO_CLK2];
+
+ ret = clk_set_parent(i2s_sc_priv->clk_i2s, clk_parent);
+ } else {
+ dev_err(i2s_sc_priv->dev, "Invalid sample rate\n");
+ return -EINVAL;
+ }
+ if (ret) {
+ dev_err(i2s_sc_priv->dev, "Failed to set i2s clock parent\n");
+ return ret;
+ }
+
+ i2s_clk_rate = clk_get_rate(i2s_sc_priv->clk_i2s);
+ dev_dbg(dev, "i2s clk rate = %lu\n", i2s_clk_rate);
+
+ if (i2s_clk_rate < rate * slot_width * frame_slots * I2S_CLK_RATIO_MIN) {
+ dev_err(i2s_sc_priv->dev,
+ "clk freq %lu is too low, must >= (%d * %d * %d * %d = %d)\n",
+ i2s_clk_rate, rate, slot_width, frame_slots, I2S_CLK_RATIO_MIN,
+ rate * slot_width * frame_slots * I2S_CLK_RATIO_MIN);
+ return -EINVAL;
+ }
+
+ /* Set sample rate */
+ if (i2s_sc_priv->is_master_mode) {
+ sample_rate = DIV_ROUND_CLOSEST(i2s_clk_rate, (rate * frame_slots * slot_width));
+ regmap_write(i2s_sc_priv->regmap, I2S_SRATE,
+ FIELD_PREP(I2S_SRATE_SAMPLE_RATE, sample_rate));
+ }
+
+ return 0;
+}
+
+static int cdns_i2s_sc_trigger(struct snd_pcm_substream *substream,
+ int cmd, struct snd_soc_dai *cpu_dai)
+{
+ struct cdns_i2s_sc_priv *i2s_sc_priv = snd_soc_dai_get_drvdata(cpu_dai);
+
+ switch (cmd) {
+ case SNDRV_PCM_TRIGGER_START:
+ case SNDRV_PCM_TRIGGER_RESUME:
+ case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
+ scoped_guard(spinlock_irqsave, &i2s_sc_priv->lock) {
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
+ cdns_i2s_sc_tx_config(i2s_sc_priv, true);
+ else
+ cdns_i2s_sc_rx_config(i2s_sc_priv, true);
+ }
+ break;
+ case SNDRV_PCM_TRIGGER_STOP:
+ case SNDRV_PCM_TRIGGER_SUSPEND:
+ case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
+ scoped_guard(spinlock_irqsave, &i2s_sc_priv->lock) {
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
+ cdns_i2s_sc_tx_config(i2s_sc_priv, false);
+ else
+ cdns_i2s_sc_rx_config(i2s_sc_priv, false);
+ }
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int cdns_i2s_sc_dai_probe(struct snd_soc_dai *cpu_dai)
+{
+ struct cdns_i2s_sc_priv *i2s_sc_priv = snd_soc_dai_get_drvdata(cpu_dai);
+ u32 ctrl = 0;
+
+ /*
+ * Transceiver disable
+ * Transceiver clock disable
+ * All interrupts masked
+ */
+ ctrl &= ~(I2S_CTRL_I2S_EN | I2S_CTRL_INTREQ_MASK);
+ ctrl |= I2S_CTRL_I2S_STB;
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_I2S_EN | I2S_CTRL_INTREQ_MASK |
+ I2S_CTRL_LR_PACK | I2S_CTRL_I2S_STB,
+ ctrl);
+
+ /*
+ * Full-duplex mode disable
+ * Full-duplex mode transmitter disable
+ * Full-duplex mode receiver disable
+ */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_CTRL_FDX,
+ I2S_CTRL_FDX_FULL_DUPLEX | I2S_CTRL_FDX_I2S_FTX_EN |
+ I2S_CTRL_FDX_I2S_FRX_EN,
+ 0);
+
+ /* TDM mode disable, default works in standard stereo I2S mode */
+ regmap_update_bits(i2s_sc_priv->regmap, I2S_TDM_CTRL, I2S_TDM_CTRL_TDM_EN, 0);
+
+ snd_soc_dai_init_dma_data(cpu_dai, &i2s_sc_priv->playback_dma_data,
+ &i2s_sc_priv->capture_dma_data);
+
+ snd_soc_dai_set_drvdata(cpu_dai, i2s_sc_priv);
+
+ return 0;
+}
+
+static const struct snd_soc_dai_ops cdns_i2s_sc_dai_ops = {
+ .probe = cdns_i2s_sc_dai_probe,
+ .set_tdm_slot = cdns_i2s_sc_set_tdm_slot,
+ .set_fmt = cdns_i2s_sc_set_fmt,
+ .hw_params = cdns_i2s_sc_hw_params,
+ .prepare = cdns_i2s_sc_prepare,
+ .trigger = cdns_i2s_sc_trigger,
+};
+
+static struct snd_soc_dai_driver cdns_i2s_sc_dai = {
+ .name = "i2s-sc-aif",
+ .playback = {
+ .stream_name = "I2S-SC-Playback",
+ .channels_min = 1,
+ .channels_max = 16,
+ .rates = SNDRV_PCM_RATE_8000_192000,
+ .formats = (SNDRV_PCM_FMTBIT_S16_LE |
+ SNDRV_PCM_FMTBIT_S24_LE |
+ SNDRV_PCM_FMTBIT_S32_LE),
+ },
+ .capture = {
+ .stream_name = "I2S-SC-Capture",
+ .channels_min = 1,
+ .channels_max = 16,
+ .rates = SNDRV_PCM_RATE_8000_192000,
+ .formats = (SNDRV_PCM_FMTBIT_S16_LE |
+ SNDRV_PCM_FMTBIT_S24_LE |
+ SNDRV_PCM_FMTBIT_S32_LE),
+ },
+ .ops = &cdns_i2s_sc_dai_ops,
+ .symmetric_rate = 1,
+ .symmetric_channels = 1,
+};
+
+static const struct snd_soc_component_driver cdns_i2s_sc_component = {
+ .name = DRV_NAME,
+ .legacy_dai_naming = 1,
+};
+
+static const struct reg_default cdns_i2s_sc_reg_defaults[] = {
+ { I2S_CTRL, 0x000001b8 },
+ { I2S_CTRL_FDX, 0x00000010 },
+ { I2S_SRES, 0x00000000 },
+ { I2S_SRES_FDR, 0x00000000 },
+ { I2S_SRATE, 0x00000000 },
+ { I2S_STAT, 0x0003000c },
+ { I2S_FIFO_LEVEL, 0x00000000 },
+ { I2S_FIFO_AEMPTY, 0x00000000 },
+ { I2S_FIFO_AFULL, 0x0000000f },
+ { I2S_FIFO_LEVEL_FDR, 0x00000000 },
+ { I2S_FIFO_AEMPTY_FDR, 0x00000000 },
+ { I2S_FIFO_AFULL_FDR, 0x0000000f },
+ { I2S_TDM_CTRL, 0xffff0000 },
+ { I2S_TDM_FD_DIR, 0x0000ffff },
+};
+
+static bool cdns_i2s_sc_readable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case I2S_CTRL:
+ case I2S_CTRL_FDX:
+ case I2S_SRES:
+ case I2S_SRES_FDR:
+ case I2S_SRATE:
+ case I2S_STAT:
+ case I2S_FIFO_LEVEL:
+ case I2S_FIFO_AEMPTY:
+ case I2S_FIFO_AFULL:
+ case I2S_FIFO_LEVEL_FDR:
+ case I2S_FIFO_AEMPTY_FDR:
+ case I2S_FIFO_AFULL_FDR:
+ case I2S_TDM_CTRL:
+ case I2S_TDM_FD_DIR:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool cdns_i2s_sc_writeable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case I2S_CTRL:
+ case I2S_CTRL_FDX:
+ case I2S_SRES:
+ case I2S_SRES_FDR:
+ case I2S_SRATE:
+ case I2S_STAT:
+ case I2S_FIFO_AEMPTY:
+ case I2S_FIFO_AFULL:
+ case I2S_FIFO_AEMPTY_FDR:
+ case I2S_FIFO_AFULL_FDR:
+ case I2S_TDM_CTRL:
+ case I2S_TDM_FD_DIR:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool cdns_i2s_sc_volatile_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case I2S_STAT:
+ case I2S_FIFO_LEVEL:
+ case I2S_FIFO_LEVEL_FDR:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static const struct regmap_config cdns_i2s_sc_regmap_config = {
+ .reg_bits = 32,
+ .reg_stride = 4,
+ .val_bits = 32,
+
+ .max_register = I2S_TDM_FD_DIR,
+ .reg_defaults = cdns_i2s_sc_reg_defaults,
+ .num_reg_defaults = ARRAY_SIZE(cdns_i2s_sc_reg_defaults),
+ .readable_reg = cdns_i2s_sc_readable_reg,
+ .writeable_reg = cdns_i2s_sc_writeable_reg,
+ .volatile_reg = cdns_i2s_sc_volatile_reg,
+ .cache_type = REGCACHE_FLAT,
+};
+
+static void cdns_i2s_sc_rst(struct cdns_i2s_sc_priv *i2s_sc_priv)
+{
+ /* reset */
+ reset_control_assert(i2s_sc_priv->i2s_rst);
+
+ usleep_range(1, 2);
+
+ /* release reset */
+ reset_control_deassert(i2s_sc_priv->i2s_rst);
+}
+
+static int cdns_i2s_sc_probe(struct platform_device *pdev)
+{
+ struct cdns_i2s_sc_priv *i2s_sc_priv;
+ struct resource *res;
+ void __iomem *base;
+ int i, irq, ret;
+
+ i2s_sc_priv = devm_kzalloc(&pdev->dev, sizeof(*i2s_sc_priv), GFP_KERNEL);
+ if (!i2s_sc_priv)
+ return -ENOMEM;
+
+ spin_lock_init(&i2s_sc_priv->lock);
+
+ platform_set_drvdata(pdev, i2s_sc_priv);
+
+ base = devm_platform_get_and_ioremap_resource(pdev, 0, &res);
+ if (IS_ERR(base))
+ return PTR_ERR(base);
+
+ i2s_sc_priv->dev = &pdev->dev;
+ i2s_sc_priv->devtype_data = device_get_match_data(&pdev->dev);
+ if (!i2s_sc_priv->devtype_data)
+ return -ENODEV;
+
+ i2s_sc_priv->regmap = devm_regmap_init_mmio(&pdev->dev, base,
+ &cdns_i2s_sc_regmap_config);
+ if (IS_ERR(i2s_sc_priv->regmap))
+ return dev_err_probe(&pdev->dev, PTR_ERR(i2s_sc_priv->regmap),
+ "Failed to initialize managed register map\n");
+
+ i2s_sc_priv->clk_hst = devm_clk_get(&pdev->dev, "hst");
+ if (IS_ERR(i2s_sc_priv->clk_hst))
+ return dev_err_probe(&pdev->dev, PTR_ERR(i2s_sc_priv->clk_hst),
+ "Failed to get hst clock\n");
+
+ i2s_sc_priv->clk_i2s = devm_clk_get(&pdev->dev, "i2s");
+ if (IS_ERR(i2s_sc_priv->clk_i2s))
+ return dev_err_probe(&pdev->dev, PTR_ERR(i2s_sc_priv->clk_i2s),
+ "Failed to get i2s clock\n");
+
+ for (i = 0; i < AUDIO_CLK_NUM; i++) {
+ i2s_sc_priv->clks[i] = devm_clk_get(&pdev->dev,
+ cdns_i2s_sc_clk_pll_names[i]);
+ if (IS_ERR(i2s_sc_priv->clks[i]))
+ return dev_err_probe(&pdev->dev, PTR_ERR(i2s_sc_priv->clks[i]),
+ "Failed to get %s clock\n",
+ cdns_i2s_sc_clk_pll_names[i]);
+ }
+
+ i2s_sc_priv->i2s_rst = devm_reset_control_get_exclusive(&pdev->dev, NULL);
+ if (IS_ERR(i2s_sc_priv->i2s_rst))
+ return dev_err_probe(&pdev->dev, PTR_ERR(i2s_sc_priv->i2s_rst),
+ "Failed to get reset control\n");
+
+ i2s_sc_priv->playback_dma_data.addr = res->start + I2S_FIFO_ADDRESS;
+ /* Buswidth will be set by framework at runtime */
+ i2s_sc_priv->playback_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_UNDEFINED;
+ i2s_sc_priv->playback_dma_data.maxburst = 4;
+
+ i2s_sc_priv->capture_dma_data.addr = res->start + I2S_FIFO_ADDRESS;
+ /* Buswidth will be set by framework at runtime */
+ i2s_sc_priv->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_UNDEFINED;
+ i2s_sc_priv->capture_dma_data.maxburst = 4;
+
+ ret = devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret,
+ "Failed to register dmaengine component\n");
+
+ pm_runtime_get_noresume(&pdev->dev);
+ pm_runtime_set_active(&pdev->dev);
+ ret = devm_pm_runtime_enable(&pdev->dev);
+ if (ret) {
+ pm_runtime_put_noidle(&pdev->dev);
+ return dev_err_probe(&pdev->dev, ret, "Failed to enable runtime PM\n");
+ }
+
+ ret = cdns_i2s_sc_clks_enable(i2s_sc_priv);
+ if (ret) {
+ pm_runtime_put_noidle(&pdev->dev);
+ return dev_err_probe(&pdev->dev, ret, "Failed to enable clocks\n");
+ }
+
+ cdns_i2s_sc_rst(i2s_sc_priv);
+
+ irq = platform_get_irq(pdev, 0);
+ if (irq < 0) {
+ ret = irq;
+ goto err_disable_clks;
+ }
+ i2s_sc_priv->irq = irq;
+
+ ret = devm_request_irq(&pdev->dev, irq, cdns_i2s_sc_isr, 0,
+ dev_name(&pdev->dev), i2s_sc_priv);
+ if (ret) {
+ dev_err(&pdev->dev, "Failed to request irq: %d\n", ret);
+ goto err_disable_clks;
+ }
+
+ /*
+ * Let pm_runtime_put_sync() disable the clocks, and take regcache effect.
+ * If CONFIG_PM is not enabled, the clock will stay powered.
+ */
+ pm_runtime_put_sync(&pdev->dev);
+
+ ret = devm_snd_soc_register_component(&pdev->dev, &cdns_i2s_sc_component,
+ &cdns_i2s_sc_dai, 1);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret, "Failed to register soc component\n");
+
+ return 0;
+
+err_disable_clks:
+ cdns_i2s_sc_clks_disable(i2s_sc_priv);
+ pm_runtime_put_noidle(&pdev->dev);
+ return ret;
+}
+
+static int cdns_i2s_sc_runtime_suspend(struct device *dev)
+{
+ struct cdns_i2s_sc_priv *i2s_sc_priv = dev_get_drvdata(dev);
+
+ /*
+ * Disable the IRQ before switching the regcache to cache-only, so an
+ * in-flight handler can never access the registers through a
+ * cache-only regmap. disable_irq() waits for the handler to finish.
+ */
+ disable_irq(i2s_sc_priv->irq);
+ regcache_cache_only(i2s_sc_priv->regmap, true);
+ cdns_i2s_sc_clks_disable(i2s_sc_priv);
+
+ return 0;
+}
+
+static int cdns_i2s_sc_runtime_resume(struct device *dev)
+{
+ struct cdns_i2s_sc_priv *i2s_sc_priv = dev_get_drvdata(dev);
+ int ret;
+
+ ret = cdns_i2s_sc_clks_enable(i2s_sc_priv);
+ if (ret) {
+ dev_err(dev, "Failed to enable clocks:%d\n", ret);
+ return ret;
+ }
+
+ cdns_i2s_sc_rst(i2s_sc_priv);
+
+ regcache_cache_only(i2s_sc_priv->regmap, false);
+ regcache_mark_dirty(i2s_sc_priv->regmap);
+
+ ret = regcache_sync(i2s_sc_priv->regmap);
+ if (ret) {
+ dev_err(dev, "Failed to sync regcache: %d\n", ret);
+ regcache_cache_only(i2s_sc_priv->regmap, true);
+ cdns_i2s_sc_clks_disable(i2s_sc_priv);
+ return ret;
+ }
+
+ /* Enable the IRQ only after the regcache is back in sync */
+ enable_irq(i2s_sc_priv->irq);
+
+ return 0;
+}
+
+static int cdns_i2s_sc_system_suspend(struct device *dev)
+{
+ int ret;
+
+ ret = pm_runtime_force_suspend(dev);
+ if (ret)
+ return ret;
+
+ pinctrl_pm_select_sleep_state(dev);
+
+ return 0;
+}
+
+static int cdns_i2s_sc_system_resume(struct device *dev)
+{
+ int ret;
+
+ pinctrl_pm_select_default_state(dev);
+
+ ret = pm_runtime_force_resume(dev);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static const struct dev_pm_ops cdns_i2s_sc_pm_ops = {
+ RUNTIME_PM_OPS(cdns_i2s_sc_runtime_suspend, cdns_i2s_sc_runtime_resume, NULL)
+ SYSTEM_SLEEP_PM_OPS(cdns_i2s_sc_system_suspend, cdns_i2s_sc_system_resume)
+};
+
+static const struct cdns_i2s_sc_devtype_data sky1_devtype_data = {
+ .rx_fifo_aempty_threshold = 4,
+ .rx_fifo_afull_threshold = 12,
+ .tx_fifo_aempty_threshold = 4,
+ .tx_fifo_afull_threshold = 12,
+};
+
+static const struct of_device_id cdns_i2s_sc_of_match[] = {
+ { .compatible = "cix,sky1-i2s-sc", .data = &sky1_devtype_data },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, cdns_i2s_sc_of_match);
+
+static struct platform_driver cdns_i2s_sc_driver = {
+ .probe = cdns_i2s_sc_probe,
+ .driver = {
+ .name = DRV_NAME,
+ .pm = pm_ptr(&cdns_i2s_sc_pm_ops),
+ .of_match_table = cdns_i2s_sc_of_match,
+ },
+};
+module_platform_driver(cdns_i2s_sc_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Joakim Zhang <joakim.zhang@cixtech.com>");
+MODULE_DESCRIPTION("Cadence I2S-SC Controller Driver");
--
2.50.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 3/4] ASoC: dt-bindings: Add Cadence I2S-MC controller
2026-10-09 9:25 [PATCH v3 0/4] ASoC: cdns: Add Cadence I2S-SC and I2S-MC controller support joakim.zhang
2026-10-09 9:25 ` [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller joakim.zhang
2026-10-09 9:25 ` [PATCH v3 2/4] ASoC: cdns: Add Cadence I2S-SC controller driver joakim.zhang
@ 2026-10-09 9:25 ` joakim.zhang
2026-10-09 9:25 ` [PATCH v3 4/4] ASoC: cdns: Add Cadence I2S-MC controller driver joakim.zhang
3 siblings, 0 replies; 11+ messages in thread
From: joakim.zhang @ 2026-10-09 9:25 UTC (permalink / raw)
To: lgirdwood, broonie, robh, krzk+dt, conor+dt, perex, tiwai,
p.zabel
Cc: cix-kernel-upstream, linux-sound, devicetree, linux-kernel,
linux-arm-kernel, Joakim Zhang
From: Joakim Zhang <joakim.zhang@cixtech.com>
Add the DT binding for the Cadence I2S-MC multi-channel controller
integrated in the CIX SKY1 audio subsystem. The controller exposes a
transmit DAI and a receive DAI and moves audio through a configurable
set of its 8 data pins. Which pins are wired out and in which
direction is fixed by the board design and described by the
cix,pin-out-num, cix,pin-rx-mask and cix,pin-tx-mask properties.
Signed-off-by: Joakim Zhang <joakim.zhang@cixtech.com>
---
.../bindings/sound/cix,sky1-i2s-mc.yaml | 123 ++++++++++++++++++
1 file changed, 123 insertions(+)
create mode 100644 Documentation/devicetree/bindings/sound/cix,sky1-i2s-mc.yaml
diff --git a/Documentation/devicetree/bindings/sound/cix,sky1-i2s-mc.yaml b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-mc.yaml
new file mode 100644
index 000000000000..22528bb2940e
--- /dev/null
+++ b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-mc.yaml
@@ -0,0 +1,123 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/sound/cix,sky1-i2s-mc.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Cadence I2S-MC controller
+
+maintainers:
+ - Joakim Zhang <joakim.zhang@cixtech.com>
+
+description: |
+ The Cadence I2S-MC multi-channel controller integrated in the CIX SKY1
+ audio subsystem. It provides up to 8-channel playback and capture through
+ a configurable number of I2S data pins, and exposes two DAIs: a transmit
+ DAI and a receive DAI.
+
+allOf:
+ - $ref: dai-common.yaml#
+
+properties:
+ compatible:
+ const: cix,sky1-i2s-mc
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ clocks:
+ maxItems: 4
+
+ clock-names:
+ items:
+ - const: hst
+ - const: i2s
+ - const: audio-clk0
+ - const: audio-clk2
+
+ resets:
+ maxItems: 1
+
+ dmas:
+ minItems: 1
+ maxItems: 2
+ description:
+ One DMA channel for instances wired for a single direction, either
+ "tx" or "rx", or two channels for both transmit and receive.
+
+ dma-names:
+ minItems: 1
+ maxItems: 2
+ items:
+ enum: [ tx, rx ]
+
+ "#sound-dai-cells":
+ const: 1
+ description:
+ Cell selects the DAI, 0 for the transmit DAI (i2s-mc-aif1) and
+ 1 for the receive DAI (i2s-mc-aif2).
+
+ cix,pin-out-num:
+ $ref: /schemas/types.yaml#/definitions/uint8
+ minimum: 1
+ maximum: 8
+ description:
+ Number of I2S data output pins wired on this instance. Which of
+ the 8 controller data pins are wired out is fixed by the board
+ design and can be any subset.
+
+ cix,pin-rx-mask:
+ $ref: /schemas/types.yaml#/definitions/uint8
+ description:
+ Bit mask of I2S data pins used for capture, bit N selects data
+ pin N.
+
+ cix,pin-tx-mask:
+ $ref: /schemas/types.yaml#/definitions/uint8
+ description:
+ Bit mask of I2S data pins used for playback, bit N selects data
+ pin N. The rx and tx masks must not overlap, and the number of
+ set bits must not exceed cix,pin-out-num.
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - clocks
+ - clock-names
+ - resets
+ - dmas
+ - dma-names
+ - "#sound-dai-cells"
+ - cix,pin-out-num
+ - cix,pin-rx-mask
+ - cix,pin-tx-mask
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+ audss_dmac: dma-controller {
+ #dma-cells = <1>;
+ };
+
+ i2s@7050000 {
+ compatible = "cix,sky1-i2s-mc";
+ reg = <0x7050000 0x10000>;
+ interrupts = <GIC_SPI 222 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&audss_cru 17>, <&audss_cru 27>,
+ <&scmi_clk 100>, <&scmi_clk 101>;
+ clock-names = "hst", "i2s", "audio-clk0", "audio-clk2";
+ resets = <&audss_cru 1>;
+ dmas = <&audss_dmac 2>, <&audss_dmac 3>;
+ dma-names = "tx", "rx";
+ #sound-dai-cells = <1>;
+ cix,pin-out-num = /bits/ 8 <2>;
+ cix,pin-rx-mask = /bits/ 8 <0x1>;
+ cix,pin-tx-mask = /bits/ 8 <0x2>;
+ };
--
2.50.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v3 4/4] ASoC: cdns: Add Cadence I2S-MC controller driver
2026-10-09 9:25 [PATCH v3 0/4] ASoC: cdns: Add Cadence I2S-SC and I2S-MC controller support joakim.zhang
` (2 preceding siblings ...)
2026-10-09 9:25 ` [PATCH v3 3/4] ASoC: dt-bindings: Add Cadence I2S-MC controller joakim.zhang
@ 2026-10-09 9:25 ` joakim.zhang
2026-10-09 9:38 ` sashiko-bot
3 siblings, 1 reply; 11+ messages in thread
From: joakim.zhang @ 2026-10-09 9:25 UTC (permalink / raw)
To: lgirdwood, broonie, robh, krzk+dt, conor+dt, perex, tiwai,
p.zabel
Cc: cix-kernel-upstream, linux-sound, devicetree, linux-kernel,
linux-arm-kernel, Joakim Zhang
From: Joakim Zhang <joakim.zhang@cixtech.com>
Add support for the Cadence I2S-MC multi-channel controller found in
the CIX SKY1 audio subsystem.
The controller moves up to 16 channels through a configurable set of
its 8 data pins, each pin carrying two slots. Wired-out pins may sit
at any pin index and are described by cix,pin-out-num, cix,pin-rx-mask
and cix,pin-tx-mask. The driver exposes a transmit DAI and a receive
DAI which can act as clock provider or consumer independently, and
streams with fewer channels than the wired pins narrow the enabled
pin mask at runtime.
The driver registers the dmaengine PCM helper, selects the audio
reference clock parent for the 8 kHz or 11.025 kHz sample rate family
and keeps the minimum functional-clock to bit-clock ratio required
for safe clock domain crossing, with an extra margin cycle when the
bit clock is sourced externally. Runtime and system suspend/resume
restore the registers through the regcache.
Signed-off-by: Joakim Zhang <joakim.zhang@cixtech.com>
---
sound/soc/cdns/Kconfig | 10 +
sound/soc/cdns/Makefile | 3 +
sound/soc/cdns/cdns-i2s-mc.c | 1127 ++++++++++++++++++++++++++++++++++
3 files changed, 1140 insertions(+)
create mode 100644 sound/soc/cdns/cdns-i2s-mc.c
diff --git a/sound/soc/cdns/Kconfig b/sound/soc/cdns/Kconfig
index 631e2e0764cb..aa5d35992a81 100644
--- a/sound/soc/cdns/Kconfig
+++ b/sound/soc/cdns/Kconfig
@@ -9,3 +9,13 @@ config SND_SOC_CDNS_I2S_SC
controller found in the CIX SKY1 audio subsystem. It provides
full-duplex stereo playback and capture in standard I2S and
TDM modes with up to 16 slots.
+
+config SND_SOC_CDNS_I2S_MC
+ tristate "Cadence I2S-MC controller support"
+ depends on ARCH_CIX || COMPILE_TEST
+ select SND_SOC_GENERIC_DMAENGINE_PCM
+ help
+ Say Y or M if you want to add support for the Cadence I2S-MC
+ multi-channel controller found in the CIX SKY1 audio subsystem.
+ It provides multi-channel playback and capture through a
+ configurable number of I2S data pins.
diff --git a/sound/soc/cdns/Makefile b/sound/soc/cdns/Makefile
index 6abfeef29e43..e440bb2bb5ca 100644
--- a/sound/soc/cdns/Makefile
+++ b/sound/soc/cdns/Makefile
@@ -2,3 +2,6 @@
snd-soc-cdns-i2s-sc-y := cdns-i2s-sc.o
obj-$(CONFIG_SND_SOC_CDNS_I2S_SC) += snd-soc-cdns-i2s-sc.o
+
+snd-soc-cdns-i2s-mc-y := cdns-i2s-mc.o
+obj-$(CONFIG_SND_SOC_CDNS_I2S_MC) += snd-soc-cdns-i2s-mc.o
diff --git a/sound/soc/cdns/cdns-i2s-mc.c b/sound/soc/cdns/cdns-i2s-mc.c
new file mode 100644
index 000000000000..e9a0880a0616
--- /dev/null
+++ b/sound/soc/cdns/cdns-i2s-mc.c
@@ -0,0 +1,1127 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright 2026 Cix Technology Group Co., Ltd.
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/module.h>
+#include <linux/pinctrl/consumer.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/regmap.h>
+#include <linux/reset.h>
+#include <sound/dmaengine_pcm.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+
+/* Special Function Registers */
+/* Control Register */
+#define I2S_CTRL 0x00
+#define I2S_CTRL_RSYNC_LOOP_BACK BIT(28)
+#define I2S_CTRL_TSYNC_LOOP_BACK BIT(27)
+#define I2S_CTRL_RSYNC_RST BIT(26)
+#define I2S_CTRL_TSYNC_RST BIT(25)
+#define I2S_CTRL_RFIFO_RST BIT(24)
+#define I2S_CTRL_TFIFO_RST BIT(23)
+#define I2S_CTRL_R_MS BIT(22)
+#define I2S_CTRL_T_MS BIT(21)
+#define I2S_CTRL_SFR_RST BIT(20)
+#define I2S_CTRL_LOOP_BACK_6_7 BIT(19)
+#define I2S_CTRL_LOOP_BACK_4_5 BIT(18)
+#define I2S_CTRL_LOOP_BACK_2_3 BIT(17)
+#define I2S_CTRL_LOOP_BACK_0_1 BIT(16)
+#define I2S_CTRL_TR_CFG GENMASK(15, 8)
+#define I2S_CTRL_I2S_EN GENMASK(7, 0)
+
+/* Interrupt Status Register */
+#define I2S_INTR_STAT 0x04
+#define I2S_INTR_STAT_RFIFO_AFULL BIT(15)
+#define I2S_INTR_STAT_RFIFO_FULL BIT(14)
+#define I2S_INTR_STAT_RFIFO_AEMPTY BIT(13)
+#define I2S_INTR_STAT_RFIFO_EMPTY BIT(12)
+#define I2S_INTR_STAT_TFIFO_AFULL BIT(11)
+#define I2S_INTR_STAT_TFIFO_FULL BIT(10)
+#define I2S_INTR_STAT_TFIFO_AEMPTY BIT(9)
+#define I2S_INTR_STAT_TFIFO_EMPTY BIT(8)
+#define I2S_INTR_STAT_OVERR_CODE GENMASK(7, 5)
+#define I2S_INTR_STAT_RDATA_OVERR BIT(4)
+#define I2S_INTR_STAT_UNDERR_CODE GENMASK(3, 1)
+#define I2S_INTR_STAT_TDATA_UNDERR BIT(0)
+
+/* Sample Rate And Resolution Control Register */
+#define I2S_SRR 0x08
+#define I2S_SRR_RRESOLUTION GENMASK(31, 27)
+#define I2S_SRR_RSAMPLE_RATE GENMASK(26, 16)
+#define I2S_SRR_TRESOLUTION GENMASK(15, 11)
+#define I2S_SRR_TSAMPLE_RATE GENMASK(10, 0)
+
+/* Clock Strobes And Interrupt Masks Control Register */
+#define I2S_CID_CTRL 0x0c
+#define I2S_CID_CTRL_RFIFO_AFULL_MASK BIT(31)
+#define I2S_CID_CTRL_RFIFO_FULL_MASK BIT(30)
+#define I2S_CID_CTRL_RFIFO_AEMPTY_MASK BIT(29)
+#define I2S_CID_CTRL_RFIFO_EMPTY_MASK BIT(28)
+#define I2S_CID_CTRL_TFIFO_AFULL_MASK BIT(27)
+#define I2S_CID_CTRL_TFIFO_FULL_MASK BIT(26)
+#define I2S_CID_CTRL_TFIFO_AEMPTY_MASK BIT(25)
+#define I2S_CID_CTRL_TFIFO_EMPTY_MASK BIT(24)
+#define I2S_CID_CTRL_I2S_MASK GENMASK(23, 16)
+#define I2S_CID_CTRL_INTREQ_MASK BIT(15)
+#define I2S_CID_CTRL_STROBE_RS BIT(9)
+#define I2S_CID_CTRL_STROBE_TS BIT(8)
+#define I2S_CID_CTRL_I2S_STROBE GENMASK(7, 0)
+
+/* Transmit FIFO Level Status Register, read only */
+#define I2S_TFIFO_STAT 0x10
+
+/* Receive FIFO Level Status Register, read only */
+#define I2S_RFIFO_STAT 0x14
+
+/* Transmit FIFO Thresholds Control Register */
+#define I2S_TFIFO_CTRL 0x18
+#define I2S_TFIFO_CTRL_TAFULL_THRESHOLD GENMASK(31, 16)
+#define I2S_TFIFO_CTRL_TAEMPTY_THRESHOLD GENMASK(15, 0)
+
+/* Receive FIFO Thresholds Control Register */
+#define I2S_RFIFO_CTRL 0x1c
+#define I2S_RFIFO_CTRL_RAFULL_THRESHOLD GENMASK(31, 16)
+#define I2S_RFIFO_CTRL_RAEMPTY_THRESHOLD GENMASK(15, 0)
+
+/* Device Configuration Register */
+#define I2S_DEV_CONF 0x20
+#define I2S_DEV_CONF_REC_WS_DSP_MODE BIT(11)
+#define I2S_DEV_CONF_REC_DATA_WS_DEL BIT(10)
+#define I2S_DEV_CONF_REC_I2S_ALIGN_LR BIT(9)
+#define I2S_DEV_CONF_REC_APB_ALIGN_LR BIT(8)
+#define I2S_DEV_CONF_REC_WS_POLAR BIT(7)
+#define I2S_DEV_CONF_REC_SCK_POLAR BIT(6)
+#define I2S_DEV_CONF_TRAN_WS_DSP_MODE BIT(5)
+#define I2S_DEV_CONF_TRAN_DATA_WS_DEL BIT(4)
+#define I2S_DEV_CONF_TRAN_I2S_ALIGN_LR BIT(3)
+#define I2S_DEV_CONF_TRAN_APB_ALIGN_LR BIT(2)
+#define I2S_DEV_CONF_TRAN_WS_POLAR BIT(1)
+#define I2S_DEV_CONF_TRAN_SCK_POLAR BIT(0)
+
+/* Status Register, read only */
+#define I2S_POLL_STAT 0x24
+#define I2S_POLL_STAT_RX_OVERRUN BIT(6)
+#define I2S_POLL_STAT_RX_AFULL BIT(5)
+#define I2S_POLL_STAT_RX_FULL BIT(4)
+#define I2S_POLL_STAT_TX_UNDERRUN BIT(2)
+#define I2S_POLL_STAT_TX_AEMPTY BIT(1)
+#define I2S_POLL_STAT_TX_EMPTY BIT(0)
+
+/* Transmit And Recevie FIFOs Address */
+#define I2S_FIFO_ADDRESS 0x3c
+
+/*
+ * The I2S functional clock must run at a minimum ratio above the audio
+ * bit clock to safely cross clock domains; a bit clock sourced
+ * externally (slave mode) needs one more cycle of margin. See the
+ * controller reference manual, section 3.3 "Clocks and Clock Domain
+ * Crossing".
+ */
+#define I2S_CLK_RATIO_MIN_MASTER 6
+#define I2S_CLK_RATIO_MIN_SLAVE 7
+
+#define DRV_NAME "cdns-i2s-mc"
+
+enum {
+ AUDIO_CLK0,
+ AUDIO_CLK2,
+ AUDIO_CLK_NUM,
+};
+
+static const char *cdns_i2s_mc_clk_pll_names[AUDIO_CLK_NUM] = {
+ [AUDIO_CLK0] = "audio-clk0",
+ [AUDIO_CLK2] = "audio-clk2",
+};
+
+enum {
+ I2S_MC_AIF1,
+ I2S_MC_AIF2,
+};
+
+struct cdns_i2s_mc_devtype_data {
+ u32 data_width;
+ u32 rfifo_aempty_threshold;
+ u32 rfifo_afull_threshold;
+ u32 tfifo_aempty_threshold;
+ u32 tfifo_afull_threshold;
+};
+
+struct cdns_i2s_mc_priv {
+ struct device *dev;
+ struct regmap *regmap;
+ struct reset_control *i2s_rst;
+
+ int irq;
+
+ struct clk *clk_hst;
+ struct clk *clk_i2s;
+ struct clk *clks[AUDIO_CLK_NUM];
+
+ struct snd_dmaengine_dai_dma_data capture_dma_data;
+ struct snd_dmaengine_dai_dma_data playback_dma_data;
+
+ const struct cdns_i2s_mc_devtype_data *devtype_data;
+
+ u8 pin_out_num;
+ u8 pin_rx_mask;
+ u8 pin_rx_mask_adjust;
+ u8 pin_tx_mask;
+ u8 pin_tx_mask_adjust;
+};
+
+static int cdns_i2s_mc_clks_enable(struct cdns_i2s_mc_priv *i2s_mc_priv)
+{
+ int ret;
+
+ ret = clk_prepare_enable(i2s_mc_priv->clk_hst);
+ if (ret)
+ return ret;
+
+ ret = clk_prepare_enable(i2s_mc_priv->clk_i2s);
+ if (ret)
+ clk_disable_unprepare(i2s_mc_priv->clk_hst);
+
+ return ret;
+}
+
+static void cdns_i2s_mc_clks_disable(struct cdns_i2s_mc_priv *i2s_mc_priv)
+{
+ clk_disable_unprepare(i2s_mc_priv->clk_i2s);
+ clk_disable_unprepare(i2s_mc_priv->clk_hst);
+}
+
+static void cdns_i2s_mc_adjust_pin_config(u8 *pin_mask, u32 slots)
+{
+ u8 mask = 0, num = 0;
+ int i;
+
+ /*
+ * Wired-out pins may sit at any index among the 8 data pins, so
+ * scan the whole mask and keep the lowest pins until enough slots
+ * are covered.
+ */
+ for (i = 0; i < BITS_PER_BYTE; i++) {
+ if (*pin_mask & (0x1 << i)) {
+ mask |= (0x1 << i);
+ if (++num == slots / 2) {
+ *pin_mask = mask;
+ break;
+ }
+ }
+ }
+}
+
+static void cdns_i2s_mc_rxtx_config(struct cdns_i2s_mc_priv *i2s_mc_priv, bool on,
+ u8 pin_mask, u32 strobe, u32 sync_rst)
+{
+ u32 irq_mask = 0, clk_mask = 0, i2s_mask = 0;
+
+ irq_mask |= FIELD_PREP(I2S_CID_CTRL_I2S_MASK, pin_mask);
+
+ clk_mask |= FIELD_PREP(I2S_CID_CTRL_I2S_STROBE, pin_mask) | strobe;
+
+ i2s_mask |= FIELD_PREP(I2S_CTRL_I2S_EN, pin_mask);
+
+ if (on) {
+ /* Data underrun/overrun interrupt unmask */
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CID_CTRL, irq_mask, irq_mask);
+
+ /* Data pins clock enable */
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CID_CTRL, clk_mask, 0);
+
+ /*
+ * Transmitter or receiver enable
+ * Synchronizing unit out of reset
+ */
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
+ i2s_mask | sync_rst,
+ i2s_mask | sync_rst);
+ } else {
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
+ i2s_mask | sync_rst, 0);
+
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CID_CTRL, clk_mask, clk_mask);
+
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CID_CTRL, irq_mask, 0);
+ }
+}
+
+static void cdns_i2s_mc_tx_config(struct cdns_i2s_mc_priv *i2s_mc_priv, bool on)
+{
+ cdns_i2s_mc_rxtx_config(i2s_mc_priv, on, i2s_mc_priv->pin_tx_mask_adjust,
+ I2S_CID_CTRL_STROBE_TS, I2S_CTRL_TSYNC_RST);
+}
+
+static void cdns_i2s_mc_rx_config(struct cdns_i2s_mc_priv *i2s_mc_priv, bool on)
+{
+ cdns_i2s_mc_rxtx_config(i2s_mc_priv, on, i2s_mc_priv->pin_rx_mask_adjust,
+ I2S_CID_CTRL_STROBE_RS, I2S_CTRL_RSYNC_RST);
+}
+
+static irqreturn_t cdns_i2s_mc_isr(int irq, void *devid)
+{
+ struct cdns_i2s_mc_priv *i2s_mc_priv = (struct cdns_i2s_mc_priv *)devid;
+ struct device *dev = i2s_mc_priv->dev;
+ u32 stat;
+
+ if (regmap_read(i2s_mc_priv->regmap, I2S_INTR_STAT, &stat))
+ return IRQ_NONE;
+
+ stat &= I2S_INTR_STAT_TDATA_UNDERR | I2S_INTR_STAT_RDATA_OVERR;
+ if (!stat)
+ return IRQ_NONE;
+
+ /* Clear the status */
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_INTR_STAT, stat, 0);
+
+ /* Transmitter status */
+ if (stat & I2S_INTR_STAT_TDATA_UNDERR)
+ dev_dbg(dev, "isr: tx data underrun\n");
+
+ /* Receiver status */
+ if (stat & I2S_INTR_STAT_RDATA_OVERR)
+ dev_dbg(dev, "isr: rx data overrun\n");
+
+ return IRQ_HANDLED;
+}
+
+static int cdns_i2s_mc_set_fmt(struct snd_soc_dai *cpu_dai, unsigned int fmt)
+{
+ struct cdns_i2s_mc_priv *i2s_mc_priv = snd_soc_dai_get_drvdata(cpu_dai);
+ struct device *dev = i2s_mc_priv->dev;
+ u32 ctrl = 0, dev_conf = 0;
+
+ dev_dbg(dev, "fmt = 0x%x\n", fmt);
+
+ /*
+ * Configure the data pin channel directions, shared by both DAIs:
+ * a set TR_CFG bit marks the pin as a transmitter channel, a clear
+ * one as a receiver channel. Program the combined value so the
+ * write is idempotent and independent of the set_fmt call order.
+ */
+ ctrl |= FIELD_PREP(I2S_CTRL_TR_CFG,
+ i2s_mc_priv->pin_tx_mask |
+ (u8)~i2s_mc_priv->pin_rx_mask);
+
+ if (cpu_dai->id == I2S_MC_AIF1) {
+ /* DAI hardware signal polarity */
+ switch (fmt & SND_SOC_DAIFMT_INV_MASK) {
+ case SND_SOC_DAIFMT_NB_NF:
+ /* both normal clocks */
+ dev_conf |= I2S_DEV_CONF_TRAN_SCK_POLAR;
+ dev_conf &= ~I2S_DEV_CONF_TRAN_WS_POLAR;
+ break;
+ case SND_SOC_DAIFMT_NB_IF:
+ /* invert frame clock */
+ dev_conf |= I2S_DEV_CONF_TRAN_SCK_POLAR;
+ dev_conf |= I2S_DEV_CONF_TRAN_WS_POLAR;
+ break;
+ case SND_SOC_DAIFMT_IB_NF:
+ /* invert bit clock */
+ dev_conf &= ~I2S_DEV_CONF_TRAN_SCK_POLAR;
+ dev_conf &= ~I2S_DEV_CONF_TRAN_WS_POLAR;
+ break;
+ case SND_SOC_DAIFMT_IB_IF:
+ /* invert both clocks */
+ dev_conf &= ~I2S_DEV_CONF_TRAN_SCK_POLAR;
+ dev_conf |= I2S_DEV_CONF_TRAN_WS_POLAR;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* DAI hardware audio formats */
+ switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+ case SND_SOC_DAIFMT_I2S:
+ dev_conf |= I2S_DEV_CONF_TRAN_I2S_ALIGN_LR;
+ dev_conf &= ~(I2S_DEV_CONF_TRAN_APB_ALIGN_LR |
+ I2S_DEV_CONF_TRAN_DATA_WS_DEL |
+ I2S_DEV_CONF_TRAN_WS_DSP_MODE);
+ break;
+ case SND_SOC_DAIFMT_RIGHT_J:
+ dev_conf |= I2S_DEV_CONF_TRAN_DATA_WS_DEL;
+ dev_conf &= ~(I2S_DEV_CONF_TRAN_I2S_ALIGN_LR |
+ I2S_DEV_CONF_TRAN_APB_ALIGN_LR |
+ I2S_DEV_CONF_TRAN_WS_DSP_MODE);
+ break;
+ case SND_SOC_DAIFMT_LEFT_J:
+ dev_conf |= I2S_DEV_CONF_TRAN_I2S_ALIGN_LR | I2S_DEV_CONF_TRAN_DATA_WS_DEL;
+ dev_conf &= ~(I2S_DEV_CONF_TRAN_APB_ALIGN_LR |
+ I2S_DEV_CONF_TRAN_WS_DSP_MODE);
+ break;
+ case SND_SOC_DAIFMT_DSP_A:
+ dev_conf |= I2S_DEV_CONF_TRAN_I2S_ALIGN_LR | I2S_DEV_CONF_TRAN_DATA_WS_DEL |
+ I2S_DEV_CONF_TRAN_WS_DSP_MODE;
+ dev_conf &= ~I2S_DEV_CONF_TRAN_APB_ALIGN_LR;
+ break;
+ case SND_SOC_DAIFMT_DSP_B:
+ dev_conf |= I2S_DEV_CONF_TRAN_I2S_ALIGN_LR | I2S_DEV_CONF_TRAN_WS_DSP_MODE;
+ dev_conf &= ~(I2S_DEV_CONF_TRAN_APB_ALIGN_LR |
+ I2S_DEV_CONF_TRAN_DATA_WS_DEL);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* DAI hardware clock masters */
+ switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) {
+ case SND_SOC_DAIFMT_CBC_CFC:
+ ctrl &= ~I2S_CTRL_T_MS;
+ break;
+ case SND_SOC_DAIFMT_CBP_CFP:
+ ctrl |= I2S_CTRL_T_MS;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_DEV_CONF,
+ I2S_DEV_CONF_TRAN_SCK_POLAR | I2S_DEV_CONF_TRAN_WS_POLAR |
+ I2S_DEV_CONF_TRAN_APB_ALIGN_LR | I2S_DEV_CONF_TRAN_I2S_ALIGN_LR |
+ I2S_DEV_CONF_TRAN_DATA_WS_DEL |
+ I2S_DEV_CONF_TRAN_WS_DSP_MODE, dev_conf);
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_T_MS | I2S_CTRL_TR_CFG, ctrl);
+ } else if (cpu_dai->id == I2S_MC_AIF2) {
+ /* DAI hardware signal polarity */
+ switch (fmt & SND_SOC_DAIFMT_INV_MASK) {
+ case SND_SOC_DAIFMT_NB_NF:
+ /* both normal clocks */
+ dev_conf |= I2S_DEV_CONF_REC_SCK_POLAR;
+ dev_conf &= ~I2S_DEV_CONF_REC_WS_POLAR;
+ break;
+ case SND_SOC_DAIFMT_NB_IF:
+ /* invert frame clock */
+ dev_conf |= I2S_DEV_CONF_REC_SCK_POLAR;
+ dev_conf |= I2S_DEV_CONF_REC_WS_POLAR;
+ break;
+ case SND_SOC_DAIFMT_IB_NF:
+ /* invert bit clock */
+ dev_conf &= ~I2S_DEV_CONF_REC_SCK_POLAR;
+ dev_conf &= ~I2S_DEV_CONF_REC_WS_POLAR;
+ break;
+ case SND_SOC_DAIFMT_IB_IF:
+ /* invert both clocks */
+ dev_conf &= ~I2S_DEV_CONF_REC_SCK_POLAR;
+ dev_conf |= I2S_DEV_CONF_REC_WS_POLAR;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* DAI hardware audio formats */
+ switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+ case SND_SOC_DAIFMT_I2S:
+ dev_conf |= I2S_DEV_CONF_REC_I2S_ALIGN_LR;
+ dev_conf &= ~(I2S_DEV_CONF_REC_APB_ALIGN_LR | I2S_DEV_CONF_REC_DATA_WS_DEL |
+ I2S_DEV_CONF_REC_WS_DSP_MODE);
+ break;
+ case SND_SOC_DAIFMT_RIGHT_J:
+ dev_conf |= I2S_DEV_CONF_REC_DATA_WS_DEL;
+ dev_conf &= ~(I2S_DEV_CONF_REC_I2S_ALIGN_LR |
+ I2S_DEV_CONF_REC_APB_ALIGN_LR |
+ I2S_DEV_CONF_REC_WS_DSP_MODE);
+ break;
+ case SND_SOC_DAIFMT_LEFT_J:
+ dev_conf |= I2S_DEV_CONF_REC_I2S_ALIGN_LR | I2S_DEV_CONF_REC_DATA_WS_DEL;
+ dev_conf &= ~(I2S_DEV_CONF_REC_APB_ALIGN_LR | I2S_DEV_CONF_REC_WS_DSP_MODE);
+ break;
+ case SND_SOC_DAIFMT_DSP_A:
+ dev_conf |= I2S_DEV_CONF_REC_I2S_ALIGN_LR | I2S_DEV_CONF_REC_DATA_WS_DEL |
+ I2S_DEV_CONF_REC_WS_DSP_MODE;
+ dev_conf &= ~I2S_DEV_CONF_REC_APB_ALIGN_LR;
+ break;
+ case SND_SOC_DAIFMT_DSP_B:
+ dev_conf |= I2S_DEV_CONF_REC_I2S_ALIGN_LR | I2S_DEV_CONF_REC_WS_DSP_MODE;
+ dev_conf &= ~(I2S_DEV_CONF_REC_APB_ALIGN_LR | I2S_DEV_CONF_REC_DATA_WS_DEL);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* DAI hardware clock masters */
+ switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) {
+ case SND_SOC_DAIFMT_CBC_CFC:
+ ctrl &= ~I2S_CTRL_R_MS;
+ break;
+ case SND_SOC_DAIFMT_CBP_CFP:
+ ctrl |= I2S_CTRL_R_MS;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_DEV_CONF,
+ I2S_DEV_CONF_REC_SCK_POLAR | I2S_DEV_CONF_REC_WS_POLAR |
+ I2S_DEV_CONF_REC_APB_ALIGN_LR | I2S_DEV_CONF_REC_I2S_ALIGN_LR |
+ I2S_DEV_CONF_REC_DATA_WS_DEL |
+ I2S_DEV_CONF_REC_WS_DSP_MODE, dev_conf);
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_R_MS | I2S_CTRL_TR_CFG, ctrl);
+ } else {
+ dev_err(i2s_mc_priv->dev, "Invalid dai id\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int cdns_i2s_mc_prepare(struct snd_pcm_substream *substream,
+ struct snd_soc_dai *cpu_dai)
+{
+ struct cdns_i2s_mc_priv *i2s_mc_priv = snd_soc_dai_get_drvdata(cpu_dai);
+ u32 ctrl = 0, fifo = 0;
+
+ /*
+ * FIFO reset should meet two requirements
+ * 1) after strobe_ts and strobe_rs clock on
+ * 2) before dma ready to receive trigger
+ */
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
+ /* Transmitter clock enable */
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CID_CTRL,
+ I2S_CID_CTRL_STROBE_TS, 0);
+
+ /*
+ * Transmitter FIFO reset
+ * When '0', transmit FIFO pointers are reset to zero. Threshold level for
+ * this FIFO is unchanged. This bit is automatically set to '1' after one
+ * clock cycle if TX FIFO reset has been acknowledged.
+ * Deassert then assert this bit here, since I2S_CTRL register is not
+ * volatile, would not read from hardware any longer. If not, it would
+ * clear tx fifo every time when write this register.
+ */
+ ctrl &= ~I2S_CTRL_TFIFO_RST;
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_TFIFO_RST, ctrl);
+ ctrl |= I2S_CTRL_TFIFO_RST;
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_TFIFO_RST, ctrl);
+
+ /* Transmitter FIFO threshold set */
+ fifo |= FIELD_PREP(I2S_TFIFO_CTRL_TAEMPTY_THRESHOLD,
+ i2s_mc_priv->devtype_data->tfifo_aempty_threshold) |
+ FIELD_PREP(I2S_TFIFO_CTRL_TAFULL_THRESHOLD,
+ i2s_mc_priv->devtype_data->tfifo_afull_threshold);
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_TFIFO_CTRL,
+ I2S_TFIFO_CTRL_TAEMPTY_THRESHOLD |
+ I2S_TFIFO_CTRL_TAFULL_THRESHOLD, fifo);
+ } else {
+ /* Receiver clock enable */
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CID_CTRL,
+ I2S_CID_CTRL_STROBE_RS, 0);
+
+ /*
+ * Receiver FIFO reset
+ * When '0', receive FIFO pointers are reset to zero. Threshold level for
+ * this FIFO is unchanged. This bit is automatically set to '1' after one
+ * clock cycle if RX FIFO reset has been acknowledged.
+ * Deassert then assert this bit here, since I2S_CTRL register is not
+ * volatile, would not read from hardware any longer. If not, it would
+ * clear rx fifo every time when write this register.
+ */
+ ctrl &= ~I2S_CTRL_RFIFO_RST;
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_RFIFO_RST, ctrl);
+ ctrl |= I2S_CTRL_RFIFO_RST;
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
+ I2S_CTRL_RFIFO_RST, ctrl);
+
+ /* Receiver FIFO threshold set */
+ fifo |= FIELD_PREP(I2S_RFIFO_CTRL_RAEMPTY_THRESHOLD,
+ i2s_mc_priv->devtype_data->rfifo_aempty_threshold) |
+ FIELD_PREP(I2S_RFIFO_CTRL_RAFULL_THRESHOLD,
+ i2s_mc_priv->devtype_data->rfifo_afull_threshold);
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_RFIFO_CTRL,
+ I2S_RFIFO_CTRL_RAEMPTY_THRESHOLD |
+ I2S_RFIFO_CTRL_RAFULL_THRESHOLD, fifo);
+ }
+
+ /*
+ * Enable global interrupt mask, for both transmitter and receiver, use
+ * individual interrupt masks
+ */
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CID_CTRL,
+ I2S_CID_CTRL_INTREQ_MASK, I2S_CID_CTRL_INTREQ_MASK);
+
+ return 0;
+}
+
+static int cdns_i2s_mc_hw_params(struct snd_pcm_substream *substream,
+ struct snd_pcm_hw_params *params,
+ struct snd_soc_dai *cpu_dai)
+{
+ struct cdns_i2s_mc_priv *i2s_mc_priv = snd_soc_dai_get_drvdata(cpu_dai);
+ struct device *dev = i2s_mc_priv->dev;
+ u32 rate, sample_rate = 0;
+ u32 slots, slot_width, resolution, ctrl;
+ unsigned long i2s_clk_rate;
+ u8 pin_tx_num, pin_rx_num;
+ struct clk *clk_parent;
+ bool is_master_mode;
+ int ret;
+
+ rate = params_rate(params);
+ slot_width = i2s_mc_priv->devtype_data->data_width;
+ dev_dbg(dev, "rate = %d, slot_width = %d\n", rate, slot_width);
+
+ /* switch clk mux to select the appropriate clk parent */
+ if (rate % 8000 == 0) {
+ /* Sampling rate is a multiple of 8KHz, select "audio-clk0" */
+ clk_parent = i2s_mc_priv->clks[AUDIO_CLK0];
+
+ ret = clk_set_parent(i2s_mc_priv->clk_i2s, clk_parent);
+ } else if (rate % 11025 == 0) {
+ /* Sampling rate is a multiple of 11.025KHz, select "audio-clk2" */
+ clk_parent = i2s_mc_priv->clks[AUDIO_CLK2];
+
+ ret = clk_set_parent(i2s_mc_priv->clk_i2s, clk_parent);
+ } else {
+ dev_err(i2s_mc_priv->dev, "Invalid sample rate\n");
+ return -EINVAL;
+ }
+ if (ret) {
+ dev_err(i2s_mc_priv->dev, "Failed to set i2s clock parent\n");
+ return ret;
+ }
+
+ i2s_clk_rate = clk_get_rate(i2s_mc_priv->clk_i2s);
+ dev_dbg(dev, "i2s clk rate = %lu\n", i2s_clk_rate);
+
+ regmap_read(i2s_mc_priv->regmap, I2S_CTRL, &ctrl);
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
+ is_master_mode = ctrl & I2S_CTRL_T_MS;
+ else
+ is_master_mode = ctrl & I2S_CTRL_R_MS;
+
+ if (is_master_mode) {
+ if (i2s_clk_rate < rate * 2 * slot_width * I2S_CLK_RATIO_MIN_MASTER) {
+ dev_err(i2s_mc_priv->dev,
+ "clk freq %lu is too low, must >= (%d * %d * %d * %d = %d)\n",
+ i2s_clk_rate, rate, slot_width, 2, I2S_CLK_RATIO_MIN_MASTER,
+ rate * slot_width * 2 * I2S_CLK_RATIO_MIN_MASTER);
+ return -EINVAL;
+ }
+
+ sample_rate = DIV_ROUND_CLOSEST(i2s_clk_rate, (rate * 2 * slot_width));
+ } else {
+ if (i2s_clk_rate < rate * 2 * slot_width * I2S_CLK_RATIO_MIN_SLAVE) {
+ dev_err(i2s_mc_priv->dev,
+ "clk freq %lu is too low, must >= (%d * %d * %d * %d = %d)\n",
+ i2s_clk_rate, rate, slot_width, 2, I2S_CLK_RATIO_MIN_SLAVE,
+ rate * slot_width * 2 * I2S_CLK_RATIO_MIN_SLAVE);
+ return -EINVAL;
+ }
+ }
+
+ slots = params_channels(params);
+ resolution = params_width(params);
+ dev_dbg(dev, "slots = %d, resolution = %d\n", slots, resolution);
+
+ /* Each data pin carries two slots, so the channel count must be even */
+ if (slots % 2) {
+ dev_err(i2s_mc_priv->dev, "Channel number must be a multiple of two\n");
+ return -EINVAL;
+ }
+
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
+ i2s_mc_priv->pin_tx_mask_adjust = i2s_mc_priv->pin_tx_mask;
+
+ pin_tx_num = hweight8(i2s_mc_priv->pin_tx_mask);
+ if (slots > 2 * pin_tx_num) {
+ dev_warn(i2s_mc_priv->dev,
+ "Transmit channel number is larger than hardware config\n");
+ return -EOPNOTSUPP;
+ } else if (slots < 2 * pin_tx_num) {
+ cdns_i2s_mc_adjust_pin_config(&i2s_mc_priv->pin_tx_mask_adjust, slots);
+ }
+
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_SRR, I2S_SRR_TSAMPLE_RATE,
+ FIELD_PREP(I2S_SRR_TSAMPLE_RATE, sample_rate));
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_SRR, I2S_SRR_TRESOLUTION,
+ FIELD_PREP(I2S_SRR_TRESOLUTION, (resolution - 1)));
+ } else {
+ i2s_mc_priv->pin_rx_mask_adjust = i2s_mc_priv->pin_rx_mask;
+
+ pin_rx_num = hweight8(i2s_mc_priv->pin_rx_mask);
+ if (slots > 2 * pin_rx_num) {
+ dev_warn(i2s_mc_priv->dev,
+ "Receive channel number is larger than hardware config\n");
+ return -EOPNOTSUPP;
+ } else if (slots < 2 * pin_rx_num) {
+ cdns_i2s_mc_adjust_pin_config(&i2s_mc_priv->pin_rx_mask_adjust, slots);
+ }
+
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_SRR, I2S_SRR_RSAMPLE_RATE,
+ FIELD_PREP(I2S_SRR_RSAMPLE_RATE, sample_rate));
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_SRR, I2S_SRR_RRESOLUTION,
+ FIELD_PREP(I2S_SRR_RRESOLUTION, (resolution - 1)));
+ }
+
+ return 0;
+}
+
+static int cdns_i2s_mc_trigger(struct snd_pcm_substream *substream,
+ int cmd, struct snd_soc_dai *cpu_dai)
+{
+ struct cdns_i2s_mc_priv *i2s_mc_priv = snd_soc_dai_get_drvdata(cpu_dai);
+
+ switch (cmd) {
+ case SNDRV_PCM_TRIGGER_START:
+ case SNDRV_PCM_TRIGGER_RESUME:
+ case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
+ cdns_i2s_mc_tx_config(i2s_mc_priv, true);
+ else
+ cdns_i2s_mc_rx_config(i2s_mc_priv, true);
+ break;
+ case SNDRV_PCM_TRIGGER_STOP:
+ case SNDRV_PCM_TRIGGER_SUSPEND:
+ case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
+ cdns_i2s_mc_tx_config(i2s_mc_priv, false);
+ else
+ cdns_i2s_mc_rx_config(i2s_mc_priv, false);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int cdns_i2s_mc_dai_probe(struct snd_soc_dai *cpu_dai)
+{
+ struct cdns_i2s_mc_priv *i2s_mc_priv = snd_soc_dai_get_drvdata(cpu_dai);
+ u32 ctrl = 0, cid_ctrl = 0;
+
+ /*
+ * Transceiver disable
+ * Transceiver clock disable
+ * All interrupts masked
+ * Reset for transmitter synchronizing unit
+ * Reset for receiver synchronizing unit
+ */
+ ctrl |= FIELD_PREP(I2S_CTRL_I2S_EN, 0);
+ ctrl &= ~(I2S_CTRL_TSYNC_RST | I2S_CTRL_RSYNC_RST);
+ cid_ctrl |= I2S_CID_CTRL_STROBE_TS | I2S_CID_CTRL_STROBE_RS;
+ cid_ctrl &= ~I2S_CID_CTRL_INTREQ_MASK;
+
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL, I2S_CTRL_I2S_EN, ctrl);
+ regmap_update_bits(i2s_mc_priv->regmap, I2S_CID_CTRL,
+ I2S_CID_CTRL_STROBE_TS | I2S_CID_CTRL_STROBE_RS |
+ I2S_CID_CTRL_INTREQ_MASK, cid_ctrl);
+
+ snd_soc_dai_init_dma_data(cpu_dai, &i2s_mc_priv->playback_dma_data,
+ &i2s_mc_priv->capture_dma_data);
+
+ snd_soc_dai_set_drvdata(cpu_dai, i2s_mc_priv);
+
+ return 0;
+}
+
+static const struct snd_soc_dai_ops cdns_i2s_mc_dai_ops = {
+ .probe = cdns_i2s_mc_dai_probe,
+ .set_fmt = cdns_i2s_mc_set_fmt,
+ .hw_params = cdns_i2s_mc_hw_params,
+ .prepare = cdns_i2s_mc_prepare,
+ .trigger = cdns_i2s_mc_trigger,
+};
+
+static struct snd_soc_dai_driver cdns_i2s_mc_dai[] = {
+ {
+ .name = "i2s-mc-aif1",
+ .id = I2S_MC_AIF1,
+ .playback = {
+ .stream_name = "I2S-MC-Playback",
+ .channels_min = 2,
+ .channels_max = 16,
+ .rates = SNDRV_PCM_RATE_8000_192000,
+ .formats = (SNDRV_PCM_FMTBIT_S16_LE |
+ SNDRV_PCM_FMTBIT_S24_LE |
+ SNDRV_PCM_FMTBIT_S32_LE),
+ },
+ .ops = &cdns_i2s_mc_dai_ops,
+ },
+ {
+ .name = "i2s-mc-aif2",
+ .id = I2S_MC_AIF2,
+ .capture = {
+ .stream_name = "I2S-MC-Capture",
+ .channels_min = 2,
+ .channels_max = 16,
+ .rates = SNDRV_PCM_RATE_8000_192000,
+ .formats = (SNDRV_PCM_FMTBIT_S16_LE |
+ SNDRV_PCM_FMTBIT_S24_LE |
+ SNDRV_PCM_FMTBIT_S32_LE),
+ },
+ .ops = &cdns_i2s_mc_dai_ops,
+ },
+};
+
+static const struct snd_soc_component_driver cdns_i2s_mc_component = {
+ .name = DRV_NAME,
+ .legacy_dai_naming = 1,
+};
+
+static const struct reg_default cdns_i2s_mc_reg_defaults[] = {
+ { I2S_CTRL, 0x01900000 },
+ { I2S_INTR_STAT, 0x00003300 },
+ { I2S_SRR, 0x00000000 },
+ { I2S_CID_CTRL, 0x00000000 },
+ { I2S_TFIFO_STAT, 0x00000000 },
+ { I2S_RFIFO_STAT, 0x00000000 },
+ { I2S_TFIFO_CTRL, 0x000f0000 },
+ { I2S_RFIFO_CTRL, 0x000f0000 },
+ { I2S_DEV_CONF, 0x00000208 },
+ { I2S_POLL_STAT, 0x00000003 },
+};
+
+static bool cdns_i2s_mc_readable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case I2S_CTRL:
+ case I2S_INTR_STAT:
+ case I2S_SRR:
+ case I2S_CID_CTRL:
+ case I2S_TFIFO_STAT:
+ case I2S_RFIFO_STAT:
+ case I2S_TFIFO_CTRL:
+ case I2S_RFIFO_CTRL:
+ case I2S_DEV_CONF:
+ case I2S_POLL_STAT:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool cdns_i2s_mc_writeable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case I2S_CTRL:
+ case I2S_INTR_STAT:
+ case I2S_SRR:
+ case I2S_CID_CTRL:
+ case I2S_TFIFO_CTRL:
+ case I2S_RFIFO_CTRL:
+ case I2S_DEV_CONF:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool cdns_i2s_mc_volatile_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case I2S_INTR_STAT:
+ case I2S_TFIFO_STAT:
+ case I2S_RFIFO_STAT:
+ case I2S_POLL_STAT:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static const struct regmap_config cdns_i2s_mc_regmap_config = {
+ .reg_bits = 32,
+ .reg_stride = 4,
+ .val_bits = 32,
+
+ .max_register = I2S_POLL_STAT,
+ .reg_defaults = cdns_i2s_mc_reg_defaults,
+ .num_reg_defaults = ARRAY_SIZE(cdns_i2s_mc_reg_defaults),
+ .readable_reg = cdns_i2s_mc_readable_reg,
+ .writeable_reg = cdns_i2s_mc_writeable_reg,
+ .volatile_reg = cdns_i2s_mc_volatile_reg,
+ .cache_type = REGCACHE_FLAT,
+};
+
+static int cdns_i2s_get_pin_config(struct platform_device *pdev,
+ struct cdns_i2s_mc_priv *i2s_mc_priv)
+{
+ u8 rxtx_mask;
+ int ret;
+
+ ret = device_property_read_u8(&pdev->dev,
+ "cix,pin-out-num", &i2s_mc_priv->pin_out_num);
+ if (ret) {
+ dev_err(&pdev->dev, "Failed to get pin-out-num: %d\n", ret);
+ return ret;
+ }
+
+ if (!i2s_mc_priv->pin_out_num || i2s_mc_priv->pin_out_num > BITS_PER_BYTE) {
+ dev_err(&pdev->dev, "Pin out number should be in range [1, 8]\n");
+ return -EINVAL;
+ }
+
+ ret = device_property_read_u8(&pdev->dev,
+ "cix,pin-rx-mask", &i2s_mc_priv->pin_rx_mask);
+ if (ret) {
+ dev_err(&pdev->dev, "Failed to get pin-rx-mask: %d\n", ret);
+ return ret;
+ }
+
+ ret = device_property_read_u8(&pdev->dev,
+ "cix,pin-tx-mask", &i2s_mc_priv->pin_tx_mask);
+ if (ret) {
+ dev_err(&pdev->dev, "Failed to get pin-tx-mask: %d\n", ret);
+ return ret;
+ }
+
+ rxtx_mask = i2s_mc_priv->pin_rx_mask & i2s_mc_priv->pin_tx_mask;
+ if (rxtx_mask) {
+ dev_err(&pdev->dev, "Pin configuration for transmitter and receiver is conflict\n");
+ return -EINVAL;
+ }
+
+ rxtx_mask = i2s_mc_priv->pin_rx_mask | i2s_mc_priv->pin_tx_mask;
+ if (hweight8(rxtx_mask) > i2s_mc_priv->pin_out_num) {
+ dev_err(&pdev->dev, "Pin configuration is out of range\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static void cdns_i2s_mc_rst(struct cdns_i2s_mc_priv *i2s_mc_priv)
+{
+ /* reset */
+ reset_control_assert(i2s_mc_priv->i2s_rst);
+
+ usleep_range(1, 2);
+
+ /* release reset */
+ reset_control_deassert(i2s_mc_priv->i2s_rst);
+}
+
+static int cdns_i2s_mc_probe(struct platform_device *pdev)
+{
+ struct cdns_i2s_mc_priv *i2s_mc_priv;
+ struct resource *res;
+ void __iomem *base;
+ int i, irq, ret;
+
+ i2s_mc_priv = devm_kzalloc(&pdev->dev, sizeof(*i2s_mc_priv), GFP_KERNEL);
+ if (!i2s_mc_priv)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, i2s_mc_priv);
+
+ base = devm_platform_get_and_ioremap_resource(pdev, 0, &res);
+ if (IS_ERR(base))
+ return PTR_ERR(base);
+
+ i2s_mc_priv->dev = &pdev->dev;
+ i2s_mc_priv->devtype_data = device_get_match_data(&pdev->dev);
+ if (!i2s_mc_priv->devtype_data)
+ return -ENODEV;
+
+ i2s_mc_priv->regmap = devm_regmap_init_mmio(&pdev->dev, base,
+ &cdns_i2s_mc_regmap_config);
+ if (IS_ERR(i2s_mc_priv->regmap))
+ return dev_err_probe(&pdev->dev, PTR_ERR(i2s_mc_priv->regmap),
+ "Failed to initialize managed register map\n");
+
+ i2s_mc_priv->clk_hst = devm_clk_get(&pdev->dev, "hst");
+ if (IS_ERR(i2s_mc_priv->clk_hst))
+ return dev_err_probe(&pdev->dev, PTR_ERR(i2s_mc_priv->clk_hst),
+ "Failed to get hst clock\n");
+
+ i2s_mc_priv->clk_i2s = devm_clk_get(&pdev->dev, "i2s");
+ if (IS_ERR(i2s_mc_priv->clk_i2s))
+ return dev_err_probe(&pdev->dev, PTR_ERR(i2s_mc_priv->clk_i2s),
+ "Failed to get i2s clock\n");
+
+ for (i = 0; i < AUDIO_CLK_NUM; i++) {
+ i2s_mc_priv->clks[i] = devm_clk_get(&pdev->dev,
+ cdns_i2s_mc_clk_pll_names[i]);
+ if (IS_ERR(i2s_mc_priv->clks[i]))
+ return dev_err_probe(&pdev->dev, PTR_ERR(i2s_mc_priv->clks[i]),
+ "Failed to get %s clock\n",
+ cdns_i2s_mc_clk_pll_names[i]);
+ }
+
+ i2s_mc_priv->i2s_rst = devm_reset_control_get_exclusive(&pdev->dev, NULL);
+ if (IS_ERR(i2s_mc_priv->i2s_rst))
+ return dev_err_probe(&pdev->dev, PTR_ERR(i2s_mc_priv->i2s_rst),
+ "Failed to get reset control\n");
+
+ ret = cdns_i2s_get_pin_config(pdev, i2s_mc_priv);
+ if (ret)
+ return ret;
+
+ i2s_mc_priv->playback_dma_data.addr = res->start + I2S_FIFO_ADDRESS;
+ /* Buswidth will be set by framework at runtime */
+ i2s_mc_priv->playback_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_UNDEFINED;
+ i2s_mc_priv->playback_dma_data.maxburst = 4;
+
+ i2s_mc_priv->capture_dma_data.addr = res->start + I2S_FIFO_ADDRESS;
+ /* Buswidth will be set by framework at runtime */
+ i2s_mc_priv->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_UNDEFINED;
+ i2s_mc_priv->capture_dma_data.maxburst = 4;
+
+ ret = devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret,
+ "Failed to register dmaengine component\n");
+
+ pm_runtime_get_noresume(&pdev->dev);
+ pm_runtime_set_active(&pdev->dev);
+ ret = devm_pm_runtime_enable(&pdev->dev);
+ if (ret) {
+ pm_runtime_put_noidle(&pdev->dev);
+ return dev_err_probe(&pdev->dev, ret, "Failed to enable runtime PM\n");
+ }
+
+ ret = cdns_i2s_mc_clks_enable(i2s_mc_priv);
+ if (ret) {
+ pm_runtime_put_noidle(&pdev->dev);
+ return dev_err_probe(&pdev->dev, ret, "Failed to enable clocks\n");
+ }
+
+ cdns_i2s_mc_rst(i2s_mc_priv);
+
+ irq = platform_get_irq(pdev, 0);
+ if (irq < 0) {
+ ret = irq;
+ goto err_disable_clks;
+ }
+ i2s_mc_priv->irq = irq;
+
+ ret = devm_request_irq(&pdev->dev, irq, cdns_i2s_mc_isr, 0,
+ dev_name(&pdev->dev), i2s_mc_priv);
+ if (ret) {
+ dev_err(&pdev->dev, "Failed to request irq: %d\n", ret);
+ goto err_disable_clks;
+ }
+
+ /*
+ * Let pm_runtime_put_sync() disable the clocks, and take regcache effect.
+ * If CONFIG_PM is not enabled, the clock will stay powered.
+ */
+ pm_runtime_put_sync(&pdev->dev);
+
+ ret = devm_snd_soc_register_component(&pdev->dev, &cdns_i2s_mc_component,
+ cdns_i2s_mc_dai, ARRAY_SIZE(cdns_i2s_mc_dai));
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret, "Failed to register soc component\n");
+
+ return 0;
+
+err_disable_clks:
+ cdns_i2s_mc_clks_disable(i2s_mc_priv);
+ pm_runtime_put_noidle(&pdev->dev);
+ return ret;
+}
+
+static int cdns_i2s_mc_runtime_suspend(struct device *dev)
+{
+ struct cdns_i2s_mc_priv *i2s_mc_priv = dev_get_drvdata(dev);
+
+ /*
+ * Disable the IRQ before switching the regcache to cache-only, so an
+ * in-flight handler can never access the registers through a
+ * cache-only regmap. disable_irq() waits for the handler to finish.
+ */
+ disable_irq(i2s_mc_priv->irq);
+ regcache_cache_only(i2s_mc_priv->regmap, true);
+ cdns_i2s_mc_clks_disable(i2s_mc_priv);
+
+ return 0;
+}
+
+static int cdns_i2s_mc_runtime_resume(struct device *dev)
+{
+ struct cdns_i2s_mc_priv *i2s_mc_priv = dev_get_drvdata(dev);
+ int ret;
+
+ ret = cdns_i2s_mc_clks_enable(i2s_mc_priv);
+ if (ret) {
+ dev_err(dev, "Failed to enable clocks:%d\n", ret);
+ return ret;
+ }
+
+ cdns_i2s_mc_rst(i2s_mc_priv);
+
+ regcache_cache_only(i2s_mc_priv->regmap, false);
+ regcache_mark_dirty(i2s_mc_priv->regmap);
+
+ ret = regcache_sync(i2s_mc_priv->regmap);
+ if (ret) {
+ dev_err(dev, "Failed to sync regcache: %d\n", ret);
+ regcache_cache_only(i2s_mc_priv->regmap, true);
+ cdns_i2s_mc_clks_disable(i2s_mc_priv);
+ return ret;
+ }
+
+ /* Enable the IRQ only after the regcache is back in sync */
+ enable_irq(i2s_mc_priv->irq);
+
+ return 0;
+}
+
+static int cdns_i2s_mc_system_suspend(struct device *dev)
+{
+ int ret;
+
+ ret = pm_runtime_force_suspend(dev);
+ if (ret)
+ return ret;
+
+ pinctrl_pm_select_sleep_state(dev);
+
+ return 0;
+}
+
+static int cdns_i2s_mc_system_resume(struct device *dev)
+{
+ int ret;
+
+ pinctrl_pm_select_default_state(dev);
+
+ ret = pm_runtime_force_resume(dev);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static const struct dev_pm_ops cdns_i2s_mc_pm_ops = {
+ RUNTIME_PM_OPS(cdns_i2s_mc_runtime_suspend, cdns_i2s_mc_runtime_resume, NULL)
+ SYSTEM_SLEEP_PM_OPS(cdns_i2s_mc_system_suspend, cdns_i2s_mc_system_resume)
+};
+
+static const struct cdns_i2s_mc_devtype_data sky1_devtype_data = {
+ .data_width = 32,
+ .rfifo_aempty_threshold = 4,
+ .rfifo_afull_threshold = 12,
+ .tfifo_aempty_threshold = 4,
+ .tfifo_afull_threshold = 12,
+};
+
+static const struct of_device_id cdns_i2s_mc_of_match[] = {
+ { .compatible = "cix,sky1-i2s-mc", .data = &sky1_devtype_data },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, cdns_i2s_mc_of_match);
+
+static struct platform_driver cdns_i2s_mc_driver = {
+ .probe = cdns_i2s_mc_probe,
+ .driver = {
+ .name = DRV_NAME,
+ .pm = pm_ptr(&cdns_i2s_mc_pm_ops),
+ .of_match_table = cdns_i2s_mc_of_match,
+ },
+};
+module_platform_driver(cdns_i2s_mc_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Joakim Zhang <joakim.zhang@cixtech.com>");
+MODULE_DESCRIPTION("Cadence I2S-MC Controller Driver");
--
2.50.1
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller
2026-10-09 9:25 ` [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller joakim.zhang
@ 2026-10-09 9:32 ` sashiko-bot
2026-10-09 14:15 ` Rob Herring
2026-10-09 14:16 ` Rob Herring
2026-10-09 14:18 ` Rob Herring
2 siblings, 1 reply; 11+ messages in thread
From: sashiko-bot @ 2026-10-09 9:32 UTC (permalink / raw)
To: joakim.zhang; +Cc: robh, conor+dt, devicetree
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] dt-bindings: loose schema constraint on dma-names allows invalid configurations
--
commit 41d6caf5f15433436b2c0289025efcf429404414
Author: Joakim Zhang <joakim.zhang@cixtech.com>
ASoC: dt-bindings: Add Cadence I2S-SC controller
Add the DT binding for the Cadence I2S-SC controller integrated in
the CIX SKY1 audio subsystem. I2S-SC is a full-duplex stereo
controller supporting standard I2S, left/right-justified and DSP
formats, plus TDM operation with up to 16 slots.
> diff --git a/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
> new file mode 100644
> index 0000000000000..3fdf2334c4926
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
[ ... ]
> + dmas:
> + minItems: 1
> + maxItems: 2
> + description:
> + One DMA channel for instances wired for a single direction, either
> + "tx" or "rx", or two channels for both transmit and receive.
> +
> + dma-names:
> + minItems: 1
> + maxItems: 2
> + items:
> + enum: [ tx, rx ]
[Severity: Low]
Does this schema definition permit invalid DMA combinations?
Because items is set to just an enum, this allows combinations like ["tx", "tx"],
["rx", "rx"], or out-of-order configurations like ["rx", "tx"] to silently pass
validation.
Is there a specific ordering and uniqueness constraint expected here when
providing two channels?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261009092552.2377960-1-joakim.zhang@cixtech.com?part=1
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v3 2/4] ASoC: cdns: Add Cadence I2S-SC controller driver
2026-10-09 9:25 ` [PATCH v3 2/4] ASoC: cdns: Add Cadence I2S-SC controller driver joakim.zhang
@ 2026-10-09 9:37 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-09 9:37 UTC (permalink / raw)
To: joakim.zhang; +Cc: devicetree, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] asoc: inverted master/slave clock provider configuration in set_fmt
- [High] regmap: racy read-modify-write on volatile interrupt status register I2S_STAT
- [Medium] pm: missing devm action or .remove callback leaves clocks permanently enabled
- [Low] kconfig: missing select REGMAP_MMIO causes build failure
--
commit c5bda621f50576e786b53bd1505239571117bd15
Author: Joakim Zhang <joakim.zhang@cixtech.com>
Subject: ASoC: cdns: Add Cadence I2S-SC controller driver
Add support for the Cadence I2S-SC controller found in the CIX SKY1
audio subsystem.
> diff --git a/sound/soc/cdns/Kconfig b/sound/soc/cdns/Kconfig
> new file mode 100644
> index 0000000000000..631e2e0764cb9
> --- /dev/null
> +++ b/sound/soc/cdns/Kconfig
> @@ -0,0 +1,11 @@
> +# SPDX-License-Identifier: GPL-2.0
> +
> +config SND_SOC_CDNS_I2S_SC
> + tristate "Cadence I2S-SC controller support"
> + depends on ARCH_CIX || COMPILE_TEST
> + select SND_SOC_GENERIC_DMAENGINE_PCM
[Severity: Low]
Since this driver unconditionally calls devm_regmap_init_mmio() to manage its
registers, should this also select REGMAP_MMIO?
If it is built (e.g., via COMPILE_TEST) without another driver selecting it,
it could cause an undefined reference to __devm_regmap_init_mmio_clk during
the link stage.
> diff --git a/sound/soc/cdns/cdns-i2s-sc.c b/sound/soc/cdns/cdns-i2s-sc.c
> new file mode 100644
> index 0000000000000..160a0ec573d72
> --- /dev/null
> +++ b/sound/soc/cdns/cdns-i2s-sc.c
[ ... ]
> +static irqreturn_t cdns_i2s_sc_isr(int irq, void *devid)
> +{
> + struct cdns_i2s_sc_priv *i2s_sc_priv = (struct cdns_i2s_sc_priv *)devid;
> + struct device *dev = i2s_sc_priv->dev;
> + u32 stat;
> +
> + if (regmap_read(i2s_sc_priv->regmap, I2S_STAT, &stat))
> + return IRQ_NONE;
> +
> + stat &= I2S_STAT_TDATA_UNDERR | I2S_STAT_RDATA_OVERR;
> + if (!stat)
> + return IRQ_NONE;
> +
> + /* Clear the status */
> + regmap_update_bits(i2s_sc_priv->regmap, I2S_STAT, stat, 0);
[Severity: High]
Is it safe to use regmap_update_bits() here to clear the interrupt status?
I2S_STAT is correctly marked as a volatile register in the regmap
configuration, but regmap_update_bits() performs a non-atomic physical
read-modify-write cycle.
If the hardware uses Write-1-to-Clear (W1C) or Write-0-to-Clear (W0C)
semantics, this read-modify-write might accidentally clear unhandled events
or permanently drop new interrupts asserted between the read and write.
Could we use a direct regmap_write() here instead?
[ ... ]
> +static int cdns_i2s_sc_set_fmt(struct snd_soc_dai *cpu_dai, unsigned int fmt)
> +{
[ ... ]
> + /* DAI hardware clock masters */
> + switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) {
> + case SND_SOC_DAIFMT_CBC_CFC:
> + ctrl &= ~I2S_CTRL_MS_CFG;
> + i2s_sc_priv->is_master_mode = false;
> + break;
> + case SND_SOC_DAIFMT_CBP_CFP:
> + ctrl |= I2S_CTRL_MS_CFG;
> + i2s_sc_priv->is_master_mode = true;
> + break;
[Severity: High]
Are these clock provider configurations inverted?
In the ASoC framework, SND_SOC_DAIFMT_CBC_CFC means the Codec is the
Bitclock/Frame Consumer, which makes the CPU DAI (this controller) the
Provider (Master).
SND_SOC_DAIFMT_CBP_CFP means the Codec is the Provider, so the CPU DAI must
be the Consumer (Slave).
The driver logic seems to do the exact opposite. Will this cause bus
contention by actively driving clocks when the codec is already doing so?
[ ... ]
> +static int cdns_i2s_sc_probe(struct platform_device *pdev)
> +{
[ ... ]
> + /*
> + * Let pm_runtime_put_sync() disable the clocks, and take regcache effect.
> + * If CONFIG_PM is not enabled, the clock will stay powered.
> + */
> + pm_runtime_put_sync(&pdev->dev);
> +
> + ret = devm_snd_soc_register_component(&pdev->dev, &cdns_i2s_sc_component,
> + &cdns_i2s_sc_dai, 1);
> + if (ret)
> + return dev_err_probe(&pdev->dev, ret, "Failed to register soc component\n");
[Severity: Medium]
If devm_snd_soc_register_component() fails here, the probe function returns
directly, bypassing the err_disable_clks label.
Does this leak the clocks if CONFIG_PM is disabled (since
pm_runtime_put_sync() would be a no-op)?
[ ... ]
> +static struct platform_driver cdns_i2s_sc_driver = {
> + .probe = cdns_i2s_sc_probe,
> + .driver = {
> + .name = DRV_NAME,
> + .pm = pm_ptr(&cdns_i2s_sc_pm_ops),
> + .of_match_table = cdns_i2s_sc_of_match,
> + },
> +};
[Severity: Medium]
I noticed there is no .remove callback or devm_add_action_or_reset() cleanup
for the hardware clocks enabled during probe.
During driver unbind, the driver core calls pm_runtime_get_sync() (waking the
device and enabling clocks if they were suspended) followed by
pm_runtime_disable().
Without a .remove callback to force suspend or manually disable them, does
this leave the clocks permanently enabled?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261009092552.2377960-1-joakim.zhang@cixtech.com?part=2
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v3 4/4] ASoC: cdns: Add Cadence I2S-MC controller driver
2026-10-09 9:25 ` [PATCH v3 4/4] ASoC: cdns: Add Cadence I2S-MC controller driver joakim.zhang
@ 2026-10-09 9:38 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-09 9:38 UTC (permalink / raw)
To: joakim.zhang; +Cc: robh, conor+dt, devicetree
Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] sound: cdns: clock refcount leak on late probe error and driver unbind
- [High] sound: cdns: inverted clock master/slave mode evaluation in hw_params
- [High] sound: cdns: inverted BCLK data delay for I2S and Left-Justified formats
- [High] sound: cdns: read-modify-write race on volatile interrupt status register
- [Medium] sound: cdns: synchronizing unit resets dropped due to incomplete regmap mask
- [Medium] sound: cdns: unprotected shared clock parent modification across DAIs
- [Medium] cdns: premature FIFO reset deassertion over APB bus
--
commit ba70a70c0f6376ecabc6aa01d7c62b130cb6e3a9
Author: Joakim Zhang <joakim.zhang@cixtech.com>
ASoC: cdns: Add Cadence I2S-MC controller driver
Add support for the Cadence I2S-MC multi-channel controller found in
the CIX SKY1 audio subsystem.
> diff --git a/sound/soc/cdns/cdns-i2s-mc.c b/sound/soc/cdns/cdns-i2s-mc.c
> new file mode 100644
> index 0000000000000..e9a0880a06167
> --- /dev/null
> +++ b/sound/soc/cdns/cdns-i2s-mc.c
[ ... ]
> +static irqreturn_t cdns_i2s_mc_isr(int irq, void *devid)
> +{
> + struct cdns_i2s_mc_priv *i2s_mc_priv = (struct cdns_i2s_mc_priv *)devid;
> + struct device *dev = i2s_mc_priv->dev;
> + u32 stat;
> +
> + if (regmap_read(i2s_mc_priv->regmap, I2S_INTR_STAT, &stat))
> + return IRQ_NONE;
> +
> + stat &= I2S_INTR_STAT_TDATA_UNDERR | I2S_INTR_STAT_RDATA_OVERR;
> + if (!stat)
> + return IRQ_NONE;
> +
> + /* Clear the status */
> + regmap_update_bits(i2s_mc_priv->regmap, I2S_INTR_STAT, stat, 0);
[Severity: High]
Does this read-modify-write cycle introduce a hardware race condition on the
volatile interrupt status register?
The I2S_INTR_STAT register is asynchronously updated by the hardware to flag
events. When regmap_update_bits() performs its read-modify-write cycle, any
new hardware interrupts that assert between the read and the write could
be inadvertently cleared without being handled (if W1C) or overwritten with
0 and lost (if W0C or R/W).
Would it be safer to use a direct regmap_write() here?
[ ... ]
> +static int cdns_i2s_mc_set_fmt(struct snd_soc_dai *cpu_dai, unsigned int fmt)
> +{
> + struct cdns_i2s_mc_priv *i2s_mc_priv = snd_soc_dai_get_drvdata(cpu_dai);
> + struct device *dev = i2s_mc_priv->dev;
> + u32 ctrl = 0, dev_conf = 0;
[ ... ]
> + /* DAI hardware audio formats */
> + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
> + case SND_SOC_DAIFMT_I2S:
> + dev_conf |= I2S_DEV_CONF_TRAN_I2S_ALIGN_LR;
> + dev_conf &= ~(I2S_DEV_CONF_TRAN_APB_ALIGN_LR |
> + I2S_DEV_CONF_TRAN_DATA_WS_DEL |
> + I2S_DEV_CONF_TRAN_WS_DSP_MODE);
> + break;
> + case SND_SOC_DAIFMT_RIGHT_J:
> + dev_conf |= I2S_DEV_CONF_TRAN_DATA_WS_DEL;
> + dev_conf &= ~(I2S_DEV_CONF_TRAN_I2S_ALIGN_LR |
> + I2S_DEV_CONF_TRAN_APB_ALIGN_LR |
> + I2S_DEV_CONF_TRAN_WS_DSP_MODE);
> + break;
> + case SND_SOC_DAIFMT_LEFT_J:
> + dev_conf |= I2S_DEV_CONF_TRAN_I2S_ALIGN_LR | I2S_DEV_CONF_TRAN_DATA_WS_DEL;
> + dev_conf &= ~(I2S_DEV_CONF_TRAN_APB_ALIGN_LR |
> + I2S_DEV_CONF_TRAN_WS_DSP_MODE);
> + break;
[Severity: High]
Are the BCLK data delays inverted for I2S and Left-Justified formats?
Standard I2S requires a 1 BCLK delay for the data relative to the WS signal,
while Left-Justified requires a 0 BCLK delay.
The I2S_DEV_CONF_TRAN_DATA_WS_DEL bit seems to configure this 1 BCLK delay
(as it is explicitly set for DSP_A and cleared for DSP_B).
However, this code clears this bit for SND_SOC_DAIFMT_I2S (removing the
required delay) and sets it for SND_SOC_DAIFMT_LEFT_J (adding an incorrect
delay), which might cause 1-bit audio misalignment.
[ ... ]
> +static int cdns_i2s_mc_prepare(struct snd_pcm_substream *substream,
> + struct snd_soc_dai *cpu_dai)
> +{
> + struct cdns_i2s_mc_priv *i2s_mc_priv = snd_soc_dai_get_drvdata(cpu_dai);
> + u32 ctrl = 0, fifo = 0;
[ ... ]
> + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
> + /* Transmitter clock enable */
> + regmap_update_bits(i2s_mc_priv->regmap, I2S_CID_CTRL,
> + I2S_CID_CTRL_STROBE_TS, 0);
> +
> + /*
> + * Transmitter FIFO reset
> + * When '0', transmit FIFO pointers are reset to zero. Threshold level for
> + * this FIFO is unchanged. This bit is automatically set to '1' after one
> + * clock cycle if TX FIFO reset has been acknowledged.
> + * Deassert then assert this bit here, since I2S_CTRL register is not
> + * volatile, would not read from hardware any longer. If not, it would
> + * clear tx fifo every time when write this register.
> + */
> + ctrl &= ~I2S_CTRL_TFIFO_RST;
> + regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
> + I2S_CTRL_TFIFO_RST, ctrl);
> + ctrl |= I2S_CTRL_TFIFO_RST;
> + regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
> + I2S_CTRL_TFIFO_RST, ctrl);
[ ... ]
> + } else {
> + /* Receiver clock enable */
> + regmap_update_bits(i2s_mc_priv->regmap, I2S_CID_CTRL,
> + I2S_CID_CTRL_STROBE_RS, 0);
> +
> + /*
> + * Receiver FIFO reset
> + * When '0', receive FIFO pointers are reset to zero. Threshold level for
> + * this FIFO is unchanged. This bit is automatically set to '1' after one
> + * clock cycle if RX FIFO reset has been acknowledged.
> + * Deassert then assert this bit here, since I2S_CTRL register is not
> + * volatile, would not read from hardware any longer. If not, it would
> + * clear rx fifo every time when write this register.
> + */
> + ctrl &= ~I2S_CTRL_RFIFO_RST;
> + regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
> + I2S_CTRL_RFIFO_RST, ctrl);
> + ctrl |= I2S_CTRL_RFIFO_RST;
> + regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL,
> + I2S_CTRL_RFIFO_RST, ctrl);
[Severity: Medium]
Will these back-to-back writes deassert the FIFO resets too quickly?
The hardware requires the reset bit to be held for at least 1 I2S functional
clock cycle before it auto-acknowledges and sets it to 1. Since these writes
go over the fast APB bus, they might deassert the reset in tens of nanoseconds,
and the slower clock domain crossing synchronizer could miss this pulse,
aborting the reset.
Should there be a delay here, or could the cache be manipulated exclusively
using regcache_cache_only() instead?
[ ... ]
> +static int cdns_i2s_mc_hw_params(struct snd_pcm_substream *substream,
> + struct snd_pcm_hw_params *params,
> + struct snd_soc_dai *cpu_dai)
> +{
> + struct cdns_i2s_mc_priv *i2s_mc_priv = snd_soc_dai_get_drvdata(cpu_dai);
> + struct device *dev = i2s_mc_priv->dev;
> + u32 rate, sample_rate = 0;
> + u32 slots, slot_width, resolution, ctrl;
> + unsigned long i2s_clk_rate;
> + u8 pin_tx_num, pin_rx_num;
> + struct clk *clk_parent;
> + bool is_master_mode;
> + int ret;
> +
> + rate = params_rate(params);
> + slot_width = i2s_mc_priv->devtype_data->data_width;
> + dev_dbg(dev, "rate = %d, slot_width = %d\n", rate, slot_width);
> +
> + /* switch clk mux to select the appropriate clk parent */
> + if (rate % 8000 == 0) {
> + /* Sampling rate is a multiple of 8KHz, select "audio-clk0" */
> + clk_parent = i2s_mc_priv->clks[AUDIO_CLK0];
> +
> + ret = clk_set_parent(i2s_mc_priv->clk_i2s, clk_parent);
> + } else if (rate % 11025 == 0) {
> + /* Sampling rate is a multiple of 11.025KHz, select "audio-clk2" */
> + clk_parent = i2s_mc_priv->clks[AUDIO_CLK2];
> +
> + ret = clk_set_parent(i2s_mc_priv->clk_i2s, clk_parent);
[Severity: Medium]
Does this unconditionally change the shared clock parent without protecting
against active streams on the other DAI?
Since there are two independent DAIs (I2S_MC_AIF1 and I2S_MC_AIF2), starting
a new stream on one DAI with a different sample rate will silently change
the parent of the shared clk_i2s clock.
Could this break the clock rate and corrupt the audio of a concurrently
running stream on the other DAI?
> + } else {
> + dev_err(i2s_mc_priv->dev, "Invalid sample rate\n");
> + return -EINVAL;
> + }
> + if (ret) {
> + dev_err(i2s_mc_priv->dev, "Failed to set i2s clock parent\n");
> + return ret;
> + }
> +
> + i2s_clk_rate = clk_get_rate(i2s_mc_priv->clk_i2s);
> + dev_dbg(dev, "i2s clk rate = %lu\n", i2s_clk_rate);
> +
> + regmap_read(i2s_mc_priv->regmap, I2S_CTRL, &ctrl);
> + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
> + is_master_mode = ctrl & I2S_CTRL_T_MS;
> + else
> + is_master_mode = ctrl & I2S_CTRL_R_MS;
> +
> + if (is_master_mode) {
[Severity: High]
Is the clock master/slave mode evaluation inverted here?
In cdns_i2s_mc_set_fmt(), I2S_CTRL_T_MS is cleared when the CPU is master
(SND_SOC_DAIFMT_CBC_CFC) and set when the CPU is slave (SND_SOC_DAIFMT_CBP_CFP).
However, the logic here evaluates is_master_mode as true when the CPU is
a slave, and false when the CPU is a master.
This causes the driver to incorrectly calculate and program the clock divider
when acting as a slave, and skip computing the divider entirely when acting
as a master.
[ ... ]
> +static int cdns_i2s_mc_dai_probe(struct snd_soc_dai *cpu_dai)
> +{
> + struct cdns_i2s_mc_priv *i2s_mc_priv = snd_soc_dai_get_drvdata(cpu_dai);
> + u32 ctrl = 0, cid_ctrl = 0;
> +
> + /*
> + * Transceiver disable
> + * Transceiver clock disable
> + * All interrupts masked
> + * Reset for transmitter synchronizing unit
> + * Reset for receiver synchronizing unit
> + */
> + ctrl |= FIELD_PREP(I2S_CTRL_I2S_EN, 0);
> + ctrl &= ~(I2S_CTRL_TSYNC_RST | I2S_CTRL_RSYNC_RST);
> + cid_ctrl |= I2S_CID_CTRL_STROBE_TS | I2S_CID_CTRL_STROBE_RS;
> + cid_ctrl &= ~I2S_CID_CTRL_INTREQ_MASK;
> +
> + regmap_update_bits(i2s_mc_priv->regmap, I2S_CTRL, I2S_CTRL_I2S_EN, ctrl);
[Severity: Medium]
Will this restrictive mask prevent the synchronizing unit resets from reaching
the hardware?
The driver clears I2S_CTRL_TSYNC_RST and I2S_CTRL_RSYNC_RST (bits 25 and 26)
in the ctrl variable, but issues the write using only I2S_CTRL_I2S_EN
(bits 0-7) as the mask. The intended hardware modifications to bits 25 and 26
will be silently masked out.
[ ... ]
> +static int cdns_i2s_mc_probe(struct platform_device *pdev)
> +{
> + struct cdns_i2s_mc_priv *i2s_mc_priv;
[ ... ]
> + /*
> + * Let pm_runtime_put_sync() disable the clocks, and take regcache effect.
> + * If CONFIG_PM is not enabled, the clock will stay powered.
> + */
> + pm_runtime_put_sync(&pdev->dev);
> +
> + ret = devm_snd_soc_register_component(&pdev->dev, &cdns_i2s_mc_component,
> + cdns_i2s_mc_dai, ARRAY_SIZE(cdns_i2s_mc_dai));
> + if (ret)
> + return dev_err_probe(&pdev->dev, ret, "Failed to register soc component\n");
[Severity: High]
Does returning directly here bypass the err_disable_clks cleanup path?
The clocks clk_hst and clk_i2s are manually enabled earlier in this function
via cdns_i2s_mc_clks_enable(). Returning directly on failure will leak the
clock prepare/enable reference counts.
Should this jump to err_disable_clks instead?
[ ... ]
> +static const struct of_device_id cdns_i2s_mc_of_match[] = {
> + { .compatible = "cix,sky1-i2s-mc", .data = &sky1_devtype_data },
> + { /* sentinel */ }
> +};
> +MODULE_DEVICE_TABLE(of, cdns_i2s_mc_of_match);
> +
> +static struct platform_driver cdns_i2s_mc_driver = {
> + .probe = cdns_i2s_mc_probe,
> + .driver = {
> + .name = DRV_NAME,
> + .pm = pm_ptr(&cdns_i2s_mc_pm_ops),
> + .of_match_table = cdns_i2s_mc_of_match,
> + },
> +};
[Severity: High]
Does this driver lack a .remove callback to clean up the clocks?
Because the clocks are manually enabled in probe and there is no devm action
registered to disable them, the clock references will be permanently leaked
when the module is unbound or removed.
Could this leave the hardware clocks physically powered if CONFIG_PM is
disabled?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261009092552.2377960-1-joakim.zhang@cixtech.com?part=4
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller
2026-10-09 9:32 ` sashiko-bot
@ 2026-10-09 14:15 ` Rob Herring
0 siblings, 0 replies; 11+ messages in thread
From: Rob Herring @ 2026-10-09 14:15 UTC (permalink / raw)
To: sashiko-reviews; +Cc: joakim.zhang, conor+dt, devicetree
On Fri, Oct 09, 2026 at 09:32:13AM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Low] dt-bindings: loose schema constraint on dma-names allows invalid configurations
> --
>
> commit 41d6caf5f15433436b2c0289025efcf429404414
> Author: Joakim Zhang <joakim.zhang@cixtech.com>
>
> ASoC: dt-bindings: Add Cadence I2S-SC controller
>
> Add the DT binding for the Cadence I2S-SC controller integrated in
> the CIX SKY1 audio subsystem. I2S-SC is a full-duplex stereo
> controller supporting standard I2S, left/right-justified and DSP
> formats, plus TDM operation with up to 16 slots.
>
> > diff --git a/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
> > new file mode 100644
> > index 0000000000000..3fdf2334c4926
> > --- /dev/null
> > +++ b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
>
> [ ... ]
>
> > + dmas:
> > + minItems: 1
> > + maxItems: 2
> > + description:
> > + One DMA channel for instances wired for a single direction, either
> > + "tx" or "rx", or two channels for both transmit and receive.
> > +
> > + dma-names:
> > + minItems: 1
> > + maxItems: 2
> > + items:
> > + enum: [ tx, rx ]
>
> [Severity: Low]
> Does this schema definition permit invalid DMA combinations?
>
> Because items is set to just an enum, this allows combinations like ["tx", "tx"],
> ["rx", "rx"], or out-of-order configurations like ["rx", "tx"] to silently pass
> validation.
Yes, normally that is prevented, but IIRC there's a binding that needed
something like this.
minItems: 1
uniqueItems: true
items:
- enum: [ tx, rx ]
- const: tx
Then when both are there the order is enforced.
Rob
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller
2026-10-09 9:25 ` [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller joakim.zhang
2026-10-09 9:32 ` sashiko-bot
@ 2026-10-09 14:16 ` Rob Herring
2026-10-09 14:18 ` Rob Herring
2 siblings, 0 replies; 11+ messages in thread
From: Rob Herring @ 2026-10-09 14:16 UTC (permalink / raw)
To: joakim.zhang
Cc: lgirdwood, broonie, krzk+dt, conor+dt, perex, tiwai, p.zabel,
cix-kernel-upstream, linux-sound, devicetree, linux-kernel,
linux-arm-kernel
On Fri, Oct 09, 2026 at 05:25:49PM +0800, joakim.zhang@cixtech.com wrote:
> From: Joakim Zhang <joakim.zhang@cixtech.com>
>
> Add the DT binding for the Cadence I2S-SC controller integrated in
> the CIX SKY1 audio subsystem. I2S-SC is a full-duplex stereo
> controller supporting standard I2S, left/right-justified and DSP
> formats, plus TDM operation with up to 16 slots.
>
> Signed-off-by: Joakim Zhang <joakim.zhang@cixtech.com>
> ---
> .../bindings/sound/cix,sky1-i2s-sc.yaml | 93 +++++++++++++++++++
> 1 file changed, 93 insertions(+)
> create mode 100644 Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
>
> diff --git a/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
> new file mode 100644
> index 000000000000..3fdf2334c492
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
> @@ -0,0 +1,93 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/sound/cix,sky1-i2s-sc.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: Cadence I2S-SC controller
> +
> +maintainers:
> + - Joakim Zhang <joakim.zhang@cixtech.com>
> +
> +description: |
> + The Cadence I2S-SC controller integrated in the CIX SKY1 audio subsystem.
> + It supports full-duplex stereo playback and capture in standard I2S,
> + left/right-justified and DSP modes, as well as TDM operation with up to
> + 16 slots. The transmitter and receiver are started and stopped together
> + by the controller.
> +
> +allOf:
> + - $ref: dai-common.yaml#
> +
> +properties:
> + compatible:
> + const: cix,sky1-i2s-sc
> +
> + reg:
> + maxItems: 1
> +
> + interrupts:
> + maxItems: 1
> +
> + clocks:
> + maxItems: 4
> +
> + clock-names:
> + items:
> + - const: hst
> + - const: i2s
> + - const: audio-clk0
> + - const: audio-clk2
> +
> + resets:
> + maxItems: 1
> +
> + dmas:
> + minItems: 1
> + maxItems: 2
> + description:
> + One DMA channel for instances wired for a single direction, either
> + "tx" or "rx", or two channels for both transmit and receive.
> +
> + dma-names:
> + minItems: 1
> + maxItems: 2
> + items:
> + enum: [ tx, rx ]
Address the sashiko comment.
Rob
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller
2026-10-09 9:25 ` [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller joakim.zhang
2026-10-09 9:32 ` sashiko-bot
2026-10-09 14:16 ` Rob Herring
@ 2026-10-09 14:18 ` Rob Herring
2 siblings, 0 replies; 11+ messages in thread
From: Rob Herring @ 2026-10-09 14:18 UTC (permalink / raw)
To: joakim.zhang
Cc: lgirdwood, broonie, krzk+dt, conor+dt, perex, tiwai, p.zabel,
cix-kernel-upstream, linux-sound, devicetree, linux-kernel,
linux-arm-kernel
On Fri, Oct 09, 2026 at 05:25:49PM +0800, joakim.zhang@cixtech.com wrote:
> From: Joakim Zhang <joakim.zhang@cixtech.com>
>
> Add the DT binding for the Cadence I2S-SC controller integrated in
> the CIX SKY1 audio subsystem. I2S-SC is a full-duplex stereo
> controller supporting standard I2S, left/right-justified and DSP
> formats, plus TDM operation with up to 16 slots.
>
> Signed-off-by: Joakim Zhang <joakim.zhang@cixtech.com>
> ---
> .../bindings/sound/cix,sky1-i2s-sc.yaml | 93 +++++++++++++++++++
> 1 file changed, 93 insertions(+)
> create mode 100644 Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
>
> diff --git a/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
> new file mode 100644
> index 000000000000..3fdf2334c492
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/sound/cix,sky1-i2s-sc.yaml
> @@ -0,0 +1,93 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/sound/cix,sky1-i2s-sc.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: Cadence I2S-SC controller
> +
> +maintainers:
> + - Joakim Zhang <joakim.zhang@cixtech.com>
> +
> +description: |
> + The Cadence I2S-SC controller integrated in the CIX SKY1 audio subsystem.
> + It supports full-duplex stereo playback and capture in standard I2S,
> + left/right-justified and DSP modes, as well as TDM operation with up to
> + 16 slots. The transmitter and receiver are started and stopped together
> + by the controller.
> +
> +allOf:
> + - $ref: dai-common.yaml#
> +
> +properties:
> + compatible:
> + const: cix,sky1-i2s-sc
> +
> + reg:
> + maxItems: 1
> +
> + interrupts:
> + maxItems: 1
> +
> + clocks:
> + maxItems: 4
> +
> + clock-names:
> + items:
> + - const: hst
> + - const: i2s
> + - const: audio-clk0
> + - const: audio-clk2
> +
> + resets:
> + maxItems: 1
> +
> + dmas:
> + minItems: 1
> + maxItems: 2
> + description:
> + One DMA channel for instances wired for a single direction, either
> + "tx" or "rx", or two channels for both transmit and receive.
> +
> + dma-names:
> + minItems: 1
> + maxItems: 2
> + items:
> + enum: [ tx, rx ]
> +
> + "#sound-dai-cells":
> + const: 0
> +
> +required:
> + - compatible
> + - reg
> + - interrupts
> + - clocks
> + - clock-names
> + - resets
> + - dmas
> + - dma-names
> + - "#sound-dai-cells"
> +
> +unevaluatedProperties: false
> +
> +examples:
> + - |
> + #include <dt-bindings/interrupt-controller/arm-gic.h>
> +
> + audss_dmac: dma-controller {
> + #dma-cells = <1>;
> + };
Drop. We don't put providers in examples. What example made you think
you needed it? And if you do, why not the clock, reset, and interrupt
providers?
Rob
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-10-09 14:18 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-09 9:25 [PATCH v3 0/4] ASoC: cdns: Add Cadence I2S-SC and I2S-MC controller support joakim.zhang
2026-10-09 9:25 ` [PATCH v3 1/4] ASoC: dt-bindings: Add Cadence I2S-SC controller joakim.zhang
2026-10-09 9:32 ` sashiko-bot
2026-10-09 14:15 ` Rob Herring
2026-10-09 14:16 ` Rob Herring
2026-10-09 14:18 ` Rob Herring
2026-10-09 9:25 ` [PATCH v3 2/4] ASoC: cdns: Add Cadence I2S-SC controller driver joakim.zhang
2026-10-09 9:37 ` sashiko-bot
2026-10-09 9:25 ` [PATCH v3 3/4] ASoC: dt-bindings: Add Cadence I2S-MC controller joakim.zhang
2026-10-09 9:25 ` [PATCH v3 4/4] ASoC: cdns: Add Cadence I2S-MC controller driver joakim.zhang
2026-10-09 9:38 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox