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 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 d837afcfd60e69ce496f73e48b7007f4b130e9b8 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Mon, 29 Jun 2026 00:23:51 +0200 Subject: [PATCH 06/14] arm64: dts: qcom: milos-fairphone-fp6: add NFC Add the Samsung S3NRN4V NFC + eSE controller on i2c1. Its XI clock is provided by the RF_CLK2 PMIC buffer and gated through the controller's CLK_REQ line on tlmm GPIO6. The enable line is routed to the chip's power-down input, which is asserted high to turn the chip off (the downstream driver treats VEN as active-low on this design) -- hence GPIO_ACTIVE_HIGH, unlike the exynos5433-tm2 wiring of the same driver. The pin is pulled up so the chip stays off while the line is not driven. Assisted-by: Claude:claude-opus-4-8 Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- .../boot/dts/qcom/milos-fairphone-fp6.dts | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts index e5944a1b255f..9c765bcc1f2f 100644 --- a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts +++ b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts @@ -700,9 +700,28 @@ }; &i2c1 { - /* Samsung NFC @ 0x27 */ - status = "okay"; + + /* + * Samsung S3NRN4V NFC controller. XI is driven by the RF_CLK2 PMIC + * buffer; the chip has no oscillator of its own and gates the clock + * via its CLK_REQ line, so the clock must be voted on in response + * to it. + */ + nfc@27 { + compatible = "samsung,s3nrn4v"; + reg = <0x27>; + + clk-req-gpios = <&tlmm 6 GPIO_ACTIVE_HIGH>; + clocks = <&rpmhcc RPMH_RF_CLK2>; + en-gpios = <&tlmm 56 GPIO_ACTIVE_HIGH>; + interrupt-parent = <&tlmm>; + interrupts = <31 IRQ_TYPE_EDGE_RISING>; + pinctrl-0 = <&nfc_clk_req_default>, <&nfc_irq_default>, + <&nfc_pd_default>; + pinctrl-names = "default"; + wake-gpios = <&tlmm 7 GPIO_ACTIVE_HIGH>; + }; }; &i2c3 { @@ -1091,6 +1110,13 @@ <13 1>, /* NC */ <63 2>; /* WLAN UART */ + nfc_clk_req_default: nfc-clk-req-default-state { + pins = "gpio6"; + function = "gpio"; + drive-strength = <2>; + bias-disable; + }; + ts_active: ts-irq-active-state { pins = "gpio19"; function = "gpio"; @@ -1105,6 +1131,13 @@ bias-disable; }; + nfc_irq_default: nfc-irq-default-state { + pins = "gpio31"; + function = "gpio"; + drive-strength = <2>; + bias-disable; + }; + qup_uart11_sleep_cts: qup-uart11-sleep-cts-state { pins = "gpio48"; function = "gpio"; @@ -1172,6 +1205,17 @@ bias-pull-down; }; + /* + * Pulled up so the NFC chip stays powered down while the line is + * not driven (it is the chip's active-high power-down input). + */ + nfc_pd_default: nfc-pd-default-state { + pins = "gpio56"; + function = "gpio"; + drive-strength = <2>; + bias-pull-up; + }; + sdc2_card_det_n: sdc2-card-det-state { pins = "gpio65"; function = "gpio"; From 9cd00a65e820565407a762bd7876b03bdad0ec15 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Fri, 3 Jul 2026 21:43:28 +0200 Subject: [PATCH 07/14] nfc: s3fwrn5: support the S3NRN4V variant The S3NRN4V (e.g. on the Fairphone 6, SM7635) is an S3FWRN5-family NFC controller that needs different bring-up, selected with a new samsung,s3nrn4v compatible: - It ships with working firmware behind a bootloader protocol this driver does not implement (GET_BOOTINFO times out), so the firmware download step is skipped. Its RF registers are (re)loaded with the proprietary DUAL_OPTION command (the HW and SW register blobs merged into a single stream) instead of the START/SET/STOP_RFREG sequence. - Its reference clock speed is configured with the single-byte FW_CFG form, sent from the ->setup hook (after CORE_RESET, before CORE_INIT). The selector value (0x11) is taken from the vendor configuration for this part; its encoding is not documented. - It gates its XI clock through a CLK_REQ line: the chip drives it high when it needs the clock, notably to synthesise the 13.56 MHz poll carrier. Left always-on, the free-running clock never lets the chip's TX PLL lock on a fresh start and it cannot poll (it falls back to listen only). Service the handshake when a clk-req GPIO is described, gating the clock on it; without one the clock stays always-on. The variant is carried as match data by both the OF and the I2C device id tables so the two match paths agree, and the OF table is now referenced unconditionally for its match data, so drop the of_match_ptr()/__maybe_unused annotations from it. The error policy differs between the two configuration steps on purpose: a clock misconfiguration is fatal (a ->setup failure aborts CORE_INIT), whereas an RF-register update failure is only warned about and bring-up continues, since the chip falls back to the RF registers programmed in its flash and NFC may still work. Unlike the host-endian word read in the legacy rfreg path, the DUAL_OPTION checksum is accumulated with get_unaligned_le32() and emitted little-endian explicitly, so it is correct regardless of CPU endianness. Existing S3FWRN5 / S3FWRN82 setups keep the firmware-download path and the always-on clock, unchanged. Assisted-by: Claude:claude-opus-4-8 Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- drivers/nfc/s3fwrn5/core.c | 40 +++++++++- drivers/nfc/s3fwrn5/i2c.c | 140 +++++++++++++++++++++++++++++++--- drivers/nfc/s3fwrn5/nci.c | 119 ++++++++++++++++++++++++++++- drivers/nfc/s3fwrn5/nci.h | 32 +++++++- drivers/nfc/s3fwrn5/s3fwrn5.h | 14 +++- drivers/nfc/s3fwrn5/uart.c | 2 +- 6 files changed, 331 insertions(+), 16 deletions(-) diff --git a/drivers/nfc/s3fwrn5/core.c b/drivers/nfc/s3fwrn5/core.c index af0fa8bd970b..59317eaad7ac 100644 --- a/drivers/nfc/s3fwrn5/core.c +++ b/drivers/nfc/s3fwrn5/core.c @@ -122,11 +122,47 @@ static int s3fwrn5_nci_send(struct nci_dev *ndev, struct sk_buff *skb) return 0; } +static int s3fwrn5_nci_setup(struct nci_dev *ndev) +{ + struct s3fwrn5_info *info = nci_get_drvdata(ndev); + + /* + * Runs after CORE_RESET, before CORE_INIT. The S3NRN4V needs its + * reference clock configured here (the downstream stack does it in the + * bootloader, before CORE_RESET, but this is the earliest hook the NCI + * core offers and the chip accepts it). + */ + if (info->variant == S3FWRN5_VARIANT_S3NRN4V) + return s3fwrn5_nci_clk_cfg(info); + + return 0; +} + static int s3fwrn5_nci_post_setup(struct nci_dev *ndev) { struct s3fwrn5_info *info = nci_get_drvdata(ndev); int ret; + if (info->variant == S3FWRN5_VARIANT_S3NRN4V) { + /* + * The S3NRN4V ships with working firmware behind a bootloader + * protocol this driver does not implement, so there is no + * download step; the NCI core has already done CORE_RESET + + * CORE_INIT. Just (re)load the RF registers via DUAL_OPTION. + */ + ret = s3fwrn5_nci_rf_configure_dual(info, "sec_s3nrn4v_hwreg.bin", + "sec_s3nrn4v_swreg.bin"); + /* + * Keep going even if the blobs could not be loaded: the chip + * still enumerates and falls back to the RF registers programmed + * in its flash, so NFC may work anyway. + */ + if (ret < 0) + dev_warn(&ndev->nfc_dev->dev, + "rfreg configure failed (%d)\n", ret); + return 0; + } + if (s3fwrn5_firmware_init(info)) { //skip bootloader mode return 0; @@ -152,13 +188,14 @@ static const struct nci_ops s3fwrn5_nci_ops = { .open = s3fwrn5_nci_open, .close = s3fwrn5_nci_close, .send = s3fwrn5_nci_send, + .setup = s3fwrn5_nci_setup, .post_setup = s3fwrn5_nci_post_setup, .prop_ops = s3fwrn5_nci_prop_ops, .n_prop_ops = ARRAY_SIZE(s3fwrn5_nci_prop_ops), }; int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev, - const struct s3fwrn5_phy_ops *phy_ops) + const struct s3fwrn5_phy_ops *phy_ops, enum s3fwrn5_variant variant) { struct s3fwrn5_info *info; int ret; @@ -170,6 +207,7 @@ int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev, info->phy_id = phy_id; info->pdev = pdev; info->phy_ops = phy_ops; + info->variant = variant; mutex_init(&info->mutex); s3fwrn5_set_mode(info, S3FWRN5_MODE_COLD); diff --git a/drivers/nfc/s3fwrn5/i2c.c b/drivers/nfc/s3fwrn5/i2c.c index 91b8d1445efd..7d20e737e402 100644 --- a/drivers/nfc/s3fwrn5/i2c.c +++ b/drivers/nfc/s3fwrn5/i2c.c @@ -23,9 +23,76 @@ struct s3fwrn5_i2c_phy { struct i2c_client *i2c_dev; struct clk *clk; + /* + * Optional hardware clock-request handshake. When a CLK_REQ GPIO is + * wired, the chip drives it high while it needs its XI clock -- notably + * to generate the poll/reader carrier -- and the clock is gated on it + * instead of being left always-on (which never lets the chip's TX PLL + * lock on a fresh clock start, leaving it unable to poll). + */ + struct gpio_desc *gpio_clk_req; + bool clk_on; + struct mutex clk_lock; /* serialises clk_on against the CLK_REQ irq */ + unsigned int irq_skip:1; }; +static void s3fwrn5_i2c_clk_set_locked(struct s3fwrn5_i2c_phy *phy, bool on) +{ + lockdep_assert_held(&phy->clk_lock); + + if (on && !phy->clk_on) { + int ret = clk_prepare_enable(phy->clk); + + if (ret == 0) + phy->clk_on = true; + else + dev_warn_once(&phy->i2c_dev->dev, + "failed to enable clock (%d); NFC may not poll\n", + ret); + } else if (!on && phy->clk_on) { + clk_disable_unprepare(phy->clk); + phy->clk_on = false; + } +} + +/* + * Apply the current CLK_REQ level. Reading the GPIO under clk_lock makes + * concurrent callers (the CLK_REQ irq thread and the probe-time seeding) + * safe: whoever runs last applies a level read after the earlier update, + * never a stale one. + */ +static void s3fwrn5_i2c_clk_sync(struct s3fwrn5_i2c_phy *phy) +{ + int level; + + mutex_lock(&phy->clk_lock); + level = gpiod_get_value_cansleep(phy->gpio_clk_req); + if (level >= 0) + s3fwrn5_i2c_clk_set_locked(phy, level > 0); + else + dev_warn_once(&phy->i2c_dev->dev, + "failed to read CLK_REQ (%d); keeping clock state\n", + level); + mutex_unlock(&phy->clk_lock); +} + +static void s3fwrn5_i2c_clk_disable_action(void *data) +{ + struct s3fwrn5_i2c_phy *phy = data; + + mutex_lock(&phy->clk_lock); + s3fwrn5_i2c_clk_set_locked(phy, false); + mutex_unlock(&phy->clk_lock); +} + +static irqreturn_t s3fwrn5_i2c_clk_req_thread(int irq, void *phy_id) +{ + s3fwrn5_i2c_clk_sync(phy_id); + + return IRQ_HANDLED; +} + static void s3fwrn5_i2c_set_mode(void *phy_id, enum s3fwrn5_mode mode) { struct s3fwrn5_i2c_phy *phy = phy_id; @@ -146,6 +213,7 @@ out: static int s3fwrn5_i2c_probe(struct i2c_client *client) { + enum s3fwrn5_variant variant; struct s3fwrn5_i2c_phy *phy; int ret; @@ -172,15 +240,61 @@ static int s3fwrn5_i2c_probe(struct i2c_client *client) * S3FWRN5 depends on a clock input ("XI" pin) to function properly. * Depending on the hardware configuration this could be an always-on * oscillator or some external clock that must be explicitly enabled. - * Make sure the clock is running before starting S3FWRN5. + * + * If a CLK_REQ GPIO is wired, the chip gates the clock itself (driving + * CLK_REQ high when it needs XI); service that handshake. Otherwise just + * make sure the clock is running before starting S3FWRN5. */ - phy->clk = devm_clk_get_optional_enabled(&client->dev, NULL); - if (IS_ERR(phy->clk)) - return dev_err_probe(&client->dev, PTR_ERR(phy->clk), - "failed to get clock\n"); + mutex_init(&phy->clk_lock); + phy->gpio_clk_req = devm_gpiod_get_optional(&client->dev, "clk-req", + GPIOD_IN); + if (IS_ERR(phy->gpio_clk_req)) + return PTR_ERR(phy->gpio_clk_req); + if (phy->gpio_clk_req) { + int clk_req_irq; + + phy->clk = devm_clk_get_optional(&client->dev, NULL); + if (IS_ERR(phy->clk)) + return dev_err_probe(&client->dev, PTR_ERR(phy->clk), + "failed to get clock\n"); + + /* + * Unlike the always-on branch below, this clock is enabled by + * hand from the CLK_REQ handler, so devm will not disable it on + * unbind. Gate it off explicitly if it is still on at teardown. + */ + ret = devm_add_action_or_reset(&client->dev, + s3fwrn5_i2c_clk_disable_action, + phy); + if (ret) + return ret; + + clk_req_irq = gpiod_to_irq(phy->gpio_clk_req); + if (clk_req_irq < 0) + return clk_req_irq; + + ret = devm_request_threaded_irq(&client->dev, clk_req_irq, NULL, + s3fwrn5_i2c_clk_req_thread, + IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING | + IRQF_ONESHOT, + "s3fwrn5_clk_req", phy); + if (ret) + return ret; + + /* Seed the clock state from the current CLK_REQ level. */ + s3fwrn5_i2c_clk_sync(phy); + } else { + phy->clk = devm_clk_get_optional_enabled(&client->dev, NULL); + if (IS_ERR(phy->clk)) + return dev_err_probe(&client->dev, PTR_ERR(phy->clk), + "failed to get clock\n"); + } + + variant = (uintptr_t)i2c_get_match_data(client); ret = s3fwrn5_probe(&phy->common.ndev, phy, &phy->i2c_dev->dev, - &i2c_phy_ops); + &i2c_phy_ops, variant); if (ret < 0) return ret; @@ -205,13 +319,17 @@ static void s3fwrn5_i2c_remove(struct i2c_client *client) } static const struct i2c_device_id s3fwrn5_i2c_id_table[] = { - { S3FWRN5_I2C_DRIVER_NAME }, - {} + { .name = S3FWRN5_I2C_DRIVER_NAME, .driver_data = S3FWRN5_VARIANT_FWDL }, + { .name = "s3nrn4v", .driver_data = S3FWRN5_VARIANT_S3NRN4V }, + { } }; MODULE_DEVICE_TABLE(i2c, s3fwrn5_i2c_id_table); -static const struct of_device_id of_s3fwrn5_i2c_match[] __maybe_unused = { - { .compatible = "samsung,s3fwrn5-i2c", }, +static const struct of_device_id of_s3fwrn5_i2c_match[] = { + { .compatible = "samsung,s3fwrn5-i2c", + .data = (void *)S3FWRN5_VARIANT_FWDL, }, + { .compatible = "samsung,s3nrn4v", + .data = (void *)S3FWRN5_VARIANT_S3NRN4V, }, {} }; MODULE_DEVICE_TABLE(of, of_s3fwrn5_i2c_match); @@ -219,7 +337,7 @@ MODULE_DEVICE_TABLE(of, of_s3fwrn5_i2c_match); static struct i2c_driver s3fwrn5_i2c_driver = { .driver = { .name = S3FWRN5_I2C_DRIVER_NAME, - .of_match_table = of_match_ptr(of_s3fwrn5_i2c_match), + .of_match_table = of_s3fwrn5_i2c_match, }, .probe = s3fwrn5_i2c_probe, .remove = s3fwrn5_i2c_remove, diff --git a/drivers/nfc/s3fwrn5/nci.c b/drivers/nfc/s3fwrn5/nci.c index 5a9de11bbece..7034fb810e18 100644 --- a/drivers/nfc/s3fwrn5/nci.c +++ b/drivers/nfc/s3fwrn5/nci.c @@ -8,6 +8,9 @@ #include #include +#include +#include +#include #include "s3fwrn5.h" #include "nci.h" @@ -20,7 +23,7 @@ static int s3fwrn5_nci_prop_rsp(struct nci_dev *ndev, struct sk_buff *skb) return 0; } -const struct nci_driver_ops s3fwrn5_nci_prop_ops[4] = { +const struct nci_driver_ops s3fwrn5_nci_prop_ops[5] = { { .opcode = nci_opcode_pack(NCI_GID_PROPRIETARY, NCI_PROP_SET_RFREG), @@ -41,6 +44,11 @@ const struct nci_driver_ops s3fwrn5_nci_prop_ops[4] = { NCI_PROP_FW_CFG), .rsp = s3fwrn5_nci_prop_rsp, }, + { + .opcode = nci_opcode_pack(NCI_GID_PROPRIETARY, + NCI_PROP_DUAL_OPTION), + .rsp = s3fwrn5_nci_prop_rsp, + }, }; #define S3FWRN5_RFREG_SECTION_SIZE 252 @@ -117,3 +125,112 @@ out: release_firmware(fw); return ret; } + +/* + * Configure the reference clock. The S3NRN4V expects the single-byte FW_CFG + * form (just the clock-speed selector). The downstream stack sends this in the + * bootloader before CORE_RESET; the earliest the mainline NCI core lets us in + * is the ->setup hook (after CORE_RESET, before CORE_INIT), which works. + */ +int s3fwrn5_nci_clk_cfg(struct s3fwrn5_info *info) +{ + u8 clk_speed = NCI_PROP_FW_CFG_CLK_SPEED; + + return nci_prop_cmd(info->ndev, NCI_PROP_FW_CFG, 1, &clk_speed); +} + +/* + * S3NRN4V RF register update. The HW and SW register blobs are merged into a + * single stream (HW first) and pushed via the DUAL_OPTION command: + * START_UPDATE, one SET_OPTION per 252-byte section, then STOP_UPDATE carrying + * a 16-bit checksum (running sum of the merged stream as 32-bit words). + */ +int s3fwrn5_nci_rf_configure_dual(struct s3fwrn5_info *info, + const char *hw_name, const char *sw_name) +{ + const struct firmware *hw_fw = NULL, *sw_fw = NULL; + struct nci_prop_dual_set_option_cmd set_option; + struct device *dev = &info->ndev->nfc_dev->dev; + size_t merged_size, i, len; + u8 *merged = NULL; + u8 stop_cmd[3]; + u32 checksum; + u8 sub_oid; + int ret; + + ret = request_firmware(&hw_fw, hw_name, dev); + if (ret < 0) + return ret; + ret = request_firmware(&sw_fw, sw_name, dev); + if (ret < 0) + goto out_hw; + + merged_size = hw_fw->size + sw_fw->size; + + /* + * The stream is checksummed as 32-bit words and pushed in at most 256 + * sections (the section index is a single byte); reject blobs that + * would silently break either. + */ + if (merged_size % 4 || + merged_size > 256 * NCI_PROP_DUAL_SECTION_SIZE) { + dev_err(dev, "invalid rfreg blob size (%zu)\n", merged_size); + ret = -EINVAL; + goto out; + } + + merged = kmalloc(merged_size, GFP_KERNEL); + if (!merged) { + ret = -ENOMEM; + goto out; + } + memcpy(merged, hw_fw->data, hw_fw->size); + memcpy(merged + hw_fw->size, sw_fw->data, sw_fw->size); + + /* Running sum of the merged stream as little-endian 32-bit words. */ + checksum = 0; + for (i = 0; i + 4 <= merged_size; i += 4) + checksum += get_unaligned_le32(merged + i); + + dev_dbg(dev, "rfreg dual-option update: %s + %s\n", hw_name, sw_name); + + /* START_UPDATE */ + sub_oid = NCI_PROP_DUAL_SUB_START_UPDATE; + ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION, 1, &sub_oid); + if (ret < 0) { + dev_err(dev, "Unable to start rfreg update\n"); + goto out; + } + + /* SET_OPTION per section */ + set_option.sub_oid = NCI_PROP_DUAL_SUB_SET_OPTION; + set_option.index = 0; + for (i = 0; i < merged_size; i += NCI_PROP_DUAL_SECTION_SIZE) { + len = min_t(size_t, merged_size - i, NCI_PROP_DUAL_SECTION_SIZE); + memcpy(set_option.data, merged + i, len); + ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION, + len + 2, (__u8 *)&set_option); + if (ret < 0) { + dev_err(dev, "rfreg update error (code=%d)\n", ret); + goto out; + } + set_option.index++; + } + + /* STOP_UPDATE with checksum */ + stop_cmd[0] = NCI_PROP_DUAL_SUB_STOP_UPDATE; + put_unaligned_le16(checksum, &stop_cmd[1]); + ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION, 3, stop_cmd); + if (ret < 0) { + dev_err(dev, "Unable to stop rfreg update\n"); + goto out; + } + + dev_dbg(dev, "rfreg dual-option update: success\n"); +out: + kfree(merged); + release_firmware(sw_fw); +out_hw: + release_firmware(hw_fw); + return ret; +} diff --git a/drivers/nfc/s3fwrn5/nci.h b/drivers/nfc/s3fwrn5/nci.h index bc4bce2bbc4d..23179ba095a1 100644 --- a/drivers/nfc/s3fwrn5/nci.h +++ b/drivers/nfc/s3fwrn5/nci.h @@ -40,6 +40,13 @@ struct nci_prop_stop_rfreg_rsp { #define NCI_PROP_FW_CFG 0x28 +/* + * Single-byte FW_CFG payload (clock-speed selector) for the S3NRN4V reference + * clock. Taken from the vendor configuration for this part (the encoding is + * not documented). + */ +#define NCI_PROP_FW_CFG_CLK_SPEED 0x11 + struct nci_prop_fw_cfg_cmd { __u8 clk_type; __u8 clk_speed; @@ -50,7 +57,30 @@ struct nci_prop_fw_cfg_rsp { __u8 status; }; -extern const struct nci_driver_ops s3fwrn5_nci_prop_ops[4]; +/* + * The S3NRN4V updates its RF registers through a single "dual option" command + * (a sub-OID selects the operation) instead of the START/SET/STOP_RFREG + * opcodes above, and expects the HW and SW register blobs merged into one + * stream. + */ +#define NCI_PROP_DUAL_OPTION 0x2a + +#define NCI_PROP_DUAL_SUB_START_UPDATE 0x01 +#define NCI_PROP_DUAL_SUB_SET_OPTION 0x02 +#define NCI_PROP_DUAL_SUB_STOP_UPDATE 0x03 + +#define NCI_PROP_DUAL_SECTION_SIZE 252 + +struct nci_prop_dual_set_option_cmd { + __u8 sub_oid; /* NCI_PROP_DUAL_SUB_SET_OPTION */ + __u8 index; + __u8 data[NCI_PROP_DUAL_SECTION_SIZE]; +}; + +extern const struct nci_driver_ops s3fwrn5_nci_prop_ops[5]; int s3fwrn5_nci_rf_configure(struct s3fwrn5_info *info, const char *fw_name); +int s3fwrn5_nci_rf_configure_dual(struct s3fwrn5_info *info, + const char *hw_name, const char *sw_name); +int s3fwrn5_nci_clk_cfg(struct s3fwrn5_info *info); #endif /* __LOCAL_S3FWRN5_NCI_H_ */ diff --git a/drivers/nfc/s3fwrn5/s3fwrn5.h b/drivers/nfc/s3fwrn5/s3fwrn5.h index 2b492236090b..2d8c12091fba 100644 --- a/drivers/nfc/s3fwrn5/s3fwrn5.h +++ b/drivers/nfc/s3fwrn5/s3fwrn5.h @@ -21,6 +21,17 @@ enum s3fwrn5_mode { S3FWRN5_MODE_FW, }; +enum s3fwrn5_variant { + /* S3FWRN5 / S3FWRN82: firmware is downloaded by this driver */ + S3FWRN5_VARIANT_FWDL, + /* + * S3NRN4V: ships with working firmware behind a bootloader protocol + * this driver does not implement; skip the download, configure the + * clock (FW_CFG) and update the RF registers via the DUAL_OPTION cmd. + */ + S3FWRN5_VARIANT_S3NRN4V, +}; + struct s3fwrn5_phy_ops { void (*set_wake)(void *id, bool sleep); void (*set_mode)(void *id, enum s3fwrn5_mode); @@ -36,6 +47,7 @@ struct s3fwrn5_info { const struct s3fwrn5_phy_ops *phy_ops; struct s3fwrn5_fw_info fw_info; + enum s3fwrn5_variant variant; struct mutex mutex; }; @@ -78,7 +90,7 @@ static inline int s3fwrn5_write(struct s3fwrn5_info *info, struct sk_buff *skb) } int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev, - const struct s3fwrn5_phy_ops *phy_ops); + const struct s3fwrn5_phy_ops *phy_ops, enum s3fwrn5_variant variant); void s3fwrn5_remove(struct nci_dev *ndev); int s3fwrn5_recv_frame(struct nci_dev *ndev, struct sk_buff *skb, diff --git a/drivers/nfc/s3fwrn5/uart.c b/drivers/nfc/s3fwrn5/uart.c index 540a4ddb0b05..47172d739a41 100644 --- a/drivers/nfc/s3fwrn5/uart.c +++ b/drivers/nfc/s3fwrn5/uart.c @@ -137,7 +137,7 @@ static int s3fwrn82_uart_probe(struct serdev_device *serdev) } ret = s3fwrn5_probe(&phy->common.ndev, phy, &phy->ser_dev->dev, - &uart_phy_ops); + &uart_phy_ops, S3FWRN5_VARIANT_FWDL); if (ret < 0) goto err_serdev; From 692c0cd1a495ff101f8ee44a4de1b9c52168374a Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 21:32:11 +0200 Subject: [PATCH 08/14] ASoC: qcom: sc8280xp: support Senary MI2S Extend the clock-provider DAI fmt setup to Senary MI2S; without it q6i2s_set_fmt() is never called, ws_src remains external and the DSP does not drive the I2S clocks. On the Fairphone (Gen. 6) the speaker amplifiers sit on this interface; the board DTS enabling it is headed upstream separately via linux-arm-msm. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- sound/soc/qcom/sc8280xp.c | 1 + 1 file changed, 1 insertion(+) diff --git a/sound/soc/qcom/sc8280xp.c b/sound/soc/qcom/sc8280xp.c index a36666999ac1..ce28bfac329a 100644 --- a/sound/soc/qcom/sc8280xp.c +++ b/sound/soc/qcom/sc8280xp.c @@ -34,6 +34,7 @@ static int sc8280xp_snd_init(struct snd_soc_pcm_runtime *rtd) switch (cpu_dai->id) { case PRIMARY_MI2S_RX...QUATERNARY_MI2S_TX: case QUINARY_MI2S_RX...QUINARY_MI2S_TX: + case SENARY_MI2S_RX...SENARY_MI2S_TX: snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_BP_FP); break; case WSA_CODEC_DMA_RX_0: From 79c7597fc5b6aac251a6f8acbdc3a18956be9b1e Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 21:32:11 +0200 Subject: [PATCH 09/14] ASoC: codecs: aw88261: backport mainline format negotiation + power-up fix The v7.1.2-milos base predates two aw88261 changes this branch needs: the hw_params format-negotiation series (Val Packett's work, in broonie for-7.2) and our power-up SYSST fix. The Senary MI2S carry drives the two AW88261 amps in S16_LE end to end, which relies on the amplifier negotiating its format; without it the machine driver has to force 32-bit slots. The SYSST fix is also required: aw88261_dev_start() otherwise fails "check sysst fail" on this firmware profile because it demands SWS/BSTS while the amp is still muted. Backport aw88261.c/.h wholesale to the mainline state (matching work/linux tag audio-mainline-aw88261) so this branch tracks mainline rather than the older milos base driver. Local carry only; milos absorbs it on the next rebase past those commits. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Jorijn van der Graaf --- sound/soc/codecs/aw88261.c | 646 ++++++++++++++++++++----------------- sound/soc/codecs/aw88261.h | 191 +++++++++-- 2 files changed, 527 insertions(+), 310 deletions(-) diff --git a/sound/soc/codecs/aw88261.c b/sound/soc/codecs/aw88261.c index a6805d5405cd..dbdf51188cf5 100644 --- a/sound/soc/codecs/aw88261.c +++ b/sound/soc/codecs/aw88261.c @@ -10,9 +10,12 @@ #include #include +#include #include #include #include +#include +#include #include "aw88261.h" #include "aw88395/aw88395_data_type.h" #include "aw88395/aw88395_device.h" @@ -27,64 +30,10 @@ static const struct regmap_config aw88261_remap_config = { static void aw88261_dev_set_volume(struct aw_device *aw_dev, unsigned int value) { - struct aw_volume_desc *vol_desc = &aw_dev->volume_desc; - unsigned int real_value, volume; - unsigned int reg_value; + unsigned int volume = min(value, (unsigned int)AW88261_MUTE_VOL); - volume = min((value + vol_desc->init_volume), (unsigned int)AW88261_MUTE_VOL); - real_value = DB_TO_REG_VAL(volume); - - regmap_read(aw_dev->regmap, AW88261_SYSCTRL2_REG, ®_value); - - real_value = (real_value | (reg_value & AW88261_VOL_START_MASK)); - - dev_dbg(aw_dev->dev, "value 0x%x , real_value:0x%x", value, real_value); - - regmap_write(aw_dev->regmap, AW88261_SYSCTRL2_REG, real_value); -} - -static void aw88261_dev_fade_in(struct aw_device *aw_dev) -{ - struct aw_volume_desc *desc = &aw_dev->volume_desc; - int fade_in_vol = desc->ctl_volume; - int fade_step = aw_dev->fade_step; - int i; - - if (fade_step == 0 || aw_dev->fade_in_time == 0) { - aw88261_dev_set_volume(aw_dev, fade_in_vol); - return; - } - - for (i = AW88261_MUTE_VOL; i >= fade_in_vol; i -= fade_step) { - aw88261_dev_set_volume(aw_dev, i); - usleep_range(aw_dev->fade_in_time, - aw_dev->fade_in_time + 10); - } - - if (i != fade_in_vol) - aw88261_dev_set_volume(aw_dev, fade_in_vol); -} - -static void aw88261_dev_fade_out(struct aw_device *aw_dev) -{ - struct aw_volume_desc *desc = &aw_dev->volume_desc; - int fade_step = aw_dev->fade_step; - int i; - - if (fade_step == 0 || aw_dev->fade_out_time == 0) { - aw88261_dev_set_volume(aw_dev, AW88261_MUTE_VOL); - return; - } - - for (i = desc->ctl_volume; i <= AW88261_MUTE_VOL; i += fade_step) { - aw88261_dev_set_volume(aw_dev, i); - usleep_range(aw_dev->fade_out_time, aw_dev->fade_out_time + 10); - } - - if (i != AW88261_MUTE_VOL) { - aw88261_dev_set_volume(aw_dev, AW88261_MUTE_VOL); - usleep_range(aw_dev->fade_out_time, aw_dev->fade_out_time + 10); - } + regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL2_REG, + ~AW88261_VOL_MASK, DB_TO_REG_VAL(volume)); } static void aw88261_dev_i2s_tx_enable(struct aw_device *aw_dev, bool flag) @@ -120,13 +69,13 @@ static void aw88261_dev_amppd(struct aw_device *aw_dev, bool amppd) static void aw88261_dev_mute(struct aw_device *aw_dev, bool is_mute) { if (is_mute) { - aw88261_dev_fade_out(aw_dev); + aw88261_dev_set_volume(aw_dev, AW88261_MUTE_VOL); regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL_REG, ~AW88261_HMUTE_MASK, AW88261_HMUTE_ENABLE_VALUE); } else { regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL_REG, ~AW88261_HMUTE_MASK, AW88261_HMUTE_DISABLE_VALUE); - aw88261_dev_fade_in(aw_dev); + aw88261_dev_set_volume(aw_dev, aw_dev->volume_desc.ctl_volume); } } @@ -151,21 +100,21 @@ static int aw88261_dev_get_iis_status(struct aw_device *aw_dev) if (ret) return ret; if ((reg_val & AW88261_BIT_PLL_CHECK) != AW88261_BIT_PLL_CHECK) { - dev_err(aw_dev->dev, "check pll lock fail,reg_val:0x%04x", reg_val); + dev_dbg(aw_dev->dev, "check pll lock fail,reg_val:0x%04x", reg_val); return -EINVAL; } return ret; } -static int aw88261_dev_check_mode1_pll(struct aw_device *aw_dev) +static int aw88261_dev_check_pll(struct aw_device *aw_dev) { int ret, i; for (i = 0; i < AW88261_DEV_SYSST_CHECK_MAX; i++) { ret = aw88261_dev_get_iis_status(aw_dev); if (ret) { - dev_err(aw_dev->dev, "mode1 iis signal check error"); + dev_dbg(aw_dev->dev, "mode1 iis signal check error"); usleep_range(AW88261_2000_US, AW88261_2000_US + 10); } else { return ret; @@ -175,71 +124,74 @@ static int aw88261_dev_check_mode1_pll(struct aw_device *aw_dev) return -EPERM; } -static int aw88261_dev_check_mode2_pll(struct aw_device *aw_dev) -{ - unsigned int reg_val; - int ret, i; - - ret = regmap_read(aw_dev->regmap, AW88261_PLLCTRL1_REG, ®_val); - if (ret) - return ret; - - reg_val &= (~AW88261_CCO_MUX_MASK); - if (reg_val == AW88261_CCO_MUX_DIVIDED_VALUE) { - dev_dbg(aw_dev->dev, "CCO_MUX is already divider"); - return -EPERM; - } - - /* change mode2 */ - ret = regmap_update_bits(aw_dev->regmap, AW88261_PLLCTRL1_REG, - ~AW88261_CCO_MUX_MASK, AW88261_CCO_MUX_DIVIDED_VALUE); - if (ret) - return ret; - - for (i = 0; i < AW88261_DEV_SYSST_CHECK_MAX; i++) { - ret = aw88261_dev_get_iis_status(aw_dev); - if (ret) { - dev_err(aw_dev->dev, "mode2 iis signal check error"); - usleep_range(AW88261_2000_US, AW88261_2000_US + 10); - } else { - break; - } - } - - /* change mode1 */ - ret = regmap_update_bits(aw_dev->regmap, AW88261_PLLCTRL1_REG, - ~AW88261_CCO_MUX_MASK, AW88261_CCO_MUX_BYPASS_VALUE); - if (ret == 0) { - usleep_range(AW88261_2000_US, AW88261_2000_US + 10); - for (i = 0; i < AW88261_DEV_SYSST_CHECK_MAX; i++) { - ret = aw88261_dev_check_mode1_pll(aw_dev); - if (ret) { - dev_err(aw_dev->dev, "mode2 switch to mode1, iis signal check error"); - usleep_range(AW88261_2000_US, AW88261_2000_US + 10); - } else { - break; - } - } - } - - return ret; -} - -static int aw88261_dev_check_syspll(struct aw_device *aw_dev) +static int aw88261_dev_configure_syspll(struct aw88261 *aw88261) { + struct aw_device *aw_dev = aw88261->aw_pa; int ret; - ret = aw88261_dev_check_mode1_pll(aw_dev); - if (ret) { - dev_dbg(aw_dev->dev, "mode1 check iis failed try switch to mode2 check"); - ret = aw88261_dev_check_mode2_pll(aw_dev); - if (ret) { - dev_err(aw_dev->dev, "mode2 check iis failed"); - return ret; - } - } + /* Configure TDM slots (I2S is represented as no slots) */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG, + ~AW88261_SLOT_NUM_MASK, aw88261->slot_num_value); + if (ret) + return ret; - return ret; + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG, + ~AW88261_I2S_TX_SLOTVLD_MASK, + aw88261->tx_slotvld_mask); + if (ret) + return ret; + + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG, + ~AW88261_I2S_RXL_SLOTVLD_MASK, + aw88261->rxl_slotvld_mask); + if (ret) + return ret; + + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG, + ~AW88261_I2S_RXR_SLOTVLD_MASK, + aw88261->rxr_slotvld_mask); + if (ret) + return ret; + + /* PLL divider must be used for 8/16/32 kHz modes */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_PLLCTRL1_REG, + ~AW88261_CCO_MUX_MASK, aw88261->cco_mux_value); + if (ret) + return ret; + + /* The word clock (WCK) defines the beginning of a frame */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG, + ~AW88261_I2SSR_MASK, aw88261->sr_value); + if (ret) + return ret; + + /* The bit clock (BCK) defines the length of a frame */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG, + ~AW88261_I2SBCK_MASK, + (aw88261->tdm_bck_value != AW88261_TDM_BCK_UNSET) + ? aw88261->tdm_bck_value : aw88261->bck_value); + if (ret) + return ret; + + /* The logical frame size is the width of data for 1 slot */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG, + ~AW88261_I2SFS_MASK, aw88261->fs_value); + if (ret) + return ret; + + /* The I2S interface mode (Philips standard, LSB/MSB justified) */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG, + ~AW88261_I2SMD_MASK, aw88261->md_value); + if (ret) + return ret; + + /* The polarity of the bit clock (BCK) */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL_REG, + ~AW88261_BCKINV_MASK, aw88261->bck_inv_value); + if (ret) + return ret; + + return aw88261_dev_check_pll(aw_dev); } static int aw88261_dev_check_sysst(struct aw_device *aw_dev) @@ -254,10 +206,10 @@ static int aw88261_dev_check_sysst(struct aw_device *aw_dev) return ret; check_val = reg_val & (~AW88261_BIT_SYSST_CHECK_MASK) - & AW88261_BIT_SYSST_CHECK; - if (check_val != AW88261_BIT_SYSST_CHECK) { - dev_err(aw_dev->dev, "check sysst fail, reg_val=0x%04x, check:0x%x", - reg_val, AW88261_BIT_SYSST_CHECK); + & AW88261_BIT_PLL_CHECK; + if (check_val != AW88261_BIT_PLL_CHECK) { + dev_dbg(aw_dev->dev, "check sysst fail, reg_val=0x%04x, check:0x%x", + reg_val, AW88261_BIT_PLL_CHECK); usleep_range(AW88261_2000_US, AW88261_2000_US + 10); } else { return 0; @@ -284,22 +236,22 @@ static void aw88261_reg_force_set(struct aw88261 *aw88261) if (aw88261->frcset_en == AW88261_FRCSET_ENABLE) { /* set FORCE_PWM */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL3_REG, - AW88261_FORCE_PWM_MASK, AW88261_FORCE_PWM_FORCEMINUS_PWM_VALUE); + ~AW88261_FORCE_PWM_MASK, AW88261_FORCE_PWM_FORCEMINUS_PWM_VALUE); /* set BOOST_OS_WIDTH */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL5_REG, - AW88261_BST_OS_WIDTH_MASK, AW88261_BST_OS_WIDTH_50NS_VALUE); + ~AW88261_BST_OS_WIDTH_MASK, AW88261_BST_OS_WIDTH_50NS_VALUE); /* set BURST_LOOPR */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL6_REG, - AW88261_BST_LOOPR_MASK, AW88261_BST_LOOPR_340K_VALUE); + ~AW88261_BST_LOOPR_MASK, AW88261_BST_LOOPR_340K_VALUE); /* set RSQN_DLY */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL7_REG, - AW88261_RSQN_DLY_MASK, AW88261_RSQN_DLY_35NS_VALUE); + ~AW88261_RSQN_DLY_MASK, AW88261_RSQN_DLY_35NS_VALUE); /* set BURST_SSMODE */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL8_REG, - AW88261_BURST_SSMODE_MASK, AW88261_BURST_SSMODE_FAST_VALUE); + ~AW88261_BURST_SSMODE_MASK, AW88261_BURST_SSMODE_FAST_VALUE); /* set BST_BURST */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL9_REG, - AW88261_BST_BURST_MASK, AW88261_BST_BURST_30MA_VALUE); + ~AW88261_BST_BURST_MASK, AW88261_BST_BURST_30MA_VALUE); } else { dev_dbg(aw88261->aw_pa->dev, "needn't set reg value"); } @@ -550,7 +502,7 @@ static int aw88261_dev_start(struct aw88261 *aw88261) int ret; if (aw_dev->status == AW88261_DEV_PW_ON) { - dev_info(aw_dev->dev, "already power on"); + dev_dbg(aw_dev->dev, "already power on"); return 0; } @@ -558,9 +510,9 @@ static int aw88261_dev_start(struct aw88261 *aw88261) aw88261_dev_pwd(aw_dev, false); usleep_range(AW88261_2000_US, AW88261_2000_US + 10); - ret = aw88261_dev_check_syspll(aw_dev); + ret = aw88261_dev_configure_syspll(aw88261); if (ret) { - dev_err(aw_dev->dev, "pll check failed cannot start"); + dev_dbg(aw_dev->dev, "pll check failed"); goto pll_check_fail; } @@ -571,7 +523,7 @@ static int aw88261_dev_start(struct aw88261 *aw88261) /* check i2s status */ ret = aw88261_dev_check_sysst(aw_dev); if (ret) { - dev_err(aw_dev->dev, "sysst check failed"); + dev_dbg(aw_dev->dev, "sysst check failed"); goto sysst_check_fail; } @@ -672,31 +624,25 @@ static void aw88261_start_pa(struct aw88261 *aw88261) for (i = 0; i < AW88261_START_RETRIES; i++) { ret = aw88261_reg_update(aw88261, aw88261->phase_sync); if (ret) { - dev_err(aw88261->aw_pa->dev, "fw update failed, cnt:%d\n", i); + dev_dbg(aw88261->aw_pa->dev, + "aw88261_reg_update failed, cnt:%d, ret:%d\n", i, ret); continue; } ret = aw88261_dev_start(aw88261); if (ret) { - dev_err(aw88261->aw_pa->dev, "aw88261 device start failed. retry = %d", i); + dev_dbg(aw88261->aw_pa->dev, + "aw88261_dev_start failed, cnt:%d, ret:%d\n", i, ret); continue; } else { - dev_info(aw88261->aw_pa->dev, "start success\n"); + dev_dbg(aw88261->aw_pa->dev, "start success\n"); break; } } + if (ret != 0) + dev_err(aw88261->aw_pa->dev, "start failure (%d)\n", ret); } -static void aw88261_startup_work(struct work_struct *work) -{ - struct aw88261 *aw88261 = - container_of(work, struct aw88261, start_work.work); - - mutex_lock(&aw88261->lock); - aw88261_start_pa(aw88261); - mutex_unlock(&aw88261->lock); -} - -static void aw88261_start(struct aw88261 *aw88261, bool sync_start) +static void aw88261_start(struct aw88261 *aw88261) { if (aw88261->aw_pa->fw_status != AW88261_DEV_FW_OK) return; @@ -704,14 +650,226 @@ static void aw88261_start(struct aw88261 *aw88261, bool sync_start) if (aw88261->aw_pa->status == AW88261_DEV_PW_ON) return; - if (sync_start == AW88261_SYNC_START) - aw88261_start_pa(aw88261); - else - queue_delayed_work(system_dfl_wq, - &aw88261->start_work, - AW88261_START_WORK_DELAY_MS); + aw88261_start_pa(aw88261); } +static int aw88261_set_fmt(struct snd_soc_dai *dai, unsigned int fmt) +{ + struct snd_soc_component *component = dai->component; + struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); + + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + aw88261->bck_inv_value = AW88261_BCKINV_NOT_INVERT_VALUE; + break; + case SND_SOC_DAIFMT_IB_NF: + aw88261->bck_inv_value = AW88261_BCKINV_INVERTED_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported invert mode 0x%x\n", + fmt & SND_SOC_DAIFMT_INV_MASK); + return -EINVAL; + } + + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + case SND_SOC_DAIFMT_DSP_A: + aw88261->md_value = AW88261_I2SMD_PHILIPS_STANDARD_VALUE; + break; + case SND_SOC_DAIFMT_MSB: + case SND_SOC_DAIFMT_DSP_B: + aw88261->md_value = AW88261_I2SMD_MSB_JUSTIFIED_VALUE; + break; + case SND_SOC_DAIFMT_LSB: + aw88261->md_value = AW88261_I2SMD_LSB_JUSTIFIED_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported DAI format 0x%x\n", + fmt & SND_SOC_DAIFMT_FORMAT_MASK); + return -EINVAL; + } + + return 0; +} + +static int aw88261_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct snd_soc_component *component = dai->component; + struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); + + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) + return 0; + + aw88261->cco_mux_value = AW88261_CCO_MUX_BYPASS_VALUE; + switch (params_rate(params)) { + case 8000: + aw88261->sr_value = AW88261_I2SSR_8KHZ_VALUE; + aw88261->cco_mux_value = AW88261_CCO_MUX_DIVIDED_VALUE; + break; + case 11025: + aw88261->sr_value = AW88261_I2SSR_11P025KHZ_VALUE; + break; + case 12000: + aw88261->sr_value = AW88261_I2SSR_12KHZ_VALUE; + break; + case 16000: + aw88261->sr_value = AW88261_I2SSR_16KHZ_VALUE; + aw88261->cco_mux_value = AW88261_CCO_MUX_DIVIDED_VALUE; + break; + case 22050: + aw88261->sr_value = AW88261_I2SSR_22P05KHZ_VALUE; + break; + case 24000: + aw88261->sr_value = AW88261_I2SSR_24KHZ_VALUE; + break; + case 32000: + aw88261->sr_value = AW88261_I2SSR_32KHZ_VALUE; + aw88261->cco_mux_value = AW88261_CCO_MUX_DIVIDED_VALUE; + break; + case 44100: + aw88261->sr_value = AW88261_I2SSR_44P1KHZ_VALUE; + break; + case 48000: + aw88261->sr_value = AW88261_I2SSR_48KHZ_VALUE; + break; + case 96000: + aw88261->sr_value = AW88261_I2SSR_96KHZ_VALUE; + break; + case 192000: + aw88261->sr_value = AW88261_I2SSR_192KHZ_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported sample rate %d\n", + params_rate(params)); + return -EINVAL; + } + + switch (params_width(params)) { + case 16: + aw88261->fs_value = AW88261_I2SFS_16_BITS_VALUE; + break; + case 20: + aw88261->fs_value = AW88261_I2SFS_20_BITS_VALUE; + break; + case 24: + aw88261->fs_value = AW88261_I2SFS_24_BITS_VALUE; + break; + case 32: + aw88261->fs_value = AW88261_I2SFS_32_BITS_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported bit width %d\n", + params_width(params)); + return -EINVAL; + } + + switch (params_physical_width(params)) { + case 16: + aw88261->bck_value = AW88261_I2SBCK_32FS_VALUE; + break; + case 24: + aw88261->bck_value = AW88261_I2SBCK_48FS_VALUE; + break; + case 32: + aw88261->bck_value = AW88261_I2SBCK_64FS_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported physical bit width %d\n", + params_physical_width(params)); + return -EINVAL; + } + + return 0; +} + +static int aw88261_set_tdm_slot(struct snd_soc_dai *dai, + unsigned int tx_mask, unsigned int rx_mask, int slots, int slot_width) +{ + struct snd_soc_component *component = dai->component; + struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); + int chan; + + switch (slots) { + case 0: + /* Just reset everything TDM related to I2S values */ + aw88261->slot_num_value = AW88261_SLOT_NUM_I2S_MODE_VALUE; + aw88261->tdm_bck_value = AW88261_TDM_BCK_UNSET; + aw88261->tx_slotvld_mask = 0 << AW88261_I2S_TX_SLOTVLD_START_BIT; + aw88261->rxl_slotvld_mask = 0 << AW88261_I2S_RXL_SLOTVLD_START_BIT; + aw88261->rxr_slotvld_mask = 1 << AW88261_I2S_RXR_SLOTVLD_START_BIT; + return 0; + case 1: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM1S_VALUE; + break; + case 2: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM2S_VALUE; + break; + case 4: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM4S_VALUE; + break; + case 6: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM6S_VALUE; + break; + case 8: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM8S_VALUE; + break; + case 16: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM16S_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported slot count %d\n", slots); + return -EINVAL; + } + + switch (slot_width) { + case 16: + aw88261->tdm_bck_value = AW88261_I2SBCK_32FS_VALUE; + break; + case 20: + case 24: + aw88261->tdm_bck_value = AW88261_I2SBCK_48FS_VALUE; + break; + case 32: + aw88261->tdm_bck_value = AW88261_I2SBCK_64FS_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported slot width %d\n", + slot_width); + return -EINVAL; + } + + if (tx_mask != 0) { + if ((chan = __ffs(tx_mask)) > 16) + return -EINVAL; + + aw88261->tx_slotvld_mask = chan << AW88261_I2S_TX_SLOTVLD_START_BIT; + } + + if (rx_mask != 0) { + if ((chan = __ffs(rx_mask)) > 16) + return -EINVAL; + + aw88261->rxl_slotvld_mask = chan << AW88261_I2S_RXL_SLOTVLD_START_BIT; + } + + if ((rx_mask & ~BIT(chan)) != 0) { + if ((chan = __ffs(rx_mask & ~BIT(chan))) > 16) + return -EINVAL; + + aw88261->rxr_slotvld_mask = chan << AW88261_I2S_RXR_SLOTVLD_START_BIT; + } + + return 0; +} + +static const struct snd_soc_dai_ops aw88261_dai_ops = { + .set_fmt = aw88261_set_fmt, + .hw_params = aw88261_hw_params, + .set_tdm_slot = aw88261_set_tdm_slot, +}; + static struct snd_soc_dai_driver aw88261_dai[] = { { .name = "aw88261-aif", @@ -730,78 +888,10 @@ static struct snd_soc_dai_driver aw88261_dai[] = { .rates = AW88261_RATES, .formats = AW88261_FORMATS, }, + .ops = &aw88261_dai_ops, }, }; -static int aw88261_get_fade_in_time(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); - struct aw_device *aw_dev = aw88261->aw_pa; - - ucontrol->value.integer.value[0] = aw_dev->fade_in_time; - - return 0; -} - -static int aw88261_set_fade_in_time(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); - struct soc_mixer_control *mc = - (struct soc_mixer_control *)kcontrol->private_value; - struct aw_device *aw_dev = aw88261->aw_pa; - int time; - - time = ucontrol->value.integer.value[0]; - - if (time < mc->min || time > mc->max) - return -EINVAL; - - if (time != aw_dev->fade_in_time) { - aw_dev->fade_in_time = time; - return 1; - } - - return 0; -} - -static int aw88261_get_fade_out_time(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); - struct aw_device *aw_dev = aw88261->aw_pa; - - ucontrol->value.integer.value[0] = aw_dev->fade_out_time; - - return 0; -} - -static int aw88261_set_fade_out_time(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); - struct soc_mixer_control *mc = - (struct soc_mixer_control *)kcontrol->private_value; - struct aw_device *aw_dev = aw88261->aw_pa; - int time; - - time = ucontrol->value.integer.value[0]; - if (time < mc->min || time > mc->max) - return -EINVAL; - - if (time != aw_dev->fade_out_time) { - aw_dev->fade_out_time = time; - return 1; - } - - return 0; -} - static int aw88261_dev_set_profile_index(struct aw_device *aw_dev, int index) { /* check the index whether is valid */ @@ -880,7 +970,7 @@ static int aw88261_profile_set(struct snd_kcontrol *kcontrol, if (aw88261->aw_pa->status) { aw88261_dev_stop(aw88261->aw_pa); - aw88261_start(aw88261, AW88261_SYNC_START); + aw88261_start(aw88261); } mutex_unlock(&aw88261->lock); @@ -895,7 +985,8 @@ static int aw88261_volume_get(struct snd_kcontrol *kcontrol, struct aw88261 *aw88261 = snd_soc_component_get_drvdata(codec); struct aw_volume_desc *vol_desc = &aw88261->aw_pa->volume_desc; - ucontrol->value.integer.value[0] = vol_desc->ctl_volume; + ucontrol->value.integer.value[0] = + (AW88261_MUTE_VOL - vol_desc->ctl_volume) / 2; return 0; } @@ -908,13 +999,13 @@ static int aw88261_volume_set(struct snd_kcontrol *kcontrol, struct aw_volume_desc *vol_desc = &aw88261->aw_pa->volume_desc; struct soc_mixer_control *mc = (struct soc_mixer_control *)kcontrol->private_value; - int value; - - value = ucontrol->value.integer.value[0]; + int value = ucontrol->value.integer.value[0]; if (value < mc->min || value > mc->max) return -EINVAL; + value = AW88261_MUTE_VOL - (value * 2); + if (vol_desc->ctl_volume != value) { vol_desc->ctl_volume = value; aw88261_dev_set_volume(aw88261->aw_pa, vol_desc->ctl_volume); @@ -925,48 +1016,18 @@ static int aw88261_volume_set(struct snd_kcontrol *kcontrol, return 0; } -static int aw88261_get_fade_step(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *codec = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(codec); - - ucontrol->value.integer.value[0] = aw88261->aw_pa->fade_step; - - return 0; -} - -static int aw88261_set_fade_step(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *codec = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(codec); - struct soc_mixer_control *mc = - (struct soc_mixer_control *)kcontrol->private_value; - int value; - - value = ucontrol->value.integer.value[0]; - if (value < mc->min || value > mc->max) - return -EINVAL; - - if (aw88261->aw_pa->fade_step != value) { - aw88261->aw_pa->fade_step = value; - return 1; - } - - return 0; -} +/* + * The field contains 4 bits in units of 6dB + 6 bits in units of 0.125dB + * which is too precise for TLV (!) so we have to multiply the scale by 2. + * + * The range is clamped at -90dB to prevent overflowing the 4-bit part. + */ +static const DECLARE_TLV_DB_SCALE(volume_tlv, -9000, 25, 0); static const struct snd_kcontrol_new aw88261_controls[] = { - SOC_SINGLE_EXT("PCM Playback Volume", AW88261_SYSCTRL2_REG, - 6, AW88261_MUTE_VOL, 0, aw88261_volume_get, - aw88261_volume_set), - SOC_SINGLE_EXT("Fade Step", 0, 0, AW88261_MUTE_VOL, 0, - aw88261_get_fade_step, aw88261_set_fade_step), - SOC_SINGLE_EXT("Volume Ramp Up Step", 0, 0, FADE_TIME_MAX, FADE_TIME_MIN, - aw88261_get_fade_in_time, aw88261_set_fade_in_time), - SOC_SINGLE_EXT("Volume Ramp Down Step", 0, 0, FADE_TIME_MAX, FADE_TIME_MIN, - aw88261_get_fade_out_time, aw88261_set_fade_out_time), + SOC_SINGLE_EXT_TLV("PCM Playback Volume", AW88261_SYSCTRL2_REG, + 6, AW88261_CTL_MAX_VOL, 1, + aw88261_volume_get, aw88261_volume_set, volume_tlv), AW88261_PROFILE_EXT("Profile Set", aw88261_profile_info, aw88261_profile_get, aw88261_profile_set), }; @@ -980,7 +1041,7 @@ static int aw88261_playback_event(struct snd_soc_dapm_widget *w, mutex_lock(&aw88261->lock); switch (event) { case SND_SOC_DAPM_PRE_PMU: - aw88261_start(aw88261, AW88261_ASYNC_START); + aw88261_start(aw88261); break; case SND_SOC_DAPM_POST_PMD: aw88261_dev_stop(aw88261->aw_pa); @@ -1057,8 +1118,6 @@ static int aw88261_dev_init(struct aw88261 *aw88261, struct aw_container *aw_cfg if (ret) return ret; - aw_dev->fade_in_time = AW88261_500_US; - aw_dev->fade_out_time = AW88261_500_US; aw_dev->prof_cur = AW88261_INIT_PROFILE; aw_dev->prof_index = AW88261_INIT_PROFILE; @@ -1094,6 +1153,7 @@ static int aw88261_dev_init(struct aw88261 *aw88261, struct aw_container *aw_cfg static int aw88261_request_firmware_file(struct aw88261 *aw88261) { const struct firmware *cont = NULL; + struct aw_container *aw_cfg; const char *fw_name; int ret; @@ -1111,15 +1171,17 @@ static int aw88261_request_firmware_file(struct aw88261 *aw88261) dev_info(aw88261->aw_pa->dev, "loaded %s - size: %zu\n", fw_name, cont ? cont->size : 0); - aw88261->aw_cfg = devm_kzalloc(aw88261->aw_pa->dev, cont->size + sizeof(int), GFP_KERNEL); - if (!aw88261->aw_cfg) { + aw_cfg = devm_kzalloc(aw88261->aw_pa->dev, struct_size(aw_cfg, data, cont->size), GFP_KERNEL); + if (!aw_cfg) { release_firmware(cont); return -ENOMEM; } - aw88261->aw_cfg->len = (int)cont->size; - memcpy(aw88261->aw_cfg->data, cont->data, cont->size); + aw_cfg->len = (int)cont->size; + memcpy(aw_cfg->data, cont->data, cont->size); release_firmware(cont); + aw88261->aw_cfg = aw_cfg; + ret = aw88395_dev_load_acf_check(aw88261->aw_pa, aw88261->aw_cfg); if (ret) { dev_err(aw88261->aw_pa->dev, "load [%s] failed !", fw_name); @@ -1142,8 +1204,6 @@ static int aw88261_codec_probe(struct snd_soc_component *component) struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); int ret; - INIT_DELAYED_WORK(&aw88261->start_work, aw88261_startup_work); - ret = aw88261_request_firmware_file(aw88261); if (ret) return dev_err_probe(aw88261->aw_pa->dev, ret, @@ -1167,16 +1227,8 @@ static int aw88261_codec_probe(struct snd_soc_component *component) return ret; } -static void aw88261_codec_remove(struct snd_soc_component *aw_codec) -{ - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(aw_codec); - - cancel_delayed_work_sync(&aw88261->start_work); -} - static const struct snd_soc_component_driver soc_codec_dev_aw88261 = { .probe = aw88261_codec_probe, - .remove = aw88261_codec_remove, }; static void aw88261_parse_channel_dt(struct aw88261 *aw88261) @@ -1208,7 +1260,7 @@ static int aw88261_init(struct aw88261 *aw88261, struct i2c_client *i2c, struct return ret; } if (chip_id != AW88261_CHIP_ID) { - dev_err(&i2c->dev, "unsupported device"); + dev_err(&i2c->dev, "unsupported device id = %x", chip_id); return -ENXIO; } @@ -1229,8 +1281,7 @@ static int aw88261_init(struct aw88261 *aw88261, struct i2c_client *i2c, struct aw_dev->prof_info.prof_type = AW88395_DEV_NONE_TYPE_ID; aw_dev->channel = 0; aw_dev->fw_status = AW88261_DEV_FW_FAILED; - aw_dev->fade_step = AW88261_VOLUME_STEP_DB; - aw_dev->volume_desc.ctl_volume = AW88261_VOL_DEFAULT_VALUE; + aw_dev->volume_desc.ctl_volume = AW88261_CTL_DEFAULT_VOL; aw_dev->volume_desc.mute_volume = AW88261_MUTE_VOL; aw88261_parse_channel_dt(aw88261); @@ -1249,6 +1300,17 @@ static int aw88261_i2c_probe(struct i2c_client *i2c) if (!aw88261) return -ENOMEM; + /* set defaults */ + aw88261->slot_num_value = AW88261_SLOT_NUM_I2S_MODE_VALUE; + aw88261->sr_value = AW88261_I2SSR_48KHZ_VALUE; + aw88261->cco_mux_value = AW88261_CCO_MUX_BYPASS_VALUE; + aw88261->fs_value = AW88261_I2SFS_24_BITS_VALUE; + aw88261->bck_value = AW88261_I2SBCK_64FS_VALUE; + aw88261->bck_inv_value = AW88261_BCKINV_NOT_INVERT_VALUE; + aw88261->tdm_bck_value = AW88261_TDM_BCK_UNSET; + aw88261->md_value = AW88261_I2SMD_PHILIPS_STANDARD_VALUE; + aw88261->rxr_slotvld_mask = 1 << AW88261_I2S_RXR_SLOTVLD_START_BIT; + mutex_init(&aw88261->lock); i2c_set_clientdata(i2c, aw88261); @@ -1274,7 +1336,7 @@ static int aw88261_i2c_probe(struct i2c_client *i2c) } static const struct i2c_device_id aw88261_i2c_id[] = { - { "aw88261" }, + { .name = "aw88261" }, { } }; MODULE_DEVICE_TABLE(i2c, aw88261_i2c_id); diff --git a/sound/soc/codecs/aw88261.h b/sound/soc/codecs/aw88261.h index 1fee589608d6..f16f9f8c40a2 100644 --- a/sound/soc/codecs/aw88261.h +++ b/sound/soc/codecs/aw88261.h @@ -116,6 +116,19 @@ #define AW88261_VCALK_SHIFT (0) #define AW88261_VCALKL_SHIFT (0) +#define AW88261_BCKINV_START_BIT (4) +#define AW88261_BCKINV_BITS_LEN (1) +#define AW88261_BCKINV_MASK \ + (~(((1<> AW88261_VOL_6DB_START) * \ @@ -403,11 +551,6 @@ .put = profile_set, \ } -enum { - AW88261_SYNC_START = 0, - AW88261_ASYNC_START, -}; - enum aw88261_id { AW88261_CHIP_ID = 0x2113, }; @@ -442,7 +585,6 @@ struct aw88261 { struct aw_device *aw_pa; struct mutex lock; struct gpio_desc *reset_gpio; - struct delayed_work start_work; struct regmap *regmap; struct aw_container *aw_cfg; @@ -451,6 +593,19 @@ struct aw88261 { unsigned int mute_st; unsigned int amppd_st; + unsigned int sr_value; + unsigned int cco_mux_value; + unsigned int fs_value; + unsigned int bck_value; + unsigned int bck_inv_value; + unsigned int tdm_bck_value; + unsigned int md_value; + + unsigned int slot_num_value; + unsigned int tx_slotvld_mask; + unsigned int rxl_slotvld_mask; + unsigned int rxr_slotvld_mask; + bool phase_sync; }; From d571dd42d3d94b7904b67f28462707be4ce71022 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 21:32:11 +0200 Subject: [PATCH 10/14] ASoC: qcom: q6apm-lpass-dais: start the graph at prepare The DAPM power-up sequence runs during snd_pcm prepare, but the BE port graph is only started at trigger time. A codec that powers up synchronously from a DAPM widget event and needs a running bit clock at that point - such as aw88261 since commit caea99ac809d ("ASoC: codecs: aw88261: remove async start") - can therefore never see a live clock: its power-up check runs before the trigger and fails on every stream start. Start the graph at the end of prepare instead, mirroring what q6afe_dai_prepare() does on the legacy stack, so the interface clocks already run when DAPM powers up the codec. The FE side already starts its own graph at prepare in q6apm_dai_prepare(); only the BE waited for trigger. The trigger-time start is kept as a fallback, guarded by is_port_started. Tested on the Fairphone (Gen. 6) - 2x aw88261 on Senary MI2S: without this the amplifiers fail to power up with SYSST reporting "no clock" on every stream start; with it they start synchronously, including for the first short stream of the boot. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- sound/soc/qcom/qdsp6/q6apm-lpass-dais.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c index 006b283484d9..06946add653a 100644 --- a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c +++ b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c @@ -224,6 +224,21 @@ static int q6apm_lpass_dai_prepare(struct snd_pcm_substream *substream, struct s dev_err(dai->dev, "Failed to prepare Graph %d\n", rc); goto err; } + + /* + * Start the port already at prepare, like q6afe does: this starts + * the interface clocks before the DAPM power-up sequence runs, so + * codecs that need a live BCLK at power-up (e.g. aw88261) can + * start synchronously. The trigger callback keeps its start as a + * no-op fallback via is_port_started. + */ + rc = q6apm_graph_start(dai_data->graph[dai->id]); + if (rc < 0) { + dev_err(dai->dev, "Failed to start APM port %d\n", dai->id); + goto err; + } + dai_data->is_port_started[dai->id] = true; + return 0; err: if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { From 52c785c31534cd59d2107133ad6ad8c3a4c406bc Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 21:32:11 +0200 Subject: [PATCH 11/14] arm64: dts: qcom: milos-fairphone-fp6: enable Senary MI2S dai-link Uncomment the Senary MI2S speaker dai-link. The sc8280xp machine driver gained Senary MI2S support and the aw88261 power-up check is fixed (both carried on this branch), so the link is functional: both AW88261 amps load their ACF firmware and play. This restores the enablement that was lost when the audio carries were rebased from the v7.0.8 stack onto v7.1.2-milos: only the two driver patches were carried over, leaving the sound card with zero dai-links (the card registers but stays empty - no PCMs, no controls). Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts | 2 -- 1 file changed, 2 deletions(-) diff --git a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts index e5944a1b255f..08852107c519 100644 --- a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts +++ b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts @@ -216,7 +216,6 @@ pinctrl-names = "default", "sleep"; - /* i2s-dai-link { link-name = "Senary MI2S Playback"; @@ -232,7 +231,6 @@ sound-dai = <&q6apm>; }; }; - */ }; thermal-zones { 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 12/14] 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); From 82c43dd3f18e59dbd1fe097e35dc8826eba4ac20 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Mon, 6 Jul 2026 02:18:05 +0200 Subject: [PATCH 13/14] ASoC: codecs: wcd9378: correct the ADC analog gain TLV to 1.5 dB steps Measured acoustically on the FP6 with a fixed tone played through the speakers: each TX gain code adds 1.5 dB (+6 dB per 4 codes, +30 dB over the 0..20 range), not the 0.25 dB the TLV inherited from the wcd937x driver family. With the correct scale, userspace volume mapping (e.g. PulseAudio) can use the real analog range. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- sound/soc/codecs/wcd9378.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sound/soc/codecs/wcd9378.c b/sound/soc/codecs/wcd9378.c index e1857aca52ab..4adc78246b38 100644 --- a/sound/soc/codecs/wcd9378.c +++ b/sound/soc/codecs/wcd9378.c @@ -145,7 +145,12 @@ static const struct wcd9378_smp_fn wcd9378_smp_jack_adc2 = { .act_reg = WCD9378_SMP_JACK_PDE34_ACT_PS, }; -static const DECLARE_TLV_DB_SCALE(analog_gain, 0, 25, 1); +/* + * Measured acoustically on the FP6: the TX gain field steps 1.5 dB per + * code (+6 dB per 4 codes, +30 dB over the full 0..20 range). The + * wcd937x-family drivers claim 0.25 dB steps for the same field. + */ +static const DECLARE_TLV_DB_SCALE(analog_gain, 0, 150, 0); static int wcd9378_reset(struct wcd9378_priv *wcd9378) { From 08f135ee41edce587e3e7245a6403b05ec60ca9a Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Mon, 6 Jul 2026 19:28:35 +0200 Subject: [PATCH 14/14] ASoC: codecs: wcd9378: sync with the mainline v1 submission Mirror the driver as prepared for mainline (work/linux commit 917ea639925b, "ASoC: codecs: wcd9378: add TX/capture codec driver"), per the carry-mirrors-upstream rule. On top of the previous carry state this brings the maintainer-eye pre-review fixes: - latch the per-ADC sys-usage bit and target SDCA function at PRE_PMU; POST_PMD previously recomputed them from the live input mux and could tear down the wrong function after a mux change - clear the requested sys_usage_mask bit when no profile matches - drop the unreachable -EACCES carve-out on the TX runtime-PM hold (caused a usage-count underflow in unbind) - mark the write-1-clear FUNC_STAT registers volatile - drop the unused is_dapm parameter, the stale sys_usage write-skip cache, the SDW_SCP_INT1_IMPL_DEF unmask and the PS0 re-request hack; clamp DT channel-map reads - name the SWRS_SCP_SDCA_INTRTYPE registers; trim the PS0-failure debug dump; Kconfig imply + help text One deviation from the mainline patch: v7.1.2 predates sdw_slave_wait_for_init(), so the open-coded wait_for_completion_timeout() stays here. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- sound/soc/codecs/Kconfig | 7 +- sound/soc/codecs/wcd9378-sdw.c | 12 ++- sound/soc/codecs/wcd9378.c | 191 ++++++++++++++++----------------- sound/soc/codecs/wcd9378.h | 12 ++- 4 files changed, 114 insertions(+), 108 deletions(-) diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index db114e6063d4..d902df62b184 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -301,6 +301,7 @@ config SND_SOC_ALL_CODECS imply SND_SOC_WCD9335 imply SND_SOC_WCD934X imply SND_SOC_WCD937X_SDW + imply SND_SOC_WCD9378_SDW imply SND_SOC_WCD938X_SDW imply SND_SOC_WCD939X_SDW imply SND_SOC_LPASS_MACRO_COMMON @@ -2443,8 +2444,10 @@ config SND_SOC_WCD9378_SDW depends on SOUNDWIRE select REGMAP_SOUNDWIRE help - Driver for the Qualcomm WCD9378 audio codec connected via - SoundWire, as found on SM7635 phones. + The WCD9378 is an audio codec IC connected to the host over + SoundWire, found on Qualcomm SM7635 boards such as the + Fairphone 6. It has three ADCs for analog microphones, mic + bias supplies, headphone/earpiece outputs and MBHC. To compile this codec driver say Y or m. config SND_SOC_WCD938X diff --git a/sound/soc/codecs/wcd9378-sdw.c b/sound/soc/codecs/wcd9378-sdw.c index b9ada4d3c3cd..d9a99a6db500 100644 --- a/sound/soc/codecs/wcd9378-sdw.c +++ b/sound/soc/codecs/wcd9378-sdw.c @@ -299,7 +299,12 @@ static bool wcd9378_volatile_register(struct device *dev, unsigned int reg) case WCD9378_SEQ_TX0_STAT: case WCD9378_SEQ_TX1_STAT: case WCD9378_SEQ_TX2_STAT: + case WCD9378_SMP_AMP_FUNC_STAT: + case WCD9378_SMP_JACK_FUNC_STAT: case WCD9378_SMP_JACK_PDE34_ACT_PS: + case WCD9378_SMP_MIC_FUNC_STAT(0): + case WCD9378_SMP_MIC_FUNC_STAT(1): + case WCD9378_SMP_MIC_FUNC_STAT(2): case WCD9378_SMP_MIC_OT10_USAGE(0): case WCD9378_SMP_MIC_PDE11_ACT_PS(0): case WCD9378_SMP_MIC_OT10_USAGE(1): @@ -364,8 +369,7 @@ static int wcd9378_sdw_probe(struct sdw_slave *pdev, wcd->sdev = pdev; dev_set_drvdata(dev, wcd); - pdev->prop.scp_int1_mask = SDW_SCP_INT1_IMPL_DEF | - SDW_SCP_INT1_BUS_CLASH | + pdev->prop.scp_int1_mask = SDW_SCP_INT1_BUS_CLASH | SDW_SCP_INT1_PARITY; pdev->prop.lane_control_support = true; pdev->prop.simple_clk_stop_capable = true; @@ -377,6 +381,8 @@ static int wcd9378_sdw_probe(struct sdw_slave *pdev, if (wcd->is_tx) { master_ch_mask_size = of_property_count_u8_elems(dev->of_node, "qcom,tx-channel-mapping"); + master_ch_mask_size = min_t(int, master_ch_mask_size, + ARRAY_SIZE(wcd9378_sdw_tx_ch_info)); if (master_ch_mask_size > 0) ret = of_property_read_u8_array(dev->of_node, @@ -386,6 +392,8 @@ static int wcd9378_sdw_probe(struct sdw_slave *pdev, } else { master_ch_mask_size = of_property_count_u8_elems(dev->of_node, "qcom,rx-channel-mapping"); + master_ch_mask_size = min_t(int, master_ch_mask_size, + ARRAY_SIZE(wcd9378_sdw_rx_ch_info)); if (master_ch_mask_size > 0) ret = of_property_read_u8_array(dev->of_node, diff --git a/sound/soc/codecs/wcd9378.c b/sound/soc/codecs/wcd9378.c index 4adc78246b38..38acb6b20388 100644 --- a/sound/soc/codecs/wcd9378.c +++ b/sound/soc/codecs/wcd9378.c @@ -92,7 +92,9 @@ struct wcd9378_priv { int micb_ref[WCD9378_MAX_MICBIAS]; int pullup_ref[WCD9378_MAX_MICBIAS]; unsigned long sys_usage_mask; - int sys_usage; + /* per-ADC profile bit and target function, latched at power-up */ + int tx_sys_bit[3]; + bool tx_is_jack[3]; u32 tx_mode[3]; struct gpio_desc *reset_gpio; }; @@ -152,14 +154,12 @@ static const struct wcd9378_smp_fn wcd9378_smp_jack_adc2 = { */ static const DECLARE_TLV_DB_SCALE(analog_gain, 0, 150, 0); -static int wcd9378_reset(struct wcd9378_priv *wcd9378) +static void 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; } /* @@ -343,17 +343,15 @@ static int wcd9378_sys_usage_update(struct snd_soc_component *component, } if (i == ARRAY_SIZE(wcd9378_sys_usage_profiles)) { + clear_bit(bit, &wcd9378->sys_usage_mask); 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; - } + snd_soc_component_update_bits(component, WCD9378_SYS_USAGE_CTRL, + WCD9378_SYS_USAGE_CTRL_MASK, i); return 0; } @@ -430,21 +428,30 @@ static int wcd9378_tx_sequencer_enable(struct snd_soc_dapm_widget *w, { 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]; + const struct wcd9378_smp_fn *fn; 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: + sys_bit = wcd9378_sys_usage_bit_get(component, adc, &is_jack); + if (sys_bit < 0) + return sys_bit; + + /* + * Latch the profile bit and the target function: the input + * mux can be rewritten while the path is powered, and + * power-down must tear down what was actually powered up. + */ + wcd9378->tx_sys_bit[adc] = sys_bit; + wcd9378->tx_is_jack[adc] = is_jack; + if (is_jack) + fn = &wcd9378_smp_jack_adc2; + else + fn = &wcd9378_smp_mic[adc]; + if (wcd9378_sys_usage_update(component, sys_bit, true)) return -EINVAL; @@ -478,49 +485,23 @@ static int wcd9378_tx_sequencer_enable(struct snd_soc_dapm_widget *w, 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); - + if (val != WCD9378_PDE_PS0_ON) 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)); - } + "TX%d sequencer not in PS0 (act_ps %#x, bus %u Hz)\n", + adc, val, + wcd9378->tx_sdw_dev->bus->params.curr_dr_freq); break; case SND_SOC_DAPM_POST_PMD: + sys_bit = wcd9378->tx_sys_bit[adc]; + if (sys_bit < 0) + break; + if (wcd9378->tx_is_jack[adc]) + fn = &wcd9378_smp_jack_adc2; + else + fn = &wcd9378_smp_mic[adc]; + snd_soc_component_update_bits(component, fn->usage_reg, 0xff, WCD9378_ADC_USAGE_OFF); if (fn->hpf_reg) @@ -530,6 +511,7 @@ static int wcd9378_tx_sequencer_enable(struct snd_soc_dapm_widget *w, WCD9378_PDE_PS3_OFF); usleep_range(800, 810); wcd9378_sys_usage_update(component, sys_bit, false); + wcd9378->tx_sys_bit[adc] = -1; if (!(wcd9378->sys_usage_mask & WCD9378_SYS_USAGE_TX_MASK)) wcd9378_swr_clk_indicate(wcd9378, false); @@ -540,7 +522,7 @@ static int wcd9378_tx_sequencer_enable(struct snd_soc_dapm_widget *w, } static int wcd9378_micbias_control(struct snd_soc_component *component, - int micb_num, int req, bool is_dapm) + int micb_num, int req) { struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); int mb_index = micb_num - 1; @@ -563,19 +545,19 @@ static int wcd9378_micbias_control(struct snd_soc_component *component, 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); + 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); + 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); + WCD9378_SMP_JACK_IT31_MICB, + 0xff, usage_val); } break; case MICB_DISABLE: @@ -584,29 +566,29 @@ static int wcd9378_micbias_control(struct snd_soc_component *component, 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); + WCD9378_MB_PULLUP_EN, + pullup_bit, pullup_bit); snd_soc_component_update_bits(component, usage_reg, 0xff, - WCD9378_MICB_USAGE_1P8V_OR_PULLUP); + 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); + 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); + 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); + 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); + 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); + WCD9378_ANA_MICB2_RAMP, + WCD9378_ANA_MICB2_RAMP_EN, + WCD9378_ANA_MICB2_RAMP_EN); } } break; @@ -615,14 +597,14 @@ static int wcd9378_micbias_control(struct snd_soc_component *component, 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); + WCD9378_MB_PULLUP_EN, + pullup_bit, pullup_bit); snd_soc_component_update_bits(component, usage_reg, 0xff, - WCD9378_MICB_USAGE_1P8V_OR_PULLUP); + 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); + WCD9378_SMP_JACK_IT31_MICB, 0xff, + WCD9378_MICB_USAGE_1P8V_OR_PULLUP); } break; case MICB_PULLUP_DISABLE: @@ -631,13 +613,13 @@ static int wcd9378_micbias_control(struct snd_soc_component *component, 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); + WCD9378_MB_PULLUP_EN, pullup_bit, 0x00); snd_soc_component_update_bits(component, usage_reg, 0xff, - WCD9378_MICB_USAGE_PULL_DOWN); + 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); + WCD9378_SMP_JACK_IT31_MICB, 0xff, + WCD9378_MICB_USAGE_PULL_DOWN); } break; } @@ -656,13 +638,13 @@ static int wcd9378_codec_enable_micbias(struct snd_soc_dapm_widget *w, switch (event) { case SND_SOC_DAPM_PRE_PMU: - wcd9378_micbias_control(component, micb_num, MICB_ENABLE, true); + wcd9378_micbias_control(component, micb_num, MICB_ENABLE); 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); + wcd9378_micbias_control(component, micb_num, MICB_DISABLE); break; } @@ -679,14 +661,14 @@ static int wcd9378_codec_enable_micbias_pullup(struct snd_soc_dapm_widget *w, switch (event) { case SND_SOC_DAPM_PRE_PMU: wcd9378_micbias_control(component, micb_num, - MICB_PULLUP_ENABLE, true); + MICB_PULLUP_ENABLE); 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); + MICB_PULLUP_DISABLE); break; } @@ -983,7 +965,7 @@ static void wcd9378_set_micb_usage_vals(struct snd_soc_component *component) break; default: vout = wcd_get_micb_vout_ctl_val(component->dev, - wcd9378->common.micb_mv[i]); + wcd9378->common.micb_mv[i]); if (vout < 0) { wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_1P8V_OR_PULLUP; @@ -1006,9 +988,12 @@ static int wcd9378_soc_codec_probe(struct snd_soc_component *component) unsigned long time_left; int ret; - time_left = wait_for_completion_timeout( - &wcd9378->tx_sdw_dev->initialization_complete, - msecs_to_jiffies(5000)); + /* + * Mainline uses sdw_slave_wait_for_init() here; the v7.1.2 base + * predates that helper, open-code it. + */ + 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; @@ -1024,10 +1009,14 @@ static int wcd9378_soc_codec_probe(struct snd_soc_component *component) 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); + /* + * SDCA interrupt type configuration, mirroring the downstream init + * sequence. Nothing consumes these interrupts yet (no MBHC support); + * kept so the bring-up sequence validated on hardware is unchanged. + */ + sdw_write(wcd9378->tx_sdw_dev, WCD9378_SWRS_SCP_SDCA_INTRTYPE_1, 0xff); + sdw_write(wcd9378->tx_sdw_dev, WCD9378_SWRS_SCP_SDCA_INTRTYPE_2, 0x0b); + sdw_write(wcd9378->tx_sdw_dev, WCD9378_SWRS_SCP_SDCA_INTRTYPE_3, 0xff); wcd9378_io_init(component); wcd9378_set_micb_usage_vals(component); @@ -1239,7 +1228,7 @@ static int wcd9378_bind(struct device *dev) * codec is bound. */ ret = pm_runtime_resume_and_get(wcd9378->txdev); - if (ret < 0 && ret != -EACCES) { + if (ret < 0) { dev_err(dev, "could not resume TX device\n"); goto err_remove_link3; } @@ -1321,7 +1310,7 @@ static int wcd9378_probe(struct platform_device *pdev) struct component_match *match = NULL; struct device *dev = &pdev->dev; struct wcd9378_priv *wcd9378; - int ret; + int ret, i; wcd9378 = devm_kzalloc(dev, sizeof(*wcd9378), GFP_KERNEL); if (!wcd9378) @@ -1329,6 +1318,8 @@ static int wcd9378_probe(struct platform_device *pdev) dev_set_drvdata(dev, wcd9378); mutex_init(&wcd9378->micb_lock); + for (i = 0; i < ARRAY_SIZE(wcd9378->tx_sys_bit); i++) + wcd9378->tx_sys_bit[i] = -1; wcd9378->common.dev = dev; wcd9378->common.max_bias = WCD9378_MAX_MICBIAS; diff --git a/sound/soc/codecs/wcd9378.h b/sound/soc/codecs/wcd9378.h index 762542359820..fb9e7925968a 100644 --- a/sound/soc/codecs/wcd9378.h +++ b/sound/soc/codecs/wcd9378.h @@ -128,11 +128,15 @@ /* * Raw (16-bit, non-paged) Qualcomm slave SCP registers, written with - * sdw_write() directly. Bus clock indication towards the codec. + * sdw_write() directly. Bus clock indication towards the codec and + * SDCA interrupt type configuration. */ #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_SDCA_INTRTYPE_1 0xf4 +#define WCD9378_SWRS_SCP_SDCA_INTRTYPE_2 0xf8 +#define WCD9378_SWRS_SCP_SDCA_INTRTYPE_3 0xfc #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 */ @@ -231,9 +235,9 @@ int wcd9378_sdw_hw_params(struct wcd9378_sdw_priv *wcd, 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) + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) { return -EOPNOTSUPP; }