* [PATCH 0/3] ASoC: codecs: add the Qualcomm WCD9378 audio codec
@ 2026-07-06 19:22 Jorijn van der Graaf
2026-07-06 19:22 ` [PATCH 1/3] ASoC: dt-bindings: qcom,wcd93xx-common: don't require micbias4 Jorijn van der Graaf
` (2 more replies)
0 siblings, 3 replies; 12+ messages in thread
From: Jorijn van der Graaf @ 2026-07-06 19:22 UTC (permalink / raw)
To: Mark Brown
Cc: Jorijn van der Graaf, Srinivas Kandagatla, Liam Girdwood,
Rob Herring, Krzysztof Kozlowski, Luca Weiss, Conor Dooley,
Jaroslav Kysela, Takashi Iwai, Mohammad Rafi Shaik, linux-sound,
devicetree, linux-arm-msm, linux-kernel
This series adds bindings and a codec driver for the Qualcomm WCD9378,
found on SM7635 boards such as the Fairphone 6. The chip pairs a
WCD937x-compatible analog core with SDCA-style function blocks whose
built-in sequencers handle the analog power-up/down; all control
registers, analog core included, sit in a 32-bit paged SDCA control
address space behind the TX SoundWire slave.
The driver implements the TX/capture paths (three ADCs fed from four
analog microphone inputs, three mic bias supplies) and is modeled on
wcd937x, reusing the wcd-common helpers. RX/playback, MBHC and the
SmartAMP function are follow-up work. Register sequences come from the
GPL downstream wcd9378 driver in Qualcomm's audio-kernel-ar tree; the
chip has no public datasheet.
One dependency outside this series: the paged register map needs the
Qualcomm SoundWire controller to implement SCP address paging, posted
separately to the soundwire tree as
"soundwire: qcom: add SCP address paging support". Without it the
codec enumerates but every paged register access silently reads zero.
There is no compile-time dependency, so the two can go through their
respective trees in any order; capture just needs both to land.
Patch 1 moves the qcom,micbias4-microvolt requirement out of
qcom,wcd93xx-common.yaml into the codecs that have four mic bias
supplies, so the three-bias WCD9378 binding in patch 2 can reference
the common schema. Patch 3 is the driver itself.
Tested on the Fairphone 6 (SM7635): handset mic capture through the
LPASS TX macro at 48 kHz across reboots, including UCM-driven use by
PipeWire in a Plasma Mobile session. The board DTS follows once these
bindings are accepted.
Jorijn van der Graaf (3):
ASoC: dt-bindings: qcom,wcd93xx-common: don't require micbias4
ASoC: dt-bindings: add the Qualcomm WCD9378 audio codec
ASoC: codecs: wcd9378: add TX/capture codec driver
.../bindings/sound/qcom,wcd9378-codec.yaml | 76 +
.../bindings/sound/qcom,wcd9378-sdw.yaml | 122 ++
.../bindings/sound/qcom,wcd937x.yaml | 1 +
.../bindings/sound/qcom,wcd938x.yaml | 1 +
.../bindings/sound/qcom,wcd939x.yaml | 1 +
.../bindings/sound/qcom,wcd93xx-common.yaml | 1 -
sound/soc/codecs/Kconfig | 19 +
sound/soc/codecs/Makefile | 7 +
sound/soc/codecs/wcd9378-sdw.c | 495 ++++++
sound/soc/codecs/wcd9378.c | 1382 +++++++++++++++++
sound/soc/codecs/wcd9378.h | 246 +++
11 files changed, 2350 insertions(+), 1 deletion(-)
create mode 100644 Documentation/devicetree/bindings/sound/qcom,wcd9378-codec.yaml
create mode 100644 Documentation/devicetree/bindings/sound/qcom,wcd9378-sdw.yaml
create mode 100644 sound/soc/codecs/wcd9378-sdw.c
create mode 100644 sound/soc/codecs/wcd9378.c
create mode 100644 sound/soc/codecs/wcd9378.h
--
2.55.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 1/3] ASoC: dt-bindings: qcom,wcd93xx-common: don't require micbias4 2026-07-06 19:22 [PATCH 0/3] ASoC: codecs: add the Qualcomm WCD9378 audio codec Jorijn van der Graaf @ 2026-07-06 19:22 ` Jorijn van der Graaf 2026-07-06 19:22 ` [PATCH 2/3] ASoC: dt-bindings: add the Qualcomm WCD9378 audio codec Jorijn van der Graaf 2026-07-06 19:22 ` [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver Jorijn van der Graaf 2 siblings, 0 replies; 12+ messages in thread From: Jorijn van der Graaf @ 2026-07-06 19:22 UTC (permalink / raw) To: Mark Brown Cc: Jorijn van der Graaf, Srinivas Kandagatla, Liam Girdwood, Rob Herring, Krzysztof Kozlowski, Luca Weiss, Conor Dooley, Jaroslav Kysela, Takashi Iwai, Mohammad Rafi Shaik, linux-sound, devicetree, linux-arm-msm, linux-kernel The WCD9370/9375, WCD9380/9385 and WCD9390/9395 codecs all have four mic bias supplies, but the upcoming WCD9378 only has three, so it cannot reference the common schema while qcom,micbias4-microvolt is required there. Move that requirement out of the common schema into each of the current users; no binding is relaxed by this. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net> --- Documentation/devicetree/bindings/sound/qcom,wcd937x.yaml | 1 + Documentation/devicetree/bindings/sound/qcom,wcd938x.yaml | 1 + Documentation/devicetree/bindings/sound/qcom,wcd939x.yaml | 1 + Documentation/devicetree/bindings/sound/qcom,wcd93xx-common.yaml | 1 - 4 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Documentation/devicetree/bindings/sound/qcom,wcd937x.yaml b/Documentation/devicetree/bindings/sound/qcom,wcd937x.yaml index f94203798f24..c6dbfe341370 100644 --- a/Documentation/devicetree/bindings/sound/qcom,wcd937x.yaml +++ b/Documentation/devicetree/bindings/sound/qcom,wcd937x.yaml @@ -31,6 +31,7 @@ properties: required: - compatible - vdd-px-supply + - qcom,micbias4-microvolt unevaluatedProperties: false diff --git a/Documentation/devicetree/bindings/sound/qcom,wcd938x.yaml b/Documentation/devicetree/bindings/sound/qcom,wcd938x.yaml index ab1c6285dbf8..f74beb4169a5 100644 --- a/Documentation/devicetree/bindings/sound/qcom,wcd938x.yaml +++ b/Documentation/devicetree/bindings/sound/qcom,wcd938x.yaml @@ -35,6 +35,7 @@ properties: required: - compatible + - qcom,micbias4-microvolt unevaluatedProperties: false diff --git a/Documentation/devicetree/bindings/sound/qcom,wcd939x.yaml b/Documentation/devicetree/bindings/sound/qcom,wcd939x.yaml index 85283f94465d..a70d70a05355 100644 --- a/Documentation/devicetree/bindings/sound/qcom,wcd939x.yaml +++ b/Documentation/devicetree/bindings/sound/qcom,wcd939x.yaml @@ -50,6 +50,7 @@ properties: required: - compatible + - qcom,micbias4-microvolt unevaluatedProperties: false diff --git a/Documentation/devicetree/bindings/sound/qcom,wcd93xx-common.yaml b/Documentation/devicetree/bindings/sound/qcom,wcd93xx-common.yaml index f78ba148ad25..61e437378cfe 100644 --- a/Documentation/devicetree/bindings/sound/qcom,wcd93xx-common.yaml +++ b/Documentation/devicetree/bindings/sound/qcom,wcd93xx-common.yaml @@ -89,7 +89,6 @@ required: - qcom,micbias1-microvolt - qcom,micbias2-microvolt - qcom,micbias3-microvolt - - qcom,micbias4-microvolt - "#sound-dai-cells" additionalProperties: true -- 2.55.0 ^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH 2/3] ASoC: dt-bindings: add the Qualcomm WCD9378 audio codec 2026-07-06 19:22 [PATCH 0/3] ASoC: codecs: add the Qualcomm WCD9378 audio codec Jorijn van der Graaf 2026-07-06 19:22 ` [PATCH 1/3] ASoC: dt-bindings: qcom,wcd93xx-common: don't require micbias4 Jorijn van der Graaf @ 2026-07-06 19:22 ` Jorijn van der Graaf 2026-07-06 21:17 ` Srinivas Kandagatla 2026-07-06 19:22 ` [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver Jorijn van der Graaf 2 siblings, 1 reply; 12+ messages in thread From: Jorijn van der Graaf @ 2026-07-06 19:22 UTC (permalink / raw) To: Mark Brown Cc: Jorijn van der Graaf, Srinivas Kandagatla, Liam Girdwood, Rob Herring, Krzysztof Kozlowski, Luca Weiss, Conor Dooley, Jaroslav Kysela, Takashi Iwai, Mohammad Rafi Shaik, linux-sound, devicetree, linux-arm-msm, linux-kernel The Qualcomm WCD9378 is a standalone audio codec IC found on SM7635 boards such as the Fairphone 6. Like the WCD937x/938x/939x codecs it presents RX and TX SoundWire slave devices controlled by a common parent node, so the parent binding references qcom,wcd93xx-common.yaml; unlike those codecs it has three ADCs and three mic bias supplies rather than four, hence qcom,micbias4-microvolt is rejected. The slave devices enumerate with manufacturer ID 0x0217 and part ID 0x0110, hence the sdw20217011000 compatible. The TX slave carries five device ports (ADC1, ADC2, ADC3, DMIC0/1 plus MBHC, DMIC2-5) and the RX slave the usual five (HPH, CLSH, COMP, LO, DSD). The -codec suffix in the parent compatible follows the existing qcom,wcd93xx/pm4125 family compatibles and matches the compatible shipped by production devicetrees for this chip. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net> --- .../bindings/sound/qcom,wcd9378-codec.yaml | 76 +++++++++++ .../bindings/sound/qcom,wcd9378-sdw.yaml | 122 ++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 Documentation/devicetree/bindings/sound/qcom,wcd9378-codec.yaml create mode 100644 Documentation/devicetree/bindings/sound/qcom,wcd9378-sdw.yaml diff --git a/Documentation/devicetree/bindings/sound/qcom,wcd9378-codec.yaml b/Documentation/devicetree/bindings/sound/qcom,wcd9378-codec.yaml new file mode 100644 index 000000000000..32554541b279 --- /dev/null +++ b/Documentation/devicetree/bindings/sound/qcom,wcd9378-codec.yaml @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/sound/qcom,wcd9378-codec.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Qualcomm WCD9378 Audio Codec + +maintainers: + - Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net> + +description: + The Qualcomm WCD9378 is a standalone Hi-Fi audio codec IC with three + ADCs, three mic bias supplies, headphone/earpiece/line outputs and + MBHC. It has RX and TX SoundWire slave devices; the control registers + live in the SDCA control address space accessed through the TX slave. + +allOf: + - $ref: dai-common.yaml# + - $ref: qcom,wcd93xx-common.yaml# + +properties: + compatible: + const: qcom,wcd9378-codec + + qcom,micbias4-microvolt: false + +required: + - compatible + +unevaluatedProperties: false + +examples: + - | + #include <dt-bindings/gpio/gpio.h> + audio-codec { + compatible = "qcom,wcd9378-codec"; + 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>; + }; + + /* ... */ + + soundwire@3210000 { + reg = <0x03210000 0x2000>; + #address-cells = <2>; + #size-cells = <0>; + + wcd9378_rx: audio-codec@0,4 { + compatible = "sdw20217011000"; + reg = <0 4>; + qcom,rx-port-mapping = <1 2 3 4 5>; + }; + }; + + soundwire@33b0000 { + reg = <0x033b0000 0x2000>; + #address-cells = <2>; + #size-cells = <0>; + + wcd9378_tx: audio-codec@0,3 { + compatible = "sdw20217011000"; + reg = <0 3>; + qcom,tx-port-mapping = <1 1 1 2 3>; + }; + }; +... diff --git a/Documentation/devicetree/bindings/sound/qcom,wcd9378-sdw.yaml b/Documentation/devicetree/bindings/sound/qcom,wcd9378-sdw.yaml new file mode 100644 index 000000000000..a46938338f90 --- /dev/null +++ b/Documentation/devicetree/bindings/sound/qcom,wcd9378-sdw.yaml @@ -0,0 +1,122 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/sound/qcom,wcd9378-sdw.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Qualcomm SoundWire Slave devices on WCD9378 + +maintainers: + - Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net> + +description: + The Qualcomm WCD9378 codec has RX and TX SoundWire slave devices. + This binding is for the slave devices. + +properties: + compatible: + const: sdw20217011000 + + reg: + maxItems: 1 + + qcom,tx-port-mapping: + description: | + Specifies static port mapping between device and host tx ports. + In the order of the device port index, which is adc1_port, + adc2_port, adc3_port, dmic01_mbhc_port, dmic25_port. + + WCD9378 TX Port 1 (ADC1) <=> SWR master Port 1 + WCD9378 TX Port 2 (ADC2) <=> SWR master Port 1 + WCD9378 TX Port 3 (ADC3) <=> SWR master Port 1 + WCD9378 TX Port 4 (DMIC0,1 & MBHC) <=> SWR master Port 2 + WCD9378 TX Port 5 (DMIC2,3,4,5) <=> SWR master Port 3 + + $ref: /schemas/types.yaml#/definitions/uint32-array + minItems: 5 + maxItems: 5 + items: + enum: [1, 2, 3, 4] + + qcom,rx-port-mapping: + description: | + Specifies static port mapping between device and host rx ports. + In the order of the device port index, which is hph_port, + clsh_port, comp_port, lo_port, dsd_port. + + WCD9378 RX Port 1 (HPH_L/R) <=> SWR master Port 1 + WCD9378 RX Port 2 (CLSH) <=> SWR master Port 2 + WCD9378 RX Port 3 (COMP_L/R) <=> SWR master Port 3 + WCD9378 RX Port 4 (LO) <=> SWR master Port 4 + WCD9378 RX Port 5 (DSD_L/R) <=> SWR master Port 5 + + $ref: /schemas/types.yaml#/definitions/uint32-array + minItems: 5 + maxItems: 5 + items: + enum: [1, 2, 3, 4, 5] + + qcom,tx-channel-mapping: + description: | + Specifies static channel mapping between slave and master tx port + channels. + In the order of slave port channels, which is adc1, adc2, adc3, + dmic0, dmic1, mbhc, dmic2, dmic3, dmic4, dmic5. + $ref: /schemas/types.yaml#/definitions/uint8-array + minItems: 10 + maxItems: 10 + items: + enum: + - 1 # WCD9378_SWRM_CH1 + - 2 # WCD9378_SWRM_CH2 + - 3 # WCD9378_SWRM_CH3 + - 4 # WCD9378_SWRM_CH4 + + qcom,rx-channel-mapping: + description: | + Specifies static channel mapping between slave and master rx port + channels. + In the order of slave port channels, which is hph_l, hph_r, clsh, + comp_l, comp_r, lo, dsd_l, dsd_r. + $ref: /schemas/types.yaml#/definitions/uint8-array + minItems: 8 + maxItems: 8 + items: + enum: + - 1 # WCD9378_SWRM_CH1 + - 2 # WCD9378_SWRM_CH2 + - 3 # WCD9378_SWRM_CH3 + - 4 # WCD9378_SWRM_CH4 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + soundwire@3210000 { + reg = <0x03210000 0x2000>; + #address-cells = <2>; + #size-cells = <0>; + + wcd9378_rx: audio-codec@0,4 { + compatible = "sdw20217011000"; + reg = <0 4>; + qcom,rx-port-mapping = <1 2 3 4 5>; + }; + }; + + soundwire@33b0000 { + reg = <0x033b0000 0x2000>; + #address-cells = <2>; + #size-cells = <0>; + + wcd9378_tx: audio-codec@0,3 { + compatible = "sdw20217011000"; + reg = <0 3>; + qcom,tx-port-mapping = <1 1 1 2 3>; + }; + }; +... -- 2.55.0 ^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH 2/3] ASoC: dt-bindings: add the Qualcomm WCD9378 audio codec 2026-07-06 19:22 ` [PATCH 2/3] ASoC: dt-bindings: add the Qualcomm WCD9378 audio codec Jorijn van der Graaf @ 2026-07-06 21:17 ` Srinivas Kandagatla 2026-07-06 22:22 ` Jorijn van der Graaf 0 siblings, 1 reply; 12+ messages in thread From: Srinivas Kandagatla @ 2026-07-06 21:17 UTC (permalink / raw) To: Jorijn van der Graaf, Mark Brown Cc: Srinivas Kandagatla, Liam Girdwood, Rob Herring, Krzysztof Kozlowski, Luca Weiss, Conor Dooley, Jaroslav Kysela, Takashi Iwai, Mohammad Rafi Shaik, linux-sound, devicetree, linux-arm-msm, linux-kernel On 7/6/26 8:22 PM, Jorijn van der Graaf wrote: > The Qualcomm WCD9378 is a standalone audio codec IC found on SM7635 > boards such as the Fairphone 6. Like the WCD937x/938x/939x codecs it > presents RX and TX SoundWire slave devices controlled by a common > parent node, so the parent binding references qcom,wcd93xx-common.yaml; > unlike those codecs it has three ADCs and three mic bias supplies > rather than four, hence qcom,micbias4-microvolt is rejected. > > The slave devices enumerate with manufacturer ID 0x0217 and part ID > 0x0110, hence the sdw20217011000 compatible. The TX slave carries five > device ports (ADC1, ADC2, ADC3, DMIC0/1 plus MBHC, DMIC2-5) and the RX > slave the usual five (HPH, CLSH, COMP, LO, DSD). > > The -codec suffix in the parent compatible follows the existing > qcom,wcd93xx/pm4125 family compatibles and matches the compatible > shipped by production devicetrees for this chip. > > Assisted-by: Claude:claude-fable-5 > Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net> > --- > .../bindings/sound/qcom,wcd9378-codec.yaml | 76 +++++++++++ > .../bindings/sound/qcom,wcd9378-sdw.yaml | 122 ++++++++++++++++++ > 2 files changed, 198 insertions(+) > create mode 100644 Documentation/devicetree/bindings/sound/qcom,wcd9378-codec.yaml > create mode 100644 Documentation/devicetree/bindings/sound/qcom,wcd9378-sdw.yaml > > diff --git a/Documentation/devicetree/bindings/sound/qcom,wcd9378-codec.yaml b/Documentation/devicetree/bindings/sound/qcom,wcd9378-codec.yaml > new file mode 100644 > index 000000000000..32554541b279 > --- /dev/null > +++ b/Documentation/devicetree/bindings/sound/qcom,wcd9378-codec.yaml > @@ -0,0 +1,76 @@ > +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) > +%YAML 1.2 > +--- > +$id: http://devicetree.org/schemas/sound/qcom,wcd9378-codec.yaml# > +$schema: http://devicetree.org/meta-schemas/core.yaml# > + > +title: Qualcomm WCD9378 Audio Codec > + > +maintainers: > + - Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net> > + > +description: > + The Qualcomm WCD9378 is a standalone Hi-Fi audio codec IC with three > + ADCs, three mic bias supplies, headphone/earpiece/line outputs and > + MBHC. It has RX and TX SoundWire slave devices; the control registers > + live in the SDCA control address space accessed through the TX slave. > + > +allOf: > + - $ref: dai-common.yaml# > + - $ref: qcom,wcd93xx-common.yaml# > + > +properties: > + compatible: > + const: qcom,wcd9378-codec > + > + qcom,micbias4-microvolt: false > + > +required: > + - compatible > + > +unevaluatedProperties: false > + > +examples: > + - | > + #include <dt-bindings/gpio/gpio.h> > + audio-codec { > + compatible = "qcom,wcd9378-codec"; > + 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>; > + }; > + > + /* ... */ > + > + soundwire@3210000 { > + reg = <0x03210000 0x2000>; > + #address-cells = <2>; > + #size-cells = <0>; > + > + wcd9378_rx: audio-codec@0,4 { > + compatible = "sdw20217011000"; > + reg = <0 4>; > + qcom,rx-port-mapping = <1 2 3 4 5>; > + }; > + }; > + > + soundwire@33b0000 { > + reg = <0x033b0000 0x2000>; > + #address-cells = <2>; > + #size-cells = <0>; > + > + wcd9378_tx: audio-codec@0,3 { > + compatible = "sdw20217011000"; > + reg = <0 3>; > + qcom,tx-port-mapping = <1 1 1 2 3>; > + }; > + }; > +... > diff --git a/Documentation/devicetree/bindings/sound/qcom,wcd9378-sdw.yaml b/Documentation/devicetree/bindings/sound/qcom,wcd9378-sdw.yaml > new file mode 100644 > index 000000000000..a46938338f90 > --- /dev/null > +++ b/Documentation/devicetree/bindings/sound/qcom,wcd9378-sdw.yaml > @@ -0,0 +1,122 @@ > +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) > +%YAML 1.2 > +--- > +$id: http://devicetree.org/schemas/sound/qcom,wcd9378-sdw.yaml# > +$schema: http://devicetree.org/meta-schemas/core.yaml# > + > +title: Qualcomm SoundWire Slave devices on WCD9378 > + > +maintainers: > + - Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net> > + > +description: > + The Qualcomm WCD9378 codec has RX and TX SoundWire slave devices. > + This binding is for the slave devices. > + > +properties: > + compatible: > + const: sdw20217011000 Interestingly this same codec with same compatible is wiredup differently on compute devices like Glymur but only with one slave, rather than tx and rx. This is how the codec works in SDCA mode. So these binding would need some room to accommodate those changes. > + > + reg: > + maxItems: 1 > + > + qcom,tx-port-mapping: > + description: | > + Specifies static port mapping between device and host tx ports. > + In the order of the device port index, which is adc1_port, > + adc2_port, adc3_port, dmic01_mbhc_port, dmic25_port. > + > + WCD9378 TX Port 1 (ADC1) <=> SWR master Port 1 > + WCD9378 TX Port 2 (ADC2) <=> SWR master Port 1 > + WCD9378 TX Port 3 (ADC3) <=> SWR master Port 1 > + WCD9378 TX Port 4 (DMIC0,1 & MBHC) <=> SWR master Port 2 > + WCD9378 TX Port 5 (DMIC2,3,4,5) <=> SWR master Port 3 > + > + $ref: /schemas/types.yaml#/definitions/uint32-array > + minItems: 5 > + maxItems: 5 > + items: > + enum: [1, 2, 3, 4] > + > + qcom,rx-port-mapping: > + description: | > + Specifies static port mapping between device and host rx ports. > + In the order of the device port index, which is hph_port, > + clsh_port, comp_port, lo_port, dsd_port. > + > + WCD9378 RX Port 1 (HPH_L/R) <=> SWR master Port 1 > + WCD9378 RX Port 2 (CLSH) <=> SWR master Port 2 > + WCD9378 RX Port 3 (COMP_L/R) <=> SWR master Port 3 > + WCD9378 RX Port 4 (LO) <=> SWR master Port 4 > + WCD9378 RX Port 5 (DSD_L/R) <=> SWR master Port 5 > + > + $ref: /schemas/types.yaml#/definitions/uint32-array > + minItems: 5 > + maxItems: 5 > + items: > + enum: [1, 2, 3, 4, 5] > + > + qcom,tx-channel-mapping: Do you need these channel mapping(both tx and rx), are they different to what slave ch-map is? > + description: | > + Specifies static channel mapping between slave and master tx port > + channels. > + In the order of slave port channels, which is adc1, adc2, adc3, > + dmic0, dmic1, mbhc, dmic2, dmic3, dmic4, dmic5. > + $ref: /schemas/types.yaml#/definitions/uint8-array > + minItems: 10 > + maxItems: 10 > + items: > + enum: > + - 1 # WCD9378_SWRM_CH1 > + - 2 # WCD9378_SWRM_CH2 > + - 3 # WCD9378_SWRM_CH3 > + - 4 # WCD9378_SWRM_CH4 What is this enum imply? channel mask? or ch-index? > + > + qcom,rx-channel-mapping: > + description: | > + Specifies static channel mapping between slave and master rx port > + channels. > + In the order of slave port channels, which is hph_l, hph_r, clsh, > + comp_l, comp_r, lo, dsd_l, dsd_r. > + $ref: /schemas/types.yaml#/definitions/uint8-array > + minItems: 8 > + maxItems: 8 > + items: > + enum: > + - 1 # WCD9378_SWRM_CH1 > + - 2 # WCD9378_SWRM_CH2 > + - 3 # WCD9378_SWRM_CH3 > + - 4 # WCD9378_SWRM_CH4 > + > +required: > + - compatible > + - reg > + > +additionalProperties: false > + > +examples: > + - | > + soundwire@3210000 { > + reg = <0x03210000 0x2000>; > + #address-cells = <2>; > + #size-cells = <0>; > + > + wcd9378_rx: audio-codec@0,4 { > + compatible = "sdw20217011000"; > + reg = <0 4>; > + qcom,rx-port-mapping = <1 2 3 4 5>; > + }; > + }; > + > + soundwire@33b0000 { > + reg = <0x033b0000 0x2000>; > + #address-cells = <2>; > + #size-cells = <0>; > + > + wcd9378_tx: audio-codec@0,3 { > + compatible = "sdw20217011000"; > + reg = <0 3>; > + qcom,tx-port-mapping = <1 1 1 2 3>; > + }; > + }; > +... ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 2/3] ASoC: dt-bindings: add the Qualcomm WCD9378 audio codec 2026-07-06 21:17 ` Srinivas Kandagatla @ 2026-07-06 22:22 ` Jorijn van der Graaf 0 siblings, 0 replies; 12+ messages in thread From: Jorijn van der Graaf @ 2026-07-06 22:22 UTC (permalink / raw) To: Srinivas Kandagatla Cc: Jorijn van der Graaf, Mark Brown, Liam Girdwood, Rob Herring, Krzysztof Kozlowski, Conor Dooley, Luca Weiss, Mohammad Rafi Shaik, Jaroslav Kysela, Takashi Iwai, linux-sound, devicetree, linux-arm-msm, linux-kernel On Mon, 6 Jul 2026 22:17:56 +0100, Srinivas Kandagatla wrote: >> +properties: >> + compatible: >> + const: sdw20217011000 > > Interestingly this same codec with same compatible is wiredup > differently on compute devices like Glymur but only with one slave, > rather than tx and rx. This is how the codec works in SDCA mode. > > So these binding would need some room to accommodate those changes. Good to know, thanks - the Fairphone 6 uses the split RX/TX wiring throughout, so the single-slave SDCA presentation wasn't on my radar. For v2 I would reword both binding descriptions so the split wiring is one of two presentations of the chip rather than the only one, keeping the port-mapping properties optional, so a single-slave node with neither property still validates against this schema. The parent qcom,wcd9378-codec binding then documents the split wiring only (presumably SDCA mode has no parent node aggregating two slaves). Given the single-driver exploration you mentioned on patch 3/3, I'll keep v2 minimal on this point (reworded descriptions, mappings optional). If the Glymur wiring turns out to need more room reserved here, happy to shape it so the compute devicetrees don't need a binding rework later. >> + qcom,tx-channel-mapping: > > Do you need these channel mapping(both tx and rx), are they different to > what slave ch-map is? They only remap the master-side channel masks; the slave-side channel map is fixed in the driver. Both properties were inherited from qcom,wcd937x-sdw.yaml, and the Fairphone 6 doesn't use them (the defaults match the downstream tx_swr_ch_map). So not needed today - v2 will drop both properties and the driver parsing for them; the qcom,pm4125-sdw.yaml binding likewise documents no channel mapping (though that driver still carries the parsing). They can come back with a board that actually needs the remap. (Dropping the parsing also removes the static ch_info mutation the bot review flagged, superseding the devm-copy fix promised in that triage.) >> + items: >> + enum: >> + - 1 # WCD9378_SWRM_CH1 >> + - 2 # WCD9378_SWRM_CH2 >> + - 3 # WCD9378_SWRM_CH3 >> + - 4 # WCD9378_SWRM_CH4 > > What is this enum imply? channel mask? or ch-index? A 1-based master channel index; the driver converts it to a bit mask (BIT(index - 1)). Same semantics as wcd937x-sdw, and moot in v2 with the properties gone. Thanks, Jorijn ^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver 2026-07-06 19:22 [PATCH 0/3] ASoC: codecs: add the Qualcomm WCD9378 audio codec Jorijn van der Graaf 2026-07-06 19:22 ` [PATCH 1/3] ASoC: dt-bindings: qcom,wcd93xx-common: don't require micbias4 Jorijn van der Graaf 2026-07-06 19:22 ` [PATCH 2/3] ASoC: dt-bindings: add the Qualcomm WCD9378 audio codec Jorijn van der Graaf @ 2026-07-06 19:22 ` Jorijn van der Graaf 2026-07-06 19:38 ` sashiko-bot ` (2 more replies) 2 siblings, 3 replies; 12+ messages in thread From: Jorijn van der Graaf @ 2026-07-06 19:22 UTC (permalink / raw) To: Mark Brown Cc: Jorijn van der Graaf, Srinivas Kandagatla, Liam Girdwood, Rob Herring, Krzysztof Kozlowski, Luca Weiss, Conor Dooley, Jaroslav Kysela, Takashi Iwai, Mohammad Rafi Shaik, linux-sound, devicetree, linux-arm-msm, linux-kernel The Qualcomm WCD9378 is the audio codec found on SM7635 boards such as the Fairphone 6. It 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, requesting power state 0 on the function's power domain entity and letting the sequencer ramp the ADC and the mic bias selected through SMx_MB_SEL. Like the other codecs of the family the WCD9378 presents two SoundWire slaves (manufacturer 0x0217, part 0x0110); a platform device created from the codec DT node acts as component master over the two slave devices, reusing the wcd-common helpers. Unlike the older family members, all control registers - the SDCA function blocks and the analog core alike - live in a 32-bit paged SDCA control address space accessed through the TX slave, so the slave regmap needs prop.paging_support (on Qualcomm controllers this in turn needs SCP address paging support in the controller driver). Two hardware behaviours deserve a note: - The SDCA function engine does not survive SoundWire bus clock-stop. Every register keeps its value (so a regcache sync restores nothing), but the sequencers ignore all subsequent power requests, including forced trigger and soft-reset writes; only a codec reset revives the engine. The downstream driver sidesteps this by marking the TX SoundWire master "qcom,is-always-on"; do the equivalent and hold a runtime PM reference on the TX slave for as long as the codec is bound. The FUNC_ACT class-load activation is likewise done with plain writes rather than update_bits so the 0->1 activation edge always reaches the wire regardless of regcache state. Scoping the reference to active capture and reviving the engine with a codec reset on runtime resume was considered and rejected for now: the reset tears down every SDCA function (including future jack-detection state), forces re-enumeration of both slaves and adds the ~65 ms class-load ritual to each capture start. System sleep is untested and will need a reset-based recovery on resume, since a runtime PM reference does not prevent clock-stop across system suspend; that is a known limitation of this patch. - The TX analog gain field steps 1.5 dB per code (+30 dB over the 0..20 range), measured acoustically on a Fairphone 6 against a fixed reference tone. The downstream wcd9378 driver corroborates this: its TLV declares a 0..30 dB min/max over the same range. (The wcd937x-family drivers declare 0.25 dB steps for the equivalent field; whether that is correct for those chips has not been verified here.) Only the TX/capture paths (ADC1-3 fed from AMIC1-4, mic bias 1-3) are implemented; RX/playback, MBHC and the SmartAMP function can be added on top. The RX slave and a playback DAI are still registered: the codec is one component master over both slaves (the power topology and device links need the RX device bound), and registering both DAIs now keeps the DAI indices stable for machine drivers once playback lands. The ADC input muxes expose the full hardware routing; combinations not covered by any of the chip's 13 canned sys-usage profiles are rejected at sequencer power-up. Register sequences are derived from the GPL downstream wcd9378 driver in Qualcomm's audio-kernel-ar tree. Verified on the Fairphone 6: handset mic capture through the LPASS TX macro at 48 kHz, repeated across reboots, including UCM-driven use by PipeWire in a Plasma Mobile session. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net> --- sound/soc/codecs/Kconfig | 19 + sound/soc/codecs/Makefile | 7 + sound/soc/codecs/wcd9378-sdw.c | 495 ++++++++++++ sound/soc/codecs/wcd9378.c | 1382 ++++++++++++++++++++++++++++++++ sound/soc/codecs/wcd9378.h | 246 ++++++ 5 files changed, 2149 insertions(+) create mode 100644 sound/soc/codecs/wcd9378-sdw.c create mode 100644 sound/soc/codecs/wcd9378.c create mode 100644 sound/soc/codecs/wcd9378.h diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index a41d0f50825a..ac926a41305b 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -305,6 +305,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 @@ -2469,6 +2470,24 @@ 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" + select SND_SOC_WCD9378 + depends on SOUNDWIRE + select REGMAP_SOUNDWIRE + help + 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 depends on SND_SOC_WCD938X_SDW tristate diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index aa0396e5b575..ef987493054e 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -360,6 +360,8 @@ 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 snd-soc-wcd939x-y := wcd939x.o @@ -804,6 +806,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) += 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 new file mode 100644 index 000000000000..d9a99a6db500 --- /dev/null +++ b/sound/soc/codecs/wcd9378-sdw.c @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2026, Jorijn van der Graaf + * + * SoundWire slave driver for the Qualcomm WCD9378 audio codec. + * + * 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 <linux/component.h> +#include <linux/device.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/pm_runtime.h> +#include <linux/regmap.h> +#include <linux/soundwire/sdw.h> +#include <linux/soundwire/sdw_registers.h> +#include <linux/soundwire/sdw_type.h> +#include <sound/pcm_params.h> + +#include "wcd-common.h" +#include "wcd9378.h" + +#define WCD9378_SDW_CH(id, pn, cmask, mmask) \ + [id] = { \ + .port_num = pn, \ + .ch_mask = cmask, \ + .master_ch_mask = mmask, \ + } + +/* + * 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 sdw_port_config port_config[WCD9378_MAX_SWR_PORTS]; + unsigned long ch_mask; + int i, j; + + 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; + + 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_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_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): + 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, + const struct sdw_device_id *id) +{ + 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_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; + + memset(master_ch_mask, 0, WCD9378_MAX_SWR_CH_IDS); + + 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, + "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"); + 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, + "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, + .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 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..f4c4792d9d33 --- /dev/null +++ b/sound/soc/codecs/wcd9378.c @@ -0,0 +1,1382 @@ +// 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 <linux/component.h> +#include <linux/device.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/regmap.h> +#include <linux/regulator/consumer.h> +#include <linux/slab.h> +#include <linux/soundwire/sdw.h> +#include <linux/soundwire/sdw_registers.h> +#include <linux/soundwire/sdw_type.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/tlv.h> + +#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; + /* 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; +}; + +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, +}; + +/* + * 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 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); +} + +/* + * 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; + + /* + * 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_write(component, WCD9378_SMP_AMP_FUNC_STAT, 0xff); + + 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_write(component, WCD9378_SMP_JACK_FUNC_STAT, 0xff); + + for (i = 0; i < 3; i++) { + 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_write(component, + WCD9378_SMP_MIC_FUNC_STAT(i), 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)) { + 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; + } + + snd_soc_component_update_bits(component, WCD9378_SYS_USAGE_CTRL, + WCD9378_SYS_USAGE_CTRL_MASK, 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; + int adc = w->shift; + bool is_jack = false; + int sys_bit, retries; + u32 val; + + 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; + + /* + * 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; + usleep_range(500, 510); + } while (--retries); + if (val != WCD9378_PDE_PS0_ON) + dev_warn(component->dev, + "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) + 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); + wcd9378->tx_sys_bit[adc] = -1; + + 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) +{ + 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); + 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); + 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); + 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); + 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; + int ret; + + ret = sdw_slave_wait_for_init(wcd9378->tx_sdw_dev, 5000); + if (ret) + return ret; + + 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 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); + + 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; + } + + /* + * 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) { + 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; + } + + 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); + pm_runtime_put(wcd9378->txdev); + 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, i; + + wcd9378 = devm_kzalloc(dev, sizeof(*wcd9378), GFP_KERNEL); + if (!wcd9378) + return -ENOMEM; + + 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; + + 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 new file mode 100644 index 000000000000..fb9e7925968a --- /dev/null +++ b/sound/soc/codecs/wcd9378.h @@ -0,0 +1,246 @@ +/* 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; 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 <linux/regmap.h> +#include <linux/soundwire/sdw.h> +#include <linux/soundwire/sdw_type.h> +#include <sound/soc.h> + +/* 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_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 + +/* 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_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 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 */ +#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__ */ -- 2.55.0 ^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver 2026-07-06 19:22 ` [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver Jorijn van der Graaf @ 2026-07-06 19:38 ` sashiko-bot 2026-07-06 20:35 ` Jorijn van der Graaf 2026-07-06 21:41 ` Srinivas Kandagatla 2026-07-07 11:42 ` Pierre-Louis Bossart 2 siblings, 1 reply; 12+ messages in thread From: sashiko-bot @ 2026-07-06 19:38 UTC (permalink / raw) To: Jorijn van der Graaf; +Cc: robh, conor+dt, devicetree Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider: - [High] Modifying a static global array during probe prevents multiple device instances from working correctly. - [High] `pm_runtime_enable()` is called after `component_master_add_with_match()`, causing component probe to fail synchronously. - [High] Missing bounds check in `wcd9378_tx_mode_put()` allows out-of-bounds enum values to be written and retrieved. - [High] Inconsistent SoundWire bus clock scaling calculation between `wcd9378_bus_config()` and `wcd9378_swr_clk_indicate()`. - [High] Destroying `micb_lock` in the component unbind callback leads to a use-after-destroy upon component re-bind. - [Medium] Off-by-one error when calculating the SoundWire channel count causes incorrect bandwidth allocation. - [Medium] Missing High-Pass Filter (HPF) init pulse configuration for ADC2 when routed via SmartJACK. - [Medium] Concurrent ALSA control puts cause read-modify-write data races on shared driver channel maps. - [Medium] Using asynchronous `pm_runtime_put()` in `wcd9378_unbind()` causes a race condition where the TX SoundWire slave device may not be properly suspended before removal. -- commit 336c5ad4fab99f2ac788ce17fc541b077bfc53c9 Author: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net> Subject: ASoC: codecs: wcd9378: add TX/capture codec driver This patch adds the TX/capture driver for the Qualcomm WCD9378 audio codec over SoundWire. It introduces analog core configuration and SDCA-style function blocks for SmartMIC and SmartJACK sequencers. > diff --git a/sound/soc/codecs/wcd9378-sdw.c b/sound/soc/codecs/wcd9378-sdw.c > --- a/sound/soc/codecs/wcd9378-sdw.c > +++ b/sound/soc/codecs/wcd9378-sdw.c [ ... ] > +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 sdw_port_config port_config[WCD9378_MAX_SWR_PORTS]; > + unsigned long ch_mask; > + int i, j; > + > + wcd->sconfig.ch_count = 1; [Severity: Medium] Does initializing the channel count to 1 here cause an off-by-one error? It looks like if there are two active channels, the count will become 3, which might lead to overallocating SoundWire bus bandwidth. [ ... ] > +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; > + } [Severity: High] Is there a calculation mismatch between wcd9378_bus_config() and wcd9378_swr_clk_indicate()? Here, a 9.6MHz double-rate frequency gets WCD9378_SWRS_CLK_SCALE_DIV2. However, in wcd9378_swr_clk_indicate() in wcd9378.c, the bus clock is calculated by dividing the DR frequency by 2, resulting in 4.8MHz. That function then evaluates 4800000 < 9600000 and assigns WCD9378_SWRS_CLK_SCALE_DIV4 instead. Could this stall the sequencer if the configurations conflict? [ ... ] > +static int wcd9378_sdw_probe(struct sdw_slave *pdev, > + const struct sdw_device_id *id) > +{ [ ... ] > + 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]); [Severity: High] Will this loop permanently modify the global static array wcd9378_sdw_tx_ch_info? Since the master_ch_mask is written directly to the shared array, probing a second instance of this codec could overwrite the configuration for all active devices. > diff --git a/sound/soc/codecs/wcd9378.c b/sound/soc/codecs/wcd9378.c > --- a/sound/soc/codecs/wcd9378.c > +++ b/sound/soc/codecs/wcd9378.c [ ... ] > +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, > +}; [Severity: Medium] Should this struct also include the hpf_reg and hpf_mask fields? When ADC2 is fed from AMIC2 via SmartJACK, the missing filter configuration means the sequencer enable path will read zeroes, skipping the WCD9378_ANA_TX_CH2_HPF2_INIT pulse and potentially leaving a DC offset. [ ... ] > +static int wcd9378_connect_port(struct wcd9378_sdw_priv *wcd, u8 port_idx, > + u8 ch_id, bool enable) > +{ [ ... ] > + 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; > + } [Severity: Medium] Can concurrent ALSA control operations corrupt the channel masks? Since wcd9378_set_swr_port() is reachable via ALSA put callbacks, multiple userspace threads toggling switches might cause a read-modify-write data race on port_config->ch_mask and wcd->master_channel_map. [ ... ] > +static int wcd9378_tx_mode_put(struct snd_kcontrol *kcontrol, > + struct snd_ctl_elem_value *ucontrol) > +{ [ ... ] > + u32 mode_val = ucontrol->value.enumerated.item[0]; > + > + if (mode_val == wcd9378->tx_mode[adc]) > + return 0; > + > + wcd9378->tx_mode[adc] = mode_val; [Severity: High] Could this store an out-of-bounds enum value? Without a check verifying that mode_val is less than ARRAY_SIZE(tx_mode_mux_text), userspace can write an invalid integer here. This value is later exposed via wcd9378_tx_mode_get(), which might crash userspace audio servers when used as an array index. [ ... ] > +static int wcd9378_soc_codec_probe(struct snd_soc_component *component) > +{ [ ... ] > + ret = pm_runtime_resume_and_get(dev); > + if (ret < 0) > + return ret; [Severity: High] Will this pm_runtime_resume_and_get() call fail with -EACCES? It looks like component_master_add_with_match() in wcd9378_probe() triggers the component bind synchronously if all slaves are present, which in turn calls wcd9378_soc_codec_probe() here. Because pm_runtime_enable() hasn't been called yet in the master probe, the disable depth is still > 0, causing the resume to abort audio initialization. [ ... ] > +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); [Severity: Medium] Should this be pm_runtime_put_sync() instead? Using the asynchronous pm_runtime_put() queues a suspend operation that might be cancelled immediately when the driver core calls pm_runtime_disable() upon device removal, leaving the TX SoundWire slave device powered on. > + 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); [Severity: High] Does destroying micb_lock here prevent a successful re-bind? The component framework allows unbinding and rebinding without fully removing the master platform device. Since mutex_init() is only called in wcd9378_probe(), a subsequent wcd9378_bind() will leave the destroyed mutex uninitialized, leading to undefined behavior or a crash when wcd9378_micbias_control() tries to lock it. -- Sashiko AI review · https://sashiko.dev/#/patchset/20260706192229.144137-1-jorijnvdgraaf@catcrafts.net?part=3 ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver 2026-07-06 19:38 ` sashiko-bot @ 2026-07-06 20:35 ` Jorijn van der Graaf 0 siblings, 0 replies; 12+ messages in thread From: Jorijn van der Graaf @ 2026-07-06 20:35 UTC (permalink / raw) To: Mark Brown, sashiko-reviews Cc: Jorijn van der Graaf, Srinivas Kandagatla, Liam Girdwood, Rob Herring, Krzysztof Kozlowski, Conor Dooley, Luca Weiss, Mohammad Rafi Shaik, linux-sound, devicetree, linux-arm-msm, linux-kernel On Mon, 06 Jul 2026 19:38:26 +0000, sashiko-bot@kernel.org wrote: > Thank you for your contribution! Sashiko AI review found 9 potential > issue(s) to consider: Went through all nine findings; triage below. Two are real issues (the bus_config clock-scale inconsistency and the runtime-PM enable ordering) and three more are warts inherited from the wcd937x-family drivers; all five will be fixed in v2. Of the remaining four, the control-put race is real but family-wide, and three don't hold up against the hardware, the downstream reference or the family precedent. Details inline. > - [High] Modifying a static global array during probe prevents > multiple device instances from working correctly. Real, though inherited: wcd937x-sdw.c mutates its static ch_info array with the DT channel map the same way. It only breaks with two instances of the same codec on one board, which no current user has. v2 will use a devm-allocated copy. > - [High] `pm_runtime_enable()` is called after > `component_master_add_with_match()`, causing component probe to > fail synchronously. Not unconditional - it needs the bind (and the card bind that snd_soc_register_component() can trigger) to run before the pm_runtime_enable() a few lines later, e.g. synchronously inside component_master_add_with_match(). That doesn't happen on the tested board, and wcd937x/938x/939x have the identical ordering today. The race is real though; v2 will enable runtime PM before registering the component master. > - [High] Missing bounds check in `wcd9378_tx_mode_put()` allows > out-of-bounds enum values to be written and retrieved. In-kernel the stored value is only ever mapped through wcd9378_get_mode_val(), whose default arm absorbs out-of-range values, and the same unvalidated put exists in wcd938x. But the get side does re-expose whatever was stored, and the check is two lines; v2 will reject items >= e->items. > - [High] Inconsistent SoundWire bus clock scaling calculation > between `wcd9378_bus_config()` and `wcd9378_swr_clk_indicate()`. Good catch, but inverted. bus->params.curr_dr_freq carries the double-rate frequency: sdw_bus_master_add() sets it from max_dr_freq (2x clock), overriding the qcom controller's probe-time value, so wcd9378_swr_clk_indicate()'s /2 is the correct interpretation - on the Fairphone 6 that yields 19.2 MHz DR -> 9.6 MHz clock -> DIV2, matching the mode behaviour verified acoustically on hardware. It is wcd9378_bus_config()'s switch labels that wrongly treat curr_dr_freq as the clock (the core's own sdw_slave_get_scale_index() likewise halves curr_dr_freq to get the bus clock). The two agree at 19.2 MHz DR - the only value an upstream qcom bus presents today given the hardcoded 9.6 MHz max_clk_freq - and diverge on a 4.8 MHz-clock bus, so the bug is latent rather than user-visible. Still worth fixing; v2 will derive the scale in one shared helper. > - [High] Destroying `micb_lock` in the component unbind callback > leads to a use-after-destroy upon component re-bind. Real, inherited verbatim from wcd937x_unbind(). v2 will move the mutex_destroy() to driver remove. > - [Medium] Off-by-one error when calculating the SoundWire channel > count causes incorrect bandwidth allocation. No functional effect on the qcom controller: the padded count does feed the core's bandwidth accounting, but qcom_swrm_compute_params() never consumes it, and the loop matches wcd938x_sdw_hw_params() unchanged. If that family convention wants cleaning up it should be a separate series. > - [Medium] Missing High-Pass Filter (HPF) init pulse configuration > for ADC2 when routed via SmartJACK. Intentional and faithful to the hardware sequence: Qualcomm's downstream wcd9378 driver likewise powers up the SmartJACK PDE34 for AMIC2-routed ADC2 without an HPF init pulse - only the SmartMIC-routed branch pulses HPF2. > - [Medium] Concurrent ALSA control puts cause read-modify-write > data races on shared driver channel maps. Real in theory and shared with every wcd93xx driver (the same unlocked pattern exists since wcd938x); in practice the port switches are toggled by UCM at device open/close. Happy to add locking here and do the family-wide fix as a follow-up if the maintainers prefer. > - [Medium] Using asynchronous `pm_runtime_put()` in > `wcd9378_unbind()` causes a race condition where the TX SoundWire > slave device may not be properly suspended before removal. The put only drops the reference that keeps the bus out of clock-stop; whether the slave suspends immediately or via the queued idle request makes no functional difference during teardown, and the slave device with its driver outlives the codec component. A rebind re-takes the reference through pm_runtime_resume_and_get(). I'll hold v2 until the human reviewers have had a pass so everything lands in one respin; the fixes above will be retested on the Fairphone 6 before sending. Thanks, Jorijn ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver 2026-07-06 19:22 ` [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver Jorijn van der Graaf 2026-07-06 19:38 ` sashiko-bot @ 2026-07-06 21:41 ` Srinivas Kandagatla 2026-07-06 22:22 ` Jorijn van der Graaf 2026-07-07 11:42 ` Pierre-Louis Bossart 2 siblings, 1 reply; 12+ messages in thread From: Srinivas Kandagatla @ 2026-07-06 21:41 UTC (permalink / raw) To: Jorijn van der Graaf, Mark Brown Cc: Srinivas Kandagatla, Liam Girdwood, Rob Herring, Krzysztof Kozlowski, Luca Weiss, Conor Dooley, Jaroslav Kysela, Takashi Iwai, Mohammad Rafi Shaik, linux-sound, devicetree, linux-arm-msm, linux-kernel, Ravi Hothi On 7/6/26 8:22 PM, Jorijn van der Graaf wrote: > The Qualcomm WCD9378 is the audio codec found on SM7635 boards such as > the Fairphone 6. It 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, requesting power > state 0 on the function's power domain entity and letting the sequencer > ramp the ADC and the mic bias selected through SMx_MB_SEL. Thanks Jorijn for sending these patches over. > > Like the other codecs of the family the WCD9378 presents two SoundWire > slaves (manufacturer 0x0217, part 0x0110); a platform device created > from the codec DT node acts as component master over the two slave > devices, reusing the wcd-common helpers. Unlike the older family > members, all control registers - the SDCA function blocks and the > analog core alike - live in a 32-bit paged SDCA control address space > accessed through the TX slave, so the slave regmap needs > prop.paging_support (on Qualcomm controllers this in turn needs SCP > address paging support in the controller driver). > > Two hardware behaviours deserve a note: > > - The SDCA function engine does not survive SoundWire bus clock-stop. > Every register keeps its value (so a regcache sync restores > nothing), but the sequencers ignore all subsequent power requests, > including forced trigger and soft-reset writes; only a codec reset > revives the engine. The downstream driver sidesteps this by marking > the TX SoundWire master "qcom,is-always-on"; do the equivalent and > hold a runtime PM reference on the TX slave for as long as the > codec is bound. The FUNC_ACT class-load activation is likewise done > with plain writes rather than update_bits so the 0->1 activation > edge always reaches the wire regardless of regcache state. > > Scoping the reference to active capture and reviving the engine > with a codec reset on runtime resume was considered and rejected > for now: the reset tears down every SDCA function (including > future jack-detection state), forces re-enumeration of both slaves > and adds the ~65 ms class-load ritual to each capture start. > System sleep is untested and will need a reset-based recovery on > resume, since a runtime PM reference does not prevent clock-stop > across system suspend; that is a known limitation of this patch. > > - The TX analog gain field steps 1.5 dB per code (+30 dB over the > 0..20 range), measured acoustically on a Fairphone 6 against a > fixed reference tone. The downstream wcd9378 driver corroborates > this: its TLV declares a 0..30 dB min/max over the same range. > (The wcd937x-family drivers declare 0.25 dB steps for the > equivalent field; whether that is correct for those chips has not > been verified here.) > > Only the TX/capture paths (ADC1-3 fed from AMIC1-4, mic bias 1-3) are > implemented; RX/playback, MBHC and the SmartAMP function can be added > on top. The RX slave and a playback DAI are still registered: the > codec is one component master over both slaves (the power topology > and device links need the RX device bound), and registering both DAIs > now keeps the DAI indices stable for machine drivers once playback > lands. The ADC input muxes expose the full hardware routing; > combinations not covered by any of the chip's 13 canned sys-usage > profiles are rejected at sequencer power-up. Was there a reason why RX not added? I have been working on adding an SDCA compliant driver for this codec for Glymur compute laptops just few weeks back, not fully ready yet. @Ravi has been working on the same codec for a non compute device (Eliza) and has made some good progress. As this is an SDCA compliant codec, there might be way to get both modes working with single driver (either with or without generic drivers), which is something that I will be exploring in parallel. Overall Looks like we could bring everything together to add better support for this codec. In the meantime Myself and Ravi will continue to test this codec along with other changes and get back Thanks for all the work and sharing it on the list. --srini > > Register sequences are derived from the GPL downstream wcd9378 driver > in Qualcomm's audio-kernel-ar tree. > > Verified on the Fairphone 6: handset mic capture through the LPASS TX > macro at 48 kHz, repeated across reboots, including UCM-driven use by > PipeWire in a Plasma Mobile session. > > Assisted-by: Claude:claude-fable-5 > Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net> > --- > sound/soc/codecs/Kconfig | 19 + > sound/soc/codecs/Makefile | 7 + > sound/soc/codecs/wcd9378-sdw.c | 495 ++++++++++++ > sound/soc/codecs/wcd9378.c | 1382 ++++++++++++++++++++++++++++++++ > sound/soc/codecs/wcd9378.h | 246 ++++++ > 5 files changed, 2149 insertions(+) > create mode 100644 sound/soc/codecs/wcd9378-sdw.c > create mode 100644 sound/soc/codecs/wcd9378.c > create mode 100644 sound/soc/codecs/wcd9378.h > > diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig > index a41d0f50825a..ac926a41305b 100644 > --- a/sound/soc/codecs/Kconfig > +++ b/sound/soc/codecs/Kconfig > @@ -305,6 +305,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 > @@ -2469,6 +2470,24 @@ 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" > + select SND_SOC_WCD9378 > + depends on SOUNDWIRE > + select REGMAP_SOUNDWIRE > + help > + 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 > depends on SND_SOC_WCD938X_SDW > tristate > diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile > index aa0396e5b575..ef987493054e 100644 > --- a/sound/soc/codecs/Makefile > +++ b/sound/soc/codecs/Makefile > @@ -360,6 +360,8 @@ 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 > snd-soc-wcd939x-y := wcd939x.o > @@ -804,6 +806,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) += 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 > new file mode 100644 > index 000000000000..d9a99a6db500 > --- /dev/null > +++ b/sound/soc/codecs/wcd9378-sdw.c > @@ -0,0 +1,495 @@ > +// SPDX-License-Identifier: GPL-2.0-only > +/* > + * Copyright (c) 2026, Jorijn van der Graaf > + * > + * SoundWire slave driver for the Qualcomm WCD9378 audio codec. > + * > + * 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 <linux/component.h> > +#include <linux/device.h> > +#include <linux/module.h> > +#include <linux/of.h> > +#include <linux/pm_runtime.h> > +#include <linux/regmap.h> > +#include <linux/soundwire/sdw.h> > +#include <linux/soundwire/sdw_registers.h> > +#include <linux/soundwire/sdw_type.h> > +#include <sound/pcm_params.h> > + > +#include "wcd-common.h" > +#include "wcd9378.h" > + > +#define WCD9378_SDW_CH(id, pn, cmask, mmask) \ > + [id] = { \ > + .port_num = pn, \ > + .ch_mask = cmask, \ > + .master_ch_mask = mmask, \ > + } > + > +/* > + * 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 sdw_port_config port_config[WCD9378_MAX_SWR_PORTS]; > + unsigned long ch_mask; > + int i, j; > + > + 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; > + > + 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_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_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): > + 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, > + const struct sdw_device_id *id) > +{ > + 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_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; > + > + memset(master_ch_mask, 0, WCD9378_MAX_SWR_CH_IDS); > + > + 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, > + "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"); > + 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, > + "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, > + .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 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..f4c4792d9d33 > --- /dev/null > +++ b/sound/soc/codecs/wcd9378.c > @@ -0,0 +1,1382 @@ > +// 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 <linux/component.h> > +#include <linux/device.h> > +#include <linux/gpio/consumer.h> > +#include <linux/module.h> > +#include <linux/of.h> > +#include <linux/platform_device.h> > +#include <linux/pm_runtime.h> > +#include <linux/regmap.h> > +#include <linux/regulator/consumer.h> > +#include <linux/slab.h> > +#include <linux/soundwire/sdw.h> > +#include <linux/soundwire/sdw_registers.h> > +#include <linux/soundwire/sdw_type.h> > +#include <sound/soc.h> > +#include <sound/soc-dapm.h> > +#include <sound/tlv.h> > + > +#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; > + /* 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; > +}; > + > +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, > +}; > + > +/* > + * 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 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); > +} > + > +/* > + * 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; > + > + /* > + * 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_write(component, WCD9378_SMP_AMP_FUNC_STAT, 0xff); > + > + 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_write(component, WCD9378_SMP_JACK_FUNC_STAT, 0xff); > + > + for (i = 0; i < 3; i++) { > + 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_write(component, > + WCD9378_SMP_MIC_FUNC_STAT(i), 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)) { > + 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; > + } > + > + snd_soc_component_update_bits(component, WCD9378_SYS_USAGE_CTRL, > + WCD9378_SYS_USAGE_CTRL_MASK, 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; > + int adc = w->shift; > + bool is_jack = false; > + int sys_bit, retries; > + u32 val; > + > + 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; > + > + /* > + * 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; > + usleep_range(500, 510); > + } while (--retries); > + if (val != WCD9378_PDE_PS0_ON) > + dev_warn(component->dev, > + "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) > + 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); > + wcd9378->tx_sys_bit[adc] = -1; > + > + 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) > +{ > + 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); > + 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); > + 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); > + 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); > + 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; > + int ret; > + > + ret = sdw_slave_wait_for_init(wcd9378->tx_sdw_dev, 5000); > + if (ret) > + return ret; > + > + 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 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); > + > + 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; > + } > + > + /* > + * 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) { > + 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; > + } > + > + 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); > + pm_runtime_put(wcd9378->txdev); > + 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, i; > + > + wcd9378 = devm_kzalloc(dev, sizeof(*wcd9378), GFP_KERNEL); > + if (!wcd9378) > + return -ENOMEM; > + > + 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; > + > + 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 > new file mode 100644 > index 000000000000..fb9e7925968a > --- /dev/null > +++ b/sound/soc/codecs/wcd9378.h > @@ -0,0 +1,246 @@ > +/* 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; 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 <linux/regmap.h> > +#include <linux/soundwire/sdw.h> > +#include <linux/soundwire/sdw_type.h> > +#include <sound/soc.h> > + > +/* 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_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 > + > +/* 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_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 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 */ > +#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__ */ ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver 2026-07-06 21:41 ` Srinivas Kandagatla @ 2026-07-06 22:22 ` Jorijn van der Graaf 0 siblings, 0 replies; 12+ messages in thread From: Jorijn van der Graaf @ 2026-07-06 22:22 UTC (permalink / raw) To: Srinivas Kandagatla Cc: Jorijn van der Graaf, Mark Brown, Ravi Hothi, Liam Girdwood, Rob Herring, Krzysztof Kozlowski, Conor Dooley, Luca Weiss, Mohammad Rafi Shaik, Jaroslav Kysela, Takashi Iwai, linux-sound, devicetree, linux-arm-msm, linux-kernel On Mon, 6 Jul 2026 22:41:48 +0100, Srinivas Kandagatla wrote: > Thanks Jorijn for sending these patches over. > > Was there a reason why RX not added? Only that the Fairphone 6 gives me no way to test it: the phone has no headset jack (hence MBHC being left out too), and its earpiece is one of the two Awinic speaker amps in receiver mode rather than the codec's EAR output - so none of the codec's analog outputs appears to drive anything on this board. TX/capture was the feature the phone was missing, and I did not want to send RX code I cannot exercise on hardware. > I have been working on adding an SDCA compliant driver for this codec > for Glymur compute laptops just few weeks back, not fully ready yet. > > @Ravi has been working on the same codec for a non compute device > (Eliza) and has made some good progress. > > As this is an SDCA compliant codec, there might be way to get both modes > working with single driver (either with or without generic drivers), > which is something that I will be exploring in parallel. That is very good news. Two findings from the FP6 bring-up that will matter for the SDCA-mode work as well: - The SDCA function engine does not survive bus clock-stop (details in the commit message): registers keep their values, but the sequencers ignore every subsequent power request and only a codec reset revives them. The downstream stack works around it with "qcom,is-always-on" on the TX master. Whatever driver shape this ends up with will need a story for that. - Register accesses above the 15-bit direct space need SCP address paging in the qcom controller, which mainline does not implement; I posted "soundwire: qcom: add SCP address paging support" (20260706192150.143921-1-jorijnvdgraaf@catcrafts.net) alongside this series. The Glymur and Eliza drivers will need that too: all of the chip's codec control registers - SDCA functions and analog core alike - sit above the 15-bit direct space. > Overall Looks like we could bring everything together to add better > support for this codec. > > In the meantime Myself and Ravi will continue to test this codec along > with other changes and get back Sounds like a plan. I'll hold v2 for now so it can take a shape that fits the single-driver direction; if the testing takes a while I'll respin with the review fixes collected so far and we can converge from there. The Fairphone 6 is a readily available retail device, so consider it a standing test vehicle for the mobile split-slave mode - happy to test patches on it. Thanks, Jorijn ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver 2026-07-06 19:22 ` [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver Jorijn van der Graaf 2026-07-06 19:38 ` sashiko-bot 2026-07-06 21:41 ` Srinivas Kandagatla @ 2026-07-07 11:42 ` Pierre-Louis Bossart 2026-07-07 15:13 ` Jorijn van der Graaf 2 siblings, 1 reply; 12+ messages in thread From: Pierre-Louis Bossart @ 2026-07-07 11:42 UTC (permalink / raw) To: Jorijn van der Graaf, Mark Brown Cc: Srinivas Kandagatla, Liam Girdwood, Rob Herring, Krzysztof Kozlowski, Luca Weiss, Conor Dooley, Jaroslav Kysela, Takashi Iwai, Mohammad Rafi Shaik, linux-sound, devicetree, linux-arm-msm, linux-kernel > +/* > + * 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; > +} Changes to SCP registers are generally done by the SoundWire core. IIRC there is already support for setting the bus scaling registers. Not sure why this function is needed? > +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; > + 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_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; > + > + memset(master_ch_mask, 0, WCD9378_MAX_SWR_CH_IDS); > + > + 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, > + "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"); > + 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, > + "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); is this set_suspended() necessary? pm_runtime isn't even enabled in this probe. > + > + return 0; > +} > + > +static void wcd9378_sdw_remove(struct sdw_slave *pdev) > +{ > + struct device *dev = &pdev->dev; > + > + component_del(dev, &wcd_sdw_component_ops); pm_runtime_disable() ? > +} > + > +static const struct sdw_device_id wcd9378_sdw_id[] = { > + SDW_SLAVE_ENTRY(0x0217, 0x0110, 0), if this was an SDCA device shouldn't you rely on the _EXT macro? > + { }, > +}; > +MODULE_DEVICE_TABLE(sdw, wcd9378_sdw_id); > diff --git a/sound/soc/codecs/wcd9378.c b/sound/soc/codecs/wcd9378.c > new file mode 100644 > index 000000000000..f4c4792d9d33 > --- /dev/null > +++ b/sound/soc/codecs/wcd9378.c > @@ -0,0 +1,1382 @@ > +// 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 SDCA-style or SDCA-compliant? > + * 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. > + */ > +/* sys-usage capability bits (SYS_USAGE_CTRL profile contents) */ what does sys-usage mean? it was never clear in the SDCA spec, not sure what this refers to here. > +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 */ I guess this maps to the enum values above? > +static const unsigned int wcd9378_sys_usage_profiles[] = { > + 0x0c95, 0x12a7, 0x0c99, 0x1aab, 0x0894, 0x11a6, 0x0898, > + 0x11ab, 0x126a, 0x116b, 0x1ca7, 0x1195, 0x1296, > +}; > +/* > + * 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; > + > + /* > + * 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_write(component, WCD9378_SMP_AMP_FUNC_STAT, 0xff); if this is really SDCA, this would be very odd to set all bits in the function status register. The function status register includers fields such as needs reset, function busy, etc. > + > + 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_write(component, WCD9378_SMP_JACK_FUNC_STAT, 0xff); > + > + for (i = 0; i < 3; i++) { > + 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_write(component, > + WCD9378_SMP_MIC_FUNC_STAT(i), 0xff); > + } quite a lot of voodoo magic in this function, eh? > +} > +/* > + * 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); is this SCP_COMMIT needed? This is only used for dual-ranked registers, and there's currently no mechanism to handle them. The bus scale registers are banked, this is a different concept. And to repeat my question above, is this really the codec driver job to muck with SCP registers? > + } 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; > + int adc = w->shift; > + bool is_jack = false; > + int sys_bit, retries; > + u32 val; > + > + 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; > + > + /* > + * 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); if this is remotely inspired by SDCA, the normal sequence is to set the REQUESTED_POWER then poll the ACTUAL_POWER register. > + 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; > + usleep_range(500, 510); > + } while (--retries); > + if (val != WCD9378_PDE_PS0_ON) > + dev_warn(component->dev, > + "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) > + 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); > + wcd9378->tx_sys_bit[adc] = -1; > + > + if (!(wcd9378->sys_usage_mask & WCD9378_SYS_USAGE_TX_MASK)) > + wcd9378_swr_clk_indicate(wcd9378, false); > + break; > + } > + > + 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; > +} it's quite unclear to me why ports need to be exposed to user space? This is really low-level stuff that isn't typically handled by user-space. > +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; > + int ret; > + > + ret = sdw_slave_wait_for_init(wcd9378->tx_sdw_dev, 5000); > + if (ret) > + return ret; That's rather unusual, the 'standard' way is to use the update_status callback when the core logic completes the enumeration/initialization. > + > + snd_soc_component_init_regmap(component, wcd9378->regmap); > + > + ret = pm_runtime_resume_and_get(dev); > + if (ret < 0) > + return ret; and presumably if you wait for the init above the device is already functional? > + > + 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 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); doing the io_init() after waiting for the init seems odd to me, the flow isn't classic and deserves more comments IMHO. > + wcd9378_set_micb_usage_vals(component); > + > + pm_runtime_mark_last_busy(dev); > + pm_runtime_put_autosuspend(dev); > + > + return 0; > +} > +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); isn't there a better way to detect that the bus and devices are operational? > + > + 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; > + } > + > + /* > + * 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); same comment as above, if the device is always on then presumably it should never suspend, which begs the question why pm_runtime was introduced? > + if (ret < 0) { > + 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; > + } > + > + 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 int wcd9378_probe(struct platform_device *pdev) > +{ > + struct component_match *match = NULL; > + struct device *dev = &pdev->dev; > + struct wcd9378_priv *wcd9378; > + int ret, i; > + > + wcd9378 = devm_kzalloc(dev, sizeof(*wcd9378), GFP_KERNEL); > + if (!wcd9378) > + return -ENOMEM; > + > + 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; > + > + 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); 1s for pm_runtime autosuspend is rather low, no? Usually values are in the 3s range to avoid suspend-resume transitions while the user is playing with the UI. > + 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); the last two calls seem unnecessary if pm_runtime is disabled already? > +} > +/* > + * Raw (16-bit, non-paged) Qualcomm slave SCP registers, written with > + * 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 */ > +#define WCD9378_SWRS_CLK_SCALE_DIV4 0x03 /* 4.8 MHz */ all of those seem like standard registers, do you really need/want to use your own definitions? ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver 2026-07-07 11:42 ` Pierre-Louis Bossart @ 2026-07-07 15:13 ` Jorijn van der Graaf 0 siblings, 0 replies; 12+ messages in thread From: Jorijn van der Graaf @ 2026-07-07 15:13 UTC (permalink / raw) To: Pierre-Louis Bossart Cc: Jorijn van der Graaf, Mark Brown, Srinivas Kandagatla, Ravi Hothi, Vinod Koul, Bard Liao, Liam Girdwood, Rob Herring, Krzysztof Kozlowski, Conor Dooley, Luca Weiss, Mohammad Rafi Shaik, Jaroslav Kysela, Takashi Iwai, linux-sound, devicetree, linux-arm-msm, linux-kernel [ +Cc Vinod and Bard for the drivers/soundwire items below ] On Tue, 7 Jul 2026 13:42:26 +0200, Pierre-Louis Bossart wrote: >> +static int wcd9378_bus_config(struct sdw_slave *slave, >> + struct sdw_bus_params *params) > > Changes to SCP registers are generally done by the SoundWire core. > IIRC there is already support for setting the bus scaling registers. > Not sure why this function is needed? You're right, and thanks for the pointer. sdw_slave_set_frequency() computes exactly the values this codec needs (base 19.2 MHz, scale index 2 for the 9.6 MHz bus here). It just never triggers on qcom platforms today: the chip enumerates with class_id 0x00, qcom.c does not set bus->prop.mclk_freq, and so sdw_slave_get_scale_index() would return -EINVAL - which I suspect is why the downstream stack, and this driver following it, hand-roll these writes. Plan for v2: set prop.mclk_freq in the qcom controller (small separate patch), set prop.clock_reg_supported here, and delete wcd9378_bus_config() plus the hand-rolled base/scale writes. One gap I noticed while checking this: sdw_program_params() gates the bank-switch scale reprogramming on is_clock_scaling_supported_by_slave(), which checks class_id only and not prop.clock_reg_supported, so a class-0 device never gets the next-bank scale written there. That is harmless here, since sdw_slave_set_frequency() programs both banks at enumeration and the qcom bus clock is fixed, but I can extend the helper to honour clock_reg_supported as well - as part of the soundwire-side series with the mclk_freq patch, or however Vinod prefers to route it. Either way I want to re-validate on hardware before deleting: the sequencer stalling on a missing clock indication was the hardest bug of this bring-up, so I'd like to see the core-programmed values keep it alive first. (This also supersedes the shared clock-scale helper I promised in the bot-review triage; with both hand-rolled paths deleted there is nothing left to share.) >> + /* Set suspended until aggregate device is bind */ >> + pm_runtime_set_suspended(dev); > > is this set_suspended() necessary? pm_runtime isn't even enabled in > this probe. Inherited from wcd937x-sdw.c (runtime PM for the slaves is enabled later, in wcd-common's component bind). It is most likely redundant since devices default to the suspended state; I'll check and drop it in v2 if so. >> + component_del(dev, &wcd_sdw_component_ops); > > pm_runtime_disable() ? It is there, just not visible in this function: runtime PM is enabled in wcd-common's component bind and disabled in the matching component unbind, which the component_del() here triggers when the aggregate tears down - and if the aggregate never bound, runtime PM was never enabled either. Same structure as the other wcd93xx sdw drivers. I'll add a comment pointing at wcd-common. >> + SDW_SLAVE_ENTRY(0x0217, 0x0110, 0), > > if this was an SDCA device shouldn't you rely on the _EXT macro? The device enumerates with class_id 0x00 (sdw:x:0:0217:0110:00:y on the bus), so SDW_SLAVE_ENTRY, which is _EXT with version and class zero, matches what the hardware reports: despite the SDCA-shaped control model it does not announce an SDCA class. (That is also why the class_id gate in sdw_slave_set_frequency() never fired here.) >> + * The WCD9378 pairs a WCD937x-compatible analog core with SDCA-style > > SDCA-style or SDCA-compliant? Deliberately "style": the control model is SDCA-shaped (functions, ITs/PDEs at SDCA-looking control addresses) but the chip reports no SDCA class ID and the downstream driver programs it through a proprietary sequencer layer rather than generic SDCA. Srinivas mentioned elsewhere in this thread that he has been building an SDCA-compliant driver for this codec for Glymur - if that pans out, this wording (and much more) gets revisited. I'll tighten the comment in v2. >> +/* sys-usage capability bits (SYS_USAGE_CTRL profile contents) */ > > what does sys-usage mean? it was never clear in the SDCA spec, not > sure what this refers to here. It's not an SDCA concept: SYS_USAGE_CTRL is a vendor register in the chip's sequencer block that selects one of 13 canned "active entity set" profiles. The enum lists the capability bits those profiles advertise, and yes, the hex table below maps profiles to those enum bits (derived from the downstream driver, which also supplied the names). v2 will say all of that in the comment. >> + snd_soc_component_write(component, WCD9378_SMP_AMP_FUNC_STAT, 0xff); > > if this is really SDCA, this would be very odd to set all bits in the > function status register. The function status register includers > fields such as needs reset, function busy, etc. The chip has no public documentation, but these behave as the SDCA Function Status sticky flags, which are write-1-to-clear - so I read the 0xff (taken from the downstream driver) as a clear-all after activation rather than a configuration write. v2 will write only the named sticky bits and leave the live FUNCTION_BUSY bit alone, the way sound/soc/sdca clears this register. > quite a lot of voodoo magic in this function, eh? No argument. It's a transcription of the downstream activation sequence, delays included, validated on this hardware; the chip has no public documentation beyond that source. v2 will label the steps. >> + sdw_write(tx, SDW_SCP_COMMIT, 0x02); > > is this SCP_COMMIT needed? This is only used for dual-ranked > registers, and there's currently no mechanism to handle them. > The bus scale registers are banked, this is a different concept. Agreed, it looks misplaced. It was carried over from the downstream driver during bring-up - from its headphone-sequencer path, as it turns out; the downstream capture-start clock writes don't include it - and survived because the sequencer's failure mode made me conservative. With the clock programming moving to the core per your first comment, this write goes away with the rest, verified on hardware. >> + snd_soc_component_update_bits(component, fn->req_reg, 0xff, >> + WCD9378_PDE_PS0_ON); > > if this is remotely inspired by SDCA, the normal sequence is to set > the REQUESTED_POWER then poll the ACTUAL_POWER register. That is what happens: requested power is written here and the ACTUAL_POWER poll is a few lines further down. The clock indication is currently sandwiched between the two because the sequencer only advances with the bus clock indicated; once the clock registers are programmed at enumeration time by the core, this mostly collapses to the classic request-then-poll (the vendor HOST_CLK_DIV2_CTL writes still need a home). > it's quite unclear to me why ports need to be exposed to user space? > This is really low-level stuff that isn't typically handled by > user-space. Same model as wcd937x/938x/939x, and the qcom UCM configurations set these switches today. I'd rather keep this driver consistent with the family and its userspace than diverge on one chip; if the model should change, that seems like a family-wide discussion. >> + ret = sdw_slave_wait_for_init(wcd9378->tx_sdw_dev, 5000); > > That's rather unusual, the 'standard' way is to use the update_status > callback when the core logic completes the enumeration/initialization. The update_status callback is still part of what completes the initialization being waited on (wcd_update_status via wcd-common); the wait itself is the pattern the recent enumeration-helper conversion moved the family to - wcd937x/938x/939x and pm4125 all call sdw_slave_wait_for_init() from their component probe now. > and presumably if you wait for the init above the device is already > functional? The slave is; the pm_runtime_resume_and_get() just below is on the codec's platform device (the reset/regulator provider), a different device, so it isn't redundant with the slave init. > doing the io_init() after waiting for the init seems odd to me, the > flow isn't classic and deserves more comments IMHO. Fair. The constraint is that io_init() programs the analog core and runs the function activation, which must hit an enumerated codec with the bus clock running; component probe after the init wait is the earliest point where that all holds. v2 will spell that out. >> + /* Give the SDW subdevices some more time to settle */ >> + usleep_range(5000, 5010); > > isn't there a better way to detect that the bus and devices are > operational? Inherited verbatim from wcd937x_bind(); I don't have a better signal to offer and didn't want to diverge from the family blindly. I can experiment with dropping it as a follow-up. > same comment as above, if the device is always on then presumably it > should never suspend, which begs the question why pm_runtime was > introduced? The hold pins only the TX slave, and only while the codec is bound: the RX slave and the codec's platform device still runtime-suspend normally, and unbind drops the hold. Keeping the runtime PM plumbing also leaves room for the reset-plus-reinit recovery path (needed for system sleep anyway, as noted in the commit message), which would allow narrowing the TX hold to active capture later. >> + pm_runtime_set_autosuspend_delay(dev, 1000); > > 1s for pm_runtime autosuspend is rather low, no? Usually values are > in the 3s range to avoid suspend-resume transitions while the user > is playing with the UI. Copied from the family (wcd937x/938x/939x all use 1000 ms). Happy to make it 3 s in v2. >> + pm_runtime_disable(dev); >> + pm_runtime_set_suspended(dev); >> + pm_runtime_dont_use_autosuspend(dev); > > the last two calls seem unnecessary if pm_runtime is disabled already? Copied from wcd937x_remove(), but checking the mechanics you are right about both: set_suspended() is redundant because the driver core already resets a disabled-but-still-active device to suspended after unbind (pm_runtime_reinit()), and dont_use_autosuspend() plus disable() is exactly what the core's own devm teardown does (pm_runtime_disable_action()). v2 will switch the probe to pm_runtime_use_autosuspend() + devm_pm_runtime_set_active_enabled() and delete this whole block from remove(). >> +#define WCD9378_SWRS_SCP_BASE_CLK 0x4d >> [...] > > all of those seem like standard registers, do you really need/want > to use your own definitions? 0x4d/0x62/0x72 are indeed SDW_SCP_BUS_CLOCK_BASE and SDW_SCP_BUSCLOCK_SCALE_B0/B1 - those defines disappear together with the hand-rolled writes per your first comment. The remaining ones (HOST_CLK_DIV2_CTL at 0xe0/0xf0 and the INTRTYPE registers at 0xf4-0xfc) have no equivalents in the standard register map - wsa881x already carries the identical HOST_CLK_DIV2_CTL vendor register - so those keep vendor-prefixed names, with a comment saying exactly that. Thanks for the detailed pass - the clock-register pointer alone removes most of the SCP special-casing from this driver. Thanks, Jorijn ^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-07-07 15:14 UTC | newest] Thread overview: 12+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-07-06 19:22 [PATCH 0/3] ASoC: codecs: add the Qualcomm WCD9378 audio codec Jorijn van der Graaf 2026-07-06 19:22 ` [PATCH 1/3] ASoC: dt-bindings: qcom,wcd93xx-common: don't require micbias4 Jorijn van der Graaf 2026-07-06 19:22 ` [PATCH 2/3] ASoC: dt-bindings: add the Qualcomm WCD9378 audio codec Jorijn van der Graaf 2026-07-06 21:17 ` Srinivas Kandagatla 2026-07-06 22:22 ` Jorijn van der Graaf 2026-07-06 19:22 ` [PATCH 3/3] ASoC: codecs: wcd9378: add TX/capture codec driver Jorijn van der Graaf 2026-07-06 19:38 ` sashiko-bot 2026-07-06 20:35 ` Jorijn van der Graaf 2026-07-06 21:41 ` Srinivas Kandagatla 2026-07-06 22:22 ` Jorijn van der Graaf 2026-07-07 11:42 ` Pierre-Louis Bossart 2026-07-07 15:13 ` Jorijn van der Graaf
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.