From 8ed47c2b0ad9ae9170d213b8eb84d62e16da9d68 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 02:52:32 +0200 Subject: [PATCH 1/6] soundwire: qcom: add SCP address paging support The Qualcomm SoundWire controller driver ignored the paging fields of struct sdw_msg, so any register access above the 16-bit address space (e.g. the SDCA control space used by the WCD9378 codec) silently read the low 15 bits only. The core already splits the address into addr_page1/addr_page2 and sets msg->page; write the two SCP_AddrPage registers through the command FIFO before the transfer, as the vendor swr-mstr-ctrl driver does. Verified on Fairphone 6 (SM7635): WCD9378 SDCA registers (0x40000000+) read back their documented reset defaults; without this every paged read returned zeros. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- drivers/soundwire/qcom.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/drivers/soundwire/qcom.c b/drivers/soundwire/qcom.c index 3d8f5a81eff1..9d8aa6a75c48 100644 --- a/drivers/soundwire/qcom.c +++ b/drivers/soundwire/qcom.c @@ -976,6 +976,20 @@ static enum sdw_command_response qcom_swrm_xfer_msg(struct sdw_bus *bus, struct qcom_swrm_ctrl *ctrl = to_qcom_sdw(bus); int ret, i, len; + if (msg->page) { + ret = qcom_swrm_cmd_fifo_wr_cmd(ctrl, msg->addr_page1, + msg->dev_num, + SDW_SCP_ADDRPAGE1); + if (ret) + return SDW_CMD_IGNORED; + + ret = qcom_swrm_cmd_fifo_wr_cmd(ctrl, msg->addr_page2, + msg->dev_num, + SDW_SCP_ADDRPAGE2); + if (ret) + return SDW_CMD_IGNORED; + } + if (msg->flags == SDW_MSG_FLAG_READ) { for (i = 0; i < msg->len;) { len = min(msg->len - i, QCOM_SWRM_MAX_RD_LEN); From 2fd346efe39e2d2628bad61292088d594442f0a0 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 02:52:32 +0200 Subject: [PATCH 2/6] ASoC: codecs: wcd9378: add SoundWire skeleton driver (WIP) Bring-up skeleton for the Qualcomm WCD9378 codec (SoundWire dev id 0x0217:0x0110, one slave per RX/TX bus). Probes both slaves, maps the SDCA control space (32-bit paged addresses) through regmap-sdw with prop.paging_support set, and dumps the device identity registers on ATTACHED as a transport self-test: DEV_MANU_ID_0/1 = 0x17/0x02 (Qualcomm 0x0217) DEV_PART_ID_0/1 = 0x10/0x01 (WCD9378 0x0110) ANA_TX_CH1 0x20, ANA_MICB1 0x10 (downstream reset defaults) The analog core is WCD937x register-compatible; full codec function (TX/ADC path first) to be built on top of this. Not for upstream in this form. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- sound/soc/codecs/Kconfig | 9 +++ sound/soc/codecs/Makefile | 2 + sound/soc/codecs/wcd9378-sdw.c | 135 +++++++++++++++++++++++++++++++++ sound/soc/codecs/wcd9378.h | 63 +++++++++++++++ 4 files changed, 209 insertions(+) create mode 100644 sound/soc/codecs/wcd9378-sdw.c create mode 100644 sound/soc/codecs/wcd9378.h diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index 269c31ce0814..18f489a3501b 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -2431,6 +2431,15 @@ config SND_SOC_WCD937X_SDW via soundwire. To compile this codec driver say Y or m. +config SND_SOC_WCD9378_SDW + tristate "WCD9378 Codec - SDW (bring-up skeleton)" + depends on SOUNDWIRE + select REGMAP_SOUNDWIRE + help + Bring-up skeleton driver for the Qualcomm WCD9378 audio codec + connected via SoundWire, as found on SM7635 phones. + To compile this codec driver say Y or m. + config SND_SOC_WCD938X depends on SND_SOC_WCD938X_SDW tristate diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index 172861d17cfd..466d7bd1a514 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -356,6 +356,7 @@ snd-soc-wcd9335-y := wcd9335.o snd-soc-wcd934x-y := wcd934x.o snd-soc-wcd937x-y := wcd937x.o snd-soc-wcd937x-sdw-y := wcd937x-sdw.o +snd-soc-wcd9378-sdw-y := wcd9378-sdw.o snd-soc-wcd938x-y := wcd938x.o snd-soc-wcd938x-sdw-y := wcd938x-sdw.o snd-soc-wcd939x-y := wcd939x.o @@ -796,6 +797,7 @@ ifdef CONFIG_SND_SOC_WCD937X_SDW # avoid link failure by forcing sdw code built-in when needed obj-$(CONFIG_SND_SOC_WCD937X) += snd-soc-wcd937x-sdw.o endif +obj-$(CONFIG_SND_SOC_WCD9378_SDW) += snd-soc-wcd9378-sdw.o obj-$(CONFIG_SND_SOC_WCD938X) += snd-soc-wcd938x.o ifdef CONFIG_SND_SOC_WCD938X_SDW # avoid link failure by forcing sdw code built-in when needed diff --git a/sound/soc/codecs/wcd9378-sdw.c b/sound/soc/codecs/wcd9378-sdw.c new file mode 100644 index 000000000000..e613f6277113 --- /dev/null +++ b/sound/soc/codecs/wcd9378-sdw.c @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2026, Jorijn van der Graaf + * + * SoundWire slave driver for the Qualcomm WCD9378 audio codec. + * + * Bring-up skeleton: binds to the two WCD9378 SoundWire devices (TX and + * RX), maps the SDCA control space through regmap and verifies the device + * identity registers. No audio paths yet. + */ + +#include +#include +#include +#include +#include +#include + +#include "wcd9378.h" + +struct wcd9378_sdw_priv { + struct sdw_slave *sdev; + struct regmap *regmap; +}; + +static const struct regmap_config wcd9378_sdw_regmap_config = { + .name = "wcd9378_sdw", + .reg_bits = 32, + .val_bits = 8, + .cache_type = REGCACHE_NONE, + .max_register = WCD9378_MAX_REGISTER, +}; + +static void wcd9378_sdw_read_id(struct wcd9378_sdw_priv *wcd) +{ + struct device *dev = &wcd->sdev->dev; + static const struct { + const char *name; + unsigned int reg; + } id_regs[] = { + { "DEV_MANU_ID_0", WCD9378_DEV_MANU_ID_0 }, + { "DEV_MANU_ID_1", WCD9378_DEV_MANU_ID_1 }, + { "DEV_PART_ID_0", WCD9378_DEV_PART_ID_0 }, + { "DEV_PART_ID_1", WCD9378_DEV_PART_ID_1 }, + { "DEV_VER", WCD9378_DEV_VER }, + { "FUNC_EXT_ID_0", WCD9378_FUNC_EXT_ID_0 }, + { "FUNC_EXT_ID_1", WCD9378_FUNC_EXT_ID_1 }, + { "ANA_BIAS", WCD9378_ANA_BIAS }, + { "ANA_TX_CH1", WCD9378_ANA_TX_CH1 }, + { "ANA_MICB1", WCD9378_ANA_MICB1 }, + { "SYS_USAGE_CTRL", WCD9378_SYS_USAGE_CTRL }, + }; + unsigned int val, packed, pval; + int i, ret, pret; + + for (i = 0; i < ARRAY_SIZE(id_regs); i++) { + /* downstream WCD9378_REG() wire packing */ + packed = ((id_regs[i].reg & 0x0ff00000) >> 8) | + (id_regs[i].reg & 0xfff); + + ret = regmap_read(wcd->regmap, id_regs[i].reg, &val); + pret = regmap_read(wcd->regmap, packed, &pval); + dev_info(dev, "%s: virt %#010x = %#04x (ret %d), packed %#07x = %#04x (ret %d)\n", + id_regs[i].name, + id_regs[i].reg, ret ? 0xdead : val, ret, + packed, pret ? 0xdead : pval, pret); + } +} + +static int wcd9378_sdw_update_status(struct sdw_slave *slave, + enum sdw_slave_status status) +{ + struct wcd9378_sdw_priv *wcd = dev_get_drvdata(&slave->dev); + + dev_info(&slave->dev, "status %d (dev_num %d)\n", status, + slave->dev_num); + + if (status == SDW_SLAVE_ATTACHED) + wcd9378_sdw_read_id(wcd); + + return 0; +} + +static const struct sdw_slave_ops wcd9378_sdw_ops = { + .update_status = wcd9378_sdw_update_status, +}; + +static int wcd9378_sdw_probe(struct sdw_slave *pdev, + const struct sdw_device_id *id) +{ + struct device *dev = &pdev->dev; + struct wcd9378_sdw_priv *wcd; + + wcd = devm_kzalloc(dev, sizeof(*wcd), GFP_KERNEL); + if (!wcd) + return -ENOMEM; + + wcd->sdev = pdev; + dev_set_drvdata(dev, wcd); + + pdev->prop.scp_int1_mask = SDW_SCP_INT1_IMPL_DEF | + SDW_SCP_INT1_BUS_CLASH | + SDW_SCP_INT1_PARITY; + pdev->prop.simple_clk_stop_capable = true; + /* The SDCA control space sits above the 16-bit address range */ + pdev->prop.paging_support = true; + + wcd->regmap = devm_regmap_init_sdw(pdev, &wcd9378_sdw_regmap_config); + if (IS_ERR(wcd->regmap)) + return dev_err_probe(dev, PTR_ERR(wcd->regmap), + "regmap init failed\n"); + + dev_info(dev, "wcd9378 sdw slave probed\n"); + + return 0; +} + +static const struct sdw_device_id wcd9378_sdw_id[] = { + SDW_SLAVE_ENTRY(0x0217, 0x0110, 0), + { }, +}; +MODULE_DEVICE_TABLE(sdw, wcd9378_sdw_id); + +static struct sdw_driver wcd9378_sdw_driver = { + .probe = wcd9378_sdw_probe, + .ops = &wcd9378_sdw_ops, + .id_table = wcd9378_sdw_id, + .driver = { + .name = "wcd9378-sdw", + }, +}; +module_sdw_driver(wcd9378_sdw_driver); + +MODULE_DESCRIPTION("WCD9378 SoundWire slave driver"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/wcd9378.h b/sound/soc/codecs/wcd9378.h new file mode 100644 index 000000000000..380786f2765e --- /dev/null +++ b/sound/soc/codecs/wcd9378.h @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) 2026, Jorijn van der Graaf + * + * Register map for the Qualcomm WCD9378 audio codec. + * + * The codec exposes its control registers in the SoundWire SDCA control + * address space (bit 30 set, SDCA function number in bits 25:22), accessed + * through the TX SoundWire slave. The analog core registers (function 0, + * implementation-defined region at +0x180000) are layout-compatible with + * the WCD937x family. + */ + +#ifndef __WCD9378_H__ +#define __WCD9378_H__ + +#include +#include + +/* SDCA function 0 (extension unit): device identity */ +#define WCD9378_FUNC_EXT_ID_0 0x40000048 +#define WCD9378_FUNC_EXT_ID_1 0x40000049 +#define WCD9378_FUNC_EXT_VER 0x40000050 +#define WCD9378_FUNC_STAT 0x40080000 +#define WCD9378_DEV_MANU_ID_0 0x40100060 +#define WCD9378_DEV_MANU_ID_1 0x40100061 +#define WCD9378_DEV_PART_ID_0 0x40100068 +#define WCD9378_DEV_PART_ID_1 0x40100069 +#define WCD9378_DEV_VER 0x40100070 + +/* Analog core (WCD937x-compatible layout), function 0 + 0x180000 */ +#define WCD9378_ANA_PAGE 0x40180000 +#define WCD9378_ANA_BIAS 0x40180001 +#define WCD9378_ANA_RX_SUPPLIES 0x40180008 +#define WCD9378_ANA_TX_CH1 0x4018000e +#define WCD9378_ANA_TX_CH2 0x4018000f +#define WCD9378_ANA_TX_CH3 0x40180010 +#define WCD9378_ANA_MICB1 0x40180022 +#define WCD9378_ANA_MICB2 0x40180023 +#define WCD9378_ANA_MICB2_RAMP 0x40180024 +#define WCD9378_ANA_MICB3 0x40180025 + +/* Sequencer block (SEQR) */ +#define WCD9378_SYS_USAGE_CTRL 0x40180501 +#define WCD9378_SM0_MB_SEL 0x401805b0 +#define WCD9378_SM1_MB_SEL 0x401805b1 +#define WCD9378_SM2_MB_SEL 0x401805b2 + +/* SDCA function activation (one per function) */ +#define WCD9378_SMP_AMP_FUNC_STAT 0x40880000 +#define WCD9378_SMP_AMP_FUNC_ACT 0x40880008 +#define WCD9378_SMP_JACK_FUNC_STAT 0x40c80000 +#define WCD9378_SMP_JACK_FUNC_ACT 0x40c80008 +#define WCD9378_SMP_MIC_CTRL0_FUNC_STAT 0x41080000 +#define WCD9378_SMP_MIC_CTRL0_FUNC_ACT 0x41080008 +#define WCD9378_SMP_MIC_CTRL1_FUNC_STAT 0x41480000 +#define WCD9378_SMP_MIC_CTRL1_FUNC_ACT 0x41480008 +#define WCD9378_SMP_MIC_CTRL2_FUNC_STAT 0x41880000 +#define WCD9378_SMP_MIC_CTRL2_FUNC_ACT 0x41880008 + +#define WCD9378_MAX_REGISTER 0x41900070 + +#endif /* __WCD9378_H__ */ From 5c9c899ba9a21366c82e41c842207075d3a0f5f0 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 02:52:32 +0200 Subject: [PATCH 3/6] arm64: dts: qcom: milos-fairphone-fp6: enable SoundWire buses with WCD9378 slaves Enable the RX (swr1) and TX (swr2) SoundWire controllers and describe the two WCD9378 codec slave devices, as enumerated on hardware: unique-id 4 on the RX bus, unique-id 3 on the TX bus, SoundWire id sdw20217011000 (mfg 0x0217, part 0x0110). A firmware description is required for the sdw core to bind a driver since dynamically enumerated devices are visible but not bindable. WIP: the codec node set is minimal (no port mappings, no codec parent node yet); grows with the wcd9378 driver. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- .../boot/dts/qcom/milos-fairphone-fp6.dts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts index e5944a1b255f..0c50c91c7c82 100644 --- a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts +++ b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts @@ -1086,6 +1086,26 @@ }; }; +&swr1 { + status = "okay"; + + /* WCD9378 RX */ + wcd9378_rx: codec@0,4 { + compatible = "sdw20217011000"; + reg = <0 4>; + }; +}; + +&swr2 { + status = "okay"; + + /* WCD9378 TX */ + wcd9378_tx: codec@0,3 { + compatible = "sdw20217011000"; + reg = <0 3>; + }; +}; + &tlmm { gpio-reserved-ranges = <8 4>, /* Fingerprint SPI */ <13 1>, /* NC */ From 9df8d66027bdf839a36c3d6f8a565f7e979a5b03 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 17:37:02 +0200 Subject: [PATCH 4/6] ASoC: codecs: wcd9378: grow skeleton into TX/capture codec driver (WIP) Replace the transport-test skeleton with a functional driver modeled on wcd937x: platform parent device (qcom,wcd9378-codec) as component master over the two SoundWire slaves, owning reset GPIO, supplies and micbias config; regmap (MAPLE cache, 32-bit paged SDCA addresses) on the TX slave; capture DAI (index 1) with sdw stream plumbing; DAPM TX path AMICn -> ADCn MUX -> TXn SEQUENCER -> ADCn_OUTPUT with the SDCA SmartMIC power sequence (ITxx_USAGE mode, PDE11 PS0 request, HPF init hold) and IT11_MICB-based refcounted micbias control; sys-usage profile auto-selection; SCP bus-clock indication (base clk, busclock scale, host-clk-div2) per the downstream capture-start sequence. Verified on FP6: probes and binds without any manual per-boot hacks (gpio162 reset, runtime PM force, l8b always-on all obsolete), sound card registers, full DPCM/SoundWire/CDC-DMA transport carries data. KNOWN ISSUE: the SmartMIC sequencer never leaves PWR_DN (PDE11_ACT_PS stays PS3, SEQ_TX0_STAT=PWR_DN_RDY) although every register the downstream driver writes has been replicated and verified on hardware by bypassed readback - capture records digital silence. Investigation notes in journal/mic.md. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- sound/soc/codecs/Kconfig | 13 +- sound/soc/codecs/Makefile | 7 +- sound/soc/codecs/wcd9378-sdw.c | 486 +++++++++-- sound/soc/codecs/wcd9378.c | 1372 ++++++++++++++++++++++++++++++++ sound/soc/codecs/wcd9378.h | 195 ++++- 5 files changed, 1994 insertions(+), 79 deletions(-) create mode 100644 sound/soc/codecs/wcd9378.c diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index 18f489a3501b..db114e6063d4 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -2431,13 +2431,20 @@ config SND_SOC_WCD937X_SDW via soundwire. To compile this codec driver say Y or m. +config SND_SOC_WCD9378 + depends on SND_SOC_WCD9378_SDW + tristate + depends on SOUNDWIRE || !SOUNDWIRE + select SND_SOC_WCD_COMMON + config SND_SOC_WCD9378_SDW - tristate "WCD9378 Codec - SDW (bring-up skeleton)" + tristate "WCD9378 Codec - SDW" + select SND_SOC_WCD9378 depends on SOUNDWIRE select REGMAP_SOUNDWIRE help - Bring-up skeleton driver for the Qualcomm WCD9378 audio codec - connected via SoundWire, as found on SM7635 phones. + Driver for the Qualcomm WCD9378 audio codec connected via + SoundWire, as found on SM7635 phones. To compile this codec driver say Y or m. config SND_SOC_WCD938X diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index 466d7bd1a514..73eaf37fd4a5 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -356,6 +356,7 @@ snd-soc-wcd9335-y := wcd9335.o snd-soc-wcd934x-y := wcd934x.o snd-soc-wcd937x-y := wcd937x.o snd-soc-wcd937x-sdw-y := wcd937x-sdw.o +snd-soc-wcd9378-y := wcd9378.o snd-soc-wcd9378-sdw-y := wcd9378-sdw.o snd-soc-wcd938x-y := wcd938x.o snd-soc-wcd938x-sdw-y := wcd938x-sdw.o @@ -797,7 +798,11 @@ ifdef CONFIG_SND_SOC_WCD937X_SDW # avoid link failure by forcing sdw code built-in when needed obj-$(CONFIG_SND_SOC_WCD937X) += snd-soc-wcd937x-sdw.o endif -obj-$(CONFIG_SND_SOC_WCD9378_SDW) += snd-soc-wcd9378-sdw.o +obj-$(CONFIG_SND_SOC_WCD9378) += snd-soc-wcd9378.o +ifdef CONFIG_SND_SOC_WCD9378_SDW +# avoid link failure by forcing sdw code built-in when needed +obj-$(CONFIG_SND_SOC_WCD9378) += snd-soc-wcd9378-sdw.o +endif obj-$(CONFIG_SND_SOC_WCD938X) += snd-soc-wcd938x.o ifdef CONFIG_SND_SOC_WCD938X_SDW # avoid link failure by forcing sdw code built-in when needed diff --git a/sound/soc/codecs/wcd9378-sdw.c b/sound/soc/codecs/wcd9378-sdw.c index e613f6277113..b9ada4d3c3cd 100644 --- a/sound/soc/codecs/wcd9378-sdw.c +++ b/sound/soc/codecs/wcd9378-sdw.c @@ -4,85 +4,333 @@ * * SoundWire slave driver for the Qualcomm WCD9378 audio codec. * - * Bring-up skeleton: binds to the two WCD9378 SoundWire devices (TX and - * RX), maps the SDCA control space through regmap and verifies the device - * identity registers. No audio paths yet. + * The codec presents two SoundWire slaves (RX and TX, mfg 0x0217 part + * 0x0110); the SDCA control space is a 32-bit paged register map accessed + * through the TX slave. */ +#include #include #include +#include +#include #include #include #include #include +#include +#include "wcd-common.h" #include "wcd9378.h" -struct wcd9378_sdw_priv { - struct sdw_slave *sdev; - struct regmap *regmap; -}; - -static const struct regmap_config wcd9378_sdw_regmap_config = { - .name = "wcd9378_sdw", - .reg_bits = 32, - .val_bits = 8, - .cache_type = REGCACHE_NONE, - .max_register = WCD9378_MAX_REGISTER, -}; - -static void wcd9378_sdw_read_id(struct wcd9378_sdw_priv *wcd) -{ - struct device *dev = &wcd->sdev->dev; - static const struct { - const char *name; - unsigned int reg; - } id_regs[] = { - { "DEV_MANU_ID_0", WCD9378_DEV_MANU_ID_0 }, - { "DEV_MANU_ID_1", WCD9378_DEV_MANU_ID_1 }, - { "DEV_PART_ID_0", WCD9378_DEV_PART_ID_0 }, - { "DEV_PART_ID_1", WCD9378_DEV_PART_ID_1 }, - { "DEV_VER", WCD9378_DEV_VER }, - { "FUNC_EXT_ID_0", WCD9378_FUNC_EXT_ID_0 }, - { "FUNC_EXT_ID_1", WCD9378_FUNC_EXT_ID_1 }, - { "ANA_BIAS", WCD9378_ANA_BIAS }, - { "ANA_TX_CH1", WCD9378_ANA_TX_CH1 }, - { "ANA_MICB1", WCD9378_ANA_MICB1 }, - { "SYS_USAGE_CTRL", WCD9378_SYS_USAGE_CTRL }, - }; - unsigned int val, packed, pval; - int i, ret, pret; - - for (i = 0; i < ARRAY_SIZE(id_regs); i++) { - /* downstream WCD9378_REG() wire packing */ - packed = ((id_regs[i].reg & 0x0ff00000) >> 8) | - (id_regs[i].reg & 0xfff); - - ret = regmap_read(wcd->regmap, id_regs[i].reg, &val); - pret = regmap_read(wcd->regmap, packed, &pval); - dev_info(dev, "%s: virt %#010x = %#04x (ret %d), packed %#07x = %#04x (ret %d)\n", - id_regs[i].name, - id_regs[i].reg, ret ? 0xdead : val, ret, - packed, pret ? 0xdead : pval, pret); +#define WCD9378_SDW_CH(id, pn, cmask, mmask) \ + [id] = { \ + .port_num = pn, \ + .ch_mask = cmask, \ + .master_ch_mask = mmask, \ } -} -static int wcd9378_sdw_update_status(struct sdw_slave *slave, - enum sdw_slave_status status) +/* + * Each ADC sits alone on its own TX device port (channel 1); by default + * they land on channels 1/2/3 of the same master port (SWRM_TX1 on the + * FP6). DMIC/MBHC masks per the downstream qcom,tx_swr_ch_map. + */ +static struct wcd_sdw_ch_info wcd9378_sdw_tx_ch_info[] = { + WCD9378_SDW_CH(WCD9378_ADC1, WCD9378_ADC_1_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_ADC2, WCD9378_ADC_2_PORT, BIT(0), BIT(1)), + WCD9378_SDW_CH(WCD9378_ADC3, WCD9378_ADC_3_PORT, BIT(0), BIT(2)), + WCD9378_SDW_CH(WCD9378_DMIC0, WCD9378_DMIC_0_1_MBHC_PORT, BIT(2), BIT(0)), + WCD9378_SDW_CH(WCD9378_DMIC1, WCD9378_DMIC_0_1_MBHC_PORT, BIT(3), BIT(1)), + WCD9378_SDW_CH(WCD9378_MBHC, WCD9378_DMIC_0_1_MBHC_PORT, BIT(2), BIT(2)), + WCD9378_SDW_CH(WCD9378_DMIC2, WCD9378_DMIC_2_5_PORT, BIT(0), BIT(2)), + WCD9378_SDW_CH(WCD9378_DMIC3, WCD9378_DMIC_2_5_PORT, BIT(1), BIT(3)), + WCD9378_SDW_CH(WCD9378_DMIC4, WCD9378_DMIC_2_5_PORT, BIT(2), BIT(0)), + WCD9378_SDW_CH(WCD9378_DMIC5, WCD9378_DMIC_2_5_PORT, BIT(3), BIT(1)), +}; + +static struct wcd_sdw_ch_info wcd9378_sdw_rx_ch_info[] = { + WCD9378_SDW_CH(WCD9378_HPH_L, WCD9378_HPH_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_HPH_R, WCD9378_HPH_PORT, BIT(1), BIT(1)), + WCD9378_SDW_CH(WCD9378_CLSH, WCD9378_CLSH_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_COMP_L, WCD9378_COMP_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_COMP_R, WCD9378_COMP_PORT, BIT(1), BIT(1)), + WCD9378_SDW_CH(WCD9378_LO, WCD9378_LO_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_DSD_L, WCD9378_DSD_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_DSD_R, WCD9378_DSD_PORT, BIT(1), BIT(1)), +}; + +static struct sdw_dpn_prop wcd9378_dpn_prop[WCD9378_MAX_SWR_PORTS] = { + { + .num = 1, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 8, + .simple_ch_prep_sm = true, + }, { + .num = 2, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 4, + .simple_ch_prep_sm = true, + }, { + .num = 3, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 4, + .simple_ch_prep_sm = true, + }, { + .num = 4, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 4, + .simple_ch_prep_sm = true, + }, { + .num = 5, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 4, + .simple_ch_prep_sm = true, + } +}; + +int wcd9378_sdw_hw_params(struct wcd9378_sdw_priv *wcd, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) { - struct wcd9378_sdw_priv *wcd = dev_get_drvdata(&slave->dev); + struct sdw_port_config port_config[WCD9378_MAX_SWR_PORTS]; + unsigned long ch_mask; + int i, j; - dev_info(&slave->dev, "status %d (dev_num %d)\n", status, - slave->dev_num); + wcd->sconfig.ch_count = 1; + wcd->active_ports = 0; + for (i = 0; i < WCD9378_MAX_SWR_PORTS; i++) { + ch_mask = wcd->port_config[i].ch_mask; + if (!ch_mask) + continue; - if (status == SDW_SLAVE_ATTACHED) - wcd9378_sdw_read_id(wcd); + for_each_set_bit(j, &ch_mask, 4) + wcd->sconfig.ch_count++; + + port_config[wcd->active_ports] = wcd->port_config[i]; + wcd->active_ports++; + } + + wcd->sconfig.bps = 1; + wcd->sconfig.frame_rate = params_rate(params); + wcd->sconfig.direction = wcd->is_tx ? SDW_DATA_DIR_TX : SDW_DATA_DIR_RX; + wcd->sconfig.type = SDW_STREAM_PCM; + + return sdw_stream_add_slave(wcd->sdev, &wcd->sconfig, + &port_config[0], wcd->active_ports, + wcd->sruntime); +} +EXPORT_SYMBOL_GPL(wcd9378_sdw_hw_params); + +/* + * Tell the codec the bus clock: base 19.2 MHz plus a scale (div) per bank. + * The downstream driver writes these raw SCP registers on every capture + * start; here the bus_config callback covers bank switches. + */ +static int wcd9378_bus_config(struct sdw_slave *slave, + struct sdw_bus_params *params) +{ + u8 scale; + + switch (params->curr_dr_freq) { + case 4800000: + scale = WCD9378_SWRS_CLK_SCALE_DIV4; + break; + case 9600000: + default: + scale = WCD9378_SWRS_CLK_SCALE_DIV2; + break; + } + + sdw_write(slave, WCD9378_SWRS_SCP_HOST_CLK_DIV2_CTL(params->next_bank), + 0x01); + sdw_write(slave, WCD9378_SWRS_SCP_BASE_CLK, + WCD9378_SWRS_BASE_CLK_19P2MHZ); + sdw_write(slave, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK0, scale); + sdw_write(slave, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK1, scale); return 0; } -static const struct sdw_slave_ops wcd9378_sdw_ops = { - .update_status = wcd9378_sdw_update_status, +static const struct sdw_slave_ops wcd9378_slave_ops = { + .update_status = wcd_update_status, + .bus_config = wcd9378_bus_config, +}; + +static const struct reg_default wcd9378_defaults[] = { + { WCD9378_ANA_BIAS, 0x00 }, + { WCD9378_ANA_TX_CH1, 0x20 }, + { WCD9378_ANA_TX_CH2, 0x00 }, + { WCD9378_ANA_TX_CH3, 0x20 }, + { WCD9378_ANA_TX_CH3_HPF, 0x00 }, + { WCD9378_ANA_MICB2_RAMP, 0x00 }, + { WCD9378_BIAS_VBG_FINE_ADJ, 0x55 }, + { WCD9378_MBHC_CTL_SPARE_1, 0x02 }, + { WCD9378_MICB1_TEST_CTL_2, 0x00 }, + { WCD9378_MICB2_TEST_CTL_2, 0x00 }, + { WCD9378_MICB3_TEST_CTL_2, 0x80 }, + { WCD9378_TX_COM_TXFE_DIV_CTL, 0x22 }, + { WCD9378_SLEEP_CTL, 0x16 }, + { WCD9378_TX_NEW_CH12_MUX, 0x11 }, + { WCD9378_TX_NEW_CH34_MUX, 0x23 }, + { WCD9378_TOP_CLK_CFG, 0x00 }, + { WCD9378_CDC_ANA_TX_CLK_CTL, 0x0e }, + { WCD9378_CDC_AMIC_CTL, 0x07 }, + { WCD9378_PDM_WD_CTL0, 0x0f }, + { WCD9378_PDM_WD_CTL1, 0x0f }, + { WCD9378_PLATFORM_CTL, 0x01 }, + { WCD9378_SYS_USAGE_CTRL, 0x00 }, + { WCD9378_HPH_UP_T0, 0x02 }, + { WCD9378_HPH_UP_T9, 0x02 }, + { WCD9378_HPH_DN_T0, 0x05 }, + { WCD9378_MICB_REMAP_TABLE_VAL_3, 0x00 }, + { WCD9378_MICB_REMAP_TABLE_VAL_4, 0x00 }, + { WCD9378_MICB_REMAP_TABLE_VAL_5, 0x00 }, + { WCD9378_SM0_MB_SEL, 0x00 }, + { WCD9378_SM1_MB_SEL, 0x00 }, + { WCD9378_SM2_MB_SEL, 0x00 }, + { WCD9378_MB_PULLUP_EN, 0x00 }, + { WCD9378_SMP_AMP_FUNC_ACT, 0x00 }, + { WCD9378_CMT_GRP_MASK, 0x00 }, + { WCD9378_SMP_JACK_IT31_MICB, 0x00 }, + { WCD9378_SMP_JACK_IT31_USAGE, 0x03 }, + { WCD9378_SMP_JACK_PDE34_REQ_PS, 0x03 }, + { WCD9378_SMP_JACK_FUNC_ACT, 0x00 }, + { WCD9378_SMP_MIC_IT11_MICB(0), 0x00 }, + { WCD9378_SMP_MIC_IT11_USAGE(0), 0x03 }, + { WCD9378_SMP_MIC_PDE11_REQ_PS(0), 0x03 }, + { WCD9378_SMP_MIC_FUNC_ACT(0), 0x00 }, + { WCD9378_SMP_MIC_IT11_MICB(1), 0x00 }, + { WCD9378_SMP_MIC_IT11_USAGE(1), 0x03 }, + { WCD9378_SMP_MIC_PDE11_REQ_PS(1), 0x03 }, + { WCD9378_SMP_MIC_FUNC_ACT(1), 0x00 }, + { WCD9378_SMP_MIC_IT11_MICB(2), 0x00 }, + { WCD9378_SMP_MIC_IT11_USAGE(2), 0x03 }, + { WCD9378_SMP_MIC_PDE11_REQ_PS(2), 0x03 }, + { WCD9378_SMP_MIC_FUNC_ACT(2), 0x00 }, +}; + +static bool wcd9378_rdwr_register(struct device *dev, unsigned int reg) +{ + switch (reg) { + case WCD9378_ANA_BIAS: + case WCD9378_ANA_TX_CH1: + case WCD9378_ANA_TX_CH2: + case WCD9378_ANA_TX_CH3: + case WCD9378_ANA_TX_CH3_HPF: + case WCD9378_ANA_MICB2_RAMP: + case WCD9378_BIAS_VBG_FINE_ADJ: + case WCD9378_MBHC_CTL_SPARE_1: + case WCD9378_MICB1_TEST_CTL_2: + case WCD9378_MICB2_TEST_CTL_2: + case WCD9378_MICB3_TEST_CTL_2: + case WCD9378_TX_COM_TXFE_DIV_CTL: + case WCD9378_SLEEP_CTL: + case WCD9378_TX_NEW_CH12_MUX: + case WCD9378_TX_NEW_CH34_MUX: + case WCD9378_HPH_RDAC_GAIN_CTL: + case WCD9378_HPH_RDAC_HD2_CTL_L: + case WCD9378_HPH_RDAC_HD2_CTL_R: + case WCD9378_TOP_CLK_CFG: + case WCD9378_CDC_ANA_TX_CLK_CTL: + case WCD9378_CDC_AMIC_CTL: + case WCD9378_PDM_WD_CTL0: + case WCD9378_PDM_WD_CTL1: + case WCD9378_PLATFORM_CTL: + case WCD9378_SYS_USAGE_CTRL: + case WCD9378_HPH_UP_T0: + case WCD9378_HPH_UP_T9: + case WCD9378_HPH_DN_T0: + case WCD9378_MICB_REMAP_TABLE_VAL_3: + case WCD9378_MICB_REMAP_TABLE_VAL_4: + case WCD9378_MICB_REMAP_TABLE_VAL_5: + case WCD9378_SM0_MB_SEL: + case WCD9378_SM1_MB_SEL: + case WCD9378_SM2_MB_SEL: + case WCD9378_MB_PULLUP_EN: + case WCD9378_SMP_AMP_FUNC_STAT: + case WCD9378_SMP_AMP_FUNC_ACT: + case WCD9378_CMT_GRP_MASK: + case WCD9378_SMP_JACK_IT31_MICB: + case WCD9378_SMP_JACK_IT31_USAGE: + case WCD9378_SMP_JACK_PDE34_REQ_PS: + case WCD9378_SMP_JACK_FUNC_STAT: + case WCD9378_SMP_JACK_FUNC_ACT: + case WCD9378_SMP_MIC_IT11_MICB(0): + case WCD9378_SMP_MIC_IT11_USAGE(0): + case WCD9378_SMP_MIC_PDE11_REQ_PS(0): + case WCD9378_SMP_MIC_FUNC_STAT(0): + case WCD9378_SMP_MIC_FUNC_ACT(0): + case WCD9378_SMP_MIC_IT11_MICB(1): + case WCD9378_SMP_MIC_IT11_USAGE(1): + case WCD9378_SMP_MIC_PDE11_REQ_PS(1): + case WCD9378_SMP_MIC_FUNC_STAT(1): + case WCD9378_SMP_MIC_FUNC_ACT(1): + case WCD9378_SMP_MIC_IT11_MICB(2): + case WCD9378_SMP_MIC_IT11_USAGE(2): + case WCD9378_SMP_MIC_PDE11_REQ_PS(2): + case WCD9378_SMP_MIC_FUNC_STAT(2): + case WCD9378_SMP_MIC_FUNC_ACT(2): + return true; + } + + return false; +} + +static bool wcd9378_volatile_register(struct device *dev, unsigned int reg) +{ + switch (reg) { + case WCD9378_FUNC_EXT_ID_0: + case WCD9378_FUNC_EXT_ID_1: + case WCD9378_FUNC_EXT_VER: + case WCD9378_FUNC_STAT: + case WCD9378_DEV_MANU_ID_0: + case WCD9378_DEV_MANU_ID_1: + case WCD9378_DEV_PART_ID_0: + case WCD9378_DEV_PART_ID_1: + case WCD9378_DEV_VER: + case WCD9378_EFUSE_REG_16: + case WCD9378_EFUSE_REG_29: + case WCD9378_SEQ_TX0_STAT: + case WCD9378_SEQ_TX1_STAT: + case WCD9378_SEQ_TX2_STAT: + case WCD9378_SMP_JACK_PDE34_ACT_PS: + case WCD9378_SMP_MIC_OT10_USAGE(0): + case WCD9378_SMP_MIC_PDE11_ACT_PS(0): + case WCD9378_SMP_MIC_OT10_USAGE(1): + case WCD9378_SMP_MIC_PDE11_ACT_PS(1): + case WCD9378_SMP_MIC_OT10_USAGE(2): + case WCD9378_SMP_MIC_PDE11_ACT_PS(2): + return true; + } + + return false; +} + +static bool wcd9378_readable_register(struct device *dev, unsigned int reg) +{ + if (wcd9378_volatile_register(dev, reg)) + return true; + + return wcd9378_rdwr_register(dev, reg); +} + +static const struct regmap_config wcd9378_regmap_config = { + .name = "wcd9378_csr", + .reg_bits = 32, + .val_bits = 8, + .cache_type = REGCACHE_MAPLE, + .reg_defaults = wcd9378_defaults, + .num_reg_defaults = ARRAY_SIZE(wcd9378_defaults), + .max_register = WCD9378_MAX_REGISTER, + .readable_reg = wcd9378_readable_register, + .writeable_reg = wcd9378_rdwr_register, + .volatile_reg = wcd9378_volatile_register, }; static int wcd9378_sdw_probe(struct sdw_slave *pdev, @@ -90,46 +338,150 @@ static int wcd9378_sdw_probe(struct sdw_slave *pdev, { struct device *dev = &pdev->dev; struct wcd9378_sdw_priv *wcd; + u8 master_ch_mask[WCD9378_MAX_SWR_CH_IDS]; + int master_ch_mask_size = 0; + int ret, i; wcd = devm_kzalloc(dev, sizeof(*wcd), GFP_KERNEL); if (!wcd) return -ENOMEM; + /* Port map index starts at 0, however the data ports start at index 1 */ + if (of_property_present(dev->of_node, "qcom,tx-port-mapping")) { + wcd->is_tx = true; + ret = of_property_read_u32_array(dev->of_node, "qcom,tx-port-mapping", + &pdev->m_port_map[1], + WCD9378_MAX_TX_SWR_PORTS); + } else { + ret = of_property_read_u32_array(dev->of_node, "qcom,rx-port-mapping", + &pdev->m_port_map[1], + WCD9378_MAX_SWR_PORTS); + } + if (ret < 0) + dev_info(dev, "Error getting static port mapping for %s (%d)\n", + wcd->is_tx ? "TX" : "RX", ret); + wcd->sdev = pdev; dev_set_drvdata(dev, wcd); pdev->prop.scp_int1_mask = SDW_SCP_INT1_IMPL_DEF | SDW_SCP_INT1_BUS_CLASH | SDW_SCP_INT1_PARITY; + pdev->prop.lane_control_support = true; pdev->prop.simple_clk_stop_capable = true; /* The SDCA control space sits above the 16-bit address range */ pdev->prop.paging_support = true; - wcd->regmap = devm_regmap_init_sdw(pdev, &wcd9378_sdw_regmap_config); - if (IS_ERR(wcd->regmap)) - return dev_err_probe(dev, PTR_ERR(wcd->regmap), - "regmap init failed\n"); + memset(master_ch_mask, 0, WCD9378_MAX_SWR_CH_IDS); - dev_info(dev, "wcd9378 sdw slave probed\n"); + if (wcd->is_tx) { + master_ch_mask_size = of_property_count_u8_elems(dev->of_node, + "qcom,tx-channel-mapping"); + + if (master_ch_mask_size > 0) + ret = of_property_read_u8_array(dev->of_node, + "qcom,tx-channel-mapping", + master_ch_mask, + master_ch_mask_size); + } else { + master_ch_mask_size = of_property_count_u8_elems(dev->of_node, + "qcom,rx-channel-mapping"); + + if (master_ch_mask_size > 0) + ret = of_property_read_u8_array(dev->of_node, + "qcom,rx-channel-mapping", + master_ch_mask, + master_ch_mask_size); + } + + if (wcd->is_tx) { + pdev->prop.source_ports = GENMASK(WCD9378_MAX_TX_SWR_PORTS, 1); + pdev->prop.src_dpn_prop = wcd9378_dpn_prop; + wcd->ch_info = &wcd9378_sdw_tx_ch_info[0]; + + for (i = 0; i < master_ch_mask_size; i++) + wcd->ch_info[i].master_ch_mask = WCD9378_SWRM_CH_MASK(master_ch_mask[i]); + + pdev->prop.wake_capable = true; + + wcd->regmap = devm_regmap_init_sdw(pdev, &wcd9378_regmap_config); + if (IS_ERR(wcd->regmap)) + return dev_err_probe(dev, PTR_ERR(wcd->regmap), + "Regmap init failed\n"); + + /* Start in cache-only until device is enumerated */ + regcache_cache_only(wcd->regmap, true); + } else { + pdev->prop.sink_ports = GENMASK(WCD9378_MAX_SWR_PORTS, 1); + pdev->prop.sink_dpn_prop = wcd9378_dpn_prop; + wcd->ch_info = &wcd9378_sdw_rx_ch_info[0]; + + for (i = 0; i < master_ch_mask_size; i++) + wcd->ch_info[i].master_ch_mask = WCD9378_SWRM_CH_MASK(master_ch_mask[i]); + } + + ret = component_add(dev, &wcd_sdw_component_ops); + if (ret) + return ret; + + /* Set suspended until aggregate device is bind */ + pm_runtime_set_suspended(dev); return 0; } +static void wcd9378_sdw_remove(struct sdw_slave *pdev) +{ + struct device *dev = &pdev->dev; + + component_del(dev, &wcd_sdw_component_ops); +} + static const struct sdw_device_id wcd9378_sdw_id[] = { SDW_SLAVE_ENTRY(0x0217, 0x0110, 0), { }, }; MODULE_DEVICE_TABLE(sdw, wcd9378_sdw_id); +static int wcd9378_sdw_runtime_suspend(struct device *dev) +{ + struct wcd9378_sdw_priv *wcd = dev_get_drvdata(dev); + + if (wcd->regmap) { + regcache_cache_only(wcd->regmap, true); + regcache_mark_dirty(wcd->regmap); + } + + return 0; +} + +static int wcd9378_sdw_runtime_resume(struct device *dev) +{ + struct wcd9378_sdw_priv *wcd = dev_get_drvdata(dev); + + if (wcd->regmap) { + regcache_cache_only(wcd->regmap, false); + regcache_sync(wcd->regmap); + } + + return 0; +} + +static const struct dev_pm_ops wcd9378_sdw_pm_ops = { + RUNTIME_PM_OPS(wcd9378_sdw_runtime_suspend, wcd9378_sdw_runtime_resume, NULL) +}; + static struct sdw_driver wcd9378_sdw_driver = { .probe = wcd9378_sdw_probe, - .ops = &wcd9378_sdw_ops, + .remove = wcd9378_sdw_remove, + .ops = &wcd9378_slave_ops, .id_table = wcd9378_sdw_id, .driver = { .name = "wcd9378-sdw", - }, + .pm = pm_ptr(&wcd9378_sdw_pm_ops), + } }; module_sdw_driver(wcd9378_sdw_driver); -MODULE_DESCRIPTION("WCD9378 SoundWire slave driver"); +MODULE_DESCRIPTION("WCD9378 SDW codec driver"); MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/wcd9378.c b/sound/soc/codecs/wcd9378.c new file mode 100644 index 000000000000..44a3f35dd4cf --- /dev/null +++ b/sound/soc/codecs/wcd9378.c @@ -0,0 +1,1372 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2026, Jorijn van der Graaf + * + * Qualcomm WCD9378 audio codec driver. + * + * The WCD9378 pairs a WCD937x-compatible analog core with SDCA-style + * function blocks (SmartMIC0/1/2, SmartJACK, SmartAMP) whose built-in + * sequencers perform the analog power-up/down autonomously: capture is + * started by programming the ADC usage mode (ITxx_USAGE), requesting + * power state 0 on the function's PDE, and letting the sequencer ramp + * the micbias selected through SMx_MB_SEL. + * + * TX/capture paths only for now; RX (earpiece/headphone), MBHC and the + * SmartAMP function are not implemented. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wcd-common.h" +#include "wcd9378.h" + +enum { + AIF1_PB = 0, + AIF1_CAP, + NUM_CODEC_DAIS, +}; + +enum { + MIC_BIAS_1 = 1, + MIC_BIAS_2, + MIC_BIAS_3, +}; + +enum { + MICB_PULLUP_ENABLE, + MICB_PULLUP_DISABLE, + MICB_ENABLE, + MICB_DISABLE, +}; + +/* sys-usage capability bits (SYS_USAGE_CTRL profile contents) */ +enum { + WCD9378_SYS_USAGE_CLASS_AB = 0, + WCD9378_SYS_USAGE_TX1_FOR_JACK, + WCD9378_SYS_USAGE_TX2_AMIC4, + WCD9378_SYS_USAGE_TX2_AMIC1, + WCD9378_SYS_USAGE_TX1_AMIC3, + WCD9378_SYS_USAGE_TX1_AMIC2, + WCD9378_SYS_USAGE_TX0_AMIC2, + WCD9378_SYS_USAGE_TX0_AMIC1, + WCD9378_SYS_USAGE_RX2_EAR, + WCD9378_SYS_USAGE_RX2_AUX, + WCD9378_SYS_USAGE_RX1_AUX, + WCD9378_SYS_USAGE_RX0_EAR, + WCD9378_SYS_USAGE_RX0_RX1_HPH, +}; + +/* Capability sets of the 13 canned SYS_USAGE_CTRL profiles */ +static const unsigned int wcd9378_sys_usage_profiles[] = { + 0x0c95, 0x12a7, 0x0c99, 0x1aab, 0x0894, 0x11a6, 0x0898, + 0x11ab, 0x126a, 0x116b, 0x1ca7, 0x1195, 0x1296, +}; + +struct wcd9378_priv { + struct sdw_slave *tx_sdw_dev; + struct wcd9378_sdw_priv *sdw_priv[NUM_CODEC_DAIS]; + struct device *txdev; + struct device *rxdev; + struct device_node *rxnode; + struct device_node *txnode; + struct regmap *regmap; + struct wcd_common common; + /* micbias refcount lock */ + struct mutex micb_lock; + u8 micb_usage_val[WCD9378_MAX_MICBIAS]; + int micb_ref[WCD9378_MAX_MICBIAS]; + int pullup_ref[WCD9378_MAX_MICBIAS]; + unsigned long sys_usage_mask; + int sys_usage; + u32 tx_mode[3]; + struct gpio_desc *reset_gpio; +}; + +static const char * const wcd9378_supplies[] = { + "vdd-rxtx", "vdd-io", "vdd-buck", "vdd-mic-bias", +}; + +/* SDCA function block registers driving one ADC */ +struct wcd9378_smp_fn { + u32 usage_reg; + u32 micb_reg; + u32 req_reg; + u32 act_reg; + u32 hpf_reg; + u8 hpf_mask; +}; + +/* ADC1/2/3 through SmartMIC0/1/2 */ +static const struct wcd9378_smp_fn wcd9378_smp_mic[] = { + { + .usage_reg = WCD9378_SMP_MIC_IT11_USAGE(0), + .micb_reg = WCD9378_SMP_MIC_IT11_MICB(0), + .req_reg = WCD9378_SMP_MIC_PDE11_REQ_PS(0), + .act_reg = WCD9378_SMP_MIC_PDE11_ACT_PS(0), + .hpf_reg = WCD9378_ANA_TX_CH2, + .hpf_mask = WCD9378_ANA_TX_CH2_HPF1_INIT, + }, { + .usage_reg = WCD9378_SMP_MIC_IT11_USAGE(1), + .micb_reg = WCD9378_SMP_MIC_IT11_MICB(1), + .req_reg = WCD9378_SMP_MIC_PDE11_REQ_PS(1), + .act_reg = WCD9378_SMP_MIC_PDE11_ACT_PS(1), + .hpf_reg = WCD9378_ANA_TX_CH2, + .hpf_mask = WCD9378_ANA_TX_CH2_HPF2_INIT, + }, { + .usage_reg = WCD9378_SMP_MIC_IT11_USAGE(2), + .micb_reg = WCD9378_SMP_MIC_IT11_MICB(2), + .req_reg = WCD9378_SMP_MIC_PDE11_REQ_PS(2), + .act_reg = WCD9378_SMP_MIC_PDE11_ACT_PS(2), + .hpf_reg = WCD9378_ANA_TX_CH3_HPF, + .hpf_mask = WCD9378_ANA_TX_CH3_HPF3_INIT, + }, +}; + +/* ADC2 fed from AMIC2 runs through the SmartJACK function instead */ +static const struct wcd9378_smp_fn wcd9378_smp_jack_adc2 = { + .usage_reg = WCD9378_SMP_JACK_IT31_USAGE, + .micb_reg = WCD9378_SMP_JACK_IT31_MICB, + .req_reg = WCD9378_SMP_JACK_PDE34_REQ_PS, + .act_reg = WCD9378_SMP_JACK_PDE34_ACT_PS, +}; + +static const DECLARE_TLV_DB_SCALE(analog_gain, 0, 25, 1); + +static int wcd9378_reset(struct wcd9378_priv *wcd9378) +{ + gpiod_set_value(wcd9378->reset_gpio, 1); + usleep_range(20, 30); + gpiod_set_value(wcd9378->reset_gpio, 0); + usleep_range(20, 30); + + return 0; +} + +/* + * Activate the SDCA function classes. Without FUNC_ACT the sequencer + * ignores all PDE power-state requests. + */ +static void wcd9378_class_load(struct snd_soc_component *component) +{ + int i; + + snd_soc_component_update_bits(component, WCD9378_SMP_AMP_FUNC_ACT, + 0x01, 0x01); + usleep_range(20000, 20010); + snd_soc_component_update_bits(component, WCD9378_SMP_AMP_FUNC_STAT, + 0xff, 0xff); + + snd_soc_component_update_bits(component, WCD9378_SMP_JACK_FUNC_ACT, + 0x01, 0x01); + usleep_range(30000, 30010); + snd_soc_component_update_bits(component, WCD9378_CMT_GRP_MASK, + 0xff, 0x02); + snd_soc_component_update_bits(component, WCD9378_SMP_JACK_FUNC_STAT, + 0xff, 0xff); + + for (i = 0; i < 3; i++) { + snd_soc_component_update_bits(component, + WCD9378_SMP_MIC_FUNC_ACT(i), + 0x01, 0x01); + usleep_range(5000, 5010); + snd_soc_component_update_bits(component, + WCD9378_SMP_MIC_FUNC_STAT(i), + 0xff, 0xff); + } +} + +static void wcd9378_io_init(struct snd_soc_component *component) +{ + u32 efuse; + + /* Bandgap and analog master bias, with precharge pulse */ + efuse = snd_soc_component_read(component, WCD9378_EFUSE_REG_16); + snd_soc_component_update_bits(component, WCD9378_MBHC_CTL_SPARE_1, + 0x03, efuse == 0 ? 0x03 : 0x01); + snd_soc_component_update_bits(component, WCD9378_SLEEP_CTL, + WCD9378_SLEEP_CTL_BG_CTL_MASK, 0x0e); + snd_soc_component_update_bits(component, WCD9378_SLEEP_CTL, + WCD9378_SLEEP_CTL_BG_EN, + WCD9378_SLEEP_CTL_BG_EN); + usleep_range(1000, 1010); + snd_soc_component_update_bits(component, WCD9378_SLEEP_CTL, + WCD9378_SLEEP_CTL_LDOL_BG_SEL, + WCD9378_SLEEP_CTL_LDOL_BG_SEL); + usleep_range(1000, 1010); + snd_soc_component_update_bits(component, WCD9378_BIAS_VBG_FINE_ADJ, + 0xf0, 0xb0); + snd_soc_component_update_bits(component, WCD9378_ANA_BIAS, + WCD9378_ANA_BIAS_ANALOG_BIAS_EN, + WCD9378_ANA_BIAS_ANALOG_BIAS_EN); + snd_soc_component_update_bits(component, WCD9378_ANA_BIAS, + WCD9378_ANA_BIAS_PRECHRG_EN, + WCD9378_ANA_BIAS_PRECHRG_EN); + usleep_range(10000, 10010); + snd_soc_component_update_bits(component, WCD9378_ANA_BIAS, + WCD9378_ANA_BIAS_PRECHRG_EN, 0x00); + + /* TX supporting clocks/dividers */ + snd_soc_component_update_bits(component, WCD9378_CDC_ANA_TX_CLK_CTL, + WCD9378_CDC_ANA_TXSCBIAS_CLK_EN, + WCD9378_CDC_ANA_TXSCBIAS_CLK_EN); + snd_soc_component_update_bits(component, WCD9378_TX_COM_TXFE_DIV_CTL, + WCD9378_TX_COM_TXFE_DIV_SEQ_BYPASS, + WCD9378_TX_COM_TXFE_DIV_SEQ_BYPASS); + snd_soc_component_update_bits(component, WCD9378_PDM_WD_CTL0, + 0x18, 0x10); + snd_soc_component_update_bits(component, WCD9378_PDM_WD_CTL1, + 0x18, 0x10); + + /* Micbias LDO driver bias */ + snd_soc_component_update_bits(component, WCD9378_MICB1_TEST_CTL_2, + 0x07, 0x01); + snd_soc_component_update_bits(component, WCD9378_MICB2_TEST_CTL_2, + 0x07, 0x01); + snd_soc_component_update_bits(component, WCD9378_MICB3_TEST_CTL_2, + 0x07, 0x01); + + /* RX defaults (harmless while RX is unimplemented) */ + snd_soc_component_update_bits(component, WCD9378_HPH_RDAC_HD2_CTL_L, + 0x0f, 0x04); + snd_soc_component_update_bits(component, WCD9378_HPH_RDAC_HD2_CTL_R, + 0x0f, 0x04); + snd_soc_component_update_bits(component, WCD9378_HPH_RDAC_GAIN_CTL, + 0xf0, 0x50); + snd_soc_component_update_bits(component, WCD9378_HPH_UP_T0, + 0x07, 0x05); + snd_soc_component_update_bits(component, WCD9378_HPH_UP_T9, + 0x07, 0x05); + snd_soc_component_update_bits(component, WCD9378_HPH_DN_T0, + 0x07, 0x06); + + /* SmartMIC function N powers the micbias SMx_MB_SEL points at */ + snd_soc_component_update_bits(component, WCD9378_SM0_MB_SEL, + WCD9378_SM_MB_SEL_MASK, 0x01); + snd_soc_component_update_bits(component, WCD9378_SM1_MB_SEL, + WCD9378_SM_MB_SEL_MASK, 0x02); + snd_soc_component_update_bits(component, WCD9378_SM2_MB_SEL, + WCD9378_SM_MB_SEL_MASK, 0x03); + snd_soc_component_update_bits(component, WCD9378_SYS_USAGE_CTRL, + WCD9378_SYS_USAGE_CTRL_MASK, 0x00); + + wcd9378_class_load(component); +} + +/* + * Derive the sys-usage capability bit for an ADC from its input mux. + * Only combinations the canned profiles cover are usable. + */ +static int wcd9378_sys_usage_bit_get(struct snd_soc_component *component, + int adc, bool *is_jack) +{ + u32 val; + + *is_jack = false; + + switch (adc) { + case 0: + val = snd_soc_component_read(component, WCD9378_TX_NEW_CH12_MUX) & + WCD9378_TX_NEW_CH12_MUX_CH1_SEL_MASK; + if (val == 0x01) + return WCD9378_SYS_USAGE_TX0_AMIC1; + if (val == 0x02) + return WCD9378_SYS_USAGE_TX0_AMIC2; + break; + case 1: + val = (snd_soc_component_read(component, WCD9378_TX_NEW_CH12_MUX) & + WCD9378_TX_NEW_CH12_MUX_CH2_SEL_MASK) >> 3; + if (val == 0x02) { + *is_jack = true; + return WCD9378_SYS_USAGE_TX1_AMIC2; + } + if (val == 0x03) + return WCD9378_SYS_USAGE_TX1_AMIC3; + break; + case 2: + val = snd_soc_component_read(component, WCD9378_TX_NEW_CH34_MUX) & + WCD9378_TX_NEW_CH34_MUX_CH3_SEL_MASK; + if (val == 0x01) + return WCD9378_SYS_USAGE_TX2_AMIC1; + if (val == 0x03) + return WCD9378_SYS_USAGE_TX2_AMIC4; + break; + } + + dev_err(component->dev, + "ADC%d input mux selection not supported by any sys-usage profile\n", + adc + 1); + return -EINVAL; +} + +static int wcd9378_sys_usage_update(struct snd_soc_component *component, + int bit, bool enable) +{ + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + int i; + + if (!enable) { + clear_bit(bit, &wcd9378->sys_usage_mask); + return 0; + } + + set_bit(bit, &wcd9378->sys_usage_mask); + + for (i = 0; i < ARRAY_SIZE(wcd9378_sys_usage_profiles); i++) { + if ((wcd9378_sys_usage_profiles[i] & wcd9378->sys_usage_mask) == + wcd9378->sys_usage_mask) + break; + } + + if (i == ARRAY_SIZE(wcd9378_sys_usage_profiles)) { + dev_err(component->dev, + "no sys-usage profile covers active paths (mask %#lx)\n", + wcd9378->sys_usage_mask); + return -EINVAL; + } + + if (i != wcd9378->sys_usage) { + snd_soc_component_update_bits(component, WCD9378_SYS_USAGE_CTRL, + WCD9378_SYS_USAGE_CTRL_MASK, i); + wcd9378->sys_usage = i; + } + + return 0; +} + +static u32 wcd9378_get_mode_val(struct wcd9378_priv *wcd9378, int adc) +{ + switch (wcd9378->tx_mode[adc]) { + case 1: + return WCD9378_ADC_USAGE_HIFI; + case 2: + return WCD9378_ADC_USAGE_LO_HIF; + case 4: + return WCD9378_ADC_USAGE_LP; + case 0: /* ADC_INVALID (unset) */ + case 3: + default: + return WCD9378_ADC_USAGE_NORMAL; + } +} + +/* + * Indicate the bus clock to the codec through the standard SCP + * BusClock_Base/Scale registers. The TX sequencers clock off the + * SoundWire bus clock and stall without this; the downstream driver + * writes these on every capture start and clears them on the last + * teardown. + */ +/* Actual bus clock is half the SoundWire double-rate frequency */ +static unsigned int wcd9378_tx_bus_clk(struct wcd9378_priv *wcd9378) +{ + return wcd9378->tx_sdw_dev->bus->params.curr_dr_freq / 2; +} + +static void wcd9378_swr_clk_indicate(struct wcd9378_priv *wcd9378, bool enable) +{ + struct sdw_slave *tx = wcd9378->tx_sdw_dev; + u8 scale; + + if (enable) { + if (wcd9378_tx_bus_clk(wcd9378) >= 9600000) + scale = WCD9378_SWRS_CLK_SCALE_DIV2; + else + scale = WCD9378_SWRS_CLK_SCALE_DIV4; + + /* + * The downstream master broadcasts HOST_CLK_DIV2_CTL = 0x01 + * (both banks) on every capture start; the sequencer does not + * power up without it. + */ + sdw_write(tx, WCD9378_SWRS_SCP_HOST_CLK_DIV2_CTL(0), 0x01); + sdw_write(tx, WCD9378_SWRS_SCP_HOST_CLK_DIV2_CTL(1), 0x01); + sdw_write(tx, WCD9378_SWRS_SCP_BASE_CLK, + WCD9378_SWRS_BASE_CLK_19P2MHZ); + sdw_write(tx, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK0, scale); + sdw_write(tx, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK1, scale); + sdw_write(tx, SDW_SCP_COMMIT, 0x02); + } else { + sdw_write(tx, WCD9378_SWRS_SCP_BASE_CLK, 0x00); + sdw_write(tx, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK0, 0x00); + sdw_write(tx, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK1, 0x00); + } +} + +#define WCD9378_SYS_USAGE_TX_MASK (BIT(WCD9378_SYS_USAGE_TX2_AMIC4) | \ + BIT(WCD9378_SYS_USAGE_TX2_AMIC1) | \ + BIT(WCD9378_SYS_USAGE_TX1_AMIC3) | \ + BIT(WCD9378_SYS_USAGE_TX1_AMIC2) | \ + BIT(WCD9378_SYS_USAGE_TX0_AMIC2) | \ + BIT(WCD9378_SYS_USAGE_TX0_AMIC1)) + +static int wcd9378_tx_sequencer_enable(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, + int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + const struct wcd9378_smp_fn *fn = &wcd9378_smp_mic[w->shift]; + int adc = w->shift; + bool is_jack = false; + int sys_bit, retries; + u32 val; + + sys_bit = wcd9378_sys_usage_bit_get(component, adc, &is_jack); + if (sys_bit < 0) + return sys_bit; + + if (is_jack) + fn = &wcd9378_smp_jack_adc2; + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + if (wcd9378_sys_usage_update(component, sys_bit, true)) + return -EINVAL; + + /* + * NORMAL/HIFI ADC modes need a 9.6 MHz bus clock; on a + * 4.8 MHz bus only the LP mode is valid and anything else + * makes the sequencer refuse to power up. + */ + if (wcd9378_tx_bus_clk(wcd9378) < 9600000) + val = WCD9378_ADC_USAGE_LP; + else + val = wcd9378_get_mode_val(wcd9378, adc); + + snd_soc_component_update_bits(component, fn->usage_reg, 0xff, val); + if (fn->hpf_reg) + snd_soc_component_update_bits(component, fn->hpf_reg, + fn->hpf_mask, fn->hpf_mask); + snd_soc_component_update_bits(component, fn->req_reg, 0xff, + WCD9378_PDE_PS0_ON); + usleep_range(800, 810); + + wcd9378_swr_clk_indicate(wcd9378, true); + + if (fn->hpf_reg) + snd_soc_component_update_bits(component, fn->hpf_reg, + fn->hpf_mask, 0x00); + + /* Wait for the sequencer to reach PS0 */ + retries = 20; + do { + val = snd_soc_component_read(component, fn->act_reg); + if (val == WCD9378_PDE_PS0_ON) + break; + /* re-issue the request once in case it wasn't latched */ + if (retries == 10) { + snd_soc_component_update_bits(component, + fn->req_reg, 0xff, + WCD9378_PDE_PS3_OFF); + snd_soc_component_update_bits(component, + fn->req_reg, 0xff, + WCD9378_PDE_PS0_ON); + } + usleep_range(500, 510); + } while (--retries); + if (val != WCD9378_PDE_PS0_ON) { + struct regmap *rm = wcd9378->regmap; + u32 usage = 0, req = 0, seq = 0, ot10 = 0, fstat = 0, + micb = 0, bias = 0, fact = 0; + + regcache_cache_bypass(rm, true); + regmap_read(rm, fn->usage_reg, &usage); + regmap_read(rm, fn->req_reg, &req); + regmap_read(rm, WCD9378_SEQ_TX0_STAT + adc, &seq); + regmap_read(rm, WCD9378_SMP_MIC_OT10_USAGE(adc), &ot10); + regmap_read(rm, WCD9378_SMP_MIC_FUNC_STAT(adc), &fstat); + regmap_read(rm, WCD9378_SMP_MIC_FUNC_ACT(adc), &fact); + regmap_read(rm, fn->micb_reg, &micb); + regmap_read(rm, WCD9378_ANA_BIAS, &bias); + regcache_cache_bypass(rm, false); + + dev_warn(component->dev, + "TX%d sequencer not in PS0: act %#x dr_freq %u hw[usage %#x req %#x seq_stat %#x ot10 %#x func_act %#x func_stat %#x micb %#x ana_bias %#x]\n", + adc, val, wcd9378->tx_sdw_dev->bus->params.curr_dr_freq, + usage, req, seq, ot10, fact, fstat, micb, bias); + dev_warn(component->dev, + "raw slave regs: dp%den_b0 %#x b1 %#x scp_base %#x scale_b0 %#x scale_b1 %#x scp_stat %#x\n", + adc + 1, + sdw_read(wcd9378->tx_sdw_dev, 0x120 + (adc * 0x100)), + sdw_read(wcd9378->tx_sdw_dev, 0x130 + (adc * 0x100)), + sdw_read(wcd9378->tx_sdw_dev, WCD9378_SWRS_SCP_BASE_CLK), + sdw_read(wcd9378->tx_sdw_dev, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK0), + sdw_read(wcd9378->tx_sdw_dev, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK1), + sdw_read(wcd9378->tx_sdw_dev, SDW_SCP_STAT)); + } + break; + case SND_SOC_DAPM_POST_PMD: + snd_soc_component_update_bits(component, fn->usage_reg, 0xff, + WCD9378_ADC_USAGE_OFF); + if (fn->hpf_reg) + snd_soc_component_update_bits(component, fn->hpf_reg, + fn->hpf_mask, 0x00); + snd_soc_component_update_bits(component, fn->req_reg, 0xff, + WCD9378_PDE_PS3_OFF); + usleep_range(800, 810); + wcd9378_sys_usage_update(component, sys_bit, false); + + if (!(wcd9378->sys_usage_mask & WCD9378_SYS_USAGE_TX_MASK)) + wcd9378_swr_clk_indicate(wcd9378, false); + break; + } + + return 0; +} + +static int wcd9378_micbias_control(struct snd_soc_component *component, + int micb_num, int req, bool is_dapm) +{ + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + int mb_index = micb_num - 1; + u32 usage_reg; + u8 usage_val; + u8 pullup_bit; + + if (micb_num < MIC_BIAS_1 || micb_num > MIC_BIAS_3) + return -EINVAL; + + usage_reg = wcd9378_smp_mic[mb_index].micb_reg; + usage_val = wcd9378->micb_usage_val[mb_index]; + pullup_bit = BIT(mb_index); + + mutex_lock(&wcd9378->micb_lock); + + switch (req) { + case MICB_ENABLE: + wcd9378->micb_ref[mb_index]++; + if (wcd9378->micb_ref[mb_index] == 1) { + if (micb_num == MIC_BIAS_2) { + snd_soc_component_update_bits(component, + WCD9378_ANA_MICB2_RAMP, + WCD9378_ANA_MICB2_RAMP_SHIFT_CTL_MASK, + 0x0c); + snd_soc_component_update_bits(component, + WCD9378_ANA_MICB2_RAMP, + WCD9378_ANA_MICB2_RAMP_EN, 0x00); + } + snd_soc_component_update_bits(component, usage_reg, + 0xff, usage_val); + if (micb_num == MIC_BIAS_2) + snd_soc_component_update_bits(component, + WCD9378_SMP_JACK_IT31_MICB, + 0xff, usage_val); + } + break; + case MICB_DISABLE: + if (wcd9378->micb_ref[mb_index] > 0) + wcd9378->micb_ref[mb_index]--; + if (wcd9378->micb_ref[mb_index] == 0 && + wcd9378->pullup_ref[mb_index] > 0) { + snd_soc_component_update_bits(component, + WCD9378_MB_PULLUP_EN, + pullup_bit, pullup_bit); + snd_soc_component_update_bits(component, usage_reg, 0xff, + WCD9378_MICB_USAGE_1P8V_OR_PULLUP); + if (micb_num == MIC_BIAS_2) + snd_soc_component_update_bits(component, + WCD9378_SMP_JACK_IT31_MICB, 0xff, + WCD9378_MICB_USAGE_1P8V_OR_PULLUP); + } else if (wcd9378->micb_ref[mb_index] == 0) { + snd_soc_component_update_bits(component, usage_reg, + 0xff, WCD9378_MICB_USAGE_OFF); + if (micb_num == MIC_BIAS_2) { + snd_soc_component_update_bits(component, + WCD9378_SMP_JACK_IT31_MICB, + 0xff, WCD9378_MICB_USAGE_OFF); + snd_soc_component_update_bits(component, + WCD9378_ANA_MICB2_RAMP, + WCD9378_ANA_MICB2_RAMP_SHIFT_CTL_MASK, + 0x0c); + snd_soc_component_update_bits(component, + WCD9378_ANA_MICB2_RAMP, + WCD9378_ANA_MICB2_RAMP_EN, + WCD9378_ANA_MICB2_RAMP_EN); + } + } + break; + case MICB_PULLUP_ENABLE: + wcd9378->pullup_ref[mb_index]++; + if (wcd9378->pullup_ref[mb_index] == 1 && + wcd9378->micb_ref[mb_index] == 0) { + snd_soc_component_update_bits(component, + WCD9378_MB_PULLUP_EN, + pullup_bit, pullup_bit); + snd_soc_component_update_bits(component, usage_reg, 0xff, + WCD9378_MICB_USAGE_1P8V_OR_PULLUP); + if (micb_num == MIC_BIAS_2) + snd_soc_component_update_bits(component, + WCD9378_SMP_JACK_IT31_MICB, 0xff, + WCD9378_MICB_USAGE_1P8V_OR_PULLUP); + } + break; + case MICB_PULLUP_DISABLE: + if (wcd9378->pullup_ref[mb_index] > 0) + wcd9378->pullup_ref[mb_index]--; + if (wcd9378->pullup_ref[mb_index] == 0 && + wcd9378->micb_ref[mb_index] == 0) { + snd_soc_component_update_bits(component, + WCD9378_MB_PULLUP_EN, pullup_bit, 0x00); + snd_soc_component_update_bits(component, usage_reg, 0xff, + WCD9378_MICB_USAGE_PULL_DOWN); + if (micb_num == MIC_BIAS_2) + snd_soc_component_update_bits(component, + WCD9378_SMP_JACK_IT31_MICB, 0xff, + WCD9378_MICB_USAGE_PULL_DOWN); + } + break; + } + + mutex_unlock(&wcd9378->micb_lock); + + return 0; +} + +static int wcd9378_codec_enable_micbias(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, + int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + int micb_num = w->shift; + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + wcd9378_micbias_control(component, micb_num, MICB_ENABLE, true); + break; + case SND_SOC_DAPM_POST_PMU: + usleep_range(1000, 1100); + break; + case SND_SOC_DAPM_POST_PMD: + wcd9378_micbias_control(component, micb_num, MICB_DISABLE, true); + break; + } + + return 0; +} + +static int wcd9378_codec_enable_micbias_pullup(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, + int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + int micb_num = w->shift; + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + wcd9378_micbias_control(component, micb_num, + MICB_PULLUP_ENABLE, true); + break; + case SND_SOC_DAPM_POST_PMU: + usleep_range(1000, 1100); + break; + case SND_SOC_DAPM_POST_PMD: + wcd9378_micbias_control(component, micb_num, + MICB_PULLUP_DISABLE, true); + break; + } + + return 0; +} + +static int wcd9378_connect_port(struct wcd9378_sdw_priv *wcd, u8 port_idx, + u8 ch_id, bool enable) +{ + struct sdw_port_config *port_config = &wcd->port_config[port_idx - 1]; + const struct wcd_sdw_ch_info *ch_info = &wcd->ch_info[ch_id]; + u8 port_num = ch_info->port_num; + u8 ch_mask = ch_info->ch_mask; + u8 mstr_port_num, mstr_ch_mask; + struct sdw_slave *sdev = wcd->sdev; + + port_config->num = port_num; + + mstr_port_num = sdev->m_port_map[port_num]; + mstr_ch_mask = ch_info->master_ch_mask; + + if (enable) { + port_config->ch_mask |= ch_mask; + wcd->master_channel_map[mstr_port_num] |= mstr_ch_mask; + } else { + port_config->ch_mask &= ~ch_mask; + wcd->master_channel_map[mstr_port_num] &= ~mstr_ch_mask; + } + + return 0; +} + +static int wcd9378_get_swr_port(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct soc_mixer_control *mixer = (struct soc_mixer_control *)kcontrol->private_value; + struct snd_soc_component *comp = snd_kcontrol_chip(kcontrol); + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(comp); + struct wcd9378_sdw_priv *wcd; + int dai_id = mixer->shift; + int ch_idx = mixer->reg; + int portidx; + + wcd = wcd9378->sdw_priv[dai_id]; + portidx = wcd->ch_info[ch_idx].port_num; + + ucontrol->value.integer.value[0] = wcd->port_enable[portidx]; + + return 0; +} + +static int wcd9378_set_swr_port(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct soc_mixer_control *mixer = (struct soc_mixer_control *)kcontrol->private_value; + struct snd_soc_component *comp = snd_kcontrol_chip(kcontrol); + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(comp); + struct wcd9378_sdw_priv *wcd; + int dai_id = mixer->shift; + int ch_idx = mixer->reg; + int portidx; + bool enable; + + wcd = wcd9378->sdw_priv[dai_id]; + portidx = wcd->ch_info[ch_idx].port_num; + + enable = ucontrol->value.integer.value[0]; + + if (enable == wcd->port_enable[portidx]) { + wcd9378_connect_port(wcd, portidx, ch_idx, enable); + return 0; + } + + wcd->port_enable[portidx] = enable; + wcd9378_connect_port(wcd, portidx, ch_idx, enable); + + return 1; +} + +static int wcd9378_tx_mode_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + int adc = e->shift_l; + + ucontrol->value.enumerated.item[0] = wcd9378->tx_mode[adc]; + + return 0; +} + +static int wcd9378_tx_mode_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + int adc = e->shift_l; + u32 mode_val = ucontrol->value.enumerated.item[0]; + + if (mode_val == wcd9378->tx_mode[adc]) + return 0; + + wcd9378->tx_mode[adc] = mode_val; + + return 1; +} + +static const char * const tx_mode_mux_text[] = { + "ADC_INVALID", "ADC_HIFI", "ADC_LO_HIF", "ADC_NORMAL", "ADC_LP", +}; + +static const struct soc_enum tx0_mode_enum = + SOC_ENUM_SINGLE(SND_SOC_NOPM, 0, ARRAY_SIZE(tx_mode_mux_text), + tx_mode_mux_text); +static const struct soc_enum tx1_mode_enum = + SOC_ENUM_SINGLE(SND_SOC_NOPM, 1, ARRAY_SIZE(tx_mode_mux_text), + tx_mode_mux_text); +static const struct soc_enum tx2_mode_enum = + SOC_ENUM_SINGLE(SND_SOC_NOPM, 2, ARRAY_SIZE(tx_mode_mux_text), + tx_mode_mux_text); + +static const struct snd_kcontrol_new wcd9378_snd_controls[] = { + SOC_SINGLE_TLV("ADC1 Volume", WCD9378_ANA_TX_CH1, 0, 20, 0, + analog_gain), + SOC_SINGLE_TLV("ADC2 Volume", WCD9378_ANA_TX_CH2, 0, 20, 0, + analog_gain), + SOC_SINGLE_TLV("ADC3 Volume", WCD9378_ANA_TX_CH3, 0, 20, 0, + analog_gain), + + SOC_ENUM_EXT("TX0 MODE", tx0_mode_enum, + wcd9378_tx_mode_get, wcd9378_tx_mode_put), + SOC_ENUM_EXT("TX1 MODE", tx1_mode_enum, + wcd9378_tx_mode_get, wcd9378_tx_mode_put), + SOC_ENUM_EXT("TX2 MODE", tx2_mode_enum, + wcd9378_tx_mode_get, wcd9378_tx_mode_put), + + SOC_SINGLE_EXT("ADC1 Switch", WCD9378_ADC1, AIF1_CAP, 1, 0, + wcd9378_get_swr_port, wcd9378_set_swr_port), + SOC_SINGLE_EXT("ADC2 Switch", WCD9378_ADC2, AIF1_CAP, 1, 0, + wcd9378_get_swr_port, wcd9378_set_swr_port), + SOC_SINGLE_EXT("ADC3 Switch", WCD9378_ADC3, AIF1_CAP, 1, 0, + wcd9378_get_swr_port, wcd9378_set_swr_port), +}; + +static const char * const adc1_mux_text[] = { + "CH1_AMIC_DISABLE", "CH1_AMIC1", "CH1_AMIC2", "CH1_AMIC3", "CH1_AMIC4", +}; + +static const char * const adc2_mux_text[] = { + "CH2_AMIC_DISABLE", "CH2_AMIC1", "CH2_AMIC2", "CH2_AMIC3", "CH2_AMIC4", +}; + +static const char * const adc3_mux_text[] = { + "CH3_AMIC_DISABLE", "CH3_AMIC1", "CH3_AMIC3", "CH3_AMIC4", +}; + +static const struct soc_enum adc1_mux_enum = + SOC_ENUM_SINGLE(WCD9378_TX_NEW_CH12_MUX, 0, + ARRAY_SIZE(adc1_mux_text), adc1_mux_text); +static const struct soc_enum adc2_mux_enum = + SOC_ENUM_SINGLE(WCD9378_TX_NEW_CH12_MUX, 3, + ARRAY_SIZE(adc2_mux_text), adc2_mux_text); +static const struct soc_enum adc3_mux_enum = + SOC_ENUM_SINGLE(WCD9378_TX_NEW_CH34_MUX, 0, + ARRAY_SIZE(adc3_mux_text), adc3_mux_text); + +static const struct snd_kcontrol_new adc1_mux = SOC_DAPM_ENUM("ADC1 MUX", adc1_mux_enum); +static const struct snd_kcontrol_new adc2_mux = SOC_DAPM_ENUM("ADC2 MUX", adc2_mux_enum); +static const struct snd_kcontrol_new adc3_mux = SOC_DAPM_ENUM("ADC3 MUX", adc3_mux_enum); + +static const struct snd_kcontrol_new tx0_seq_switch = + SOC_DAPM_SINGLE("Switch", SND_SOC_NOPM, 0, 1, 0); +static const struct snd_kcontrol_new tx1_seq_switch = + SOC_DAPM_SINGLE("Switch", SND_SOC_NOPM, 0, 1, 0); +static const struct snd_kcontrol_new tx2_seq_switch = + SOC_DAPM_SINGLE("Switch", SND_SOC_NOPM, 0, 1, 0); + +static const struct snd_soc_dapm_widget wcd9378_dapm_widgets[] = { + /* Analog mic inputs */ + SND_SOC_DAPM_INPUT("AMIC1"), + SND_SOC_DAPM_INPUT("AMIC2"), + SND_SOC_DAPM_INPUT("AMIC3"), + SND_SOC_DAPM_INPUT("AMIC4"), + + /* ADC input muxes */ + SND_SOC_DAPM_MUX("ADC1 MUX", SND_SOC_NOPM, 0, 0, &adc1_mux), + SND_SOC_DAPM_MUX("ADC2 MUX", SND_SOC_NOPM, 0, 0, &adc2_mux), + SND_SOC_DAPM_MUX("ADC3 MUX", SND_SOC_NOPM, 0, 0, &adc3_mux), + + /* SDCA TX sequencers (widget shift = ADC index) */ + SND_SOC_DAPM_MIXER_E("TX0 SEQUENCER", SND_SOC_NOPM, 0, 0, + &tx0_seq_switch, 1, wcd9378_tx_sequencer_enable, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_MIXER_E("TX1 SEQUENCER", SND_SOC_NOPM, 1, 0, + &tx1_seq_switch, 1, wcd9378_tx_sequencer_enable, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_MIXER_E("TX2 SEQUENCER", SND_SOC_NOPM, 2, 0, + &tx2_seq_switch, 1, wcd9378_tx_sequencer_enable, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD), + + /* Micbias supplies (widget shift = micbias number) */ + SND_SOC_DAPM_SUPPLY("MIC BIAS1", SND_SOC_NOPM, MIC_BIAS_1, 0, + wcd9378_codec_enable_micbias, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_SUPPLY("MIC BIAS2", SND_SOC_NOPM, MIC_BIAS_2, 0, + wcd9378_codec_enable_micbias, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_SUPPLY("MIC BIAS3", SND_SOC_NOPM, MIC_BIAS_3, 0, + wcd9378_codec_enable_micbias, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + + SND_SOC_DAPM_SUPPLY("VA MIC BIAS1", SND_SOC_NOPM, MIC_BIAS_1, 0, + wcd9378_codec_enable_micbias_pullup, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_SUPPLY("VA MIC BIAS2", SND_SOC_NOPM, MIC_BIAS_2, 0, + wcd9378_codec_enable_micbias_pullup, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_SUPPLY("VA MIC BIAS3", SND_SOC_NOPM, MIC_BIAS_3, 0, + wcd9378_codec_enable_micbias_pullup, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + + /* Outputs towards the SoundWire TX bus / LPASS TX macro */ + SND_SOC_DAPM_OUTPUT("ADC1_OUTPUT"), + SND_SOC_DAPM_OUTPUT("ADC2_OUTPUT"), + SND_SOC_DAPM_OUTPUT("ADC3_OUTPUT"), +}; + +static const struct snd_soc_dapm_route wcd9378_audio_map[] = { + { "ADC1_OUTPUT", NULL, "TX0 SEQUENCER" }, + { "TX0 SEQUENCER", "Switch", "ADC1 MUX" }, + { "ADC1 MUX", "CH1_AMIC1", "AMIC1" }, + { "ADC1 MUX", "CH1_AMIC2", "AMIC2" }, + { "ADC1 MUX", "CH1_AMIC3", "AMIC3" }, + { "ADC1 MUX", "CH1_AMIC4", "AMIC4" }, + + { "ADC2_OUTPUT", NULL, "TX1 SEQUENCER" }, + { "TX1 SEQUENCER", "Switch", "ADC2 MUX" }, + { "ADC2 MUX", "CH2_AMIC1", "AMIC1" }, + { "ADC2 MUX", "CH2_AMIC2", "AMIC2" }, + { "ADC2 MUX", "CH2_AMIC3", "AMIC3" }, + { "ADC2 MUX", "CH2_AMIC4", "AMIC4" }, + + { "ADC3_OUTPUT", NULL, "TX2 SEQUENCER" }, + { "TX2 SEQUENCER", "Switch", "ADC3 MUX" }, + { "ADC3 MUX", "CH3_AMIC1", "AMIC1" }, + { "ADC3 MUX", "CH3_AMIC3", "AMIC3" }, + { "ADC3 MUX", "CH3_AMIC4", "AMIC4" }, +}; + +/* + * Map the DT micbias millivolt values onto ITxx_MICB usage codes; + * non-standard voltages go through the sequencer remap table. + */ +static void wcd9378_set_micb_usage_vals(struct snd_soc_component *component) +{ + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + static const u32 remap_regs[] = { + WCD9378_MICB_REMAP_TABLE_VAL_3, + WCD9378_MICB_REMAP_TABLE_VAL_4, + WCD9378_MICB_REMAP_TABLE_VAL_5, + }; + int i, vout; + + for (i = 0; i < WCD9378_MAX_MICBIAS; i++) { + switch (wcd9378->common.micb_mv[i]) { + case 1200: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_1P2V; + break; + case 1800: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_1P8V_OR_PULLUP; + break; + case 2200: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_2P2V; + break; + case 2500: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_2P5V; + break; + case 2700: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_2P7V; + break; + case 2750: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_2P75V; + break; + case 2800: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_2P8V; + break; + default: + vout = wcd_get_micb_vout_ctl_val(component->dev, + wcd9378->common.micb_mv[i]); + if (vout < 0) { + wcd9378->micb_usage_val[i] = + WCD9378_MICB_USAGE_1P8V_OR_PULLUP; + break; + } + snd_soc_component_update_bits(component, remap_regs[i], + 0xff, vout); + wcd9378->micb_usage_val[i] = + WCD9378_MICB_USAGE_REMAP_TABLE_3 + i; + break; + } + } +} + +static int wcd9378_soc_codec_probe(struct snd_soc_component *component) +{ + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + struct device *dev = component->dev; + unsigned int part0 = 0, part1 = 0; + unsigned long time_left; + int ret; + + time_left = wait_for_completion_timeout( + &wcd9378->tx_sdw_dev->initialization_complete, + msecs_to_jiffies(5000)); + if (!time_left) { + dev_err(dev, "soundwire device init timeout\n"); + return -ETIMEDOUT; + } + + snd_soc_component_init_regmap(component, wcd9378->regmap); + + ret = pm_runtime_resume_and_get(dev); + if (ret < 0) + return ret; + + regmap_read(wcd9378->regmap, WCD9378_DEV_PART_ID_0, &part0); + regmap_read(wcd9378->regmap, WCD9378_DEV_PART_ID_1, &part1); + dev_dbg(dev, "WCD9378 part id %#x\n", (part1 << 8) | part0); + + /* SDCA interrupt type config, as done by the downstream driver */ + sdw_write(wcd9378->tx_sdw_dev, 0xf4, 0xff); + sdw_write(wcd9378->tx_sdw_dev, 0xf8, 0x0b); + sdw_write(wcd9378->tx_sdw_dev, 0xfc, 0xff); + + wcd9378_io_init(component); + wcd9378_set_micb_usage_vals(component); + + pm_runtime_mark_last_busy(dev); + pm_runtime_put_autosuspend(dev); + + return 0; +} + +static const struct snd_soc_component_driver soc_codec_dev_wcd9378 = { + .name = "wcd9378_codec", + .probe = wcd9378_soc_codec_probe, + .controls = wcd9378_snd_controls, + .num_controls = ARRAY_SIZE(wcd9378_snd_controls), + .dapm_widgets = wcd9378_dapm_widgets, + .num_dapm_widgets = ARRAY_SIZE(wcd9378_dapm_widgets), + .dapm_routes = wcd9378_audio_map, + .num_dapm_routes = ARRAY_SIZE(wcd9378_audio_map), + .endianness = 1, +}; + +static int wcd9378_codec_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dai->dev); + struct wcd9378_sdw_priv *wcd = wcd9378->sdw_priv[dai->id]; + + return wcd9378_sdw_hw_params(wcd, substream, params, dai); +} + +static int wcd9378_codec_free(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dai->dev); + struct wcd9378_sdw_priv *wcd = wcd9378->sdw_priv[dai->id]; + + return sdw_stream_remove_slave(wcd->sdev, wcd->sruntime); +} + +static int wcd9378_codec_set_sdw_stream(struct snd_soc_dai *dai, + void *stream, int direction) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dai->dev); + struct wcd9378_sdw_priv *wcd = wcd9378->sdw_priv[dai->id]; + + wcd->sruntime = stream; + + return 0; +} + +static int wcd9378_get_channel_map(const struct snd_soc_dai *dai, + unsigned int *tx_num, unsigned int *tx_slot, + unsigned int *rx_num, unsigned int *rx_slot) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dai->dev); + struct wcd9378_sdw_priv *wcd = wcd9378->sdw_priv[dai->id]; + int i; + + switch (dai->id) { + case AIF1_PB: + if (!rx_slot || !rx_num) { + dev_err(dai->dev, "Invalid rx_slot %p or rx_num %p\n", + rx_slot, rx_num); + return -EINVAL; + } + + for (i = 0; i < SDW_MAX_PORTS; i++) + rx_slot[i] = wcd->master_channel_map[i]; + + *rx_num = i; + break; + case AIF1_CAP: + if (!tx_slot || !tx_num) { + dev_err(dai->dev, "Invalid tx_slot %p or tx_num %p\n", + tx_slot, tx_num); + return -EINVAL; + } + + for (i = 0; i < SDW_MAX_PORTS; i++) + tx_slot[i] = wcd->master_channel_map[i]; + + *tx_num = i; + break; + default: + break; + } + + return 0; +} + +static const struct snd_soc_dai_ops wcd9378_sdw_dai_ops = { + .hw_params = wcd9378_codec_hw_params, + .hw_free = wcd9378_codec_free, + .set_stream = wcd9378_codec_set_sdw_stream, + .get_channel_map = wcd9378_get_channel_map, +}; + +#define WCD9378_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000 |\ + SNDRV_PCM_RATE_32000 | SNDRV_PCM_RATE_48000 |\ + SNDRV_PCM_RATE_96000 | SNDRV_PCM_RATE_192000) +#define WCD9378_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE |\ + SNDRV_PCM_FMTBIT_S24_3LE | SNDRV_PCM_FMTBIT_S32_LE) + +static struct snd_soc_dai_driver wcd9378_dais[] = { + [0] = { + .name = "wcd9378-sdw-rx", + .playback = { + .stream_name = "WCD AIF Playback", + .rates = WCD9378_RATES, + .formats = WCD9378_FORMATS, + .rate_min = 8000, + .rate_max = 192000, + .channels_min = 1, + .channels_max = 4, + }, + .ops = &wcd9378_sdw_dai_ops, + }, + [1] = { + .name = "wcd9378-sdw-tx", + .capture = { + .stream_name = "WCD AIF Capture", + .rates = WCD9378_RATES, + .formats = WCD9378_FORMATS, + .rate_min = 8000, + .rate_max = 192000, + .channels_min = 1, + .channels_max = 4, + }, + .ops = &wcd9378_sdw_dai_ops, + }, +}; + +static int wcd9378_bind(struct device *dev) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dev); + int ret; + + /* Give the SDW subdevices some more time to settle */ + usleep_range(5000, 5010); + + ret = component_bind_all(dev, wcd9378); + if (ret) { + dev_err(dev, "Slave bind failed, ret = %d\n", ret); + return ret; + } + + wcd9378->rxdev = of_sdw_find_device_by_node(wcd9378->rxnode); + if (!wcd9378->rxdev) { + dev_err(dev, "could not find rx slave with matching of node\n"); + ret = -EINVAL; + goto err_component_unbind; + } + + wcd9378->sdw_priv[AIF1_PB] = dev_get_drvdata(wcd9378->rxdev); + wcd9378->sdw_priv[AIF1_PB]->wcd9378 = wcd9378; + + wcd9378->txdev = of_sdw_find_device_by_node(wcd9378->txnode); + if (!wcd9378->txdev) { + dev_err(dev, "could not find tx slave with matching of node\n"); + ret = -EINVAL; + goto err_put_rxdev; + } + + wcd9378->sdw_priv[AIF1_CAP] = dev_get_drvdata(wcd9378->txdev); + wcd9378->sdw_priv[AIF1_CAP]->wcd9378 = wcd9378; + wcd9378->tx_sdw_dev = dev_to_sdw_dev(wcd9378->txdev); + + /* + * As TX is the main CSR reg interface, it should not be suspended + * first. Explicitly add the dependency link. + */ + if (!device_link_add(wcd9378->rxdev, wcd9378->txdev, + DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME)) { + dev_err(dev, "Could not devlink TX and RX\n"); + ret = -EINVAL; + goto err_put_txdev; + } + + if (!device_link_add(dev, wcd9378->txdev, + DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME)) { + dev_err(dev, "Could not devlink WCD and TX\n"); + ret = -EINVAL; + goto err_remove_link1; + } + + if (!device_link_add(dev, wcd9378->rxdev, + DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME)) { + dev_err(dev, "Could not devlink WCD and RX\n"); + ret = -EINVAL; + goto err_remove_link2; + } + + wcd9378->regmap = wcd9378->sdw_priv[AIF1_CAP]->regmap; + if (!wcd9378->regmap) { + dev_err(dev, "could not get TX device regmap\n"); + ret = -EINVAL; + goto err_remove_link3; + } + + ret = snd_soc_register_component(dev, &soc_codec_dev_wcd9378, + wcd9378_dais, ARRAY_SIZE(wcd9378_dais)); + if (ret) { + dev_err(dev, "Codec registration failed\n"); + goto err_remove_link3; + } + + return ret; + +err_remove_link3: + device_link_remove(dev, wcd9378->rxdev); +err_remove_link2: + device_link_remove(dev, wcd9378->txdev); +err_remove_link1: + device_link_remove(wcd9378->rxdev, wcd9378->txdev); +err_put_txdev: + put_device(wcd9378->txdev); +err_put_rxdev: + put_device(wcd9378->rxdev); +err_component_unbind: + component_unbind_all(dev, wcd9378); + return ret; +} + +static void wcd9378_unbind(struct device *dev) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dev); + + snd_soc_unregister_component(dev); + device_link_remove(dev, wcd9378->txdev); + device_link_remove(dev, wcd9378->rxdev); + device_link_remove(wcd9378->rxdev, wcd9378->txdev); + component_unbind_all(dev, wcd9378); + mutex_destroy(&wcd9378->micb_lock); + put_device(wcd9378->txdev); + put_device(wcd9378->rxdev); +} + +static const struct component_master_ops wcd9378_comp_ops = { + .bind = wcd9378_bind, + .unbind = wcd9378_unbind, +}; + +static int wcd9378_add_slave_components(struct wcd9378_priv *wcd9378, + struct device *dev, + struct component_match **matchptr) +{ + struct device_node *np = dev->of_node; + + wcd9378->rxnode = of_parse_phandle(np, "qcom,rx-device", 0); + if (!wcd9378->rxnode) { + dev_err(dev, "Couldn't parse phandle to qcom,rx-device!\n"); + return -ENODEV; + } + + component_match_add_release(dev, matchptr, component_release_of, + component_compare_of, wcd9378->rxnode); + + wcd9378->txnode = of_parse_phandle(np, "qcom,tx-device", 0); + if (!wcd9378->txnode) { + dev_err(dev, "Couldn't parse phandle to qcom,tx-device\n"); + return -ENODEV; + } + + component_match_add_release(dev, matchptr, component_release_of, + component_compare_of, wcd9378->txnode); + + return 0; +} + +static int wcd9378_probe(struct platform_device *pdev) +{ + struct component_match *match = NULL; + struct device *dev = &pdev->dev; + struct wcd9378_priv *wcd9378; + int ret; + + wcd9378 = devm_kzalloc(dev, sizeof(*wcd9378), GFP_KERNEL); + if (!wcd9378) + return -ENOMEM; + + dev_set_drvdata(dev, wcd9378); + mutex_init(&wcd9378->micb_lock); + wcd9378->common.dev = dev; + wcd9378->common.max_bias = WCD9378_MAX_MICBIAS; + + wcd9378->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW); + if (IS_ERR(wcd9378->reset_gpio)) + return dev_err_probe(dev, PTR_ERR(wcd9378->reset_gpio), + "failed to get reset gpio\n"); + + ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(wcd9378_supplies), + wcd9378_supplies); + if (ret) + return dev_err_probe(dev, ret, + "Failed to get and enable supplies\n"); + + ret = wcd_dt_parse_micbias_info(&wcd9378->common); + if (ret) + return dev_err_probe(dev, ret, "Failed to get micbias\n"); + + ret = wcd9378_add_slave_components(wcd9378, dev, &match); + if (ret) + return ret; + + wcd9378_reset(wcd9378); + + ret = component_master_add_with_match(dev, &wcd9378_comp_ops, match); + if (ret) + return ret; + + pm_runtime_set_autosuspend_delay(dev, 1000); + pm_runtime_use_autosuspend(dev); + pm_runtime_mark_last_busy(dev); + pm_runtime_set_active(dev); + pm_runtime_enable(dev); + pm_runtime_idle(dev); + + return 0; +} + +static void wcd9378_remove(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + + component_master_del(dev, &wcd9378_comp_ops); + + pm_runtime_disable(dev); + pm_runtime_set_suspended(dev); + pm_runtime_dont_use_autosuspend(dev); +} + +static const struct of_device_id wcd9378_of_match[] = { + { .compatible = "qcom,wcd9378-codec" }, + { } +}; +MODULE_DEVICE_TABLE(of, wcd9378_of_match); + +static struct platform_driver wcd9378_codec_driver = { + .probe = wcd9378_probe, + .remove = wcd9378_remove, + .driver = { + .name = "wcd9378_codec", + .of_match_table = wcd9378_of_match, + .suppress_bind_attrs = true, + }, +}; +module_platform_driver(wcd9378_codec_driver); + +MODULE_DESCRIPTION("WCD9378 Codec driver"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/wcd9378.h b/sound/soc/codecs/wcd9378.h index 380786f2765e..762542359820 100644 --- a/sound/soc/codecs/wcd9378.h +++ b/sound/soc/codecs/wcd9378.h @@ -8,13 +8,17 @@ * address space (bit 30 set, SDCA function number in bits 25:22), accessed * through the TX SoundWire slave. The analog core registers (function 0, * implementation-defined region at +0x180000) are layout-compatible with - * the WCD937x family. + * the WCD937x family; on top of that the chip adds SDCA-style functions + * (SmartMIC0/1/2, SmartJACK, SmartAMP) whose sequencers drive the analog + * power-up autonomously. */ #ifndef __WCD9378_H__ #define __WCD9378_H__ #include +#include +#include #include /* SDCA function 0 (extension unit): device identity */ @@ -31,33 +35,208 @@ /* Analog core (WCD937x-compatible layout), function 0 + 0x180000 */ #define WCD9378_ANA_PAGE 0x40180000 #define WCD9378_ANA_BIAS 0x40180001 +#define WCD9378_ANA_BIAS_ANALOG_BIAS_EN BIT(7) +#define WCD9378_ANA_BIAS_PRECHRG_EN BIT(6) #define WCD9378_ANA_RX_SUPPLIES 0x40180008 #define WCD9378_ANA_TX_CH1 0x4018000e #define WCD9378_ANA_TX_CH2 0x4018000f +#define WCD9378_ANA_TX_CH2_HPF1_INIT BIT(6) +#define WCD9378_ANA_TX_CH2_HPF2_INIT BIT(5) #define WCD9378_ANA_TX_CH3 0x40180010 +#define WCD9378_ANA_TX_CH3_HPF 0x40180011 +#define WCD9378_ANA_TX_CH3_HPF3_INIT BIT(6) +#define WCD9378_ANA_TX_GAIN_MASK GENMASK(4, 0) #define WCD9378_ANA_MICB1 0x40180022 #define WCD9378_ANA_MICB2 0x40180023 #define WCD9378_ANA_MICB2_RAMP 0x40180024 +#define WCD9378_ANA_MICB2_RAMP_SHIFT_CTL_MASK GENMASK(4, 2) +#define WCD9378_ANA_MICB2_RAMP_EN BIT(7) #define WCD9378_ANA_MICB3 0x40180025 +#define WCD9378_BIAS_VBG_FINE_ADJ 0x40180029 +#define WCD9378_MBHC_CTL_SPARE_1 0x40180058 +#define WCD9378_MICB1_TEST_CTL_2 0x4018006c +#define WCD9378_MICB2_TEST_CTL_2 0x4018006f +#define WCD9378_MICB3_TEST_CTL_2 0x40180072 +#define WCD9378_TX_COM_TXFE_DIV_CTL 0x4018007b +#define WCD9378_TX_COM_TXFE_DIV_SEQ_BYPASS BIT(7) +#define WCD9378_SLEEP_CTL 0x40180103 +#define WCD9378_SLEEP_CTL_BG_CTL_MASK GENMASK(3, 1) +#define WCD9378_SLEEP_CTL_BG_EN BIT(7) +#define WCD9378_SLEEP_CTL_LDOL_BG_SEL BIT(6) +#define WCD9378_TX_NEW_CH12_MUX 0x4018012e +#define WCD9378_TX_NEW_CH12_MUX_CH1_SEL_MASK GENMASK(2, 0) +#define WCD9378_TX_NEW_CH12_MUX_CH2_SEL_MASK GENMASK(5, 3) +#define WCD9378_TX_NEW_CH34_MUX 0x4018012f +#define WCD9378_TX_NEW_CH34_MUX_CH3_SEL_MASK GENMASK(2, 0) +#define WCD9378_HPH_RDAC_GAIN_CTL 0x40180132 +#define WCD9378_HPH_RDAC_HD2_CTL_L 0x40180133 +#define WCD9378_HPH_RDAC_HD2_CTL_R 0x40180136 + +/* Digital page */ +#define WCD9378_TOP_CLK_CFG 0x40180407 +#define WCD9378_CDC_ANA_TX_CLK_CTL 0x40180417 +#define WCD9378_CDC_ANA_TXSCBIAS_CLK_EN BIT(0) +#define WCD9378_CDC_AMIC_CTL 0x4018045a +#define WCD9378_PDM_WD_CTL0 0x40180465 +#define WCD9378_PDM_WD_CTL1 0x40180466 +#define WCD9378_EFUSE_REG_16 0x401804c0 +#define WCD9378_EFUSE_REG_29 0x401804cd +#define WCD9378_PLATFORM_CTL 0x401804f0 /* Sequencer block (SEQR) */ #define WCD9378_SYS_USAGE_CTRL 0x40180501 +#define WCD9378_SYS_USAGE_CTRL_MASK GENMASK(3, 0) +#define WCD9378_HPH_UP_T0 0x40180510 +#define WCD9378_HPH_UP_T9 0x40180519 +#define WCD9378_HPH_DN_T0 0x4018051b +#define WCD9378_SEQ_TX0_STAT 0x40180592 +#define WCD9378_SEQ_TX1_STAT 0x40180593 +#define WCD9378_SEQ_TX2_STAT 0x40180594 +#define WCD9378_MICB_REMAP_TABLE_VAL_3 0x401805a3 +#define WCD9378_MICB_REMAP_TABLE_VAL_4 0x401805a4 +#define WCD9378_MICB_REMAP_TABLE_VAL_5 0x401805a5 #define WCD9378_SM0_MB_SEL 0x401805b0 #define WCD9378_SM1_MB_SEL 0x401805b1 #define WCD9378_SM2_MB_SEL 0x401805b2 +#define WCD9378_SM_MB_SEL_MASK GENMASK(1, 0) +#define WCD9378_MB_PULLUP_EN 0x401805b3 -/* SDCA function activation (one per function) */ +/* SmartAMP SDCA function */ #define WCD9378_SMP_AMP_FUNC_STAT 0x40880000 #define WCD9378_SMP_AMP_FUNC_ACT 0x40880008 + +/* SmartJACK SDCA function (hosts ADC2 when fed from AMIC2) */ +#define WCD9378_CMT_GRP_MASK 0x40c00008 +#define WCD9378_SMP_JACK_IT31_MICB 0x40c00798 +#define WCD9378_SMP_JACK_IT31_USAGE 0x40c007a0 +#define WCD9378_SMP_JACK_PDE34_REQ_PS 0x40c00808 #define WCD9378_SMP_JACK_FUNC_STAT 0x40c80000 #define WCD9378_SMP_JACK_FUNC_ACT 0x40c80008 -#define WCD9378_SMP_MIC_CTRL0_FUNC_STAT 0x41080000 -#define WCD9378_SMP_MIC_CTRL0_FUNC_ACT 0x41080008 -#define WCD9378_SMP_MIC_CTRL1_FUNC_STAT 0x41480000 -#define WCD9378_SMP_MIC_CTRL1_FUNC_ACT 0x41480008 -#define WCD9378_SMP_MIC_CTRL2_FUNC_STAT 0x41880000 -#define WCD9378_SMP_MIC_CTRL2_FUNC_ACT 0x41880008 +#define WCD9378_SMP_JACK_PDE34_ACT_PS 0x40c80800 + +/* SmartMIC0/1/2 SDCA functions (ADC1/ADC2/ADC3 sequencers) */ +#define WCD9378_SMP_MIC_BASE(n) (0x41000000 + (n) * 0x400000) +#define WCD9378_SMP_MIC_IT11_MICB(n) (WCD9378_SMP_MIC_BASE(n) + 0x98) +#define WCD9378_SMP_MIC_IT11_USAGE(n) (WCD9378_SMP_MIC_BASE(n) + 0xa0) +#define WCD9378_SMP_MIC_PDE11_REQ_PS(n) (WCD9378_SMP_MIC_BASE(n) + 0x108) +#define WCD9378_SMP_MIC_OT10_USAGE(n) (WCD9378_SMP_MIC_BASE(n) + 0x3a0) +#define WCD9378_SMP_MIC_FUNC_STAT(n) (WCD9378_SMP_MIC_BASE(n) + 0x80000) +#define WCD9378_SMP_MIC_FUNC_ACT(n) (WCD9378_SMP_MIC_BASE(n) + 0x80008) +#define WCD9378_SMP_MIC_PDE11_ACT_PS(n) (WCD9378_SMP_MIC_BASE(n) + 0x80100) #define WCD9378_MAX_REGISTER 0x41900070 +/* + * Raw (16-bit, non-paged) Qualcomm slave SCP registers, written with + * sdw_write() directly. Bus clock indication towards the codec. + */ +#define WCD9378_SWRS_SCP_BASE_CLK 0x4d +#define WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK0 0x62 +#define WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK1 0x72 +#define WCD9378_SWRS_SCP_HOST_CLK_DIV2_CTL(m) (0xe0 + 0x10 * (m)) +#define WCD9378_SWRS_BASE_CLK_19P2MHZ 0x01 +#define WCD9378_SWRS_CLK_SCALE_DIV2 0x02 /* 9.6 MHz */ +#define WCD9378_SWRS_CLK_SCALE_DIV4 0x03 /* 4.8 MHz */ + +/* ITxx_USAGE ADC mode values */ +#define WCD9378_ADC_USAGE_HIFI 0x01 +#define WCD9378_ADC_USAGE_LO_HIF 0x02 +#define WCD9378_ADC_USAGE_NORMAL 0x03 +#define WCD9378_ADC_USAGE_LP 0x05 +#define WCD9378_ADC_USAGE_OFF 0x00 + +/* ITxx_MICB usage values */ +#define WCD9378_MICB_USAGE_OFF 0x00 +#define WCD9378_MICB_USAGE_PULL_DOWN 0x01 +#define WCD9378_MICB_USAGE_1P2V 0x02 +#define WCD9378_MICB_USAGE_1P8V_OR_PULLUP 0x03 +#define WCD9378_MICB_USAGE_2P5V 0x04 +#define WCD9378_MICB_USAGE_2P75V 0x05 +#define WCD9378_MICB_USAGE_2P2V 0xf0 +#define WCD9378_MICB_USAGE_2P7V 0xf1 +#define WCD9378_MICB_USAGE_2P8V 0xf2 +#define WCD9378_MICB_USAGE_REMAP_TABLE_3 0xf3 +#define WCD9378_MICB_USAGE_REMAP_TABLE_4 0xf4 +#define WCD9378_MICB_USAGE_REMAP_TABLE_5 0xf5 + +/* PDExx_REQ_PS power states */ +#define WCD9378_PDE_PS0_ON 0x00 +#define WCD9378_PDE_PS3_OFF 0x03 + +#define WCD9378_MAX_MICBIAS 3 +#define WCD9378_MAX_SWR_CH_IDS 15 +#define WCD9378_SWRM_CH_MASK(ch_idx) BIT((ch_idx) - 1) + +enum wcd9378_tx_sdw_ports { + WCD9378_ADC_1_PORT = 1, + WCD9378_ADC_2_PORT, + WCD9378_ADC_3_PORT, + WCD9378_DMIC_0_1_MBHC_PORT, + WCD9378_DMIC_2_5_PORT, + WCD9378_MAX_TX_SWR_PORTS = WCD9378_DMIC_2_5_PORT, +}; + +enum wcd9378_rx_sdw_ports { + WCD9378_HPH_PORT = 1, + WCD9378_CLSH_PORT, + WCD9378_COMP_PORT, + WCD9378_LO_PORT, + WCD9378_DSD_PORT, + WCD9378_MAX_SWR_PORTS = WCD9378_DSD_PORT, +}; + +enum wcd9378_tx_sdw_channels { + WCD9378_ADC1, + WCD9378_ADC2, + WCD9378_ADC3, + WCD9378_DMIC0, + WCD9378_DMIC1, + WCD9378_MBHC, + WCD9378_DMIC2, + WCD9378_DMIC3, + WCD9378_DMIC4, + WCD9378_DMIC5, +}; + +enum wcd9378_rx_sdw_channels { + WCD9378_HPH_L, + WCD9378_HPH_R, + WCD9378_CLSH, + WCD9378_COMP_L, + WCD9378_COMP_R, + WCD9378_LO, + WCD9378_DSD_L, + WCD9378_DSD_R, +}; + +struct wcd9378_priv; +struct wcd9378_sdw_priv { + struct sdw_slave *sdev; + struct sdw_stream_config sconfig; + struct sdw_stream_runtime *sruntime; + struct sdw_port_config port_config[WCD9378_MAX_SWR_PORTS]; + struct wcd_sdw_ch_info *ch_info; + bool port_enable[WCD9378_MAX_SWR_CH_IDS]; + unsigned int master_channel_map[SDW_MAX_PORTS]; + int active_ports; + bool is_tx; + struct wcd9378_priv *wcd9378; + struct regmap *regmap; +}; + +#if IS_ENABLED(CONFIG_SND_SOC_WCD9378_SDW) +int wcd9378_sdw_hw_params(struct wcd9378_sdw_priv *wcd, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai); +#else +static inline int wcd9378_sdw_hw_params(struct wcd9378_sdw_priv *wcd, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + return -EOPNOTSUPP; +} +#endif + #endif /* __WCD9378_H__ */ From e9a6b531cf34a217caf9b9a9bdab468e6b61c993 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 17:37:15 +0200 Subject: [PATCH 5/6] arm64: dts: qcom: milos-fairphone-fp6: add WCD9378 codec node and capture dai-link Add the wcd9378 audio-codec parent node (reset gpio162, l7b/l8b/bob supplies, 1.8 V micbias x3, rx/tx slave phandles), tx/rx port mappings on the SoundWire slave nodes (ADC1/2/3 on device ports 1/2/3 all mapped to master port 1, per the downstream volcano tx_swr_ch_map), the WCD Capture dai-link on TX_CODEC_DMA_TX_3 and the capture audio-routing (TX SWR_INPUTn inputs - the milos TX macro is a v9.2 variant, not the SWR_ADCn naming qcm6490 uses). The vreg_l8b regulator-always-on DTB hack is obsolete: the codec node now holds vdd-buck. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- .../boot/dts/qcom/milos-fairphone-fp6.dts | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts index 0c50c91c7c82..13fbc66e2394 100644 --- a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts +++ b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts @@ -209,13 +209,34 @@ sound { compatible = "qcom,milos-sndcard", "qcom,sm8450-sndcard"; model = "Fairphone (Gen. 6)"; - // audio-routing = ... + audio-routing = "AMIC1", "MIC BIAS1", + "AMIC2", "MIC BIAS2", + "AMIC3", "MIC BIAS3", + "TX SWR_INPUT0", "ADC1_OUTPUT", + "TX SWR_INPUT1", "ADC2_OUTPUT", + "TX SWR_INPUT2", "ADC3_OUTPUT"; pinctrl-0 = <&lpi_i2s2_active>; pinctrl-1 = <&lpi_i2s2_sleep>; pinctrl-names = "default", "sleep"; + wcd-capture-dai-link { + link-name = "WCD Capture"; + + codec { + sound-dai = <&wcd9378 1>, <&swr2 0>, <&lpass_txmacro 0>; + }; + + cpu { + sound-dai = <&q6apmbedai TX_CODEC_DMA_TX_3>; + }; + + platform { + sound-dai = <&q6apm>; + }; + }; + /* i2s-dai-link { link-name = "Senary MI2S Playback"; @@ -235,6 +256,29 @@ */ }; + wcd9378: audio-codec { + compatible = "qcom,wcd9378-codec"; + + pinctrl-0 = <&wcd_reset_n_active>; + pinctrl-names = "default"; + + reset-gpios = <&tlmm 162 GPIO_ACTIVE_LOW>; + + vdd-buck-supply = <&vreg_l8b>; + vdd-rxtx-supply = <&vreg_l7b>; + vdd-io-supply = <&vreg_l7b>; + vdd-mic-bias-supply = <&vreg_bob>; + + qcom,micbias1-microvolt = <1800000>; + qcom,micbias2-microvolt = <1800000>; + qcom,micbias3-microvolt = <1800000>; + + qcom,rx-device = <&wcd9378_rx>; + qcom,tx-device = <&wcd9378_tx>; + + #sound-dai-cells = <1>; + }; + thermal-zones { pm8008-thermal { polling-delay-passive = <100>; @@ -1093,6 +1137,7 @@ wcd9378_rx: codec@0,4 { compatible = "sdw20217011000"; reg = <0 4>; + qcom,rx-port-mapping = <1 2 3 4 5>; }; }; @@ -1103,6 +1148,15 @@ wcd9378_tx: codec@0,3 { compatible = "sdw20217011000"; reg = <0 3>; + + /* + * WCD9378 TX port 1 (ADC1) <=> SWR2 port 1 (SWRM_TX1) + * WCD9378 TX port 2 (ADC2) <=> SWR2 port 1 + * WCD9378 TX port 3 (ADC3) <=> SWR2 port 1 + * WCD9378 TX port 4 (DMIC0,1, MBHC) <=> SWR2 port 2 + * WCD9378 TX port 5 (DMIC2..5) <=> SWR2 port 3 + */ + qcom,tx-port-mapping = <1 1 1 2 3>; }; }; @@ -1233,6 +1287,13 @@ drive-strength = <2>; bias-pull-down; }; + + wcd_reset_n_active: wcd-reset-n-active-state { + pins = "gpio162"; + function = "gpio"; + drive-strength = <16>; + output-high; + }; }; &uart5 { From 5694c4a5f397c718b74327ed4f74872f21f76332 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Mon, 6 Jul 2026 00:58:51 +0200 Subject: [PATCH 6/6] ASoC: codecs: wcd9378: keep the TX SoundWire bus out of clock-stop The SDCA function engine (the SmartMIC/SmartJACK/SmartAMP sequencer machinery activated by the FUNC_ACT class-load) dies when the TX SoundWire bus enters clock-stop. All its registers keep their values, so a regcache sync on resume restores nothing visible - the PDE simply never services power-state requests again: PDE11_ACT_PS stays in PS3, SEQ_TXn_STAT stays at pwr_dn_rdy, and even the TXn_VALID_CFG_OVR / TXn_SEQ_TRIGGER_OVR sequencer overrides and a TX0_SEQ_SOFT_RST pulse are ignored. Re-toggling FUNC_ACT (a real 0->1 edge on the bus) does not revive it either; only a full codec reset does. The result was capture recording pure digital silence: the whole DPCM -> CDC-DMA -> TX macro -> SoundWire transport ran, but the ADC never powered. Hold a runtime PM reference on the TX slave for as long as the codec is bound, so the bus never clock-stops. This matches the downstream stack, which marks the TX SoundWire master 'qcom,is-always-on' - with full documentation available, Qualcomm made the same trade-off. Also perform the class-load activation with plain writes instead of update_bits so the 0->1 activation edge always reaches the hardware regardless of regcache state. Verified on the FP6: from a fresh boot, repeated captures across what were previously bus suspend/resume cycles now power the sequencer every time (PDE11 reaches PS0) and record live mic signal instead of zeros. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- sound/soc/codecs/wcd9378.c | 50 +++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/sound/soc/codecs/wcd9378.c b/sound/soc/codecs/wcd9378.c index 44a3f35dd4cf..e1857aca52ab 100644 --- a/sound/soc/codecs/wcd9378.c +++ b/sound/soc/codecs/wcd9378.c @@ -165,28 +165,33 @@ static void wcd9378_class_load(struct snd_soc_component *component) { int i; - snd_soc_component_update_bits(component, WCD9378_SMP_AMP_FUNC_ACT, - 0x01, 0x01); + /* + * Plain writes, not update_bits, so the 0->1 activation edge + * always reaches the hardware regardless of regcache state. + * The engine boots from this edge only on a freshly reset + * codec; once it dies (bus clock-stop) no register write + * revives it, see the TX bus PM hold in wcd9378_bind(). + */ + snd_soc_component_write(component, WCD9378_SMP_AMP_FUNC_ACT, 0x00); + snd_soc_component_write(component, WCD9378_SMP_AMP_FUNC_ACT, 0x01); usleep_range(20000, 20010); - snd_soc_component_update_bits(component, WCD9378_SMP_AMP_FUNC_STAT, - 0xff, 0xff); + snd_soc_component_write(component, WCD9378_SMP_AMP_FUNC_STAT, 0xff); - snd_soc_component_update_bits(component, WCD9378_SMP_JACK_FUNC_ACT, - 0x01, 0x01); + snd_soc_component_write(component, WCD9378_SMP_JACK_FUNC_ACT, 0x00); + snd_soc_component_write(component, WCD9378_SMP_JACK_FUNC_ACT, 0x01); usleep_range(30000, 30010); snd_soc_component_update_bits(component, WCD9378_CMT_GRP_MASK, 0xff, 0x02); - snd_soc_component_update_bits(component, WCD9378_SMP_JACK_FUNC_STAT, - 0xff, 0xff); + snd_soc_component_write(component, WCD9378_SMP_JACK_FUNC_STAT, 0xff); for (i = 0; i < 3; i++) { - snd_soc_component_update_bits(component, - WCD9378_SMP_MIC_FUNC_ACT(i), - 0x01, 0x01); + snd_soc_component_write(component, + WCD9378_SMP_MIC_FUNC_ACT(i), 0x00); + snd_soc_component_write(component, + WCD9378_SMP_MIC_FUNC_ACT(i), 0x01); usleep_range(5000, 5010); - snd_soc_component_update_bits(component, - WCD9378_SMP_MIC_FUNC_STAT(i), - 0xff, 0xff); + snd_soc_component_write(component, + WCD9378_SMP_MIC_FUNC_STAT(i), 0xff); } } @@ -1219,10 +1224,26 @@ static int wcd9378_bind(struct device *dev) goto err_remove_link3; } + /* + * The SDCA function engine dies when the TX bus enters clock-stop + * and only a codec reset revives it — registers keep their values + * so a regcache sync or a FUNC_ACT re-toggle does not help. The + * downstream stack sidesteps the same problem by marking the TX + * SoundWire master "qcom,is-always-on"; do the equivalent and + * keep the TX slave (and thus its bus) runtime-active while the + * codec is bound. + */ + ret = pm_runtime_resume_and_get(wcd9378->txdev); + if (ret < 0 && ret != -EACCES) { + dev_err(dev, "could not resume TX device\n"); + goto err_remove_link3; + } + ret = snd_soc_register_component(dev, &soc_codec_dev_wcd9378, wcd9378_dais, ARRAY_SIZE(wcd9378_dais)); if (ret) { dev_err(dev, "Codec registration failed\n"); + pm_runtime_put(wcd9378->txdev); goto err_remove_link3; } @@ -1248,6 +1269,7 @@ static void wcd9378_unbind(struct device *dev) struct wcd9378_priv *wcd9378 = dev_get_drvdata(dev); snd_soc_unregister_component(dev); + pm_runtime_put(wcd9378->txdev); device_link_remove(dev, wcd9378->txdev); device_link_remove(dev, wcd9378->rxdev); device_link_remove(wcd9378->rxdev, wcd9378->txdev);