Compare commits

...
Sign in to create a new pull request.

17 commits

Author SHA1 Message Date
2e9adc633e Merge branch 'sensors' into combined-stable
# Conflicts:
#	arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts
2026-07-16 15:41:12 +02:00
cf98406408 arm64: dts: qcom: milos-fairphone-fp6: add sensor i2c bus and magnetometer
The FP6 sensor suite (QMC6308 magnetometer @ 0x2c, SPL07 barometer
@ 0x76, STK3BCx ALS/proximity @ 0x48) sits on an I2C bus wired to
TLMM gpio153/gpio154. Those pads are eGPIOs with no AP serial-engine
function: on the stock OS the bus is driven by an island QUP owned by
the Snapdragon Sensor Core on the ADSP. From the AP they are only
usable as software GPIOs, so describe the bus as i2c-gpio, and add the
QMC6308 magnetometer node: powered from the always-on vreg_l10b (per
the schematics), mount matrix from the vendor SSC registry
(volcano_qmc630x_0.json .orient: sensor x -> device +y, sensor y ->
device -x, z -> z), validated against Earth's field with a four-point
cardinal rotation.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-14 22:17:50 +02:00
50cd4e823f iio: magnetometer: add support for QST QMC6308
The QST QMC6308 is a 3-axis AMR magnetometer on I2C, a single-supply
4-pin WLCSP part with no interrupt/DRDY pin, found e.g. in the
Fairphone 6. Its register map differs from the QMC5883L (chip ID at
0x00 instead of 0x0D, data at 0x01..0x06, and the range field living
in control register 2), so add a separate driver rather than extending
the QMC5883L driver.

Support raw X/Y/Z reads, output data rates 10/50/100/200 Hz, field
ranges +-30/12/8/2 Gauss, filter oversampling ratios (OSR1) 8/4/2/1,
the mount matrix, and runtime PM. The second-stage decimation filter
(OSR2) is left at its power-on default. The package has no DRDY pin,
so there is no trigger support.

Run measurements in the chip's periodic "normal" mode paced by the
DRDY flag rather than in its one-shot "single" mode: the datasheet
specifies no conversion time that could bound a one-shot wait, while
normal mode is paced by the specified output data rates, which also
keeps the sampling_frequency ABI meaningful.

Runtime PM puts the chip into its suspend mode after 500 ms without a
reading, dropping supply current from tens-to-hundreds of microamps to
2-3 uA (datasheet Table 2). The suspended chip retains its registers
and keeps responding on I2C, so resuming only rewrites the mode field
and discards one stale sample, and configuration changes can be
applied even while suspended. VDD is left enabled across runtime
suspend: the on-chip suspend draw is already negligible, and register
retention is what keeps the resume path trivial.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-14 22:17:49 +02:00
066348d67b Merge branch 'mic' into combined 2026-07-06 20:53:40 +02:00
08f135ee41 ASoC: codecs: wcd9378: sync with the mainline v1 submission
Mirror the driver as prepared for mainline (work/linux commit
917ea639925b, "ASoC: codecs: wcd9378: add TX/capture codec driver"),
per the carry-mirrors-upstream rule. On top of the previous carry
state this brings the maintainer-eye pre-review fixes:

 - latch the per-ADC sys-usage bit and target SDCA function at
   PRE_PMU; POST_PMD previously recomputed them from the live input
   mux and could tear down the wrong function after a mux change
 - clear the requested sys_usage_mask bit when no profile matches
 - drop the unreachable -EACCES carve-out on the TX runtime-PM hold
   (caused a usage-count underflow in unbind)
 - mark the write-1-clear FUNC_STAT registers volatile
 - drop the unused is_dapm parameter, the stale sys_usage write-skip
   cache, the SDW_SCP_INT1_IMPL_DEF unmask and the PS0 re-request
   hack; clamp DT channel-map reads
 - name the SWRS_SCP_SDCA_INTRTYPE registers; trim the PS0-failure
   debug dump; Kconfig imply + help text

One deviation from the mainline patch: v7.1.2 predates
sdw_slave_wait_for_init(), so the open-coded
wait_for_completion_timeout() stays here.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-06 20:53:39 +02:00
82284cc84a Merge branch 'mic' into combined 2026-07-06 02:18:05 +02:00
82c43dd3f1 ASoC: codecs: wcd9378: correct the ADC analog gain TLV to 1.5 dB steps
Measured acoustically on the FP6 with a fixed tone played through the
speakers: each TX gain code adds 1.5 dB (+6 dB per 4 codes, +30 dB over
the 0..20 range), not the 0.25 dB the TLV inherited from the wcd937x
driver family. With the correct scale, userspace volume mapping (e.g.
PulseAudio) can use the real analog range.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-06 02:18:05 +02:00
2931c83762 Merge branch 'mic' into combined
# Conflicts:
#	arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts
2026-07-06 01:14:20 +02:00
5694c4a5f3 ASoC: codecs: wcd9378: keep the TX SoundWire bus out of clock-stop
The SDCA function engine (the SmartMIC/SmartJACK/SmartAMP sequencer
machinery activated by the FUNC_ACT class-load) dies when the TX
SoundWire bus enters clock-stop. All its registers keep their values,
so a regcache sync on resume restores nothing visible - the PDE simply
never services power-state requests again: PDE11_ACT_PS stays in PS3,
SEQ_TXn_STAT stays at pwr_dn_rdy, and even the TXn_VALID_CFG_OVR /
TXn_SEQ_TRIGGER_OVR sequencer overrides and a TX0_SEQ_SOFT_RST pulse
are ignored. Re-toggling FUNC_ACT (a real 0->1 edge on the bus) does
not revive it either; only a full codec reset does. The result was
capture recording pure digital silence: the whole DPCM -> CDC-DMA ->
TX macro -> SoundWire transport ran, but the ADC never powered.

Hold a runtime PM reference on the TX slave for as long as the codec
is bound, so the bus never clock-stops. This matches the downstream
stack, which marks the TX SoundWire master 'qcom,is-always-on' - with
full documentation available, Qualcomm made the same trade-off.

Also perform the class-load activation with plain writes instead of
update_bits so the 0->1 activation edge always reaches the hardware
regardless of regcache state.

Verified on the FP6: from a fresh boot, repeated captures across what
were previously bus suspend/resume cycles now power the sequencer every
time (PDE11 reaches PS0) and record live mic signal instead of zeros.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-06 00:58:51 +02:00
d4c5ebbeee Merge branches 'audio' and 'nfc' into combined
combined = flashable working image: v2 speaker audio + NFC reader.
Excludes mic (WCD9378 capture, WIP) and nfc-ce (card emulation, broken).
2026-07-05 21:33:08 +02:00
9cd00a65e8 nfc: s3fwrn5: support the S3NRN4V variant
The S3NRN4V (e.g. on the Fairphone 6, SM7635) is an S3FWRN5-family NFC
controller that needs different bring-up, selected with a new
samsung,s3nrn4v compatible:

 - It ships with working firmware behind a bootloader protocol this
   driver does not implement (GET_BOOTINFO times out), so the firmware
   download step is skipped. Its RF registers are (re)loaded with the
   proprietary DUAL_OPTION command (the HW and SW register blobs merged
   into a single stream) instead of the START/SET/STOP_RFREG sequence.

 - Its reference clock speed is configured with the single-byte FW_CFG
   form, sent from the ->setup hook (after CORE_RESET, before CORE_INIT).
   The selector value (0x11) is taken from the vendor configuration for
   this part; its encoding is not documented.

 - It gates its XI clock through a CLK_REQ line: the chip drives it high
   when it needs the clock, notably to synthesise the 13.56 MHz poll
   carrier. Left always-on, the free-running clock never lets the chip's
   TX PLL lock on a fresh start and it cannot poll (it falls back to
   listen only). Service the handshake when a clk-req GPIO is described,
   gating the clock on it; without one the clock stays always-on.

The variant is carried as match data by both the OF and the I2C device
id tables so the two match paths agree, and the OF table is now
referenced unconditionally for its match data, so drop the
of_match_ptr()/__maybe_unused annotations from it.

The error policy differs between the two configuration steps on purpose:
a clock misconfiguration is fatal (a ->setup failure aborts CORE_INIT),
whereas an RF-register update failure is only warned about and bring-up
continues, since the chip falls back to the RF registers programmed in
its flash and NFC may still work.

Unlike the host-endian word read in the legacy rfreg path, the
DUAL_OPTION checksum is accumulated with get_unaligned_le32() and emitted
little-endian explicitly, so it is correct regardless of CPU endianness.

Existing S3FWRN5 / S3FWRN82 setups keep the firmware-download path and
the always-on clock, unchanged.

Assisted-by: Claude:claude-opus-4-8
Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 20:37:57 +02:00
d837afcfd6 arm64: dts: qcom: milos-fairphone-fp6: add NFC
Add the Samsung S3NRN4V NFC + eSE controller on i2c1. Its XI clock is
provided by the RF_CLK2 PMIC buffer and gated through the controller's
CLK_REQ line on tlmm GPIO6.

The enable line is routed to the chip's power-down input, which is
asserted high to turn the chip off (the downstream driver treats VEN as
active-low on this design) -- hence GPIO_ACTIVE_HIGH, unlike the
exynos5433-tm2 wiring of the same driver. The pin is pulled up so the
chip stays off while the line is not driven.

Assisted-by: Claude:claude-opus-4-8
Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 18:02:28 +02:00
e9a6b531cf arm64: dts: qcom: milos-fairphone-fp6: add WCD9378 codec node and capture dai-link
Add the wcd9378 audio-codec parent node (reset gpio162, l7b/l8b/bob
supplies, 1.8 V micbias x3, rx/tx slave phandles), tx/rx port mappings
on the SoundWire slave nodes (ADC1/2/3 on device ports 1/2/3 all mapped
to master port 1, per the downstream volcano tx_swr_ch_map), the WCD
Capture dai-link on TX_CODEC_DMA_TX_3 and the capture audio-routing
(TX SWR_INPUTn inputs - the milos TX macro is a v9.2 variant, not the
SWR_ADCn naming qcm6490 uses).

The vreg_l8b regulator-always-on DTB hack is obsolete: the codec node
now holds vdd-buck.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 17:37:15 +02:00
9df8d66027 ASoC: codecs: wcd9378: grow skeleton into TX/capture codec driver (WIP)
Replace the transport-test skeleton with a functional driver modeled on
wcd937x: platform parent device (qcom,wcd9378-codec) as component master
over the two SoundWire slaves, owning reset GPIO, supplies and micbias
config; regmap (MAPLE cache, 32-bit paged SDCA addresses) on the TX
slave; capture DAI (index 1) with sdw stream plumbing; DAPM TX path
AMICn -> ADCn MUX -> TXn SEQUENCER -> ADCn_OUTPUT with the SDCA
SmartMIC power sequence (ITxx_USAGE mode, PDE11 PS0 request, HPF init
hold) and IT11_MICB-based refcounted micbias control; sys-usage profile
auto-selection; SCP bus-clock indication (base clk, busclock scale,
host-clk-div2) per the downstream capture-start sequence.

Verified on FP6: probes and binds without any manual per-boot hacks
(gpio162 reset, runtime PM force, l8b always-on all obsolete), sound
card registers, full DPCM/SoundWire/CDC-DMA transport carries data.

KNOWN ISSUE: the SmartMIC sequencer never leaves PWR_DN (PDE11_ACT_PS
stays PS3, SEQ_TX0_STAT=PWR_DN_RDY) although every register the
downstream driver writes has been replicated and verified on hardware
by bypassed readback - capture records digital silence. Investigation
notes in journal/mic.md.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 17:37:02 +02:00
5c9c899ba9 arm64: dts: qcom: milos-fairphone-fp6: enable SoundWire buses with WCD9378 slaves
Enable the RX (swr1) and TX (swr2) SoundWire controllers and describe
the two WCD9378 codec slave devices, as enumerated on hardware:
unique-id 4 on the RX bus, unique-id 3 on the TX bus, SoundWire id
sdw20217011000 (mfg 0x0217, part 0x0110). A firmware description is
required for the sdw core to bind a driver since dynamically enumerated
devices are visible but not bindable.

WIP: the codec node set is minimal (no port mappings, no codec parent
node yet); grows with the wcd9378 driver.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 02:52:32 +02:00
2fd346efe3 ASoC: codecs: wcd9378: add SoundWire skeleton driver (WIP)
Bring-up skeleton for the Qualcomm WCD9378 codec (SoundWire dev id
0x0217:0x0110, one slave per RX/TX bus). Probes both slaves, maps the
SDCA control space (32-bit paged addresses) through regmap-sdw with
prop.paging_support set, and dumps the device identity registers on
ATTACHED as a transport self-test:

  DEV_MANU_ID_0/1 = 0x17/0x02 (Qualcomm 0x0217)
  DEV_PART_ID_0/1 = 0x10/0x01 (WCD9378 0x0110)
  ANA_TX_CH1 0x20, ANA_MICB1 0x10 (downstream reset defaults)

The analog core is WCD937x register-compatible; full codec function
(TX/ADC path first) to be built on top of this. Not for upstream in
this form.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 02:52:32 +02:00
8ed47c2b0a soundwire: qcom: add SCP address paging support
The Qualcomm SoundWire controller driver ignored the paging fields of
struct sdw_msg, so any register access above the 16-bit address space
(e.g. the SDCA control space used by the WCD9378 codec) silently read
the low 15 bits only. The core already splits the address into
addr_page1/addr_page2 and sets msg->page; write the two SCP_AddrPage
registers through the command FIFO before the transfer, as the vendor
swr-mstr-ctrl driver does.

Verified on Fairphone 6 (SM7635): WCD9378 SDCA registers (0x40000000+)
read back their documented reset defaults; without this every paged
read returned zeros.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 02:52:32 +02:00
16 changed files with 3269 additions and 19 deletions

View file

@ -27,6 +27,34 @@
serial1 = &uart11;
};
/*
* The sensor I2C bus sits on TLMM eGPIO pads that have no AP QUP
* function; on the stock OS it is driven by an island QUP of the
* Snapdragon Sensor Core (ADSP). From the AP the pads are only
* usable as software GPIOs, so bit-bang the bus.
* Devices on the bus: QMC6308 magnetometer @ 0x2c, SPL07
* barometer @ 0x76, STK3BCx ALS/proximity @ 0x48.
*/
i2c-sensors {
compatible = "i2c-gpio";
sda-gpios = <&tlmm 153 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>;
scl-gpios = <&tlmm 154 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>;
i2c-gpio,delay-us = <2>;
pinctrl-0 = <&sensor_i2c_default>;
pinctrl-names = "default";
#address-cells = <1>;
#size-cells = <0>;
magnetometer@2c {
compatible = "qstcorp,qmc6308";
reg = <0x2c>;
vdd-supply = <&vreg_l10b>;
mount-matrix = "0", "-1", "0",
"1", "0", "0",
"0", "0", "1";
};
};
gpio-keys {
compatible = "gpio-keys";
@ -209,13 +237,34 @@
sound {
compatible = "qcom,milos-sndcard", "qcom,sm8450-sndcard";
model = "Fairphone (Gen. 6)";
// audio-routing = ...
audio-routing = "AMIC1", "MIC BIAS1",
"AMIC2", "MIC BIAS2",
"AMIC3", "MIC BIAS3",
"TX SWR_INPUT0", "ADC1_OUTPUT",
"TX SWR_INPUT1", "ADC2_OUTPUT",
"TX SWR_INPUT2", "ADC3_OUTPUT";
pinctrl-0 = <&lpi_i2s2_active>;
pinctrl-1 = <&lpi_i2s2_sleep>;
pinctrl-names = "default",
"sleep";
wcd-capture-dai-link {
link-name = "WCD Capture";
codec {
sound-dai = <&wcd9378 1>, <&swr2 0>, <&lpass_txmacro 0>;
};
cpu {
sound-dai = <&q6apmbedai TX_CODEC_DMA_TX_3>;
};
platform {
sound-dai = <&q6apm>;
};
};
i2s-dai-link {
link-name = "Senary MI2S Playback";
@ -233,6 +282,29 @@
};
};
wcd9378: audio-codec {
compatible = "qcom,wcd9378-codec";
pinctrl-0 = <&wcd_reset_n_active>;
pinctrl-names = "default";
reset-gpios = <&tlmm 162 GPIO_ACTIVE_LOW>;
vdd-buck-supply = <&vreg_l8b>;
vdd-rxtx-supply = <&vreg_l7b>;
vdd-io-supply = <&vreg_l7b>;
vdd-mic-bias-supply = <&vreg_bob>;
qcom,micbias1-microvolt = <1800000>;
qcom,micbias2-microvolt = <1800000>;
qcom,micbias3-microvolt = <1800000>;
qcom,rx-device = <&wcd9378_rx>;
qcom,tx-device = <&wcd9378_tx>;
#sound-dai-cells = <1>;
};
thermal-zones {
pm8008-thermal {
polling-delay-passive = <100>;
@ -698,9 +770,28 @@
};
&i2c1 {
/* Samsung NFC @ 0x27 */
status = "okay";
/*
* Samsung S3NRN4V NFC controller. XI is driven by the RF_CLK2 PMIC
* buffer; the chip has no oscillator of its own and gates the clock
* via its CLK_REQ line, so the clock must be voted on in response
* to it.
*/
nfc@27 {
compatible = "samsung,s3nrn4v";
reg = <0x27>;
clk-req-gpios = <&tlmm 6 GPIO_ACTIVE_HIGH>;
clocks = <&rpmhcc RPMH_RF_CLK2>;
en-gpios = <&tlmm 56 GPIO_ACTIVE_HIGH>;
interrupt-parent = <&tlmm>;
interrupts = <31 IRQ_TYPE_EDGE_RISING>;
pinctrl-0 = <&nfc_clk_req_default>, <&nfc_irq_default>,
<&nfc_pd_default>;
pinctrl-names = "default";
wake-gpios = <&tlmm 7 GPIO_ACTIVE_HIGH>;
};
};
&i2c3 {
@ -1084,11 +1175,56 @@
};
};
&swr1 {
status = "okay";
/* WCD9378 RX */
wcd9378_rx: codec@0,4 {
compatible = "sdw20217011000";
reg = <0 4>;
qcom,rx-port-mapping = <1 2 3 4 5>;
};
};
&swr2 {
status = "okay";
/* WCD9378 TX */
wcd9378_tx: codec@0,3 {
compatible = "sdw20217011000";
reg = <0 3>;
/*
* WCD9378 TX port 1 (ADC1) <=> SWR2 port 1 (SWRM_TX1)
* WCD9378 TX port 2 (ADC2) <=> SWR2 port 1
* WCD9378 TX port 3 (ADC3) <=> SWR2 port 1
* WCD9378 TX port 4 (DMIC0,1, MBHC) <=> SWR2 port 2
* WCD9378 TX port 5 (DMIC2..5) <=> SWR2 port 3
*/
qcom,tx-port-mapping = <1 1 1 2 3>;
};
};
&tlmm {
gpio-reserved-ranges = <8 4>, /* Fingerprint SPI */
<13 1>, /* NC */
<63 2>; /* WLAN UART */
nfc_clk_req_default: nfc-clk-req-default-state {
pins = "gpio6";
function = "gpio";
drive-strength = <2>;
bias-disable;
};
sensor_i2c_default: sensor-i2c-default-state {
/* SDA, SCL; external pull-ups to vreg_l10b */
pins = "gpio153", "gpio154";
function = "gpio";
drive-strength = <2>;
bias-disable;
};
ts_active: ts-irq-active-state {
pins = "gpio19";
function = "gpio";
@ -1103,6 +1239,13 @@
bias-disable;
};
nfc_irq_default: nfc-irq-default-state {
pins = "gpio31";
function = "gpio";
drive-strength = <2>;
bias-disable;
};
qup_uart11_sleep_cts: qup-uart11-sleep-cts-state {
pins = "gpio48";
function = "gpio";
@ -1170,6 +1313,17 @@
bias-pull-down;
};
/*
* Pulled up so the NFC chip stays powered down while the line is
* not driven (it is the chip's active-high power-down input).
*/
nfc_pd_default: nfc-pd-default-state {
pins = "gpio56";
function = "gpio";
drive-strength = <2>;
bias-pull-up;
};
sdc2_card_det_n: sdc2-card-det-state {
pins = "gpio65";
function = "gpio";
@ -1211,6 +1365,13 @@
drive-strength = <2>;
bias-pull-down;
};
wcd_reset_n_active: wcd-reset-n-active-state {
pins = "gpio162";
function = "gpio";
drive-strength = <16>;
output-high;
};
};
&uart5 {

View file

@ -198,6 +198,17 @@ config INFINEON_TLV493D
To compile this driver as a module, choose M here: the module
will be called tlv493d.
config QMC6308
tristate "QST QMC6308 3-Axis Magnetic Sensor"
depends on I2C
select REGMAP_I2C
help
Say Y here to add support for the QST QMC6308 3-Axis
Magnetic Sensor.
To compile this driver as a module, choose M here: the
module will be called qmc6308.
config SENSORS_HMC5843
tristate
select IIO_BUFFER

View file

@ -26,6 +26,8 @@ obj-$(CONFIG_IIO_ST_MAGN_SPI_3AXIS) += st_magn_spi.o
obj-$(CONFIG_INFINEON_TLV493D) += tlv493d.o
obj-$(CONFIG_QMC6308) += qmc6308.o
obj-$(CONFIG_SENSORS_HMC5843) += hmc5843_core.o
obj-$(CONFIG_SENSORS_HMC5843_I2C) += hmc5843_i2c.o
obj-$(CONFIG_SENSORS_HMC5843_SPI) += hmc5843_spi.o

View file

@ -0,0 +1,590 @@
// SPDX-License-Identifier: GPL-2.0 OR BSD-2-Clause
/*
* Support for QST QMC6308 3-Axis Magnetic Sensor on I2C bus.
*
* Copyright (C) 2026 Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
*
* Datasheet available at
* <https://qstcorp.com/upload/pdf/202202/13-52-15%20QMC6308%20Datasheet%20Rev.%20F(1).pdf>
*/
#include <linux/array_size.h>
#include <linux/bitfield.h>
#include <linux/bits.h>
#include <linux/cleanup.h>
#include <linux/delay.h>
#include <linux/dev_printk.h>
#include <linux/err.h>
#include <linux/i2c.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
#include <linux/time.h>
#include <linux/types.h>
#include <asm/byteorder.h>
#include <linux/iio/iio.h>
#define QMC6308_REG_ID 0x00
#define QMC6308_REG_X_LSB 0x01
#define QMC6308_REG_STATUS 0x09
#define QMC6308_REG_CTRL1 0x0A
#define QMC6308_REG_CTRL2 0x0B
#define QMC6308_REG_CTRL3 0x0D
#define QMC6308_REG_CTRL4 0x29
#define QMC6308_CHIP_ID 0x80
/* Control register 1 */
#define QMC6308_MODE_MASK GENMASK(1, 0)
#define QMC6308_ODR_MASK GENMASK(3, 2)
#define QMC6308_OSR1_MASK GENMASK(5, 4)
#define QMC6308_OSR2_MASK GENMASK(7, 6)
#define QMC6308_MODE_SUSPEND 0x00
#define QMC6308_MODE_NORMAL 0x01
#define QMC6308_ODR_10HZ 0x00
#define QMC6308_ODR_50HZ 0x01
#define QMC6308_ODR_100HZ 0x02
#define QMC6308_ODR_200HZ 0x03
#define QMC6308_OSR1_8 0x00
#define QMC6308_OSR1_4 0x01
#define QMC6308_OSR1_2 0x02
#define QMC6308_OSR1_1 0x03
/* Control register 2 */
#define QMC6308_SET_RESET_MASK GENMASK(1, 0)
#define QMC6308_RNG_MASK GENMASK(3, 2)
#define QMC6308_SELF_TEST BIT(6)
#define QMC6308_SOFT_RST BIT(7)
#define QMC6308_SET_RESET_ON 0x00
#define QMC6308_RNG_30G 0x00
#define QMC6308_RNG_12G 0x01
#define QMC6308_RNG_8G 0x02
#define QMC6308_RNG_2G 0x03
/* Status register */
#define QMC6308_STATUS_DRDY BIT(0)
#define QMC6308_STATUS_OVFL BIT(1)
/*
* Power-on completion time (datasheet Table 7), also used as a
* conservative bound after soft reset, for which the datasheet
* gives no figure.
*/
#define QMC6308_POR_US 250
#define QMC6308_AUTOSUSPEND_DELAY_MS 500
struct qmc6308_data {
struct regmap *regmap;
/*
* Protect data->range/odr/osr.
* Protect poll and read during measurement (reading the status
* register clears DRDY).
*/
struct mutex mutex;
struct iio_mount_matrix orientation;
u8 range;
u8 odr;
u8 osr;
};
enum qmc6308_axis {
QMC6308_AXIS_X,
QMC6308_AXIS_Y,
QMC6308_AXIS_Z,
};
static const int qmc6308_odr_avail[] = {
[QMC6308_ODR_10HZ] = 10,
[QMC6308_ODR_50HZ] = 50,
[QMC6308_ODR_100HZ] = 100,
[QMC6308_ODR_200HZ] = 200,
};
static const int qmc6308_osr1_avail[] = {
[QMC6308_OSR1_8] = 8,
[QMC6308_OSR1_4] = 4,
[QMC6308_OSR1_2] = 2,
[QMC6308_OSR1_1] = 1,
};
/*
* Sensitivity is 1000/2500/3750/15000 LSB/Gauss for the
* +-30/12/8/2 Gauss ranges respectively.
*/
static const int qmc6308_scales[][2] = {
[QMC6308_RNG_30G] = { 0, 1000000 },
[QMC6308_RNG_12G] = { 0, 400000 },
[QMC6308_RNG_8G] = { 0, 266667 },
[QMC6308_RNG_2G] = { 0, 66667 },
};
static int qmc6308_set_mode(struct qmc6308_data *data, unsigned int mode)
{
return regmap_update_bits(data->regmap, QMC6308_REG_CTRL1,
QMC6308_MODE_MASK,
FIELD_PREP(QMC6308_MODE_MASK, mode));
}
static int qmc6308_take_measurement(struct iio_dev *indio_dev, int index,
int *val)
{
struct qmc6308_data *data = iio_priv(indio_dev);
struct regmap *map = data->regmap;
struct device *dev = regmap_get_device(map);
unsigned int status;
__le16 buf[3];
int ret;
ret = pm_runtime_resume_and_get(dev);
if (ret) {
/* EACCES means a read raced runtime PM disable on suspend */
if (ret != -EACCES)
dev_err(dev, "Failed to power on (%d)\n", ret);
return ret;
}
scoped_guard(mutex, &data->mutex) {
/* 50ms headroom over the slowest ODR (10Hz) */
ret = regmap_read_poll_timeout(map, QMC6308_REG_STATUS,
status,
(status & QMC6308_STATUS_DRDY),
2 * USEC_PER_MSEC,
150 * USEC_PER_MSEC);
if (ret)
goto out_rpm_put;
ret = regmap_bulk_read(map, QMC6308_REG_X_LSB, buf,
sizeof(buf));
if (ret)
goto out_rpm_put;
if (status & QMC6308_STATUS_OVFL)
ret = -ERANGE;
}
out_rpm_put:
pm_runtime_put_autosuspend(dev);
if (ret)
return ret;
*val = (s16)le16_to_cpu(buf[index]);
return 0;
}
static int qmc6308_read_raw(struct iio_dev *indio_dev,
const struct iio_chan_spec *chan,
int *val, int *val2, long mask)
{
struct qmc6308_data *data = iio_priv(indio_dev);
int ret;
switch (mask) {
case IIO_CHAN_INFO_RAW:
ret = qmc6308_take_measurement(indio_dev, chan->address, val);
if (ret)
return ret;
return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE: {
guard(mutex)(&data->mutex);
*val = qmc6308_scales[data->range][0];
*val2 = qmc6308_scales[data->range][1];
return IIO_VAL_INT_PLUS_NANO;
}
case IIO_CHAN_INFO_SAMP_FREQ: {
guard(mutex)(&data->mutex);
*val = qmc6308_odr_avail[data->odr];
return IIO_VAL_INT;
}
case IIO_CHAN_INFO_OVERSAMPLING_RATIO: {
guard(mutex)(&data->mutex);
*val = qmc6308_osr1_avail[data->osr];
return IIO_VAL_INT;
}
default:
return -EINVAL;
}
}
static int qmc6308_write_raw(struct iio_dev *indio_dev,
const struct iio_chan_spec *chan,
int val, int val2, long mask)
{
struct qmc6308_data *data = iio_priv(indio_dev);
unsigned int status;
unsigned int i;
int ret;
switch (mask) {
case IIO_CHAN_INFO_SCALE: {
if (val != 0)
return -EINVAL;
for (i = 0; i < ARRAY_SIZE(qmc6308_scales); i++) {
if (val2 == qmc6308_scales[i][1])
break;
}
if (i == ARRAY_SIZE(qmc6308_scales))
return -EINVAL;
guard(mutex)(&data->mutex);
ret = regmap_update_bits(data->regmap, QMC6308_REG_CTRL2,
QMC6308_RNG_MASK,
FIELD_PREP(QMC6308_RNG_MASK, i));
if (ret)
return ret;
data->range = i;
/*
* The data registers still hold (and DRDY still
* advertises) a sample converted at the previous range;
* discard it so that the next read returns data matching
* the new scale.
*/
return regmap_read(data->regmap, QMC6308_REG_STATUS,
&status);
}
case IIO_CHAN_INFO_SAMP_FREQ: {
for (i = 0; i < ARRAY_SIZE(qmc6308_odr_avail); i++) {
if (val == qmc6308_odr_avail[i])
break;
}
if (i == ARRAY_SIZE(qmc6308_odr_avail))
return -EINVAL;
guard(mutex)(&data->mutex);
ret = regmap_update_bits(data->regmap, QMC6308_REG_CTRL1,
QMC6308_ODR_MASK,
FIELD_PREP(QMC6308_ODR_MASK, i));
if (ret)
return ret;
data->odr = i;
return 0;
}
case IIO_CHAN_INFO_OVERSAMPLING_RATIO: {
for (i = 0; i < ARRAY_SIZE(qmc6308_osr1_avail); i++) {
if (val == qmc6308_osr1_avail[i])
break;
}
if (i == ARRAY_SIZE(qmc6308_osr1_avail))
return -EINVAL;
guard(mutex)(&data->mutex);
ret = regmap_update_bits(data->regmap, QMC6308_REG_CTRL1,
QMC6308_OSR1_MASK,
FIELD_PREP(QMC6308_OSR1_MASK, i));
if (ret)
return ret;
data->osr = i;
return 0;
}
default:
return -EINVAL;
}
}
static int qmc6308_read_avail(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
const int **vals, int *type, int *length,
long mask)
{
switch (mask) {
case IIO_CHAN_INFO_SAMP_FREQ:
*vals = qmc6308_odr_avail;
*type = IIO_VAL_INT;
*length = ARRAY_SIZE(qmc6308_odr_avail);
return IIO_AVAIL_LIST;
case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
*vals = qmc6308_osr1_avail;
*type = IIO_VAL_INT;
*length = ARRAY_SIZE(qmc6308_osr1_avail);
return IIO_AVAIL_LIST;
case IIO_CHAN_INFO_SCALE:
*vals = (const int *)qmc6308_scales;
*type = IIO_VAL_INT_PLUS_NANO;
*length = ARRAY_SIZE(qmc6308_scales) * 2;
return IIO_AVAIL_LIST;
default:
return -EINVAL;
}
}
static int qmc6308_write_raw_get_fmt(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
long mask)
{
switch (mask) {
case IIO_CHAN_INFO_SCALE:
return IIO_VAL_INT_PLUS_NANO;
default:
return IIO_VAL_INT;
}
}
static const struct iio_mount_matrix *
qmc6308_get_mount_matrix(const struct iio_dev *indio_dev,
const struct iio_chan_spec *chan)
{
struct qmc6308_data *data = iio_priv(indio_dev);
return &data->orientation;
}
static const struct iio_chan_spec_ext_info qmc6308_ext_info[] = {
IIO_MOUNT_MATRIX(IIO_SHARED_BY_DIR, qmc6308_get_mount_matrix),
{ }
};
static const struct iio_info qmc6308_info = {
.read_raw = qmc6308_read_raw,
.write_raw = qmc6308_write_raw,
.read_avail = qmc6308_read_avail,
.write_raw_get_fmt = qmc6308_write_raw_get_fmt,
};
static int qmc6308_init(struct qmc6308_data *data)
{
struct regmap *map = data->regmap;
unsigned int reg;
int ret;
ret = regmap_read(map, QMC6308_REG_ID, &reg);
if (ret)
return ret;
/* Allow unknown IDs so that fallback compatibles work */
if (reg != QMC6308_CHIP_ID)
dev_warn(regmap_get_device(map),
"Unknown chip id: 0x%02x, continuing\n", reg);
/* The SOFT_RST bit is not auto-cleared and must be written back 0 */
ret = regmap_write(map, QMC6308_REG_CTRL2, QMC6308_SOFT_RST);
if (ret)
return ret;
fsleep(QMC6308_POR_US);
data->range = QMC6308_RNG_30G;
ret = regmap_write(map, QMC6308_REG_CTRL2,
FIELD_PREP(QMC6308_SET_RESET_MASK,
QMC6308_SET_RESET_ON) |
FIELD_PREP(QMC6308_RNG_MASK, data->range));
if (ret)
return ret;
data->odr = QMC6308_ODR_50HZ;
data->osr = QMC6308_OSR1_8;
return regmap_write(map, QMC6308_REG_CTRL1,
FIELD_PREP(QMC6308_MODE_MASK,
QMC6308_MODE_NORMAL) |
FIELD_PREP(QMC6308_ODR_MASK, data->odr) |
FIELD_PREP(QMC6308_OSR1_MASK, data->osr));
}
static void qmc6308_power_down_action(void *priv)
{
struct qmc6308_data *data = priv;
if (!pm_runtime_status_suspended(regmap_get_device(data->regmap)))
qmc6308_set_mode(data, QMC6308_MODE_SUSPEND);
}
static bool qmc6308_volatile_reg(struct device *dev, unsigned int reg)
{
return reg >= QMC6308_REG_X_LSB && reg <= QMC6308_REG_STATUS;
}
static bool qmc6308_writable_reg(struct device *dev, unsigned int reg)
{
switch (reg) {
case QMC6308_REG_CTRL1:
case QMC6308_REG_CTRL2:
case QMC6308_REG_CTRL3:
case QMC6308_REG_CTRL4:
return true;
default:
return false;
}
}
static const struct regmap_config qmc6308_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = QMC6308_REG_CTRL4,
.cache_type = REGCACHE_MAPLE,
.volatile_reg = qmc6308_volatile_reg,
.writeable_reg = qmc6308_writable_reg,
};
#define QMC6308_CHANNEL(_axis) \
{ \
.type = IIO_MAGN, \
.modified = 1, \
.channel2 = IIO_MOD_##_axis, \
.address = QMC6308_AXIS_##_axis, \
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \
.info_mask_shared_by_type = \
BIT(IIO_CHAN_INFO_SCALE) | \
BIT(IIO_CHAN_INFO_SAMP_FREQ) | \
BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \
.info_mask_shared_by_type_available = \
BIT(IIO_CHAN_INFO_SCALE) | \
BIT(IIO_CHAN_INFO_SAMP_FREQ) | \
BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \
.ext_info = qmc6308_ext_info, \
}
static const struct iio_chan_spec qmc6308_channels[] = {
QMC6308_CHANNEL(X),
QMC6308_CHANNEL(Y),
QMC6308_CHANNEL(Z),
};
static int qmc6308_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
struct qmc6308_data *data;
struct iio_dev *indio_dev;
struct regmap *map;
int ret;
indio_dev = devm_iio_device_alloc(dev, sizeof(*data));
if (!indio_dev)
return -ENOMEM;
i2c_set_clientdata(client, indio_dev);
map = devm_regmap_init_i2c(client, &qmc6308_regmap_config);
if (IS_ERR(map))
return dev_err_probe(dev, PTR_ERR(map),
"regmap initialization failed\n");
ret = devm_regulator_get_enable(dev, "vdd");
if (ret)
return dev_err_probe(dev, ret,
"Failed to enable VDD regulator\n");
fsleep(QMC6308_POR_US);
data = iio_priv(indio_dev);
data->regmap = map;
ret = devm_mutex_init(dev, &data->mutex);
if (ret)
return ret;
ret = iio_read_mount_matrix(dev, &data->orientation);
if (ret)
return dev_err_probe(dev, ret,
"Failed to read mount matrix\n");
indio_dev->name = "qmc6308";
indio_dev->info = &qmc6308_info;
indio_dev->channels = qmc6308_channels;
indio_dev->num_channels = ARRAY_SIZE(qmc6308_channels);
indio_dev->modes = INDIO_DIRECT_MODE;
ret = qmc6308_init(data);
if (ret)
return dev_err_probe(dev, ret, "qmc6308 init failed\n");
pm_runtime_set_active(dev);
ret = devm_add_action_or_reset(dev, qmc6308_power_down_action, data);
if (ret)
return ret;
pm_runtime_get_noresume(dev);
pm_runtime_use_autosuspend(dev);
pm_runtime_set_autosuspend_delay(dev, QMC6308_AUTOSUSPEND_DELAY_MS);
ret = devm_pm_runtime_enable(dev);
if (ret)
return ret;
pm_runtime_put_autosuspend(dev);
return devm_iio_device_register(dev, indio_dev);
}
static int qmc6308_runtime_suspend(struct device *dev)
{
struct iio_dev *indio_dev = dev_get_drvdata(dev);
struct qmc6308_data *data = iio_priv(indio_dev);
return qmc6308_set_mode(data, QMC6308_MODE_SUSPEND);
}
static int qmc6308_runtime_resume(struct device *dev)
{
struct iio_dev *indio_dev = dev_get_drvdata(dev);
struct qmc6308_data *data = iio_priv(indio_dev);
unsigned int status;
int ret;
ret = qmc6308_set_mode(data, QMC6308_MODE_NORMAL);
if (ret)
return ret;
/*
* DRDY may still be set for a sample converted before the last
* suspend; clear it so the next read waits for fresh data.
*/
return regmap_read(data->regmap, QMC6308_REG_STATUS, &status);
}
static DEFINE_RUNTIME_DEV_PM_OPS(qmc6308_pm_ops, qmc6308_runtime_suspend,
qmc6308_runtime_resume, NULL);
static const struct of_device_id qmc6308_match[] = {
{ .compatible = "qstcorp,qmc6308" },
{ }
};
MODULE_DEVICE_TABLE(of, qmc6308_match);
static const struct i2c_device_id qmc6308_id[] = {
{ .name = "qmc6308" },
{ }
};
MODULE_DEVICE_TABLE(i2c, qmc6308_id);
static struct i2c_driver qmc6308_driver = {
.driver = {
.name = "qmc6308",
.of_match_table = qmc6308_match,
.pm = pm_ptr(&qmc6308_pm_ops),
},
.id_table = qmc6308_id,
.probe = qmc6308_probe,
};
module_i2c_driver(qmc6308_driver);
MODULE_DESCRIPTION("QST QMC6308 3-Axis Magnetic Sensor driver");
MODULE_AUTHOR("Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>");
MODULE_LICENSE("Dual BSD/GPL");

View file

@ -122,11 +122,47 @@ static int s3fwrn5_nci_send(struct nci_dev *ndev, struct sk_buff *skb)
return 0;
}
static int s3fwrn5_nci_setup(struct nci_dev *ndev)
{
struct s3fwrn5_info *info = nci_get_drvdata(ndev);
/*
* Runs after CORE_RESET, before CORE_INIT. The S3NRN4V needs its
* reference clock configured here (the downstream stack does it in the
* bootloader, before CORE_RESET, but this is the earliest hook the NCI
* core offers and the chip accepts it).
*/
if (info->variant == S3FWRN5_VARIANT_S3NRN4V)
return s3fwrn5_nci_clk_cfg(info);
return 0;
}
static int s3fwrn5_nci_post_setup(struct nci_dev *ndev)
{
struct s3fwrn5_info *info = nci_get_drvdata(ndev);
int ret;
if (info->variant == S3FWRN5_VARIANT_S3NRN4V) {
/*
* The S3NRN4V ships with working firmware behind a bootloader
* protocol this driver does not implement, so there is no
* download step; the NCI core has already done CORE_RESET +
* CORE_INIT. Just (re)load the RF registers via DUAL_OPTION.
*/
ret = s3fwrn5_nci_rf_configure_dual(info, "sec_s3nrn4v_hwreg.bin",
"sec_s3nrn4v_swreg.bin");
/*
* Keep going even if the blobs could not be loaded: the chip
* still enumerates and falls back to the RF registers programmed
* in its flash, so NFC may work anyway.
*/
if (ret < 0)
dev_warn(&ndev->nfc_dev->dev,
"rfreg configure failed (%d)\n", ret);
return 0;
}
if (s3fwrn5_firmware_init(info)) {
//skip bootloader mode
return 0;
@ -152,13 +188,14 @@ static const struct nci_ops s3fwrn5_nci_ops = {
.open = s3fwrn5_nci_open,
.close = s3fwrn5_nci_close,
.send = s3fwrn5_nci_send,
.setup = s3fwrn5_nci_setup,
.post_setup = s3fwrn5_nci_post_setup,
.prop_ops = s3fwrn5_nci_prop_ops,
.n_prop_ops = ARRAY_SIZE(s3fwrn5_nci_prop_ops),
};
int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev,
const struct s3fwrn5_phy_ops *phy_ops)
const struct s3fwrn5_phy_ops *phy_ops, enum s3fwrn5_variant variant)
{
struct s3fwrn5_info *info;
int ret;
@ -170,6 +207,7 @@ int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev,
info->phy_id = phy_id;
info->pdev = pdev;
info->phy_ops = phy_ops;
info->variant = variant;
mutex_init(&info->mutex);
s3fwrn5_set_mode(info, S3FWRN5_MODE_COLD);

View file

@ -23,9 +23,76 @@ struct s3fwrn5_i2c_phy {
struct i2c_client *i2c_dev;
struct clk *clk;
/*
* Optional hardware clock-request handshake. When a CLK_REQ GPIO is
* wired, the chip drives it high while it needs its XI clock -- notably
* to generate the poll/reader carrier -- and the clock is gated on it
* instead of being left always-on (which never lets the chip's TX PLL
* lock on a fresh clock start, leaving it unable to poll).
*/
struct gpio_desc *gpio_clk_req;
bool clk_on;
struct mutex clk_lock; /* serialises clk_on against the CLK_REQ irq */
unsigned int irq_skip:1;
};
static void s3fwrn5_i2c_clk_set_locked(struct s3fwrn5_i2c_phy *phy, bool on)
{
lockdep_assert_held(&phy->clk_lock);
if (on && !phy->clk_on) {
int ret = clk_prepare_enable(phy->clk);
if (ret == 0)
phy->clk_on = true;
else
dev_warn_once(&phy->i2c_dev->dev,
"failed to enable clock (%d); NFC may not poll\n",
ret);
} else if (!on && phy->clk_on) {
clk_disable_unprepare(phy->clk);
phy->clk_on = false;
}
}
/*
* Apply the current CLK_REQ level. Reading the GPIO under clk_lock makes
* concurrent callers (the CLK_REQ irq thread and the probe-time seeding)
* safe: whoever runs last applies a level read after the earlier update,
* never a stale one.
*/
static void s3fwrn5_i2c_clk_sync(struct s3fwrn5_i2c_phy *phy)
{
int level;
mutex_lock(&phy->clk_lock);
level = gpiod_get_value_cansleep(phy->gpio_clk_req);
if (level >= 0)
s3fwrn5_i2c_clk_set_locked(phy, level > 0);
else
dev_warn_once(&phy->i2c_dev->dev,
"failed to read CLK_REQ (%d); keeping clock state\n",
level);
mutex_unlock(&phy->clk_lock);
}
static void s3fwrn5_i2c_clk_disable_action(void *data)
{
struct s3fwrn5_i2c_phy *phy = data;
mutex_lock(&phy->clk_lock);
s3fwrn5_i2c_clk_set_locked(phy, false);
mutex_unlock(&phy->clk_lock);
}
static irqreturn_t s3fwrn5_i2c_clk_req_thread(int irq, void *phy_id)
{
s3fwrn5_i2c_clk_sync(phy_id);
return IRQ_HANDLED;
}
static void s3fwrn5_i2c_set_mode(void *phy_id, enum s3fwrn5_mode mode)
{
struct s3fwrn5_i2c_phy *phy = phy_id;
@ -146,6 +213,7 @@ out:
static int s3fwrn5_i2c_probe(struct i2c_client *client)
{
enum s3fwrn5_variant variant;
struct s3fwrn5_i2c_phy *phy;
int ret;
@ -172,15 +240,61 @@ static int s3fwrn5_i2c_probe(struct i2c_client *client)
* S3FWRN5 depends on a clock input ("XI" pin) to function properly.
* Depending on the hardware configuration this could be an always-on
* oscillator or some external clock that must be explicitly enabled.
* Make sure the clock is running before starting S3FWRN5.
*
* If a CLK_REQ GPIO is wired, the chip gates the clock itself (driving
* CLK_REQ high when it needs XI); service that handshake. Otherwise just
* make sure the clock is running before starting S3FWRN5.
*/
phy->clk = devm_clk_get_optional_enabled(&client->dev, NULL);
if (IS_ERR(phy->clk))
return dev_err_probe(&client->dev, PTR_ERR(phy->clk),
"failed to get clock\n");
mutex_init(&phy->clk_lock);
phy->gpio_clk_req = devm_gpiod_get_optional(&client->dev, "clk-req",
GPIOD_IN);
if (IS_ERR(phy->gpio_clk_req))
return PTR_ERR(phy->gpio_clk_req);
if (phy->gpio_clk_req) {
int clk_req_irq;
phy->clk = devm_clk_get_optional(&client->dev, NULL);
if (IS_ERR(phy->clk))
return dev_err_probe(&client->dev, PTR_ERR(phy->clk),
"failed to get clock\n");
/*
* Unlike the always-on branch below, this clock is enabled by
* hand from the CLK_REQ handler, so devm will not disable it on
* unbind. Gate it off explicitly if it is still on at teardown.
*/
ret = devm_add_action_or_reset(&client->dev,
s3fwrn5_i2c_clk_disable_action,
phy);
if (ret)
return ret;
clk_req_irq = gpiod_to_irq(phy->gpio_clk_req);
if (clk_req_irq < 0)
return clk_req_irq;
ret = devm_request_threaded_irq(&client->dev, clk_req_irq, NULL,
s3fwrn5_i2c_clk_req_thread,
IRQF_TRIGGER_RISING |
IRQF_TRIGGER_FALLING |
IRQF_ONESHOT,
"s3fwrn5_clk_req", phy);
if (ret)
return ret;
/* Seed the clock state from the current CLK_REQ level. */
s3fwrn5_i2c_clk_sync(phy);
} else {
phy->clk = devm_clk_get_optional_enabled(&client->dev, NULL);
if (IS_ERR(phy->clk))
return dev_err_probe(&client->dev, PTR_ERR(phy->clk),
"failed to get clock\n");
}
variant = (uintptr_t)i2c_get_match_data(client);
ret = s3fwrn5_probe(&phy->common.ndev, phy, &phy->i2c_dev->dev,
&i2c_phy_ops);
&i2c_phy_ops, variant);
if (ret < 0)
return ret;
@ -205,13 +319,17 @@ static void s3fwrn5_i2c_remove(struct i2c_client *client)
}
static const struct i2c_device_id s3fwrn5_i2c_id_table[] = {
{ S3FWRN5_I2C_DRIVER_NAME },
{}
{ .name = S3FWRN5_I2C_DRIVER_NAME, .driver_data = S3FWRN5_VARIANT_FWDL },
{ .name = "s3nrn4v", .driver_data = S3FWRN5_VARIANT_S3NRN4V },
{ }
};
MODULE_DEVICE_TABLE(i2c, s3fwrn5_i2c_id_table);
static const struct of_device_id of_s3fwrn5_i2c_match[] __maybe_unused = {
{ .compatible = "samsung,s3fwrn5-i2c", },
static const struct of_device_id of_s3fwrn5_i2c_match[] = {
{ .compatible = "samsung,s3fwrn5-i2c",
.data = (void *)S3FWRN5_VARIANT_FWDL, },
{ .compatible = "samsung,s3nrn4v",
.data = (void *)S3FWRN5_VARIANT_S3NRN4V, },
{}
};
MODULE_DEVICE_TABLE(of, of_s3fwrn5_i2c_match);
@ -219,7 +337,7 @@ MODULE_DEVICE_TABLE(of, of_s3fwrn5_i2c_match);
static struct i2c_driver s3fwrn5_i2c_driver = {
.driver = {
.name = S3FWRN5_I2C_DRIVER_NAME,
.of_match_table = of_match_ptr(of_s3fwrn5_i2c_match),
.of_match_table = of_s3fwrn5_i2c_match,
},
.probe = s3fwrn5_i2c_probe,
.remove = s3fwrn5_i2c_remove,

View file

@ -8,6 +8,9 @@
#include <linux/completion.h>
#include <linux/firmware.h>
#include <linux/minmax.h>
#include <linux/slab.h>
#include <linux/unaligned.h>
#include "s3fwrn5.h"
#include "nci.h"
@ -20,7 +23,7 @@ static int s3fwrn5_nci_prop_rsp(struct nci_dev *ndev, struct sk_buff *skb)
return 0;
}
const struct nci_driver_ops s3fwrn5_nci_prop_ops[4] = {
const struct nci_driver_ops s3fwrn5_nci_prop_ops[5] = {
{
.opcode = nci_opcode_pack(NCI_GID_PROPRIETARY,
NCI_PROP_SET_RFREG),
@ -41,6 +44,11 @@ const struct nci_driver_ops s3fwrn5_nci_prop_ops[4] = {
NCI_PROP_FW_CFG),
.rsp = s3fwrn5_nci_prop_rsp,
},
{
.opcode = nci_opcode_pack(NCI_GID_PROPRIETARY,
NCI_PROP_DUAL_OPTION),
.rsp = s3fwrn5_nci_prop_rsp,
},
};
#define S3FWRN5_RFREG_SECTION_SIZE 252
@ -117,3 +125,112 @@ out:
release_firmware(fw);
return ret;
}
/*
* Configure the reference clock. The S3NRN4V expects the single-byte FW_CFG
* form (just the clock-speed selector). The downstream stack sends this in the
* bootloader before CORE_RESET; the earliest the mainline NCI core lets us in
* is the ->setup hook (after CORE_RESET, before CORE_INIT), which works.
*/
int s3fwrn5_nci_clk_cfg(struct s3fwrn5_info *info)
{
u8 clk_speed = NCI_PROP_FW_CFG_CLK_SPEED;
return nci_prop_cmd(info->ndev, NCI_PROP_FW_CFG, 1, &clk_speed);
}
/*
* S3NRN4V RF register update. The HW and SW register blobs are merged into a
* single stream (HW first) and pushed via the DUAL_OPTION command:
* START_UPDATE, one SET_OPTION per 252-byte section, then STOP_UPDATE carrying
* a 16-bit checksum (running sum of the merged stream as 32-bit words).
*/
int s3fwrn5_nci_rf_configure_dual(struct s3fwrn5_info *info,
const char *hw_name, const char *sw_name)
{
const struct firmware *hw_fw = NULL, *sw_fw = NULL;
struct nci_prop_dual_set_option_cmd set_option;
struct device *dev = &info->ndev->nfc_dev->dev;
size_t merged_size, i, len;
u8 *merged = NULL;
u8 stop_cmd[3];
u32 checksum;
u8 sub_oid;
int ret;
ret = request_firmware(&hw_fw, hw_name, dev);
if (ret < 0)
return ret;
ret = request_firmware(&sw_fw, sw_name, dev);
if (ret < 0)
goto out_hw;
merged_size = hw_fw->size + sw_fw->size;
/*
* The stream is checksummed as 32-bit words and pushed in at most 256
* sections (the section index is a single byte); reject blobs that
* would silently break either.
*/
if (merged_size % 4 ||
merged_size > 256 * NCI_PROP_DUAL_SECTION_SIZE) {
dev_err(dev, "invalid rfreg blob size (%zu)\n", merged_size);
ret = -EINVAL;
goto out;
}
merged = kmalloc(merged_size, GFP_KERNEL);
if (!merged) {
ret = -ENOMEM;
goto out;
}
memcpy(merged, hw_fw->data, hw_fw->size);
memcpy(merged + hw_fw->size, sw_fw->data, sw_fw->size);
/* Running sum of the merged stream as little-endian 32-bit words. */
checksum = 0;
for (i = 0; i + 4 <= merged_size; i += 4)
checksum += get_unaligned_le32(merged + i);
dev_dbg(dev, "rfreg dual-option update: %s + %s\n", hw_name, sw_name);
/* START_UPDATE */
sub_oid = NCI_PROP_DUAL_SUB_START_UPDATE;
ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION, 1, &sub_oid);
if (ret < 0) {
dev_err(dev, "Unable to start rfreg update\n");
goto out;
}
/* SET_OPTION per section */
set_option.sub_oid = NCI_PROP_DUAL_SUB_SET_OPTION;
set_option.index = 0;
for (i = 0; i < merged_size; i += NCI_PROP_DUAL_SECTION_SIZE) {
len = min_t(size_t, merged_size - i, NCI_PROP_DUAL_SECTION_SIZE);
memcpy(set_option.data, merged + i, len);
ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION,
len + 2, (__u8 *)&set_option);
if (ret < 0) {
dev_err(dev, "rfreg update error (code=%d)\n", ret);
goto out;
}
set_option.index++;
}
/* STOP_UPDATE with checksum */
stop_cmd[0] = NCI_PROP_DUAL_SUB_STOP_UPDATE;
put_unaligned_le16(checksum, &stop_cmd[1]);
ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION, 3, stop_cmd);
if (ret < 0) {
dev_err(dev, "Unable to stop rfreg update\n");
goto out;
}
dev_dbg(dev, "rfreg dual-option update: success\n");
out:
kfree(merged);
release_firmware(sw_fw);
out_hw:
release_firmware(hw_fw);
return ret;
}

View file

@ -40,6 +40,13 @@ struct nci_prop_stop_rfreg_rsp {
#define NCI_PROP_FW_CFG 0x28
/*
* Single-byte FW_CFG payload (clock-speed selector) for the S3NRN4V reference
* clock. Taken from the vendor configuration for this part (the encoding is
* not documented).
*/
#define NCI_PROP_FW_CFG_CLK_SPEED 0x11
struct nci_prop_fw_cfg_cmd {
__u8 clk_type;
__u8 clk_speed;
@ -50,7 +57,30 @@ struct nci_prop_fw_cfg_rsp {
__u8 status;
};
extern const struct nci_driver_ops s3fwrn5_nci_prop_ops[4];
/*
* The S3NRN4V updates its RF registers through a single "dual option" command
* (a sub-OID selects the operation) instead of the START/SET/STOP_RFREG
* opcodes above, and expects the HW and SW register blobs merged into one
* stream.
*/
#define NCI_PROP_DUAL_OPTION 0x2a
#define NCI_PROP_DUAL_SUB_START_UPDATE 0x01
#define NCI_PROP_DUAL_SUB_SET_OPTION 0x02
#define NCI_PROP_DUAL_SUB_STOP_UPDATE 0x03
#define NCI_PROP_DUAL_SECTION_SIZE 252
struct nci_prop_dual_set_option_cmd {
__u8 sub_oid; /* NCI_PROP_DUAL_SUB_SET_OPTION */
__u8 index;
__u8 data[NCI_PROP_DUAL_SECTION_SIZE];
};
extern const struct nci_driver_ops s3fwrn5_nci_prop_ops[5];
int s3fwrn5_nci_rf_configure(struct s3fwrn5_info *info, const char *fw_name);
int s3fwrn5_nci_rf_configure_dual(struct s3fwrn5_info *info,
const char *hw_name, const char *sw_name);
int s3fwrn5_nci_clk_cfg(struct s3fwrn5_info *info);
#endif /* __LOCAL_S3FWRN5_NCI_H_ */

View file

@ -21,6 +21,17 @@ enum s3fwrn5_mode {
S3FWRN5_MODE_FW,
};
enum s3fwrn5_variant {
/* S3FWRN5 / S3FWRN82: firmware is downloaded by this driver */
S3FWRN5_VARIANT_FWDL,
/*
* S3NRN4V: ships with working firmware behind a bootloader protocol
* this driver does not implement; skip the download, configure the
* clock (FW_CFG) and update the RF registers via the DUAL_OPTION cmd.
*/
S3FWRN5_VARIANT_S3NRN4V,
};
struct s3fwrn5_phy_ops {
void (*set_wake)(void *id, bool sleep);
void (*set_mode)(void *id, enum s3fwrn5_mode);
@ -36,6 +47,7 @@ struct s3fwrn5_info {
const struct s3fwrn5_phy_ops *phy_ops;
struct s3fwrn5_fw_info fw_info;
enum s3fwrn5_variant variant;
struct mutex mutex;
};
@ -78,7 +90,7 @@ static inline int s3fwrn5_write(struct s3fwrn5_info *info, struct sk_buff *skb)
}
int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev,
const struct s3fwrn5_phy_ops *phy_ops);
const struct s3fwrn5_phy_ops *phy_ops, enum s3fwrn5_variant variant);
void s3fwrn5_remove(struct nci_dev *ndev);
int s3fwrn5_recv_frame(struct nci_dev *ndev, struct sk_buff *skb,

View file

@ -137,7 +137,7 @@ static int s3fwrn82_uart_probe(struct serdev_device *serdev)
}
ret = s3fwrn5_probe(&phy->common.ndev, phy, &phy->ser_dev->dev,
&uart_phy_ops);
&uart_phy_ops, S3FWRN5_VARIANT_FWDL);
if (ret < 0)
goto err_serdev;

View file

@ -976,6 +976,20 @@ static enum sdw_command_response qcom_swrm_xfer_msg(struct sdw_bus *bus,
struct qcom_swrm_ctrl *ctrl = to_qcom_sdw(bus);
int ret, i, len;
if (msg->page) {
ret = qcom_swrm_cmd_fifo_wr_cmd(ctrl, msg->addr_page1,
msg->dev_num,
SDW_SCP_ADDRPAGE1);
if (ret)
return SDW_CMD_IGNORED;
ret = qcom_swrm_cmd_fifo_wr_cmd(ctrl, msg->addr_page2,
msg->dev_num,
SDW_SCP_ADDRPAGE2);
if (ret)
return SDW_CMD_IGNORED;
}
if (msg->flags == SDW_MSG_FLAG_READ) {
for (i = 0; i < msg->len;) {
len = min(msg->len - i, QCOM_SWRM_MAX_RD_LEN);

View file

@ -301,6 +301,7 @@ config SND_SOC_ALL_CODECS
imply SND_SOC_WCD9335
imply SND_SOC_WCD934X
imply SND_SOC_WCD937X_SDW
imply SND_SOC_WCD9378_SDW
imply SND_SOC_WCD938X_SDW
imply SND_SOC_WCD939X_SDW
imply SND_SOC_LPASS_MACRO_COMMON
@ -2431,6 +2432,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

View file

@ -356,6 +356,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
@ -796,6 +798,11 @@ ifdef CONFIG_SND_SOC_WCD937X_SDW
# avoid link failure by forcing sdw code built-in when needed
obj-$(CONFIG_SND_SOC_WCD937X) += snd-soc-wcd937x-sdw.o
endif
obj-$(CONFIG_SND_SOC_WCD9378) += 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

View file

@ -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");

1390
sound/soc/codecs/wcd9378.c Normal file

File diff suppressed because it is too large Load diff

246
sound/soc/codecs/wcd9378.h Normal file
View file

@ -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__ */