* [PATCH v3 0/2] ASoC: codecs: add Texas Instruments TAS2557 smart amplifier driver
From: Gianluca Boiano @ 2026-07-17 23:33 UTC (permalink / raw)
To: Mark Brown, Liam Girdwood, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Jaroslav Kysela, Takashi Iwai
Cc: Gianluca Boiano, Shenghao Ding, Kevin Lu, Baojun Xu, Wang, Sen,
linux-sound, devicetree, linux-kernel
The TAS2557 is a mono Class-D smart amplifier with an on-chip DSP for
speaker protection, controlled over I2C with audio carried over
I2S/TDM. The DSP requires a board-specific firmware image generated
with TI's PurePath Console tuning tools; without it the device passes
no audio.
Patch 1 adds the devicetree binding. Patch 2 adds the ASoC codec
driver. A single node manages one physical TAS2557, or two of them
sharing one ASI bus and one reset/IRQ pair, using the reg-array
convention of ti,tas2781.yaml: the device at reg index 0 is DEV_A and
the device at index 1 is DEV_B, so no channel-selection property is
needed. The driver covers mono and dual-device stereo operation and
fault-interrupt recovery that reloads the firmware configuration on
clock loss, over-current, brownout or over-temperature.
The driver has been exercised across development of this series in
mono configuration on Xiaomi Mi A2 (SDM660, jasmine), Xiaomi Redmi
Note 6 Pro (SDM636, tulip) and Xiaomi Redmi 5 Plus (MSM8953, vince),
driving the top speaker, and in dual-device stereo on Xiaomi Mi Pad 4
(SDM660, clover). The v3 ASI word-length and data-slot-offset
changes follow hardware reports from vince (16-bit, offset 0) and
have not yet been re-confirmed on the 32-bit/offset-1 boards above.
Changes in v3:
- rework the binding and driver to a single-node, reg-array model
that manages up to two devices, matching ti,tas2781.yaml; drop the
ti,channel property entirely and derive DEV_A/DEV_B tuning from the
device order in reg (Krzysztof Kozlowski)
- rename ti,imon-slot-no/ti,vmon-slot-no to the plural
ti,imon-slot-nos/ti,vmon-slot-nos arrays (one entry per reg entry),
and constrain firmware-name to maxItems: 1 (Krzysztof Kozlowski)
- derive the ASI word length from the stream (params_width) and
re-apply it after each reset instead of hardcoding 32-bit, so
16-bit boards are no longer forced to 32-bit
- set symmetric_rate on the DAI (Mark Brown), and add
symmetric_sample_bits since one firmware config (one rate) is
active at a time and playback and capture share the single ASI
word length
- apply the same write-then-cache ordering used for the DAC-gain
control to the ISENSE/VSENSE mixer setters, so a failed
POWER_CTRL2 write no longer leaves the cached enable state stale
- only cache the DAC gain after the hardware write succeeds, so a
failed write no longer leaves the cache out of sync (Mark Brown)
- default the ASI data-slot offset to 0; an explicit
snd_soc_dai_set_tdm_slot() still takes precedence
Changes in v2:
- add the four mandatory supplies (vbat/iovdd/avdd/dvdd) to the
devicetree binding's "required" list (Wang Sen)
- propagate register-write failures out of hw_params() instead of
only logging them (Wang Sen)
- return IRQ_NONE instead of IRQ_HANDLED from the fault interrupt
handler when the device isn't in a state to have raised it, so
shared-IRQ accounting stays correct (Mark Brown)
- re-apply cached runtime state (DAI format, sense enables, DAC gain,
ASI slot offsets) after every device reset, including fault
recovery, instead of letting a hardcoded startup sequence silently
override set_dai_fmt()/the ISENSE/VSENSE mixer controls (Mark Brown)
- honour explicit snd_soc_dai_set_tdm_slot() configuration in
hw_params, and restore TX (OFFSET2) slot offsets after reset
- wait for the asynchronous firmware load on first stream start
instead of failing with -ENODEV
- make mute_stream direction-aware so capture-only IV-sense streams
are unmuted and stopping playback no longer mutes a live capture
Link to v2: https://lore.kernel.org/linux-sound/20260711145722.188556-1-morf3089@gmail.com/
Link to v1: https://lore.kernel.org/linux-sound/20260709221331.989109-1-morf3089@gmail.com/
Gianluca Boiano (2):
dt-bindings: sound: add Texas Instruments TAS2557
ASoC: codecs: add TAS2557 mono smart amplifier driver
.../devicetree/bindings/sound/ti,tas2557.yaml | 174 +
MAINTAINERS | 1 +
sound/soc/codecs/Kconfig | 11 +
sound/soc/codecs/Makefile | 2 +
sound/soc/codecs/tas2557.c | 3067 +++++++++++++++++
sound/soc/codecs/tas2557.h | 228 ++
6 files changed, 3483 insertions(+)
create mode 100644 Documentation/devicetree/bindings/sound/ti,tas2557.yaml
create mode 100644 sound/soc/codecs/tas2557.c
create mode 100644 sound/soc/codecs/tas2557.h
base-commit: a13c140cc289c0b7b3770bce5b3ad42ab35074aa
--
2.55.0
^ permalink raw reply
* [PATCH v3 1/2] dt-bindings: sound: add Texas Instruments TAS2557
From: Gianluca Boiano @ 2026-07-17 23:33 UTC (permalink / raw)
To: Mark Brown, Liam Girdwood, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Jaroslav Kysela, Takashi Iwai
Cc: Gianluca Boiano, Shenghao Ding, Kevin Lu, Baojun Xu, Wang, Sen,
linux-sound, devicetree, linux-kernel
In-Reply-To: <20260717233402.414326-1-morf3089@gmail.com>
Document the TAS2557 mono Class-D smart amplifier with an integrated
DSP for speaker protection. The device is controlled over I2C and
receives audio over I2S/TDM (ASI). One node can describe either a
single mono device or, via a two-entry `reg`, a stereo pair sharing
one bus: the physical device at each `reg` index applies that half
(DEV_A/DEV_B) of a stereo-tuned firmware image, so no separate
channel-selection property is needed -- device order in `reg` is
the only configuration required, matching the convention already
used by ti,tas2781.yaml/ti,tas2563.yaml for the same TI firmware
toolchain. `ti,imon-slot-nos`/`ti,vmon-slot-nos` follow the same
per-index convention for the TDM sense-data slots. `reg` is capped
at two entries because the uCDSP firmware container only defines
mono or two-device (DEV_A/DEV_B) types -- there is no third slot to
extend into regardless of DT shape.
The on-chip DSP boots from a firmware image generated per speaker
design with the TI PurePath Console tuning tools; the optional
firmware-name property selects a board-specific image, shared by
every device in the group.
Addresses review from Krzysztof Kozlowski, who questioned a
per-node channel-selection property (an earlier ti,channel/
ti,audio-channel design) on the grounds that it doesn't scale and
isn't how other multi-device TI amplifiers in-tree solve the same
problem.
Signed-off-by: Gianluca Boiano <morf3089@gmail.com>
---
Changes in v3:
- single-node reg-array model (up to two devices), matching
ti,tas2781.yaml; drop ti,channel, derive DEV_A/DEV_B from reg order
- rename ti,imon-slot-no/ti,vmon-slot-no to the plural
ti,imon-slot-nos/ti,vmon-slot-nos arrays; firmware-name maxItems: 1
Changes in v2:
- add the four mandatory supplies to "required" (Wang Sen)
.../devicetree/bindings/sound/ti,tas2557.yaml | 174 ++++++++++++++++++
MAINTAINERS | 1 +
2 files changed, 175 insertions(+)
create mode 100644 Documentation/devicetree/bindings/sound/ti,tas2557.yaml
diff --git a/Documentation/devicetree/bindings/sound/ti,tas2557.yaml b/Documentation/devicetree/bindings/sound/ti,tas2557.yaml
new file mode 100644
index 000000000000..648ba605e0f4
--- /dev/null
+++ b/Documentation/devicetree/bindings/sound/ti,tas2557.yaml
@@ -0,0 +1,174 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+# Copyright (C) 2026 Gianluca Boiano <morf3089@gmail.com>
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/sound/ti,tas2557.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Texas Instruments TAS2557 Mono Smart Amplifier
+
+maintainers:
+ - Gianluca Boiano <morf3089@gmail.com>
+
+description: |
+ The TAS2557 is a mono, digital-input Class-D amplifier with an
+ integrated DSP for speaker protection. Audio is carried over I2S/TDM
+ (ASI). The on-chip DSP boots from a firmware image that is generated
+ for a specific speaker design using the TI PurePath Console tuning
+ tools.
+
+ A single node describes either one physical TAS2557, or up to two of
+ them sharing one ASI bus and one reset/IRQ pair, using the same
+ reg-array convention as
+ Documentation/devicetree/bindings/sound/ti,tas2781.yaml. When two
+ devices are listed, `reg` carries both I2C addresses and the
+ physical device at each index is implicitly assigned one half of a
+ stereo-tuned firmware image: the device at index 0 is DEV_A and the
+ device at index 1 is DEV_B. No explicit channel-selection property
+ exists or is needed -- the order of addresses in `reg` is the only
+ configuration required. `ti,imon-slot-nos` and `ti,vmon-slot-nos`
+ follow the same indexing: the item at array index N sets the TDM TX
+ slot used by the device at the Nth `reg` entry.
+
+ The uCDSP firmware format defines only the DEV_A and DEV_B device
+ types, so `reg` (and the two TDM slot properties) cannot list more
+ than two entries, regardless of how many physical speakers the board
+ has.
+
+ Datasheet: https://www.ti.com/lit/gpn/tas2557
+
+allOf:
+ - $ref: dai-common.yaml#
+
+properties:
+ compatible:
+ const: ti,tas2557
+
+ reg:
+ minItems: 1
+ maxItems: 2
+ description:
+ I2C address of each physical device sharing this node. One
+ entry configures a single mono TAS2557. Two entries configure a
+ device pair; the device at index 0 is DEV_A and the device at
+ index 1 is DEV_B.
+
+ '#sound-dai-cells':
+ const: 0
+
+ reset-gpios:
+ maxItems: 1
+ description: GPIO connected to the active-low RESET pin.
+
+ interrupts:
+ maxItems: 1
+ description: Fault interrupt output (INT).
+
+ vbat-supply:
+ description: Battery/boost input supply (VBAT), 2.9 V to 5.5 V.
+
+ iovdd-supply:
+ description: Digital I/O supply, 1.62 V to 3.6 V.
+
+ avdd-supply:
+ description: Analog supply, 1.65 V to 1.95 V.
+
+ dvdd-supply:
+ description: Digital core supply, 1.65 V to 1.95 V.
+
+ ti,imon-slot-nos:
+ $ref: /schemas/types.yaml#/definitions/uint32-array
+ minItems: 1
+ maxItems: 2
+ items:
+ minimum: 0
+ maximum: 7
+ description:
+ TDM TX time slot(s) for current-sense data, one entry per `reg`
+ entry. The item at array index N configures the device at the
+ Nth `reg` entry.
+
+ ti,vmon-slot-nos:
+ $ref: /schemas/types.yaml#/definitions/uint32-array
+ minItems: 1
+ maxItems: 2
+ items:
+ minimum: 0
+ maximum: 7
+ description:
+ TDM TX time slot(s) for voltage-sense data, one entry per `reg`
+ entry. The item at array index N configures the device at the
+ Nth `reg` entry.
+
+ firmware-name:
+ maxItems: 1
+ description: |
+ Name of the DSP firmware image to load. Defaults to a name
+ derived from the silicon revision. A device pair shares one
+ image; each device's half of it is selected by its `reg` index,
+ not by this property.
+
+required:
+ - compatible
+ - reg
+ - '#sound-dai-cells'
+ - vbat-supply
+ - iovdd-supply
+ - avdd-supply
+ - dvdd-supply
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ audio-codec@4c {
+ compatible = "ti,tas2557";
+ reg = <0x4c>;
+ #sound-dai-cells = <0>;
+ reset-gpios = <&tlmm 77 GPIO_ACTIVE_LOW>;
+ interrupt-parent = <&tlmm>;
+ interrupts = <73 IRQ_TYPE_LEVEL_HIGH>;
+
+ vbat-supply = <&vph_pwr>;
+ iovdd-supply = <&vreg_l6b_1p8>;
+ avdd-supply = <&vreg_l6b_1p8>;
+ dvdd-supply = <&vreg_l6b_1p8>;
+
+ ti,imon-slot-nos = <0>;
+ ti,vmon-slot-nos = <2>;
+ };
+ };
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ audio-codec@4c {
+ compatible = "ti,tas2557";
+ reg = <0x4c>, <0x4d>;
+ #sound-dai-cells = <0>;
+ reset-gpios = <&tlmm 77 GPIO_ACTIVE_LOW>;
+ interrupt-parent = <&tlmm>;
+ interrupts = <73 IRQ_TYPE_LEVEL_HIGH>;
+
+ vbat-supply = <&vph_pwr>;
+ iovdd-supply = <&vreg_l6b_1p8>;
+ avdd-supply = <&vreg_l6b_1p8>;
+ dvdd-supply = <&vreg_l6b_1p8>;
+
+ ti,imon-slot-nos = <0 1>;
+ ti,vmon-slot-nos = <2 3>;
+ firmware-name = "tas2557-stereo.bin";
+ };
+ };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index 806bd2d80d15..44ece19d669f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -26679,6 +26679,7 @@ M: Sen Wang <sen@ti.com>
L: linux-sound@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/sound/ti,tas2552.yaml
+F: Documentation/devicetree/bindings/sound/ti,tas2557.yaml
F: Documentation/devicetree/bindings/sound/ti,tas2562.yaml
F: Documentation/devicetree/bindings/sound/ti,tas2770.yaml
F: Documentation/devicetree/bindings/sound/ti,tas27xx.yaml
--
2.55.0
^ permalink raw reply related
* [PATCH v3 2/2] ASoC: codecs: add TAS2557 mono smart amplifier driver
From: Gianluca Boiano @ 2026-07-17 23:34 UTC (permalink / raw)
To: Mark Brown, Liam Girdwood, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Jaroslav Kysela, Takashi Iwai
Cc: Gianluca Boiano, Shenghao Ding, Kevin Lu, Baojun Xu, Wang, Sen,
linux-sound, devicetree, linux-kernel
In-Reply-To: <20260717233402.414326-1-morf3089@gmail.com>
The TAS2557 is a mono Class-D smart amplifier with an on-chip DSP for
speaker protection. Audio is received over I2S/TDM (ASI) and processed
by DSP firmware before reaching the Class-D output stage.
The register map is book/page indirected behind an 8-bit regmap; the
driver layers composite book/page/register addressing on top of it.
The TI "uCDSP" firmware format (PLL, program and configuration blocks)
is parsed with bounds checking, and program 0 with a sample-rate
matched configuration is applied whenever the amplifier powers up;
the device cannot pass audio without this firmware. Firmware images
are produced per speaker design with TI's PurePath Console tuning
tools, so they are board specific; the default image name is derived
from the detected silicon revision and can be overridden with the
firmware-name device tree property.
A node manages 1 or 2 physical chips (`reg` cardinality), mirroring
the multi-device architecture already used by tas2781.c/tas2781-fmwlib.c
for the same TI firmware toolchain: one shared regmap retargets its
I2C address per device before each transfer (tas2557_change_book_page()),
one shared firmware request serves the whole group, and per-device
DSP tuning (DEV_A/DEV_B) is dispatched purely by device index -- no
channel-selection devicetree property exists or is needed. All
per-device state (book/page cache, program/config indices, ASI
sense-slot assignment) lives in a per-device array; group-level state
(firmware, DAI format, TDM configuration, power/mute state) stays
singular, since one DAI and one firmware image drive the whole group.
Amplifier power is managed through DAPM. A fault interrupt handler
polls every device's fault registers and recovers the group from
clock loss, over-current, brownout and over-temperature events by
reloading the firmware configuration. Speaker volume and I/V-sense
switches are exposed as ALSA controls, applied uniformly across the
group. IRQ, reset and supply lines are shared across the group,
matching common board wiring where both chips are strapped to the
same GPIOs and rails.
Addresses review from Krzysztof Kozlowski (see the binding commit)
by replacing the earlier per-node ti,channel/ti,audio-channel
property with device-index dispatch, matching TAS2781's established
convention instead of inventing a new one.
Signed-off-by: Gianluca Boiano <morf3089@gmail.com>
---
Changes in v3:
- rework to a single-node, reg-array driver managing up to two
devices (per-device book/page/program/config state, group DAI/IRQ)
- derive the ASI word length from params_width() and re-apply it
after each reset instead of hardcoding 32-bit
- set symmetric_rate on the DAI (Mark Brown), and symmetric_sample_bits
(playback and capture share the single ASI word length)
- commit the DAC-gain cache only after the hardware write succeeds
(Mark Brown), and apply the same ordering to the ISENSE/VSENSE
mixer setters
- default the ASI data-slot offset to 0; explicit
snd_soc_dai_set_tdm_slot() still takes precedence
Changes in v2:
- propagate hw_params() register-write failures (Wang Sen)
- return IRQ_NONE when the device was not in a state to raise the
fault IRQ, for correct shared-IRQ accounting (Mark Brown)
- re-apply cached runtime state after every reset, including fault
recovery (Mark Brown)
- honour explicit snd_soc_dai_set_tdm_slot(); restore TX slot
offsets after reset
- wait for the async firmware load on first stream start
- make mute_stream direction-aware
sound/soc/codecs/Kconfig | 11 +
sound/soc/codecs/Makefile | 2 +
sound/soc/codecs/tas2557.c | 3067 ++++++++++++++++++++++++++++++++++++
sound/soc/codecs/tas2557.h | 228 +++
4 files changed, 3308 insertions(+)
create mode 100644 sound/soc/codecs/tas2557.c
create mode 100644 sound/soc/codecs/tas2557.h
diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig
index 252f683be3c1..cf56b554dcca 100644
--- a/sound/soc/codecs/Kconfig
+++ b/sound/soc/codecs/Kconfig
@@ -268,6 +268,7 @@ config SND_SOC_ALL_CODECS
imply SND_SOC_STI_SAS
imply SND_SOC_TAC5XX2_SDW
imply SND_SOC_TAS2552
+ imply SND_SOC_TAS2557
imply SND_SOC_TAS2562
imply SND_SOC_TAS2764
imply SND_SOC_TAS2770
@@ -2167,6 +2168,16 @@ config SND_SOC_TAS2552
tristate "Texas Instruments TAS2552 Mono Audio amplifier"
depends on I2C
+config SND_SOC_TAS2557
+ tristate "Texas Instruments TAS2557 Mono Smart Amplifier"
+ depends on I2C
+ select REGMAP_I2C
+ help
+ Enable support for the Texas Instruments TAS2557 mono smart
+ amplifier with integrated DSP. The on-chip DSP requires firmware
+ (tas2557_uCDSP.bin) to be present in the firmware search path;
+ audio cannot pass through the device without it.
+
config SND_SOC_TAS2562
tristate "Texas Instruments TAS2562 Mono Audio amplifier"
depends on I2C
diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile
index aa0396e5b575..b8a2f639a24d 100644
--- a/sound/soc/codecs/Makefile
+++ b/sound/soc/codecs/Makefile
@@ -433,6 +433,7 @@ snd-soc-max98504-y := max98504.o
snd-soc-simple-amplifier-y := simple-amplifier.o
snd-soc-tpa6130a2-y := tpa6130a2.o
snd-soc-tas2552-y := tas2552.o
+snd-soc-tas2557-y := tas2557.o
snd-soc-tas2562-y := tas2562.o
snd-soc-tas2764-y := tas2764.o
snd-soc-tas2780-y := tas2780.o
@@ -754,6 +755,7 @@ obj-$(CONFIG_SND_SOC_STAC9766) += snd-soc-stac9766.o
obj-$(CONFIG_SND_SOC_STI_SAS) += snd-soc-sti-sas.o
obj-$(CONFIG_SND_SOC_TAC5XX2_SDW) += snd-soc-tac5xx2-sdw.o
obj-$(CONFIG_SND_SOC_TAS2552) += snd-soc-tas2552.o
+obj-$(CONFIG_SND_SOC_TAS2557) += snd-soc-tas2557.o
obj-$(CONFIG_SND_SOC_TAS2562) += snd-soc-tas2562.o
obj-$(CONFIG_SND_SOC_TAS2764) += snd-soc-tas2764.o
obj-$(CONFIG_SND_SOC_TAS2780) += snd-soc-tas2780.o
diff --git a/sound/soc/codecs/tas2557.c b/sound/soc/codecs/tas2557.c
new file mode 100644
index 000000000000..ffbab1e5e0de
--- /dev/null
+++ b/sound/soc/codecs/tas2557.c
@@ -0,0 +1,3067 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// ALSA SoC Texas Instruments TAS2557 Smart Amplifier
+//
+// Copyright (C) 2016 Texas Instruments Inc.
+// Copyright (C) 2026 Gianluca Boiano <morf3089@gmail.com>
+//
+// The TAS2557 requires DSP firmware (tas2557_uCDSP.bin for PG2.x,
+// tas2557_pg1p0_uCDSP.bin for PG1.0) to route audio through its internal
+// DSP and DAC. Without loaded firmware the device produces no audio.
+//
+// Probe reads REV_PGID to detect PG version, then requests firmware
+// asynchronously. Every call to tas2557_enable() re-applies the firmware
+// program + config (firmware-driven PLL) before powering the Class-D stage.
+//
+// One DT node/i2c_client drives an array of up to TAS2557_MAX_DEV physical
+// TAS2557 chips (struct tas2557_priv::devs[]), all sharing one regmap by
+// swapping the i2c_client's address before each access (see
+// tas2557_change_book_page()) -- mirroring TI's own tas2781/tas2563
+// grouped-device driver instead of instantiating one driver per chip.
+// Firmware, DAI, power and mute state stay singular for the whole group;
+// only register-level state (book/page, ASI slot offsets, DSP program
+// bookkeeping) is tracked per physical chip.
+
+#include <linux/module.h>
+#include <linux/i2c.h>
+#include <linux/gpio/consumer.h>
+#include <linux/regmap.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/delay.h>
+#include <linux/firmware.h>
+#include <linux/interrupt.h>
+#include <linux/regulator/consumer.h>
+#include <linux/completion.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/unaligned.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+#include <sound/soc-dapm.h>
+#include <sound/tlv.h>
+
+#include "tas2557.h"
+
+/* Special delay markers embedded in register sequence tables */
+#define TAS2557_UDELAY 0xFFFFFFFE
+#define TAS2557_MDELAY 0xFFFFFFFD
+
+/* Supported PCM formats exposed to the ASoC core */
+#define TAS2557_FORMATS \
+ (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | \
+ SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE)
+
+/* struct tas2557_block - one block of register-write commands from firmware */
+struct tas2557_block {
+ u32 type;
+ u32 num_commands;
+ u8 *data; /* num_commands * 4 bytes: [book][page][reg][val] */
+};
+
+/* struct tas2557_data - named container of firmware blocks */
+struct tas2557_data {
+ char name[64];
+ u32 num_blocks;
+ struct tas2557_block *blocks;
+};
+
+/* struct tas2557_pll - one PLL configuration */
+struct tas2557_pll {
+ char name[64];
+ struct tas2557_block block;
+};
+
+/* struct tas2557_program - one DSP program */
+struct tas2557_program {
+ char name[64];
+ struct tas2557_data data;
+};
+
+/* struct tas2557_config - one audio configuration (tied to a sample rate) */
+struct tas2557_config {
+ char name[64];
+ u32 program; /* which program this config is for */
+ u32 pll; /* which PLL config to use */
+ u32 sample_rate;
+ struct tas2557_data data;
+};
+
+/* struct tas2557_firmware - top-level parsed firmware container */
+struct tas2557_firmware {
+ u32 ppc_version;
+ u32 fw_version;
+ u32 driver_version;
+ u32 device;
+ u32 num_plls;
+ struct tas2557_pll *plls;
+ u32 num_programs;
+ struct tas2557_program *programs;
+ u32 num_configs;
+ struct tas2557_config *configs;
+};
+
+/*
+ * struct tas2557_dev - per-physical-chip state
+ *
+ * One instance per entry in the DT `reg` array (see tas2557_parse_dt()).
+ */
+struct tas2557_dev {
+ unsigned int addr;
+
+ /* Book/page tracking for this chip's book-paged regmap access */
+ unsigned char current_book;
+ unsigned char current_page;
+
+ /* Hardware revision of this chip */
+ int pg_id;
+
+ /*
+ * DSP program/config currently loaded and recovery-attempt count.
+ * tas2557_set_program() always drives every devs[] entry to the
+ * same program/config together (one shared firmware image), so
+ * these stay in lock-step across the group; devs[0] is read back
+ * as the group's representative value.
+ */
+ unsigned int current_program;
+ unsigned int current_config;
+ unsigned int restart_count;
+
+ /* DT-configured audio parameters (ti,imon-slot-nos / ti,vmon-slot-nos) */
+ unsigned int imon_slot;
+ unsigned int vmon_slot;
+
+ /*
+ * Cached ASI data-slot offsets in BCLK cycles for this chip.
+ * Computed in hw_params()/set_tdm_slot() and re-applied by
+ * tas2557_apply_runtime_state() after the SW reset in
+ * tas2557_set_program() wipes the ASI offset registers.
+ */
+ unsigned int asi_offset; /* RX (playback) data-slot offset */
+ int asi_tx_offset; /* TX (capture) offset, -1 = never set */
+};
+
+/* struct tas2557_priv - group driver state (one node, up to TAS2557_MAX_DEV chips) */
+struct tas2557_priv {
+ struct device *dev;
+ struct regmap *regmap;
+ struct mutex dev_lock; /* protects device register I/O and book/page state */
+ struct mutex lock; /* protects all other driver state below */
+
+ struct gpio_desc *reset_gpio;
+ int irq;
+ bool irq_enabled;
+
+ /* Physical chips managed by this node */
+ struct tas2557_dev devs[TAS2557_MAX_DEV];
+ unsigned int ndev;
+
+ bool isense_enabled;
+ bool vsense_enabled;
+
+ /* Power state */
+ bool powered;
+ bool dac_muted; /* mute state for SNDRV_PCM_STREAM_PLAYBACK */
+ bool sense_muted; /* mute state for SNDRV_PCM_STREAM_CAPTURE */
+
+ /* Firmware state -- one shared image for the whole group */
+ struct tas2557_firmware *fw; /* NULL until tas2557_fw_ready() parses it */
+ char fw_name[64]; /* filename requested at probe */
+ bool fw_requested; /* request_firmware_nowait() call is pending */
+ struct completion fw_done; /* signalled when tas2557_fw_ready() returns */
+
+ /* Runtime audio configuration */
+ unsigned int sample_rate;
+ int dai_fmt; /* ASI format from set_dai_fmt(), -1 = never set */
+ unsigned int wordlength; /* ASI WORDLENGTH_* bits from hw_params */
+
+ int tdm_slot_width;
+ bool tdm_configured; /* true once set_tdm_slot() set an RX slot */
+ /*
+ * RX slot index from set_tdm_slot(), -1 = never set (mono, or a
+ * stereo board whose machine driver hasn't configured TDM yet).
+ * Informational only: this group has exactly one DAI, so every
+ * devs[] entry shares the same TDM slot; which firmware half
+ * (DEV_A/DEV_B) each chip receives is selected by its index in
+ * devs[]/the DT `reg` array instead (see tas2557_block_chn_range()).
+ */
+ int tdm_rx_slot;
+
+ /* Volume control */
+ unsigned int dac_gain;
+};
+
+/* Forward declarations */
+static void tas2557_hw_reset(struct tas2557_priv *tas2557);
+static int tas2557_failsafe_recovery(struct tas2557_priv *tas2557);
+static int tas2557_apply_runtime_state(struct tas2557_priv *tas2557);
+
+/*
+ * Default device initialization sequence - identical to the TI Android
+ * driver (p_tas2557_default_data). Written once after every
+ * hardware/software reset.
+ */
+static const unsigned int tas2557_default_data[] = {
+ TAS2557_SAR_ADC2_REG, 0x05, /* enable SAR ADC */
+ TAS2557_CLK_ERR_CTRL2, 0x21, /* clk1: hysteresis 0.34 ms */
+ TAS2557_CLK_ERR_CTRL3, 0x21, /* clk2: rampDown 15 dB/us */
+ TAS2557_SAFE_GUARD_REG, TAS2557_SAFE_GUARD_PATTERN,
+ 0xFFFFFFFF, 0xFFFFFFFF
+};
+
+/*
+ * Interrupt output configuration - identical to the TI reference
+ * (p_tas2557_irq_config). Reloaded after every reset.
+ */
+static const unsigned int tas2557_irq_config[] = {
+ TAS2557_CLK_HALT_REG, 0x71, /* enable clk-halt detect2 interrupt */
+ TAS2557_INT_GEN1_REG, 0x11, /* enable SPK OC and OV */
+ TAS2557_INT_GEN2_REG, 0x11, /* enable clk-err1 and die OT */
+ TAS2557_INT_GEN3_REG, 0x11, /* enable clk-err2 and brownout */
+ TAS2557_INT_GEN4_REG, 0x01, /* disable SAR, enable clk-halt */
+ TAS2557_GPIO4_PIN_REG, 0x07, /* GPIO4 = INT1 output */
+ TAS2557_INT_MODE_REG, 0x80, /* active-high, held until cleared */
+ 0xFFFFFFFF, 0xFFFFFFFF
+};
+
+/*
+ * Startup sequence - power up Class-D, Boost, DSP/PLL clocks.
+ * The ASI2 pin/divider setup (GPIO5-8) is specific to this driver's
+ * dual-amp stereo board topology, not part of TI's reference sequence;
+ * both ASI interfaces are enabled and configured identically, and the
+ * board wiring determines which one carries audio.
+ * PLL configuration is NOT done here; it comes from the firmware PLL
+ * block loaded by tas2557_set_program() before this sequence runs.
+ */
+static const unsigned int tas2557_startup_data[] = {
+ TAS2557_GPI_PIN_REG, 0x15, /* enable DIN, MCLK, CCI */
+ TAS2557_GPIO1_PIN_REG, 0x01, /* enable BCLK (ASI1) */
+ TAS2557_GPIO2_PIN_REG, 0x01, /* enable WCLK (ASI1) */
+ /* ASI2 GPIO wiring; arm the BCLK source (GPIO5) before its dividers */
+ TAS2557_GPIO6_PIN_REG, 0x01, /* GPIO6 = ASI2 WCLK input */
+ TAS2557_GPIO8_PIN_REG, 0x02, /* GPIO8 = ASI2 DIN */
+ TAS2557_GPIO5_PIN_REG, 0x01, /* GPIO5 = ASI2 BCLK input */
+ TAS2557_ASI2_BDIV_CLK_SEL_REG, 0x01,
+ TAS2557_ASI2_BDIV_CLK_RATIO_REG, 0x01,
+ TAS2557_ASI2_BDIV_CLK_RATIO_REG, 0x81, /* power up BDIV */
+ TAS2557_ASI2_WDIV_CLK_RATIO_REG, 0x40,
+ TAS2557_ASI2_WDIV_CLK_RATIO_REG, 0xc0, /* power up WDIV */
+ TAS2557_GPIO7_PIN_REG, 0x15, /* GPIO7 = ASI2 DOUT */
+ /* Power sequencing: Class-D + Boost first, then DSP/PLL */
+ TAS2557_POWER_CTRL2_REG, 0xA0, /* Class-D, Boost power up */
+ TAS2557_POWER_CTRL2_REG, 0xA3, /* Class-D, Boost, IV-sense power up */
+ TAS2557_POWER_CTRL1_REG, 0xF8, /* PLL, DSP, clock dividers power up */
+ TAS2557_UDELAY, 2000, /* 2 ms stabilisation */
+ TAS2557_CLK_ERR_CTRL, 0x2b, /* enable clock error detection */
+ TAS2557_DBOOST_CFG_REG, 0x0b, /* reduce full-band noise */
+ 0xFFFFFFFF, 0xFFFFFFFF
+};
+
+/* Unmute sequence */
+static const unsigned int tas2557_unmute_data[] = {
+ TAS2557_MUTE_REG, 0x00, /* unmute Class-D + ISENSE */
+ TAS2557_SOFT_MUTE_REG, 0x00, /* clear soft-mute in DSP */
+ 0xFFFFFFFF, 0xFFFFFFFF
+};
+
+/* Shutdown sequence */
+static const unsigned int tas2557_shutdown_data[] = {
+ TAS2557_CLK_ERR_CTRL, 0x00, /* disable clock error detection */
+ TAS2557_SOFT_MUTE_REG, 0x01, /* soft mute */
+ TAS2557_UDELAY, 10000, /* 10 ms */
+ TAS2557_MUTE_REG, 0x03, /* hard mute */
+ TAS2557_POWER_CTRL1_REG, 0x60, /* DSP power down */
+ TAS2557_UDELAY, 2000, /* 2 ms */
+ TAS2557_POWER_CTRL2_REG, 0x00, /* Class-D, Boost power down */
+ TAS2557_POWER_CTRL1_REG, 0x00, /* all power down */
+ /* Disable ASI2 GPIOs */
+ TAS2557_GPIO5_PIN_REG, 0x00,
+ TAS2557_GPIO6_PIN_REG, 0x00,
+ TAS2557_GPIO7_PIN_REG, 0x00,
+ TAS2557_GPIO8_PIN_REG, 0x00,
+ TAS2557_GPIO1_PIN_REG, 0x00,
+ TAS2557_GPIO2_PIN_REG, 0x00,
+ TAS2557_GPI_PIN_REG, 0x00,
+ 0xFFFFFFFF, 0xFFFFFFFF
+};
+
+/*
+ * regmap configuration
+ *
+ * Book/page switching is handled manually (see below); regmap only sees
+ * the raw 7-bit page-register address, with caching disabled because the
+ * TAS2557 firmware can update registers autonomously.
+ */
+static const struct regmap_config tas2557_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .cache_type = REGCACHE_NONE,
+ .max_register = 0x7f,
+};
+
+/*
+ * Book / page / register access
+ *
+ * The TAS2557 address space uses a book + page prefix. The regmap is
+ * configured for 8-bit register width; book/page selection is done by
+ * writing to TAS2557_BOOK_REG (0x7f) and TAS2557_PAGE_REG (0x00).
+ *
+ * All helpers take a composite register address encoded as
+ * TAS2557_REG(book, page, reg) = book*256*128 + page*128 + reg
+ * plus @chn, the index into tas2557->devs[] to address.
+ */
+
+/*
+ * tas2557_change_book_page - switch book/page, retargeting the chip first
+ * @chn: index into tas2557->devs[]
+ *
+ * All chips in the group share one i2c_client/regmap; redirecting register
+ * I/O to devs[chn] is done by mutating client->addr immediately before the
+ * access, the same trick as tasdevice_change_chn_book() in
+ * tas2781-comlib-i2c.c. Just like that function, an address switch forces
+ * an immediate page-0 write: the devs[chn].current_page this function
+ * tracks describes what *that chip's* page register last was, but nothing
+ * has re-verified that since the bus was last aimed at devs[chn] -- another
+ * chip's own register writes in between never touch devs[chn]'s silicon, so
+ * the cached value is still correct, but forcing page 0 here removes any
+ * doubt and keeps every subsequent book switch starting from a known page,
+ * exactly as tasdevice_change_chn_book() does before checking cur_book.
+ */
+static int tas2557_change_book_page(struct tas2557_priv *tas2557,
+ unsigned int chn,
+ unsigned char book, unsigned char page)
+{
+ struct i2c_client *client = to_i2c_client(tas2557->dev);
+ struct tas2557_dev *dv = &tas2557->devs[chn];
+ int ret;
+
+ if (client->addr != dv->addr) {
+ client->addr = dv->addr;
+
+ ret = regmap_write(tas2557->regmap, TAS2557_PAGE_REG, 0);
+ if (ret < 0) {
+ dev_err(tas2557->dev,
+ "chn %u: page-0 switch after addr change failed: %d\n",
+ chn, ret);
+ return ret;
+ }
+ dv->current_page = 0;
+ }
+
+ if (dv->current_book == book && dv->current_page == page)
+ return 0;
+
+ if (dv->current_book != book) {
+ /* Always switch to page 0 before changing books */
+ ret = regmap_write(tas2557->regmap, TAS2557_PAGE_REG, 0);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "chn %u: page-0 switch failed: %d\n",
+ chn, ret);
+ return ret;
+ }
+ dv->current_page = 0;
+
+ ret = regmap_write(tas2557->regmap, TAS2557_BOOK_REG, book);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "chn %u: book switch to %u failed: %d\n",
+ chn, book, ret);
+ return ret;
+ }
+ dv->current_book = book;
+ }
+
+ if (dv->current_page != page) {
+ ret = regmap_write(tas2557->regmap, TAS2557_PAGE_REG, page);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "chn %u: page switch to %u failed: %d\n",
+ chn, page, ret);
+ return ret;
+ }
+ dv->current_page = page;
+ }
+
+ return 0;
+}
+
+static int tas2557_dev_read(struct tas2557_priv *tas2557, unsigned int chn,
+ unsigned int reg, unsigned int *value)
+{
+ int ret;
+
+ mutex_lock(&tas2557->dev_lock);
+ ret = tas2557_change_book_page(tas2557, chn, TAS2557_BOOK_ID(reg),
+ TAS2557_PAGE_ID(reg));
+ if (ret < 0)
+ goto out;
+
+ ret = regmap_read(tas2557->regmap, TAS2557_PAGE_REG_ADDR(reg), value);
+ if (ret < 0)
+ dev_err(tas2557->dev, "chn %u: read reg 0x%06x failed: %d\n",
+ chn, reg, ret);
+out:
+ mutex_unlock(&tas2557->dev_lock);
+ return ret;
+}
+
+static int tas2557_dev_write(struct tas2557_priv *tas2557, unsigned int chn,
+ unsigned int reg, unsigned int value)
+{
+ int ret;
+
+ mutex_lock(&tas2557->dev_lock);
+ ret = tas2557_change_book_page(tas2557, chn, TAS2557_BOOK_ID(reg),
+ TAS2557_PAGE_ID(reg));
+ if (ret < 0)
+ goto out;
+
+ ret = regmap_write(tas2557->regmap, TAS2557_PAGE_REG_ADDR(reg), value);
+ if (ret < 0)
+ dev_err(tas2557->dev, "chn %u: write reg 0x%06x = 0x%02x failed: %d\n",
+ chn, reg, value, ret);
+out:
+ mutex_unlock(&tas2557->dev_lock);
+ return ret;
+}
+
+static int tas2557_dev_update_bits(struct tas2557_priv *tas2557, unsigned int chn,
+ unsigned int reg, unsigned int mask,
+ unsigned int value)
+{
+ int ret;
+
+ mutex_lock(&tas2557->dev_lock);
+ ret = tas2557_change_book_page(tas2557, chn, TAS2557_BOOK_ID(reg),
+ TAS2557_PAGE_ID(reg));
+ if (ret < 0)
+ goto out;
+
+ ret = regmap_update_bits(tas2557->regmap,
+ TAS2557_PAGE_REG_ADDR(reg), mask, value);
+ if (ret < 0)
+ dev_err(tas2557->dev, "chn %u: update_bits reg 0x%06x failed: %d\n",
+ chn, reg, ret);
+out:
+ mutex_unlock(&tas2557->dev_lock);
+ return ret;
+}
+
+static int tas2557_dev_bulk_write(struct tas2557_priv *tas2557, unsigned int chn,
+ unsigned int reg, const u8 *data, size_t len)
+{
+ int ret;
+
+ /* The chip auto-increments past reg 0x7f into the page register */
+ if (TAS2557_PAGE_REG_ADDR(reg) + len > 128)
+ return -EINVAL;
+
+ mutex_lock(&tas2557->dev_lock);
+ ret = tas2557_change_book_page(tas2557, chn, TAS2557_BOOK_ID(reg),
+ TAS2557_PAGE_ID(reg));
+ if (ret < 0)
+ goto out;
+
+ ret = regmap_bulk_write(tas2557->regmap,
+ TAS2557_PAGE_REG_ADDR(reg), data, len);
+ if (ret < 0)
+ dev_err(tas2557->dev, "chn %u: bulk_write reg 0x%06x failed: %d\n",
+ chn, reg, ret);
+out:
+ mutex_unlock(&tas2557->dev_lock);
+ return ret;
+}
+
+/*
+ * tas2557_load_data - walk a null-terminated {reg,val} sequence table for
+ * one chip.
+ *
+ * Special reg values TAS2557_UDELAY and TAS2557_MDELAY insert delays;
+ * 0xFFFFFFFF terminates.
+ */
+static int tas2557_load_data(struct tas2557_priv *tas2557, unsigned int chn,
+ const unsigned int *data)
+{
+ unsigned int reg, val;
+ int i = 0, ret = 0;
+
+ while (1) {
+ reg = data[i * 2];
+ val = data[i * 2 + 1];
+
+ if (reg == 0xFFFFFFFF)
+ break;
+
+ if (reg == TAS2557_UDELAY) {
+ usleep_range(val, val + 100);
+ } else if (reg == TAS2557_MDELAY) {
+ msleep(val);
+ } else {
+ ret = tas2557_dev_write(tas2557, chn, reg, val);
+ if (ret < 0)
+ break;
+ }
+ i++;
+ }
+
+ return ret;
+}
+
+/*
+ * tas2557_load_data_all - apply a {reg,val} sequence table to every chip
+ * in the group. Used for the group-uniform tables above (defaults, IRQ
+ * config, startup/unmute/shutdown sequences): every chip gets identical
+ * values, but each chip's own registers still need their own write.
+ */
+static int tas2557_load_data_all(struct tas2557_priv *tas2557,
+ const unsigned int *data)
+{
+ unsigned int chn;
+ int ret;
+
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ ret = tas2557_load_data(tas2557, chn, data);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+/* tas2557_write_all - write the same register/value to every chip */
+static int tas2557_write_all(struct tas2557_priv *tas2557,
+ unsigned int reg, unsigned int value)
+{
+ unsigned int chn;
+ int ret;
+
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ ret = tas2557_dev_write(tas2557, chn, reg, value);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+/* tas2557_update_bits_all - update_bits the same register/mask/value on every chip */
+static int tas2557_update_bits_all(struct tas2557_priv *tas2557,
+ unsigned int reg, unsigned int mask,
+ unsigned int value)
+{
+ unsigned int chn;
+ int ret;
+
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ ret = tas2557_dev_update_bits(tas2557, chn, reg, mask, value);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+/* =========================================================================
+ * Firmware parsing
+ *
+ * Format (all multi-byte fields are big-endian):
+ * 4 bytes magic 0x35 0x35 0x35 0x32 ("5552")
+ * 4 bytes size
+ * 4 bytes checksum
+ * 4 bytes ppc_version
+ * 4 bytes fw_version
+ * 4 bytes driver_version
+ * 4 bytes timestamp
+ * 64 bytes ddc_name (null-padded)
+ * N bytes description (null-terminated string)
+ * 4 bytes device_family (must be 0)
+ * 4 bytes device (2 = mono, 3 = stereo)
+ * then PLLs, programs, configs
+ *
+ * The on-disk layout follows the TI reference firmware format. Fields
+ * that the driver does not need at runtime (size, checksum, timestamp,
+ * ddc_name, description, per-block checksums, ...) are consumed from the
+ * byte stream to keep the parser in sync but are not stored.
+ * =========================================================================
+ */
+
+static void tas2557_fw_free(struct tas2557_firmware *fw)
+{
+ unsigned int i, j;
+
+ if (!fw)
+ return;
+
+ if (fw->plls) {
+ for (i = 0; i < fw->num_plls; i++)
+ kfree(fw->plls[i].block.data);
+ kfree(fw->plls);
+ }
+
+ if (fw->programs) {
+ for (i = 0; i < fw->num_programs; i++) {
+ if (fw->programs[i].data.blocks) {
+ for (j = 0; j < fw->programs[i].data.num_blocks; j++)
+ kfree(fw->programs[i].data.blocks[j].data);
+ kfree(fw->programs[i].data.blocks);
+ }
+ }
+ kfree(fw->programs);
+ }
+
+ if (fw->configs) {
+ for (i = 0; i < fw->num_configs; i++) {
+ if (fw->configs[i].data.blocks) {
+ for (j = 0; j < fw->configs[i].data.num_blocks; j++)
+ kfree(fw->configs[i].data.blocks[j].data);
+ kfree(fw->configs[i].data.blocks);
+ }
+ }
+ kfree(fw->configs);
+ }
+
+ kfree(fw);
+}
+
+static int fw_parse_header(struct tas2557_priv *tas2557,
+ struct tas2557_firmware *fw,
+ const u8 *data, size_t size)
+{
+ const u8 *start = data;
+ size_t desc_len;
+ u32 device_family;
+ char ddc_name[64];
+
+ if (size < 104) {
+ dev_err(tas2557->dev, "firmware header too short (%zu bytes)\n",
+ size);
+ return -EINVAL;
+ }
+
+ /* Magic: four bytes "5552" (0x35 0x35 0x35 0x32) */
+ if (data[0] != 0x35 || data[1] != 0x35 ||
+ data[2] != 0x35 || data[3] != 0x32) {
+ dev_err(tas2557->dev,
+ "firmware magic mismatch: %02x%02x%02x%02x (expected 35353532)\n",
+ data[0], data[1], data[2], data[3]);
+ return -EINVAL;
+ }
+ data += 4;
+
+ /* size, checksum: not needed at runtime, skip */
+ data += 8;
+
+ fw->ppc_version = get_unaligned_be32(data); data += 4;
+ fw->fw_version = get_unaligned_be32(data); data += 4;
+ fw->driver_version = get_unaligned_be32(data); data += 4;
+
+ /* timestamp: not needed at runtime, skip */
+ data += 4;
+
+ memcpy(ddc_name, data, 64);
+ ddc_name[63] = '\0';
+ data += 64;
+
+ desc_len = strnlen(data, size - (size_t)(data - start));
+ if (desc_len >= size - (size_t)(data - start)) {
+ dev_err(tas2557->dev, "firmware header: description not NUL-terminated\n");
+ return -EINVAL;
+ }
+ /* description string: informational only, not stored */
+ data += desc_len + 1;
+
+ if ((data - start) + 8 > (ptrdiff_t)size) {
+ dev_err(tas2557->dev, "firmware header truncated\n");
+ return -EINVAL;
+ }
+
+ device_family = get_unaligned_be32(data); data += 4;
+ if (device_family != 0) {
+ dev_err(tas2557->dev, "unsupported device family: %u\n",
+ device_family);
+ return -EINVAL;
+ }
+
+ fw->device = get_unaligned_be32(data); data += 4;
+ if (fw->device != TAS2557_FW_DEVICE_MONO &&
+ fw->device != TAS2557_FW_DEVICE_STEREO) {
+ dev_err(tas2557->dev,
+ "unsupported device %u in firmware (expected 2=mono or 3=stereo)\n",
+ fw->device);
+ return -EINVAL;
+ }
+
+ dev_dbg(tas2557->dev,
+ "firmware header: DDC=%s fw_ver=0x%x ppc=0x%x drv=0x%x\n",
+ ddc_name, fw->fw_version, fw->ppc_version, fw->driver_version);
+
+ return data - start;
+}
+
+static int fw_parse_block(struct tas2557_priv *tas2557,
+ struct tas2557_firmware *fw,
+ struct tas2557_block *block,
+ const u8 *data, size_t remaining)
+{
+ const u8 *start = data;
+ size_t data_len;
+
+ if (remaining < 4)
+ return -EINVAL;
+
+ block->type = get_unaligned_be32(data);
+ data += 4;
+ remaining -= 4;
+
+ if (fw->driver_version >= PPC_DRIVER_CRCCHK) {
+ /* Per-block checksum bytes (pchksum/ychksum): not used, skip */
+ if (remaining < 4)
+ return -EINVAL;
+ data += 4;
+ remaining -= 4;
+ }
+
+ if (remaining < 4)
+ return -EINVAL;
+
+ block->num_commands = get_unaligned_be32(data);
+ data += 4;
+ remaining -= 4;
+
+ /*
+ * num_commands comes straight from the firmware file; bound it
+ * against the bytes left before multiplying so the size cannot wrap.
+ */
+ if (block->num_commands > remaining / 4) {
+ dev_err(tas2557->dev,
+ "block type 0x%x truncated: %u cmds, %zu bytes left\n",
+ block->type, block->num_commands, remaining);
+ return -EINVAL;
+ }
+ data_len = (size_t)block->num_commands * 4;
+
+ if (data_len > 0) {
+ block->data = kmemdup(data, data_len, GFP_KERNEL);
+ if (!block->data)
+ return -ENOMEM;
+ }
+ data += data_len;
+
+ return data - start;
+}
+
+static int fw_parse_data(struct tas2557_priv *tas2557,
+ struct tas2557_firmware *fw,
+ struct tas2557_data *img_data,
+ const u8 *data, size_t remaining)
+{
+ const u8 *start = data;
+ size_t desc_len;
+ unsigned int i;
+ int ret;
+
+ if (remaining < 64)
+ return -EINVAL;
+
+ memcpy(img_data->name, data, 64);
+ img_data->name[63] = '\0';
+ data += 64;
+ remaining -= 64;
+
+ desc_len = strnlen(data, remaining);
+ if (desc_len >= remaining)
+ return -EINVAL;
+ /* description string: informational only, not stored */
+ data += desc_len + 1;
+ remaining -= desc_len + 1;
+
+ if (remaining < 2)
+ return -EINVAL;
+
+ img_data->num_blocks = get_unaligned_be16(data);
+ data += 2;
+ remaining -= 2;
+
+ if (img_data->num_blocks == 0)
+ return data - start;
+
+ img_data->blocks = kcalloc(img_data->num_blocks,
+ sizeof(struct tas2557_block), GFP_KERNEL);
+ if (!img_data->blocks)
+ return -ENOMEM;
+
+ for (i = 0; i < img_data->num_blocks; i++) {
+ ret = fw_parse_block(tas2557, fw, &img_data->blocks[i],
+ data, remaining);
+ if (ret < 0)
+ return ret;
+ data += ret;
+ remaining -= ret;
+ }
+
+ return data - start;
+}
+
+static int fw_parse_plls(struct tas2557_priv *tas2557,
+ struct tas2557_firmware *fw,
+ const u8 *data, size_t remaining)
+{
+ const u8 *start = data;
+ size_t desc_len;
+ unsigned int i;
+ int ret;
+
+ if (remaining < 2)
+ return -EINVAL;
+
+ fw->num_plls = get_unaligned_be16(data);
+ data += 2;
+ remaining -= 2;
+
+ if (fw->num_plls == 0)
+ return data - start;
+
+ fw->plls = kcalloc(fw->num_plls, sizeof(struct tas2557_pll),
+ GFP_KERNEL);
+ if (!fw->plls)
+ return -ENOMEM;
+
+ for (i = 0; i < fw->num_plls; i++) {
+ if (remaining < 64)
+ return -EINVAL;
+
+ memcpy(fw->plls[i].name, data, 64);
+ fw->plls[i].name[63] = '\0';
+ data += 64;
+ remaining -= 64;
+
+ desc_len = strnlen(data, remaining);
+ if (desc_len >= remaining)
+ return -EINVAL;
+ data += desc_len + 1;
+ remaining -= desc_len + 1;
+
+ ret = fw_parse_block(tas2557, fw, &fw->plls[i].block,
+ data, remaining);
+ if (ret < 0)
+ return ret;
+ data += ret;
+ remaining -= ret;
+ }
+
+ return data - start;
+}
+
+static int fw_parse_programs(struct tas2557_priv *tas2557,
+ struct tas2557_firmware *fw,
+ const u8 *data, size_t remaining)
+{
+ const u8 *start = data;
+ size_t desc_len;
+ unsigned int i;
+ int ret;
+
+ if (remaining < 2)
+ return -EINVAL;
+
+ fw->num_programs = get_unaligned_be16(data);
+ data += 2;
+ remaining -= 2;
+
+ if (fw->num_programs == 0) {
+ dev_err(tas2557->dev, "firmware contains no programs\n");
+ return -EINVAL;
+ }
+
+ fw->programs = kcalloc(fw->num_programs, sizeof(struct tas2557_program),
+ GFP_KERNEL);
+ if (!fw->programs)
+ return -ENOMEM;
+
+ for (i = 0; i < fw->num_programs; i++) {
+ if (remaining < 64)
+ return -EINVAL;
+
+ memcpy(fw->programs[i].name, data, 64);
+ fw->programs[i].name[63] = '\0';
+ data += 64;
+ remaining -= 64;
+
+ desc_len = strnlen(data, remaining);
+ if (desc_len >= remaining)
+ return -EINVAL;
+ data += desc_len + 1;
+ remaining -= desc_len + 1;
+
+ /* app_mode (1 byte) + boost (2 bytes): not used, skip */
+ if (remaining < 3)
+ return -EINVAL;
+ data += 3;
+ remaining -= 3;
+
+ ret = fw_parse_data(tas2557, fw, &fw->programs[i].data,
+ data, remaining);
+ if (ret < 0)
+ return ret;
+ data += ret;
+ remaining -= ret;
+ }
+
+ return data - start;
+}
+
+static int fw_parse_configs(struct tas2557_priv *tas2557,
+ struct tas2557_firmware *fw,
+ const u8 *data, size_t remaining)
+{
+ const u8 *start = data;
+ size_t desc_len;
+ unsigned int i;
+ int ret;
+
+ if (remaining < 2)
+ return -EINVAL;
+
+ fw->num_configs = get_unaligned_be16(data);
+ data += 2;
+ remaining -= 2;
+
+ if (fw->num_configs == 0) {
+ dev_err(tas2557->dev, "firmware contains no configurations\n");
+ return -EINVAL;
+ }
+
+ fw->configs = kcalloc(fw->num_configs, sizeof(struct tas2557_config),
+ GFP_KERNEL);
+ if (!fw->configs)
+ return -ENOMEM;
+
+ for (i = 0; i < fw->num_configs; i++) {
+ if (remaining < 64)
+ return -EINVAL;
+
+ memcpy(fw->configs[i].name, data, 64);
+ fw->configs[i].name[63] = '\0';
+ data += 64;
+ remaining -= 64;
+
+ desc_len = strnlen(data, remaining);
+ if (desc_len >= remaining)
+ return -EINVAL;
+ data += desc_len + 1;
+ remaining -= desc_len + 1;
+
+ /*
+ * Device-count field present from driver_version 0x101.
+ * Consume it (unused here) across the [0x101, 0x1ff] band
+ * and for >= 0x300, matching the TI fw_parse_configuration.
+ */
+ if (fw->driver_version >= PPC_DRIVER_CONFDEV ||
+ (fw->driver_version >= PPC_DRIVER_CFGDEV_NONCRC &&
+ fw->driver_version < PPC_DRIVER_CRCCHK)) {
+ if (remaining < 2)
+ return -EINVAL;
+ data += 2; remaining -= 2;
+ }
+
+ if (remaining < 6)
+ return -EINVAL;
+
+ fw->configs[i].program = data[0]; data++; remaining--;
+ fw->configs[i].pll = data[0]; data++; remaining--;
+ fw->configs[i].sample_rate = get_unaligned_be32(data);
+ data += 4; remaining -= 4;
+
+ /* PLL source fields present when driver >= MTPLLSRC: skip */
+ if (fw->driver_version >= PPC_DRIVER_MTPLLSRC) {
+ if (remaining < 5)
+ return -EINVAL;
+ data += 5; remaining -= 5;
+ }
+
+ ret = fw_parse_data(tas2557, fw, &fw->configs[i].data,
+ data, remaining);
+ if (ret < 0)
+ return ret;
+ data += ret;
+ remaining -= ret;
+ }
+
+ return data - start;
+}
+
+static struct tas2557_firmware *fw_parse(struct tas2557_priv *tas2557,
+ const u8 *data, size_t size)
+{
+ struct tas2557_firmware *fw;
+ size_t remaining = size;
+ int ret;
+
+ fw = kzalloc_obj(*fw, GFP_KERNEL);
+ if (!fw)
+ return ERR_PTR(-ENOMEM);
+
+ ret = fw_parse_header(tas2557, fw, data, remaining);
+ if (ret < 0)
+ goto err;
+ data += ret; remaining -= ret;
+
+ ret = fw_parse_plls(tas2557, fw, data, remaining);
+ if (ret < 0)
+ goto err;
+ data += ret; remaining -= ret;
+
+ ret = fw_parse_programs(tas2557, fw, data, remaining);
+ if (ret < 0)
+ goto err;
+ data += ret; remaining -= ret;
+
+ ret = fw_parse_configs(tas2557, fw, data, remaining);
+ if (ret < 0)
+ goto err;
+
+ return fw;
+
+err:
+ tas2557_fw_free(fw);
+ return ERR_PTR(ret);
+}
+
+/* =========================================================================
+ * Firmware application
+ *
+ * Each firmware block contains a sequence of 4-byte commands:
+ * [book][page][reg][val] - single register write
+ * [hi][lo][0x81][--] - msleep((hi<<8)|lo)
+ * [hi][lo][0x85][first] - bulk write: length=(hi<<8)|lo, data follows
+ * =========================================================================
+ */
+
+static int tas2557_load_block(struct tas2557_priv *tas2557,
+ struct tas2557_block *block, unsigned int chn)
+{
+ const u8 *data = block->data;
+ unsigned int i;
+ u8 book, page, offset, value;
+ u16 sleep_time, bulk_len;
+ int ret;
+
+ dev_dbg(tas2557->dev, "chn %u: load block type=0x%x cmds=%u\n",
+ chn, block->type, block->num_commands);
+
+ for (i = 0; i < block->num_commands; ) {
+ book = data[i * 4];
+ page = data[i * 4 + 1];
+ offset = data[i * 4 + 2];
+ value = data[i * 4 + 3];
+ i++;
+
+ if (offset <= 0x7f) {
+ ret = tas2557_dev_write(tas2557, chn,
+ TAS2557_REG(book, page, offset),
+ value);
+ if (ret < 0)
+ return ret;
+ } else if (offset == 0x81) {
+ sleep_time = ((u16)book << 8) | page;
+ msleep(sleep_time);
+ } else if (offset == 0x85) {
+ bulk_len = ((u16)book << 8) | page;
+
+ if (bulk_len == 0)
+ return -EINVAL;
+
+ if (i >= block->num_commands)
+ return -EINVAL;
+ /* Verify bulk payload stays within block->data */
+ if ((size_t)i * 4 + 3 + bulk_len >
+ (size_t)block->num_commands * 4)
+ return -EINVAL;
+
+ book = data[i * 4];
+ page = data[i * 4 + 1];
+ offset = data[i * 4 + 2];
+
+ if (bulk_len > 1)
+ ret = tas2557_dev_bulk_write(tas2557, chn,
+ TAS2557_REG(book, page, offset),
+ &data[i * 4 + 3], bulk_len);
+ else
+ ret = tas2557_dev_write(tas2557, chn,
+ TAS2557_REG(book, page, offset),
+ data[i * 4 + 3]);
+ if (ret < 0)
+ return ret;
+
+ i++;
+ if (bulk_len >= 2)
+ i += (bulk_len - 2) / 4 + 1;
+ }
+ }
+
+ return 0;
+}
+
+/*
+ * tas2557_block_chn_range - which devs[] a firmware block type targets
+ *
+ * Mirrors tasdevice_load_block()'s dispatch in tas2781-fmwlib.c: DEV_A/
+ * DEV_B blocks target exactly one physical chip (selected by its index in
+ * the DT `reg` array, see the ti,tas2557.yaml binding), device-independent
+ * blocks (PGM_ALL, CFG_POST, CFG_POST_POWER) and the PLL block target every
+ * chip in the group. @chnend is clamped to tas2557->ndev, so a DEV_B block
+ * becomes an empty range on a single-chip group instead of touching a
+ * devs[] slot that was never populated. Returns false for a block type
+ * this driver does not recognise, so the caller does not apply it blindly.
+ */
+static bool tas2557_block_chn_range(struct tas2557_priv *tas2557,
+ unsigned int type,
+ unsigned int *chn, unsigned int *chnend)
+{
+ switch (type) {
+ case TAS2557_BLOCK_PGM_DEV_A:
+ case TAS2557_BLOCK_CFG_COEFF_DEV_A:
+ case TAS2557_BLOCK_CFG_PRE_DEV_A:
+ *chn = 0;
+ *chnend = 1;
+ break;
+ case TAS2557_BLOCK_PGM_DEV_B:
+ case TAS2557_BLOCK_CFG_COEFF_DEV_B:
+ case TAS2557_BLOCK_CFG_PRE_DEV_B:
+ *chn = 1;
+ *chnend = 2;
+ break;
+ case TAS2557_BLOCK_PLL:
+ case TAS2557_BLOCK_PGM_ALL:
+ case TAS2557_BLOCK_CFG_POST:
+ case TAS2557_BLOCK_CFG_POST_POWER:
+ *chn = 0;
+ *chnend = tas2557->ndev;
+ break;
+ default:
+ return false;
+ }
+
+ if (*chnend > tas2557->ndev)
+ *chnend = tas2557->ndev;
+
+ return true;
+}
+
+/*
+ * tas2557_load_fw_data - apply every block of a data set in storage order,
+ * each to whichever devs[] its type targets. Used for program->data,
+ * which only ever contains PGM_ALL/PGM_DEV_A/PGM_DEV_B blocks with no
+ * ordering constraint between them.
+ */
+static int tas2557_load_fw_data(struct tas2557_priv *tas2557,
+ struct tas2557_data *img_data)
+{
+ unsigned int i, chn, chnend, c;
+ int ret;
+
+ for (i = 0; i < img_data->num_blocks; i++) {
+ if (!tas2557_block_chn_range(tas2557, img_data->blocks[i].type,
+ &chn, &chnend))
+ continue;
+
+ for (c = chn; c < chnend; c++) {
+ ret = tas2557_load_block(tas2557, &img_data->blocks[i], c);
+ if (ret < 0)
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+/*
+ * tas2557_load_fw_data_type - apply only the blocks of a single type,
+ * in storage order, each to whichever devs[] that type targets.
+ */
+static int tas2557_load_fw_data_type(struct tas2557_priv *tas2557,
+ struct tas2557_data *img_data,
+ unsigned int type)
+{
+ unsigned int i, chn, chnend, c;
+ int ret;
+
+ if (!tas2557_block_chn_range(tas2557, type, &chn, &chnend))
+ return 0;
+
+ for (i = 0; i < img_data->num_blocks; i++) {
+ if (img_data->blocks[i].type != type)
+ continue;
+
+ for (c = chn; c < chnend; c++) {
+ ret = tas2557_load_block(tas2557, &img_data->blocks[i], c);
+ if (ret < 0)
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+/*
+ * tas2557_load_config_data - apply a configuration's pre-power blocks in the
+ * order the hardware expects: each chip's pre-coefficient block, then its
+ * coefficient block (DEV_A before DEV_B), then the device-independent
+ * CFG_POST block. The CFG_POST_POWER block is NOT applied here; it needs
+ * active clocks and is loaded by tas2557_load_config_post_power() after the
+ * device is powered up. Blocks are applied by type, not storage order (per
+ * TI); tas2557_block_chn_range() decides which chip(s) each type reaches.
+ */
+static int tas2557_load_config_data(struct tas2557_priv *tas2557,
+ struct tas2557_data *img_data)
+{
+ static const unsigned int order[] = {
+ TAS2557_BLOCK_CFG_PRE_DEV_A,
+ TAS2557_BLOCK_CFG_COEFF_DEV_A,
+ TAS2557_BLOCK_CFG_PRE_DEV_B,
+ TAS2557_BLOCK_CFG_COEFF_DEV_B,
+ TAS2557_BLOCK_CFG_POST,
+ };
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < ARRAY_SIZE(order); i++) {
+ ret = tas2557_load_fw_data_type(tas2557, img_data, order[i]);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+/*
+ * tas2557_load_config_post_power - apply the CFG_POST_POWER block of the
+ * current configuration. These register writes target DSP/clock state that
+ * only latches once the Class-D/DSP power-up sequence has run, so this is
+ * called from the power-up path after tas2557_startup_data, not during the
+ * (powered-down) configuration load. current_config is kept in lock-step
+ * across every devs[] entry by tas2557_set_program(), so devs[0] is read
+ * back as the group's representative value.
+ */
+static int tas2557_load_config_post_power(struct tas2557_priv *tas2557)
+{
+ struct tas2557_config *config;
+
+ if (!tas2557->fw || tas2557->devs[0].current_config >= tas2557->fw->num_configs)
+ return 0;
+
+ config = &tas2557->fw->configs[tas2557->devs[0].current_config];
+
+ return tas2557_load_fw_data_type(tas2557, &config->data,
+ TAS2557_BLOCK_CFG_POST_POWER);
+}
+
+/*
+ * tas2557_load_pll - apply a firmware PLL block to every chip in the group.
+ * PLL configuration is device-independent (each chip clocks itself off the
+ * same shared reference), so this is loaded on every devs[] entry.
+ */
+static int tas2557_load_pll(struct tas2557_priv *tas2557, unsigned int pll_idx)
+{
+ unsigned int chn;
+ int ret;
+
+ if (!tas2557->fw || pll_idx >= tas2557->fw->num_plls)
+ return -EINVAL;
+
+ dev_dbg(tas2557->dev, "loading PLL[%u]: %s\n",
+ pll_idx, tas2557->fw->plls[pll_idx].name);
+
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ ret = tas2557_load_block(tas2557,
+ &tas2557->fw->plls[pll_idx].block, chn);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+/*
+ * tas2557_set_program - reset device group and load firmware program + config
+ *
+ * This is the authoritative firmware-apply path. It performs:
+ * (graceful shutdown if currently powered) -> hardware reset ->
+ * software reset -> default registers -> IRQ config (all per chip) ->
+ * program blocks (DSP coefficients) -> PLL block for config ->
+ * config blocks (sample-rate-specific settings)
+ *
+ * It does not re-power the device group: callers that need the amplifiers
+ * running again after this call (tas2557_enable(), failsafe recovery)
+ * re-apply the startup sequence themselves.
+ *
+ * PLL configuration comes entirely from the firmware PLL block; no manual
+ * PLL register writes are performed here or anywhere else.
+ *
+ * Caller must hold tas2557->lock.
+ *
+ * @config_idx: -1 to auto-select based on current sample_rate (0 = any)
+ */
+static int tas2557_set_program(struct tas2557_priv *tas2557,
+ unsigned int prog_idx, int config_idx)
+{
+ struct tas2557_firmware *fw = tas2557->fw;
+ struct tas2557_program *program;
+ struct tas2557_config *config;
+ unsigned int cfg_idx, chn;
+ int ret;
+
+ lockdep_assert_held(&tas2557->lock);
+
+ if (!fw || !fw->programs || !fw->configs) {
+ dev_err(tas2557->dev, "firmware not loaded, cannot set program\n");
+ return -EINVAL;
+ }
+
+ if (prog_idx >= fw->num_programs) {
+ dev_err(tas2557->dev, "program %u out of range (%u total)\n",
+ prog_idx, fw->num_programs);
+ return -EINVAL;
+ }
+
+ /* Auto-select config: match program + sample_rate (0 = accept any) */
+ if (config_idx < 0) {
+ config_idx = -1;
+ for (cfg_idx = 0; cfg_idx < fw->num_configs; cfg_idx++) {
+ if (fw->configs[cfg_idx].program != prog_idx)
+ continue;
+ if (tas2557->sample_rate == 0 ||
+ tas2557->sample_rate ==
+ fw->configs[cfg_idx].sample_rate) {
+ config_idx = (int)cfg_idx;
+ break;
+ }
+ }
+ if (config_idx < 0) {
+ dev_err(tas2557->dev,
+ "no config for program %u at %u Hz\n",
+ prog_idx, tas2557->sample_rate);
+ return -EINVAL;
+ }
+ }
+
+ if ((unsigned int)config_idx >= fw->num_configs) {
+ dev_err(tas2557->dev, "config %d out of range (%u total)\n",
+ config_idx, fw->num_configs);
+ return -EINVAL;
+ }
+
+ config = &fw->configs[config_idx];
+ if (config->program != prog_idx) {
+ dev_err(tas2557->dev,
+ "config %u belongs to program %u, not %u\n",
+ config_idx, config->program, prog_idx);
+ return -EINVAL;
+ }
+
+ program = &fw->programs[prog_idx];
+
+ /* Ramp down gracefully before the reset if currently powered */
+ if (tas2557->powered) {
+ ret = tas2557_load_data_all(tas2557, tas2557_shutdown_data);
+ if (ret < 0)
+ return ret;
+ }
+
+ /* Hard + soft reset (every chip), then baseline register setup */
+ tas2557_hw_reset(tas2557);
+
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ ret = tas2557_dev_write(tas2557, chn, TAS2557_SW_RESET_REG, 0x01);
+ if (ret < 0)
+ return ret;
+ }
+ usleep_range(1000, 2000);
+
+ ret = tas2557_load_data_all(tas2557, tas2557_default_data);
+ if (ret < 0)
+ return ret;
+
+ /* Restore IRQ configuration lost by the reset */
+ ret = tas2557_load_data_all(tas2557, tas2557_irq_config);
+ if (ret < 0)
+ dev_warn(tas2557->dev, "IRQ config reload failed: %d\n", ret);
+
+ /*
+ * Load DSP program blocks. Which chip(s) each block targets is
+ * decided purely by the block's own type (PGM_ALL/PGM_DEV_A/
+ * PGM_DEV_B) via tas2557_block_chn_range(); DEV_A/DEV_B tuning maps
+ * onto devs[0]/devs[1], i.e. onto the chip order of the DT `reg`
+ * array, not any runtime TDM slot selection.
+ */
+ dev_dbg(tas2557->dev, "loading program %u (%s)\n",
+ prog_idx, program->name);
+ ret = tas2557_load_fw_data(tas2557, &program->data);
+ if (ret < 0)
+ return ret;
+
+ for (chn = 0; chn < tas2557->ndev; chn++)
+ tas2557->devs[chn].current_program = prog_idx;
+
+ /* Load PLL configuration for this config (firmware-driven) */
+ if (config->pll < fw->num_plls) {
+ ret = tas2557_load_pll(tas2557, config->pll);
+ if (ret < 0)
+ return ret;
+ }
+
+ /* Load config blocks (coefficients, sample-rate-specific settings) */
+ dev_dbg(tas2557->dev,
+ "loading config %u (%s) rate=%u Hz PLL[%u]\n",
+ config_idx, config->name, config->sample_rate, config->pll);
+ ret = tas2557_load_config_data(tas2557, &config->data);
+ if (ret < 0)
+ return ret;
+
+ for (chn = 0; chn < tas2557->ndev; chn++)
+ tas2557->devs[chn].current_config = config_idx;
+
+ return 0;
+}
+
+static const char *tas2557_pg_name(int pg_id)
+{
+ switch (pg_id) {
+ case TAS2557_PG_VERSION_1P0:
+ return "PG1.0";
+ case TAS2557_PG_VERSION_2P0:
+ return "PG2.0";
+ case TAS2557_PG_VERSION_2P1:
+ return "PG2.1";
+ default:
+ return "PG?";
+ }
+}
+
+/*
+ * tas2557_fw_ready - firmware load callback
+ *
+ * Called from request_firmware_nowait(). Parses and stores the firmware,
+ * then pre-loads program 0 / config 0 so the DSP registers are ready for
+ * the first tas2557_enable() call. Every path (missing firmware, parse
+ * error, success) reaches the completion at the end, so
+ * tas2557_fw_teardown() can always wait for this callback to finish.
+ */
+static void tas2557_fw_ready(const struct firmware *fw_entry, void *context)
+{
+ struct tas2557_priv *tas2557 = context;
+ struct tas2557_firmware *fw = NULL;
+ int ret;
+
+ if (!fw_entry || !fw_entry->data || fw_entry->size == 0) {
+ /*
+ * Firmware is required: without it the DSP cannot route
+ * audio and the Class-D stage stays muted.
+ */
+ dev_err(tas2557->dev,
+ "firmware '%s' not found, the amplifier cannot work without it\n",
+ tas2557->fw_name);
+ release_firmware(fw_entry);
+ goto done;
+ }
+
+ dev_dbg(tas2557->dev, "firmware received, size=%zu\n", fw_entry->size);
+
+ fw = fw_parse(tas2557, fw_entry->data, fw_entry->size);
+ release_firmware(fw_entry);
+
+ if (IS_ERR(fw)) {
+ dev_err(tas2557->dev,
+ "firmware '%s' parse error %ld, it may be corrupt\n",
+ tas2557->fw_name, PTR_ERR(fw));
+ fw = NULL;
+ goto done;
+ }
+
+ mutex_lock(&tas2557->lock);
+
+ tas2557_fw_free(tas2557->fw);
+ tas2557->fw = fw;
+
+ ret = tas2557_set_program(tas2557, 0, -1);
+ if (ret < 0)
+ dev_err(tas2557->dev, "initial program load failed: %d\n", ret);
+ else
+ dev_info(tas2557->dev,
+ "%s ready: fw '%s' ver=0x%x drv=0x%x (%u prog, %u cfg)\n",
+ tas2557_pg_name(tas2557->devs[0].pg_id), tas2557->fw_name,
+ fw->fw_version, fw->driver_version,
+ fw->num_programs, fw->num_configs);
+
+ mutex_unlock(&tas2557->lock);
+
+done:
+ complete_all(&tas2557->fw_done);
+}
+
+/*
+ * tas2557_fw_teardown - release firmware resources on driver teardown
+ *
+ * Waits for a pending tas2557_fw_ready() callback (if a request is still
+ * in flight) before freeing the parsed firmware, closing the
+ * use-after-free window where the firmware core could still be about to
+ * invoke tas2557_fw_ready() while the device is being unbound. See the
+ * comment above its devm_add_action_or_reset() registration in
+ * tas2557_i2c_probe() for why this ordering is safe.
+ */
+static void tas2557_fw_teardown(void *data)
+{
+ struct tas2557_priv *tas2557 = data;
+
+ if (tas2557->fw_requested)
+ wait_for_completion(&tas2557->fw_done);
+
+ mutex_lock(&tas2557->lock);
+ tas2557_fw_free(tas2557->fw);
+ tas2557->fw = NULL;
+ mutex_unlock(&tas2557->lock);
+}
+
+/* =========================================================================
+ * Hardware reset
+ * =========================================================================
+ */
+static void tas2557_hw_reset(struct tas2557_priv *tas2557)
+{
+ unsigned int chn;
+
+ if (!tas2557->reset_gpio) {
+ mutex_lock(&tas2557->dev_lock);
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ tas2557->devs[chn].current_book = 0xff;
+ tas2557->devs[chn].current_page = 0xff;
+ }
+ mutex_unlock(&tas2557->dev_lock);
+ return;
+ }
+
+ /* Assert reset (active-low: logical 1 = reset asserted) */
+ gpiod_set_value_cansleep(tas2557->reset_gpio, 1);
+ usleep_range(5000, 6000);
+ /* Release reset -- resets every chip on the shared reset line */
+ gpiod_set_value_cansleep(tas2557->reset_gpio, 0);
+ usleep_range(10000, 11000);
+
+ mutex_lock(&tas2557->dev_lock);
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ tas2557->devs[chn].current_book = 0xff;
+ tas2557->devs[chn].current_page = 0xff;
+ }
+ mutex_unlock(&tas2557->dev_lock);
+}
+
+/* =========================================================================
+ * IRQ handling + failsafe recovery
+ * =========================================================================
+ */
+
+/*
+ * tas2557_irq_arm()/tas2557_irq_disarm() - enable/disable the fault IRQ
+ * from the power path. Called with tas2557->lock NOT held: enable_irq()/
+ * disable_irq() must not run while the threaded handler could be blocked
+ * waiting for tas2557->lock, or a self-deadlock results (disable_irq()
+ * waits for the handler thread to finish, which is waiting on the lock
+ * the caller is holding).
+ */
+static void tas2557_irq_arm(struct tas2557_priv *tas2557)
+{
+ if (tas2557->irq && !tas2557->irq_enabled) {
+ tas2557->irq_enabled = true;
+ enable_irq(tas2557->irq);
+ }
+}
+
+static void tas2557_irq_disarm(struct tas2557_priv *tas2557)
+{
+ if (tas2557->irq && tas2557->irq_enabled) {
+ tas2557->irq_enabled = false;
+ disable_irq(tas2557->irq);
+ }
+}
+
+/*
+ * tas2557_irq_handler - threaded fault IRQ handler
+ *
+ * GPIO4 is programmed as the INT1 output (see tas2557_irq_config) on every
+ * chip; the group shares a single host INT line, so all chips' INT outputs
+ * are gated off together while diagnosing a fault and re-enabled together
+ * once the condition has been read/handled. FLAGS_1/FLAGS_2/POWER_UP_FLAG
+ * are read back from each chip in turn: any single chip reporting a fault
+ * triggers failsafe recovery for the whole group.
+ */
+static irqreturn_t tas2557_irq_handler(int irq, void *data)
+{
+ struct tas2557_priv *tas2557 = data;
+ unsigned int chn;
+ bool fault = false;
+ int ret = 0;
+
+ mutex_lock(&tas2557->lock);
+
+ if (!tas2557->fw || !tas2557->powered) {
+ mutex_unlock(&tas2557->lock);
+ return IRQ_NONE;
+ }
+
+ /* Gate the chips' INT output while diagnosing the fault */
+ tas2557_write_all(tas2557, TAS2557_GPIO4_PIN_REG, 0x00);
+
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ unsigned int int1 = 0, int2 = 0, pwr_flag = 0;
+
+ ret = tas2557_dev_read(tas2557, chn, TAS2557_FLAGS_1, &int1);
+ if (ret >= 0)
+ ret = tas2557_dev_read(tas2557, chn, TAS2557_FLAGS_2, &int2);
+
+ if (ret < 0) {
+ dev_err(tas2557->dev, "IRQ: chn %u failed to read FLAGS\n", chn);
+ fault = true;
+ break;
+ }
+
+ if ((int1 & 0xfc) || (int2 & 0x0c)) {
+ /*
+ * Clock faults while muted are expected: the host is
+ * allowed to stop the I2S clocks around stream stop
+ * while the amplifier is still powered. Nothing to
+ * recover from.
+ */
+ if (tas2557->dac_muted && !(int1 & 0xd8)) {
+ dev_dbg(tas2557->dev,
+ "IRQ: chn %u clock stop while muted (FLAGS_1=0x%02x FLAGS_2=0x%02x)\n",
+ chn, int1, int2);
+ continue;
+ }
+ dev_err(tas2557->dev, "IRQ: chn %u FLAGS_1=0x%02x FLAGS_2=0x%02x\n",
+ chn, int1, int2);
+ fault = true;
+ break;
+ }
+
+ if (!tas2557->dac_muted) {
+ ret = tas2557_dev_read(tas2557, chn, TAS2557_POWER_UP_FLAG_REG,
+ &pwr_flag);
+ if (ret < 0) {
+ fault = true;
+ break;
+ }
+
+ if ((pwr_flag & 0xc0) != 0xc0) {
+ dev_err(tas2557->dev, "IRQ: chn %u power-up flag=0x%02x\n",
+ chn, pwr_flag);
+ fault = true;
+ break;
+ }
+ }
+ }
+
+ if (!fault) {
+ /* No fault (or expected clock stop): re-enable the INT output */
+ tas2557_write_all(tas2557, TAS2557_GPIO4_PIN_REG, 0x07);
+ mutex_unlock(&tas2557->lock);
+ return IRQ_HANDLED;
+ }
+
+ if (tas2557_failsafe_recovery(tas2557) == 0)
+ tas2557_write_all(tas2557, TAS2557_GPIO4_PIN_REG, 0x07);
+
+ mutex_unlock(&tas2557->lock);
+ return IRQ_HANDLED;
+}
+
+/*
+ * tas2557_clk_err_detect - gate the chips' clock error/halt detection
+ *
+ * The host is allowed to stop the I2S clocks whenever no audio is
+ * playing (amplifier muted), so detection is armed only between unmute
+ * and mute; otherwise a normal stream stop looks like a clock-halt
+ * fault. This matches the TI reference sequences, which disable
+ * detection as the first step of the shutdown sequence.
+ */
+static int tas2557_clk_err_detect(struct tas2557_priv *tas2557, bool enable)
+{
+ return tas2557_write_all(tas2557, TAS2557_CLK_ERR_CTRL,
+ enable ? 0x2b : 0x00);
+}
+
+/*
+ * tas2557_sns_ctrl_value - compute TAS2557_SNS_CTRL_REG for one chip from
+ * cached state
+ *
+ * Shared by tas2557_apply_runtime_state() and the ISENSE/VSENSE mixer
+ * controls, so toggling either sense while already powered routes it to
+ * the same DT-configured slot a fresh power-up would use. isense_enabled/
+ * vsense_enabled are shared group-level switches; imon_slot/vmon_slot are
+ * per chip (from ti,imon-slot-nos/ti,vmon-slot-nos).
+ */
+static unsigned int tas2557_sns_ctrl_value(struct tas2557_priv *tas2557,
+ unsigned int chn)
+{
+ unsigned int sns = 0;
+
+ if (tas2557->isense_enabled)
+ sns |= (tas2557->devs[chn].imon_slot & 0x7) << TAS2557_ISNS_SLOT_SHIFT;
+ if (tas2557->vsense_enabled)
+ sns |= (tas2557->devs[chn].vmon_slot & 0x7) << TAS2557_VSNS_SLOT_SHIFT;
+
+ return sns;
+}
+
+/* tas2557_sns_ctrl_write_all - apply tas2557_sns_ctrl_value() to every chip */
+static int tas2557_sns_ctrl_write_all(struct tas2557_priv *tas2557)
+{
+ unsigned int chn;
+ int ret;
+
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ ret = tas2557_dev_write(tas2557, chn, TAS2557_SNS_CTRL_REG,
+ tas2557_sns_ctrl_value(tas2557, chn));
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+/*
+ * tas2557_failsafe_recovery - re-initialise the device group after a fault IRQ
+ *
+ * Caller must hold tas2557->lock (called from tas2557_irq_handler()).
+ * Capped at 5 attempts so a permanently faulty amplifier does not spin
+ * the IRQ thread forever; restart_count is tracked per chip but the whole
+ * group is recovered together, so every devs[] entry is incremented in
+ * lock-step and devs[0] is read back as the group's representative value.
+ */
+static int tas2557_failsafe_recovery(struct tas2557_priv *tas2557)
+{
+ unsigned int chn;
+ int ret;
+
+ lockdep_assert_held(&tas2557->lock);
+
+ for (chn = 0; chn < tas2557->ndev; chn++)
+ tas2557->devs[chn].restart_count++;
+
+ dev_info(tas2557->dev, "failsafe recovery attempt %u\n",
+ tas2557->devs[0].restart_count);
+
+ if (tas2557->devs[0].restart_count > 5) {
+ dev_err(tas2557->dev, "too many recovery attempts, giving up\n");
+ return -EIO;
+ }
+
+ tas2557_hw_reset(tas2557);
+
+ ret = tas2557_load_data_all(tas2557, tas2557_default_data);
+ if (ret < 0)
+ return ret;
+
+ if (tas2557->fw) {
+ ret = tas2557_set_program(tas2557, tas2557->devs[0].current_program,
+ (int)tas2557->devs[0].current_config);
+ if (ret < 0)
+ return ret;
+ }
+
+ if (tas2557->powered) {
+ unsigned int sense_soft_mask = TAS2557_VSENSE_SOFT_MUTE |
+ TAS2557_ISENSE_SOFT_MUTE;
+
+ ret = tas2557_load_data_all(tas2557, tas2557_startup_data);
+ if (ret < 0)
+ return ret;
+
+ ret = tas2557_apply_runtime_state(tas2557);
+ if (ret < 0)
+ return ret;
+
+ ret = tas2557_load_config_post_power(tas2557);
+ if (ret < 0)
+ return ret;
+
+ if (!tas2557->dac_muted) {
+ ret = tas2557_load_data_all(tas2557, tas2557_unmute_data);
+ if (ret >= 0 && tas2557->sense_muted)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_MUTE_REG,
+ TAS2557_ISENSE_MUTE,
+ TAS2557_ISENSE_MUTE);
+ if (ret >= 0 && tas2557->sense_muted)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_SOFT_MUTE_REG,
+ sense_soft_mask,
+ sense_soft_mask);
+ if (ret >= 0)
+ ret = tas2557_clk_err_detect(tas2557, true);
+ if (ret < 0)
+ return ret;
+ } else {
+ /* Muted: clocks not guaranteed, keep detection off */
+ ret = tas2557_clk_err_detect(tas2557, false);
+ if (ret >= 0 && !tas2557->sense_muted)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_MUTE_REG,
+ TAS2557_ISENSE_MUTE,
+ 0);
+ if (ret >= 0 && !tas2557->sense_muted)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_SOFT_MUTE_REG,
+ sense_soft_mask,
+ 0);
+ if (ret < 0)
+ return ret;
+ }
+ }
+
+ dev_info(tas2557->dev, "failsafe recovery succeeded\n");
+ return 0;
+}
+
+/* =========================================================================
+ * Power management
+ *
+ * Power-up sequence (firmware-driven), see tas2557_enable():
+ * 1. tas2557_set_program() - hw+sw reset, load defaults+IRQ config,
+ * load firmware PLL/program/config blocks (PLL set by firmware)
+ * 2. tas2557_startup_data - enable GPIO clocks, Class-D, Boost, DSP
+ * 3. Set SNS_CTRL slots, DAC gain, re-apply the ASI data-slot offset
+ * 4. Load the CFG_POST_POWER firmware block
+ * 5. Check POWER_UP_FLAG + FLAGS for PLL lock / clock errors
+ *
+ * All of the above run against every chip in the group. The device stays
+ * muted after power-up; tas2557_mute_stream() applies the unmute (or mute)
+ * register sequence separately per stream direction, so a stream that
+ * starts muted (e.g. during a volume ramp) never briefly unmutes.
+ * =========================================================================
+ */
+static void tas2557_check_pll_lock(struct tas2557_priv *tas2557)
+{
+ unsigned int chn;
+
+ /*
+ * The PLL needs a few ms to lock after the I2S bit clock starts
+ * flowing. Poll POWER_UP_FLAG rather than sampling it once, so we
+ * don't emit a spurious "not locked" warning during clock ramp-up.
+ */
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ unsigned int flag = 0, f1 = 0, f2 = 0;
+ int i;
+
+ for (i = 0; i < 20; i++) {
+ tas2557_dev_read(tas2557, chn, TAS2557_POWER_UP_FLAG_REG, &flag);
+ if ((flag & 0xc0) == 0xc0)
+ break;
+ usleep_range(2000, 2500);
+ }
+
+ tas2557_dev_read(tas2557, chn, TAS2557_FLAGS_1, &f1);
+ tas2557_dev_read(tas2557, chn, TAS2557_FLAGS_2, &f2);
+
+ if ((flag & 0xc0) == 0xc0)
+ dev_dbg(tas2557->dev,
+ "chn %u: PLL locked, power-up OK (FLAG=0x%02x F1=0x%02x F2=0x%02x)\n",
+ chn, flag, f1, f2);
+ else
+ dev_warn(tas2557->dev,
+ "chn %u: PLL or clock may not have locked (FLAG=0x%02x F1=0x%02x F2=0x%02x)\n",
+ chn, flag, f1, f2);
+
+ if (f1 & 0x04)
+ dev_warn(tas2557->dev, "chn %u: clock error detected (FLAGS_1=0x%02x)\n",
+ chn, f1);
+ }
+}
+
+/*
+ * tas2557_apply_runtime_state - re-apply cached runtime state after a reset
+ *
+ * tas2557_set_program()'s SW reset wipes the sense-slot, sense-enable,
+ * DAC gain, ASI format and ASI data-slot offset registers on every chip;
+ * this restores them all from the cached driver state. Must be called
+ * after tas2557_startup_data has powered up Class-D/Boost/DSP, since these
+ * registers only latch with clocks running.
+ *
+ * Caller must hold tas2557->lock.
+ */
+static int tas2557_apply_runtime_state(struct tas2557_priv *tas2557)
+{
+ static const unsigned int fmt_regs[] = {
+ TAS2557_ASI1_DAC_FORMAT_REG, TAS2557_ASI2_DAC_FORMAT_REG,
+ TAS2557_ASI1_ADC_FORMAT_REG, TAS2557_ASI2_ADC_FORMAT_REG,
+ };
+ unsigned int chn, i;
+ int ret;
+
+ lockdep_assert_held(&tas2557->lock);
+
+ /* Configure current/voltage sense output slots */
+ if (tas2557->isense_enabled || tas2557->vsense_enabled) {
+ ret = tas2557_sns_ctrl_write_all(tas2557);
+ if (ret < 0)
+ return ret;
+ }
+
+ /*
+ * tas2557_startup_data hardcodes both sense-enable bits on;
+ * make the cached ALSA switch state win instead.
+ */
+ ret = tas2557_update_bits_all(tas2557, TAS2557_POWER_CTRL2_REG,
+ TAS2557_ISENSE_ENABLE | TAS2557_VSENSE_ENABLE,
+ (tas2557->isense_enabled ? TAS2557_ISENSE_ENABLE : 0) |
+ (tas2557->vsense_enabled ? TAS2557_VSENSE_ENABLE : 0));
+ if (ret < 0)
+ return ret;
+
+ /* Apply DAC gain */
+ ret = tas2557_update_bits_all(tas2557, TAS2557_SPK_CTRL_REG,
+ TAS2557_DAC_GAIN_MASK,
+ tas2557->dac_gain << TAS2557_DAC_GAIN_SHIFT);
+ if (ret < 0)
+ return ret;
+
+ /*
+ * Re-apply the ASI format (if set via set_dai_fmt) and word length
+ * (from the last hw_params) that the SW reset cleared, on both the
+ * DAC (playback) and ADC (IV-sense capture) format registers of
+ * both ASIs, so capture stays in sync with playback. Word length
+ * is unconditional (a valid default is seeded at probe).
+ */
+
+ for (i = 0; i < ARRAY_SIZE(fmt_regs); i++) {
+ ret = tas2557_update_bits_all(tas2557, fmt_regs[i],
+ TAS2557_WORDLENGTH_MASK,
+ tas2557->wordlength);
+ if (ret < 0)
+ return ret;
+
+ if (tas2557->dai_fmt >= 0) {
+ ret = tas2557_update_bits_all(tas2557, fmt_regs[i],
+ TAS2557_FORMAT_MASK,
+ tas2557->dai_fmt);
+ if (ret < 0)
+ return ret;
+ }
+ }
+
+ /*
+ * Re-apply the cached ASI data-slot offset (BCLK cycles) for each
+ * chip. The SW reset in tas2557_set_program() cleared the ASI
+ * offset registers, so program them now the clocks are up.
+ * Both ASIs are written; the board wiring selects which one
+ * actually carries audio.
+ */
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ struct tas2557_dev *dv = &tas2557->devs[chn];
+
+ ret = tas2557_dev_write(tas2557, chn, TAS2557_ASI2_OFFSET1_REG,
+ dv->asi_offset);
+ if (ret < 0)
+ return ret;
+
+ ret = tas2557_dev_write(tas2557, chn, TAS2557_ASI1_OFFSET1_REG,
+ dv->asi_offset);
+ if (ret < 0)
+ return ret;
+
+ /* Re-apply the cached TX (capture) slot offset, if ever set */
+ if (dv->asi_tx_offset >= 0) {
+ ret = tas2557_dev_write(tas2557, chn, TAS2557_ASI1_OFFSET2_REG,
+ dv->asi_tx_offset);
+ if (ret < 0)
+ return ret;
+
+ ret = tas2557_dev_write(tas2557, chn, TAS2557_ASI2_OFFSET2_REG,
+ dv->asi_tx_offset);
+ if (ret < 0)
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+/*
+ * tas2557_enable - power the amplifier group up or down
+ *
+ * Caller must hold tas2557->lock. Does not touch the fault IRQ or the
+ * mute state; see tas2557_classd_event() and tas2557_mute_stream().
+ */
+static int tas2557_enable(struct tas2557_priv *tas2557, bool enable)
+{
+ unsigned int chn;
+ int ret = 0;
+
+ lockdep_assert_held(&tas2557->lock);
+
+ if (enable && !tas2557->powered) {
+ unsigned int sense_soft_mask = TAS2557_VSENSE_SOFT_MUTE |
+ TAS2557_ISENSE_SOFT_MUTE;
+
+ if (!tas2557->fw) {
+ dev_warn(tas2557->dev,
+ "firmware not loaded, cannot produce audio; install %s\n",
+ tas2557->fw_name[0] ? tas2557->fw_name
+ : TAS2557_FW_NAME);
+ return -ENODEV;
+ }
+
+ /*
+ * Re-apply firmware program + config for the current sample
+ * rate. This performs hw/sw reset and loads the firmware's
+ * PLL block; no manual PLL register writes are needed or done.
+ */
+ ret = tas2557_set_program(tas2557, tas2557->devs[0].current_program, -1);
+ if (ret < 0) {
+ dev_err(tas2557->dev,
+ "firmware program load failed on enable: %d\n",
+ ret);
+ return ret;
+ }
+
+ /* Power up Class-D + Boost + ASI2 clocks on every chip */
+ ret = tas2557_load_data_all(tas2557, tas2557_startup_data);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "startup sequence failed: %d\n",
+ ret);
+ return ret;
+ }
+
+ ret = tas2557_apply_runtime_state(tas2557);
+ if (ret < 0)
+ return ret;
+
+ /* DSP post-power coefficients need active clocks to latch */
+ ret = tas2557_load_config_post_power(tas2557);
+ if (ret < 0) {
+ dev_err(tas2557->dev,
+ "post-power config load failed: %d\n", ret);
+ return ret;
+ }
+
+ /*
+ * Apply the recorded per-direction mute state. On DPCM
+ * systems the mute_stream(0) callback for a back-end DAI
+ * runs at back-end prepare time, BEFORE the DAPM widgets
+ * power up, so the unmute may already have been recorded
+ * while the device was still off: honour it now. A
+ * capture-only (IV-sense) stream can be unmuted while the
+ * DAC stays muted, and vice versa. When the DAC is still
+ * muted, also disarm the clock error detection that
+ * tas2557_startup_data enabled, since the host may not be
+ * clocking the bus yet; tas2557_mute_stream() re-arms it.
+ */
+ if (!tas2557->dac_muted) {
+ ret = tas2557_load_data_all(tas2557, tas2557_unmute_data);
+ if (ret >= 0 && tas2557->sense_muted)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_MUTE_REG,
+ TAS2557_ISENSE_MUTE,
+ TAS2557_ISENSE_MUTE);
+ if (ret >= 0 && tas2557->sense_muted)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_SOFT_MUTE_REG,
+ sense_soft_mask,
+ sense_soft_mask);
+ if (ret >= 0)
+ ret = tas2557_clk_err_detect(tas2557, true);
+ } else {
+ ret = tas2557_clk_err_detect(tas2557, false);
+ if (ret >= 0 && !tas2557->sense_muted)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_MUTE_REG,
+ TAS2557_ISENSE_MUTE,
+ 0);
+ if (ret >= 0 && !tas2557->sense_muted)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_SOFT_MUTE_REG,
+ sense_soft_mask,
+ 0);
+ }
+ if (ret < 0)
+ return ret;
+
+ /* Verify PLL lock and clock health on every chip */
+ tas2557_check_pll_lock(tas2557);
+
+ tas2557->powered = true;
+ for (chn = 0; chn < tas2557->ndev; chn++)
+ tas2557->devs[chn].restart_count = 0;
+
+ dev_dbg(tas2557->dev, "amplifier group powered on (prog=%u cfg=%u)\n",
+ tas2557->devs[0].current_program, tas2557->devs[0].current_config);
+
+ } else if (!enable && tas2557->powered) {
+ ret = tas2557_load_data_all(tas2557, tas2557_shutdown_data);
+ if (ret < 0)
+ dev_err(tas2557->dev, "shutdown failed: %d\n", ret);
+
+ tas2557->powered = false;
+ tas2557->dac_muted = true;
+ tas2557->sense_muted = true;
+
+ dev_dbg(tas2557->dev, "amplifier group powered off\n");
+ }
+
+ return ret;
+}
+
+/* =========================================================================
+ * ASoC DAI operations
+ * =========================================================================
+ */
+static int tas2557_hw_params(struct snd_pcm_substream *substream,
+ struct snd_pcm_hw_params *params,
+ struct snd_soc_dai *dai)
+{
+ struct snd_soc_component *component = dai->component;
+ struct tas2557_priv *tas2557 = snd_soc_component_get_drvdata(component);
+ unsigned int rate = params_rate(params);
+ unsigned int chn, wordlength;
+ int ret;
+
+ mutex_lock(&tas2557->lock);
+
+ dev_dbg(tas2557->dev, "hw_params: rate=%u width=%d\n",
+ rate, params_physical_width(params));
+
+ /*
+ * Validate the ASI word length up front (before committing any
+ * cached state); it is reapplied per chip by
+ * tas2557_apply_runtime_state() since the SW reset clears it.
+ */
+ switch (params_width(params)) {
+ case 16:
+ wordlength = TAS2557_WORDLENGTH_16BIT;
+ break;
+ case 20:
+ wordlength = TAS2557_WORDLENGTH_20BIT;
+ break;
+ case 24:
+ wordlength = TAS2557_WORDLENGTH_24BIT;
+ break;
+ case 32:
+ wordlength = TAS2557_WORDLENGTH_32BIT;
+ break;
+ default:
+ dev_err(tas2557->dev, "unsupported sample width %d\n",
+ params_width(params));
+ mutex_unlock(&tas2557->lock);
+ return -EINVAL;
+ }
+
+ /*
+ * The data-slot offset defaults to 0 and is applied identically to
+ * every chip in the group. An explicit snd_soc_dai_set_tdm_slot()
+ * takes precedence over this default and leaves asi_offset
+ * untouched.
+ */
+ if (!tas2557->tdm_configured) {
+ for (chn = 0; chn < tas2557->ndev; chn++)
+ tas2557->devs[chn].asi_offset = 0;
+ }
+
+ /* Playback runs over ASI2; ASI1 is programmed too (per TI driver) */
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ unsigned int offset = tas2557->devs[chn].asi_offset;
+
+ ret = tas2557_dev_write(tas2557, chn, TAS2557_ASI2_OFFSET1_REG, offset);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "chn %u: ASI2 offset write failed: %d\n",
+ chn, ret);
+ mutex_unlock(&tas2557->lock);
+ return ret;
+ }
+
+ ret = tas2557_dev_write(tas2557, chn, TAS2557_ASI1_OFFSET1_REG, offset);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "chn %u: ASI1 offset write failed: %d\n",
+ chn, ret);
+ mutex_unlock(&tas2557->lock);
+ return ret;
+ }
+
+ dev_dbg(tas2557->dev, "chn %u: ASI offset=%u BCLK cycles\n", chn, offset);
+ }
+
+ /*
+ * PLL (MAIN_CLKIN, PLL_CLKIN, PLL J/P/D/N) is not configured here.
+ * It is set entirely by the firmware PLL block loaded via
+ * tas2557_set_program() during tas2557_enable().
+ */
+
+ /* Commit cached stream state now the hardware writes have succeeded */
+ tas2557->sample_rate = rate;
+ tas2557->wordlength = wordlength;
+
+ mutex_unlock(&tas2557->lock);
+
+ return 0;
+}
+
+static int tas2557_set_dai_fmt(struct snd_soc_dai *dai, unsigned int fmt)
+{
+ struct snd_soc_component *component = dai->component;
+ struct tas2557_priv *tas2557 = snd_soc_component_get_drvdata(component);
+ unsigned int asi_fmt = 0;
+ int ret;
+
+ switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+ case SND_SOC_DAIFMT_I2S:
+ asi_fmt = TAS2557_FORMAT_I2S;
+ break;
+ case SND_SOC_DAIFMT_DSP_A:
+ case SND_SOC_DAIFMT_DSP_B:
+ asi_fmt = TAS2557_FORMAT_DSP;
+ break;
+ case SND_SOC_DAIFMT_RIGHT_J:
+ asi_fmt = TAS2557_FORMAT_RIGHT_J;
+ break;
+ case SND_SOC_DAIFMT_LEFT_J:
+ asi_fmt = TAS2557_FORMAT_LEFT_J;
+ break;
+ default:
+ dev_err(tas2557->dev, "unsupported DAI format 0x%x\n", fmt);
+ return -EINVAL;
+ }
+
+ mutex_lock(&tas2557->lock);
+ tas2557->dai_fmt = asi_fmt;
+ ret = tas2557_update_bits_all(tas2557, TAS2557_ASI1_DAC_FORMAT_REG,
+ TAS2557_FORMAT_MASK, asi_fmt);
+ if (ret >= 0)
+ ret = tas2557_update_bits_all(tas2557, TAS2557_ASI2_DAC_FORMAT_REG,
+ TAS2557_FORMAT_MASK, asi_fmt);
+ if (ret >= 0)
+ ret = tas2557_update_bits_all(tas2557, TAS2557_ASI1_ADC_FORMAT_REG,
+ TAS2557_FORMAT_MASK, asi_fmt);
+ if (ret >= 0)
+ ret = tas2557_update_bits_all(tas2557, TAS2557_ASI2_ADC_FORMAT_REG,
+ TAS2557_FORMAT_MASK, asi_fmt);
+ mutex_unlock(&tas2557->lock);
+
+ return ret;
+}
+
+static int tas2557_set_tdm_slot(struct snd_soc_dai *dai,
+ unsigned int tx_mask, unsigned int rx_mask,
+ int slots, int slot_width)
+{
+ struct snd_soc_component *component = dai->component;
+ struct tas2557_priv *tas2557 = snd_soc_component_get_drvdata(component);
+ int rx_slot = -1, tx_slot = -1;
+ unsigned int chn;
+ int ret = 0;
+
+ if (rx_mask) {
+ rx_slot = ffs(rx_mask) - 1;
+ if (rx_slot < 0 || rx_slot > 7)
+ return -EINVAL;
+ }
+ if (tx_mask) {
+ tx_slot = ffs(tx_mask) - 1;
+ if (tx_slot < 0 || tx_slot > 7)
+ return -EINVAL;
+ }
+
+ switch (slot_width) {
+ case 0:
+ case 16:
+ case 24:
+ case 32:
+ break;
+ default:
+ dev_err(tas2557->dev, "unsupported TDM slot width %d\n",
+ slot_width);
+ return -EINVAL;
+ }
+
+ mutex_lock(&tas2557->lock);
+
+ tas2557->tdm_slot_width = slot_width;
+
+ if (rx_slot >= 0) {
+ tas2557->tdm_configured = true;
+ tas2557->tdm_rx_slot = rx_slot;
+ /*
+ * RX (playback) data-slot offset in BCLK cycles. There is
+ * one DAI for the whole group, so every chip is programmed
+ * to the same slot.
+ */
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ tas2557->devs[chn].asi_offset = rx_slot * slot_width;
+
+ ret = tas2557_dev_write(tas2557, chn, TAS2557_ASI1_OFFSET1_REG,
+ tas2557->devs[chn].asi_offset);
+ if (ret >= 0)
+ ret = tas2557_dev_write(tas2557, chn,
+ TAS2557_ASI2_OFFSET1_REG,
+ tas2557->devs[chn].asi_offset);
+ if (ret < 0)
+ break;
+ }
+ }
+
+ if (ret >= 0 && tx_slot >= 0) {
+ unsigned int tx_offset = tx_slot * slot_width;
+
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ tas2557->devs[chn].asi_tx_offset = tx_offset;
+
+ ret = tas2557_dev_write(tas2557, chn, TAS2557_ASI1_OFFSET2_REG,
+ tx_offset);
+ if (ret >= 0)
+ ret = tas2557_dev_write(tas2557, chn,
+ TAS2557_ASI2_OFFSET2_REG,
+ tx_offset);
+ if (ret < 0)
+ break;
+ }
+ }
+
+ if (ret >= 0)
+ dev_dbg(tas2557->dev, "TDM: rx=%d tx=%d width=%d\n",
+ rx_slot, tx_slot, slot_width);
+
+ mutex_unlock(&tas2557->lock);
+ return ret;
+}
+
+static int tas2557_mute_stream(struct snd_soc_dai *dai, int mute, int direction)
+{
+ struct snd_soc_component *component = dai->component;
+ struct tas2557_priv *tas2557 = snd_soc_component_get_drvdata(component);
+ unsigned int sense_soft_mask = TAS2557_VSENSE_SOFT_MUTE |
+ TAS2557_ISENSE_SOFT_MUTE;
+ int ret = 0;
+
+ mutex_lock(&tas2557->lock);
+
+ if (!tas2557->powered) {
+ if (direction == SNDRV_PCM_STREAM_CAPTURE)
+ tas2557->sense_muted = mute;
+ else
+ tas2557->dac_muted = mute;
+ mutex_unlock(&tas2557->lock);
+ return 0;
+ }
+
+ if (direction == SNDRV_PCM_STREAM_CAPTURE) {
+ ret = tas2557_update_bits_all(tas2557, TAS2557_MUTE_REG,
+ TAS2557_ISENSE_MUTE,
+ mute ? TAS2557_ISENSE_MUTE : 0);
+ if (ret >= 0)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_SOFT_MUTE_REG,
+ sense_soft_mask,
+ mute ? sense_soft_mask : 0);
+ tas2557->sense_muted = mute;
+ } else if (mute) {
+ /* Detection off first: the host is about to stop the clocks */
+ ret = tas2557_clk_err_detect(tas2557, false);
+ if (ret >= 0)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_SOFT_MUTE_REG,
+ TAS2557_PDM_SOFT_MUTE,
+ TAS2557_PDM_SOFT_MUTE);
+ if (ret >= 0) {
+ usleep_range(10000, 11000);
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_MUTE_REG,
+ TAS2557_CLASSD_MUTE,
+ TAS2557_CLASSD_MUTE);
+ }
+ tas2557->dac_muted = mute;
+ } else {
+ /* Same register values as tas2557_unmute_data, same order */
+ ret = tas2557_load_data_all(tas2557, tas2557_unmute_data);
+ /* unmute_data clears sense mutes too; re-assert if needed */
+ if (ret >= 0 && tas2557->sense_muted)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_MUTE_REG,
+ TAS2557_ISENSE_MUTE,
+ TAS2557_ISENSE_MUTE);
+ if (ret >= 0 && tas2557->sense_muted)
+ ret = tas2557_update_bits_all(tas2557,
+ TAS2557_SOFT_MUTE_REG,
+ sense_soft_mask,
+ sense_soft_mask);
+ /* Clocks are running now: arm clock error detection */
+ if (ret >= 0)
+ ret = tas2557_clk_err_detect(tas2557, true);
+ tas2557->dac_muted = mute;
+ }
+
+ mutex_unlock(&tas2557->lock);
+ return ret;
+}
+
+static const struct snd_soc_dai_ops tas2557_dai_ops = {
+ .hw_params = tas2557_hw_params,
+ .set_fmt = tas2557_set_dai_fmt,
+ .set_tdm_slot = tas2557_set_tdm_slot,
+ .mute_stream = tas2557_mute_stream,
+};
+
+static struct snd_soc_dai_driver tas2557_dai = {
+ .name = "tas2557-amplifier",
+ .playback = {
+ .stream_name = "Playback",
+ .channels_min = 1,
+ .channels_max = 2,
+ .rates = SNDRV_PCM_RATE_8000_192000,
+ .formats = TAS2557_FORMATS,
+ },
+ .capture = {
+ .stream_name = "Capture",
+ .channels_min = 1,
+ .channels_max = 2,
+ .rates = SNDRV_PCM_RATE_8000_192000,
+ .formats = TAS2557_FORMATS,
+ },
+ .ops = &tas2557_dai_ops,
+ /*
+ * One firmware config (hence one rate) is active at a time, and
+ * playback and capture share the single ASI word length, so both
+ * substreams must agree on rate and sample width.
+ */
+ .symmetric_rate = 1,
+ .symmetric_sample_bits = 1,
+};
+
+/* =========================================================================
+ * ALSA mixer controls
+ * =========================================================================
+ */
+static DECLARE_TLV_DB_SCALE(tas2557_dac_tlv, -2800, 200, 0);
+
+static int tas2557_volume_get(struct snd_kcontrol *kc,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tas2557_priv *tas2557 =
+ snd_soc_component_get_drvdata(snd_kcontrol_chip(kc));
+
+ ucontrol->value.integer.value[0] = tas2557->dac_gain;
+ return 0;
+}
+
+static int tas2557_volume_put(struct snd_kcontrol *kc,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tas2557_priv *tas2557 =
+ snd_soc_component_get_drvdata(snd_kcontrol_chip(kc));
+ unsigned int gain = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ if (gain > TAS2557_DAC_GAIN_MAX)
+ return -EINVAL;
+
+ mutex_lock(&tas2557->lock);
+
+ if (gain == tas2557->dac_gain) {
+ mutex_unlock(&tas2557->lock);
+ return 0;
+ }
+
+ if (tas2557->powered) {
+ ret = tas2557_update_bits_all(tas2557, TAS2557_SPK_CTRL_REG,
+ TAS2557_DAC_GAIN_MASK,
+ gain << TAS2557_DAC_GAIN_SHIFT);
+ if (ret < 0) {
+ mutex_unlock(&tas2557->lock);
+ return ret;
+ }
+ }
+
+ /* Only commit the cached gain once the hardware write has succeeded */
+ tas2557->dac_gain = gain;
+
+ mutex_unlock(&tas2557->lock);
+
+ return 1;
+}
+
+static int tas2557_isense_get(struct snd_kcontrol *kc,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tas2557_priv *tas2557 =
+ snd_soc_component_get_drvdata(snd_kcontrol_chip(kc));
+
+ ucontrol->value.integer.value[0] = tas2557->isense_enabled;
+ return 0;
+}
+
+static int tas2557_isense_put(struct snd_kcontrol *kc,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tas2557_priv *tas2557 =
+ snd_soc_component_get_drvdata(snd_kcontrol_chip(kc));
+ bool en = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ mutex_lock(&tas2557->lock);
+
+ if (en == tas2557->isense_enabled) {
+ mutex_unlock(&tas2557->lock);
+ return 0;
+ }
+
+ if (tas2557->powered) {
+ ret = tas2557_update_bits_all(tas2557, TAS2557_POWER_CTRL2_REG,
+ TAS2557_ISENSE_ENABLE,
+ en ? TAS2557_ISENSE_ENABLE : 0);
+ if (ret < 0) {
+ mutex_unlock(&tas2557->lock);
+ return ret;
+ }
+ }
+
+ /*
+ * Commit the cached state only once the enable bit is in the
+ * hardware (or while unpowered, where it is applied on the next
+ * power-up); tas2557_sns_ctrl_write_all() below reads it.
+ */
+ tas2557->isense_enabled = en;
+
+ if (tas2557->powered && (en || tas2557->vsense_enabled)) {
+ ret = tas2557_sns_ctrl_write_all(tas2557);
+ if (ret < 0) {
+ mutex_unlock(&tas2557->lock);
+ return ret;
+ }
+ }
+
+ mutex_unlock(&tas2557->lock);
+
+ return 1;
+}
+
+static int tas2557_vsense_get(struct snd_kcontrol *kc,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tas2557_priv *tas2557 =
+ snd_soc_component_get_drvdata(snd_kcontrol_chip(kc));
+
+ ucontrol->value.integer.value[0] = tas2557->vsense_enabled;
+ return 0;
+}
+
+static int tas2557_vsense_put(struct snd_kcontrol *kc,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct tas2557_priv *tas2557 =
+ snd_soc_component_get_drvdata(snd_kcontrol_chip(kc));
+ bool en = ucontrol->value.integer.value[0];
+ int ret = 0;
+
+ mutex_lock(&tas2557->lock);
+
+ if (en == tas2557->vsense_enabled) {
+ mutex_unlock(&tas2557->lock);
+ return 0;
+ }
+
+ if (tas2557->powered) {
+ ret = tas2557_update_bits_all(tas2557, TAS2557_POWER_CTRL2_REG,
+ TAS2557_VSENSE_ENABLE,
+ en ? TAS2557_VSENSE_ENABLE : 0);
+ if (ret < 0) {
+ mutex_unlock(&tas2557->lock);
+ return ret;
+ }
+ }
+
+ /*
+ * Commit the cached state only once the enable bit is in the
+ * hardware (or while unpowered, where it is applied on the next
+ * power-up); tas2557_sns_ctrl_write_all() below reads it.
+ */
+ tas2557->vsense_enabled = en;
+
+ if (tas2557->powered && (tas2557->isense_enabled || en)) {
+ ret = tas2557_sns_ctrl_write_all(tas2557);
+ if (ret < 0) {
+ mutex_unlock(&tas2557->lock);
+ return ret;
+ }
+ }
+
+ mutex_unlock(&tas2557->lock);
+
+ return 1;
+}
+
+static const struct snd_kcontrol_new tas2557_controls[] = {
+ SOC_SINGLE_EXT_TLV("Speaker Playback Volume", SND_SOC_NOPM, 0,
+ TAS2557_DAC_GAIN_MAX, 0,
+ tas2557_volume_get, tas2557_volume_put,
+ tas2557_dac_tlv),
+ SOC_SINGLE_BOOL_EXT("ISENSE Switch", 0,
+ tas2557_isense_get, tas2557_isense_put),
+ SOC_SINGLE_BOOL_EXT("VSENSE Switch", 0,
+ tas2557_vsense_get, tas2557_vsense_put),
+};
+
+/* =========================================================================
+ * DAPM topology
+ *
+ * Playback path: ASI1/ASI2 (AIF_IN, stream "Playback") -> DAC -> ClassD -> OUT
+ * Capture path: ClassD -> ISENSE/VSENSE -> SENSE (AIF_OUT, "Capture")
+ *
+ * Both ASI1 and ASI2 are configured identically (see tas2557_startup_data);
+ * the board wiring determines which one actually carries audio.
+ * =========================================================================
+ */
+static int tas2557_classd_event(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 tas2557_priv *tas2557 = snd_soc_component_get_drvdata(component);
+ int ret;
+
+ switch (event) {
+ case SND_SOC_DAPM_POST_PMU:
+ /*
+ * Wait here, not inside tas2557_enable(): that function
+ * holds tas2557->lock, which tas2557_fw_ready() must
+ * acquire before it can complete tas2557->fw_done, so
+ * waiting on the firmware from inside the lock would
+ * deadlock against the firmware-ready callback.
+ */
+ if (tas2557->fw_requested &&
+ !wait_for_completion_timeout(&tas2557->fw_done,
+ msecs_to_jiffies(5000)))
+ dev_warn(tas2557->dev, "firmware load timed out\n");
+
+ mutex_lock(&tas2557->lock);
+ ret = tas2557_enable(tas2557, true);
+ mutex_unlock(&tas2557->lock);
+ if (ret < 0)
+ return ret;
+ tas2557_irq_arm(tas2557);
+ return 0;
+ case SND_SOC_DAPM_PRE_PMD:
+ tas2557_irq_disarm(tas2557);
+ mutex_lock(&tas2557->lock);
+ ret = tas2557_enable(tas2557, false);
+ mutex_unlock(&tas2557->lock);
+ return ret;
+ default:
+ return 0;
+ }
+}
+
+static const struct snd_soc_dapm_widget tas2557_dapm_widgets[] = {
+ SND_SOC_DAPM_AIF_IN("ASI1", "Playback", 0, SND_SOC_NOPM, 0, 0),
+ SND_SOC_DAPM_AIF_IN("ASI2", "Playback", 0, SND_SOC_NOPM, 0, 0),
+
+ SND_SOC_DAPM_DAC("DAC", NULL, SND_SOC_NOPM, 0, 0),
+
+ SND_SOC_DAPM_OUT_DRV_E("ClassD", SND_SOC_NOPM, 0, 0, NULL, 0,
+ tas2557_classd_event,
+ SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD),
+
+ /*
+ * ISENSE and VSENSE are controlled via tas2557_enable() and the
+ * ISENSE/VSENSE mixer controls, not via DAPM register writes.
+ * Use SND_SOC_NOPM to avoid direct regmap access to book-paged regs.
+ */
+ SND_SOC_DAPM_ADC("ISENSE", NULL, SND_SOC_NOPM, 0, 0),
+ SND_SOC_DAPM_ADC("VSENSE", NULL, SND_SOC_NOPM, 0, 0),
+
+ SND_SOC_DAPM_OUTPUT("OUT"),
+ SND_SOC_DAPM_AIF_OUT("SENSE", "Capture", 0, SND_SOC_NOPM, 0, 0),
+};
+
+static const struct snd_soc_dapm_route tas2557_dapm_routes[] = {
+ { "ASI1", NULL, "Playback" },
+ { "ASI2", NULL, "Playback" },
+ { "DAC", NULL, "ASI1" },
+ { "DAC", NULL, "ASI2" },
+ { "ClassD", NULL, "DAC" },
+ { "OUT", NULL, "ClassD" },
+
+ { "ISENSE", NULL, "ClassD" },
+ { "VSENSE", NULL, "ClassD" },
+ { "SENSE", NULL, "ISENSE" },
+ { "SENSE", NULL, "VSENSE" },
+ { "Capture", NULL, "SENSE" },
+};
+
+/* =========================================================================
+ * ASoC component probe / suspend / resume
+ * =========================================================================
+ */
+static int tas2557_codec_probe(struct snd_soc_component *component)
+{
+ struct tas2557_priv *tas2557 = snd_soc_component_get_drvdata(component);
+ const char *default_fw;
+ unsigned int chn;
+ int ret;
+
+ tas2557_hw_reset(tas2557);
+
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ ret = tas2557_dev_write(tas2557, chn, TAS2557_SW_RESET_REG, 0x01);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "chn %u: software reset failed: %d\n",
+ chn, ret);
+ return ret;
+ }
+ }
+ usleep_range(1000, 2000);
+
+ for (chn = 0; chn < tas2557->ndev; chn++) {
+ unsigned int pg_id;
+
+ ret = tas2557_dev_read(tas2557, chn, TAS2557_REV_PGID_REG, &pg_id);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "chn %u: failed to read REV_PGID: %d\n",
+ chn, ret);
+ return ret;
+ }
+ tas2557->devs[chn].pg_id = pg_id;
+ }
+
+ /*
+ * Firmware selection is group-wide (one shared image); the PG
+ * version of devs[0] is used as the representative silicon
+ * revision, matching how current_program/current_config/pg_id are
+ * otherwise read back from devs[0] elsewhere in this driver.
+ */
+ switch (tas2557->devs[0].pg_id) {
+ case TAS2557_PG_VERSION_1P0:
+ dev_dbg(tas2557->dev, "silicon: PG1.0 (0x%02x)\n",
+ tas2557->devs[0].pg_id);
+ default_fw = TAS2557_PG1P0_FW_NAME;
+ break;
+ case TAS2557_PG_VERSION_2P0:
+ dev_dbg(tas2557->dev, "silicon: PG2.0 (0x%02x)\n",
+ tas2557->devs[0].pg_id);
+ default_fw = TAS2557_FW_NAME;
+ break;
+ case TAS2557_PG_VERSION_2P1:
+ dev_dbg(tas2557->dev, "silicon: PG2.1 (0x%02x)\n",
+ tas2557->devs[0].pg_id);
+ default_fw = TAS2557_FW_NAME;
+ break;
+ default:
+ dev_warn(tas2557->dev,
+ "unknown silicon version 0x%02x, assuming PG2.x fw\n",
+ tas2557->devs[0].pg_id);
+ default_fw = TAS2557_FW_NAME;
+ break;
+ }
+
+ mutex_lock(&tas2557->lock);
+ /*
+ * A "firmware-name" DT property (read at i2c probe) overrides the
+ * PG-based default; dual-amp stereo boards load a different DSP
+ * blob (e.g. tas2557s_uCDSP.bin).
+ */
+ if (!tas2557->fw_name[0])
+ strscpy(tas2557->fw_name, default_fw, sizeof(tas2557->fw_name));
+ mutex_unlock(&tas2557->lock);
+
+ ret = tas2557_load_data_all(tas2557, tas2557_default_data);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "default data load failed: %d\n", ret);
+ return ret;
+ }
+
+ ret = tas2557_load_data_all(tas2557, tas2557_irq_config);
+ if (ret < 0)
+ dev_warn(tas2557->dev, "IRQ config failed: %d\n", ret);
+
+ /*
+ * Component rebind (e.g. after an unbind/bind cycle): firmware is
+ * already parsed and stored, so do not request it again.
+ */
+ mutex_lock(&tas2557->lock);
+ if (!tas2557->fw_requested) {
+ reinit_completion(&tas2557->fw_done);
+ ret = request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT,
+ tas2557->fw_name, tas2557->dev,
+ GFP_KERNEL, tas2557,
+ tas2557_fw_ready);
+ if (ret) {
+ mutex_unlock(&tas2557->lock);
+ return dev_err_probe(tas2557->dev, ret,
+ "failed to request fw '%s'\n",
+ tas2557->fw_name);
+ }
+ tas2557->fw_requested = true;
+ }
+ mutex_unlock(&tas2557->lock);
+
+ dev_dbg(tas2557->dev, "codec probed (%u chip(s)), requesting firmware '%s'\n",
+ tas2557->ndev, tas2557->fw_name);
+ return 0;
+}
+
+static int tas2557_suspend(struct snd_soc_component *component)
+{
+ struct tas2557_priv *tas2557 = snd_soc_component_get_drvdata(component);
+
+ tas2557_irq_disarm(tas2557);
+
+ mutex_lock(&tas2557->lock);
+ if (tas2557->powered)
+ tas2557_enable(tas2557, false);
+ mutex_unlock(&tas2557->lock);
+
+ return 0;
+}
+
+static int tas2557_resume(struct snd_soc_component *component)
+{
+ struct tas2557_priv *tas2557 = snd_soc_component_get_drvdata(component);
+ int ret;
+
+ mutex_lock(&tas2557->lock);
+
+ tas2557_hw_reset(tas2557);
+
+ ret = tas2557_load_data_all(tas2557, tas2557_default_data);
+ if (ret < 0)
+ dev_err(tas2557->dev, "defaults reload on resume failed: %d\n",
+ ret);
+ else if (tas2557_load_data_all(tas2557, tas2557_irq_config) < 0)
+ dev_warn(tas2557->dev, "IRQ config reload on resume failed\n");
+
+ mutex_unlock(&tas2557->lock);
+
+ return ret;
+}
+
+static const struct snd_soc_component_driver soc_component_tas2557 = {
+ .probe = tas2557_codec_probe,
+ .suspend = tas2557_suspend,
+ .resume = tas2557_resume,
+ .controls = tas2557_controls,
+ .num_controls = ARRAY_SIZE(tas2557_controls),
+ .dapm_widgets = tas2557_dapm_widgets,
+ .num_dapm_widgets = ARRAY_SIZE(tas2557_dapm_widgets),
+ .dapm_routes = tas2557_dapm_routes,
+ .num_dapm_routes = ARRAY_SIZE(tas2557_dapm_routes),
+ .idle_bias_on = 1,
+ .use_pmdown_time = 1,
+ .endianness = 1,
+};
+
+/* =========================================================================
+ * I2C driver
+ * =========================================================================
+ */
+
+/*
+ * tas2557_parse_dt - populate tas2557->devs[]/ndev and their DT parameters
+ *
+ * `reg` holds one I2C address per physical chip in the group (up to
+ * TAS2557_MAX_DEV); read per-index with of_property_read_reg(), the same
+ * OF-array-reading loop tasdevice_parse_dt() uses in tas2781-i2c.c, since
+ * i2c_client::addr only ever reflects the first entry. Falls back to a
+ * single chip at the i2c_client's own address when there is no OF node.
+ *
+ * ti,imon-slot-nos / ti,vmon-slot-nos are u32 arrays, one entry per devs[]
+ * index; a missing property or one with fewer entries than ndev leaves the
+ * unfilled tail at the pre-seeded scalar defaults (0 / 2, matching the old
+ * single-chip property defaults, replicated across every index).
+ */
+static int tas2557_parse_dt(struct tas2557_priv *tas2557, struct i2c_client *client)
+{
+ struct device *dev = tas2557->dev;
+ struct device_node *np = dev->of_node;
+ unsigned int imon_slots[TAS2557_MAX_DEV] = { 0, 0 };
+ unsigned int vmon_slots[TAS2557_MAX_DEV] = { 2, 2 };
+ unsigned int i;
+ int ndev = 0;
+
+ if (np) {
+ u64 addr;
+
+ for (i = 0; i < TAS2557_MAX_DEV; i++) {
+ if (of_property_read_reg(np, i, &addr, NULL))
+ break;
+ tas2557->devs[ndev++].addr = addr;
+ }
+ }
+
+ if (ndev == 0) {
+ ndev = 1;
+ tas2557->devs[0].addr = client->addr;
+ }
+
+ tas2557->ndev = ndev;
+
+ of_property_read_variable_u32_array(np, "ti,imon-slot-nos",
+ imon_slots, 1, tas2557->ndev);
+ of_property_read_variable_u32_array(np, "ti,vmon-slot-nos",
+ vmon_slots, 1, tas2557->ndev);
+
+ for (i = 0; i < tas2557->ndev; i++) {
+ if (imon_slots[i] > 7)
+ return dev_err_probe(dev, -EINVAL,
+ "ti,imon-slot-nos[%u] must be 0-7\n", i);
+ if (vmon_slots[i] > 7)
+ return dev_err_probe(dev, -EINVAL,
+ "ti,vmon-slot-nos[%u] must be 0-7\n", i);
+
+ tas2557->devs[i].imon_slot = imon_slots[i];
+ tas2557->devs[i].vmon_slot = vmon_slots[i];
+ tas2557->devs[i].current_book = 0xff;
+ tas2557->devs[i].current_page = 0xff;
+ tas2557->devs[i].pg_id = 0;
+ tas2557->devs[i].current_program = 0;
+ tas2557->devs[i].current_config = 0;
+ tas2557->devs[i].restart_count = 0;
+ /*
+ * Seed the cached ASI data-slot offset before any
+ * hw_params() or set_tdm_slot() call arrives; matches the
+ * default slot (offset 0) tas2557_hw_params() falls back to
+ * when no explicit TDM configuration exists.
+ */
+ tas2557->devs[i].asi_offset = 0;
+ /* -1 means "never set"; see tas2557_apply_runtime_state() */
+ tas2557->devs[i].asi_tx_offset = -1;
+ }
+
+ return 0;
+}
+
+static int tas2557_i2c_probe(struct i2c_client *client)
+{
+ static const char * const tas2557_supplies[] = {
+ "vbat", "iovdd", "avdd", "dvdd"
+ };
+ struct device *dev = &client->dev;
+ struct tas2557_priv *tas2557;
+ const char *fw_name;
+ int ret;
+
+ tas2557 = devm_kzalloc(dev, sizeof(*tas2557), GFP_KERNEL);
+ if (!tas2557)
+ return -ENOMEM;
+
+ tas2557->dev = dev;
+ i2c_set_clientdata(client, tas2557);
+
+ mutex_init(&tas2557->dev_lock);
+ mutex_init(&tas2557->lock);
+ init_completion(&tas2557->fw_done);
+ tas2557->fw_requested = false;
+
+ ret = tas2557_parse_dt(tas2557, client);
+ if (ret)
+ return ret;
+
+ /* Default gain (0 dB) */
+ tas2557->dac_gain = TAS2557_DAC_GAIN_MAX;
+
+ /* The device group comes out of reset muted */
+ tas2557->dac_muted = true;
+ tas2557->sense_muted = true;
+
+ /* -1 means "never set"; see tas2557_apply_runtime_state() */
+ tas2557->dai_fmt = -1;
+ tas2557->tdm_rx_slot = -1;
+
+ /* Seed a valid ASI word length before the first hw_params() */
+ tas2557->wordlength = TAS2557_WORDLENGTH_32BIT;
+
+ /* Optional explicit firmware name; overrides the PG-based default */
+ if (!of_property_read_string(dev->of_node, "firmware-name", &fw_name))
+ strscpy(tas2557->fw_name, fw_name, sizeof(tas2557->fw_name));
+
+ ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(tas2557_supplies),
+ tas2557_supplies);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to get/enable supplies\n");
+ usleep_range(5000, 10000);
+
+ /*
+ * Reset GPIO is modeled active-low in DT: the logical value 0
+ * (GPIOD_OUT_LOW) deasserts reset, releasing the device group.
+ */
+ tas2557->reset_gpio = devm_gpiod_get_optional(dev, "reset",
+ GPIOD_OUT_LOW);
+ if (IS_ERR(tas2557->reset_gpio))
+ return dev_err_probe(dev, PTR_ERR(tas2557->reset_gpio),
+ "failed to get reset GPIO\n");
+
+ tas2557->regmap = devm_regmap_init_i2c(client, &tas2557_regmap_config);
+ if (IS_ERR(tas2557->regmap))
+ return dev_err_probe(dev, PTR_ERR(tas2557->regmap),
+ "regmap init failed\n");
+
+ /*
+ * Registered here, right after regmap init: devm actions run in
+ * LIFO order, so tas2557_fw_teardown() runs AFTER the component
+ * registered below is unregistered and the IRQ requested below is
+ * freed (so neither tas2557_fw_ready() nor tas2557_irq_handler()
+ * can still be running), but BEFORE regmap, the reset GPIO and
+ * the regulators are released.
+ */
+ ret = devm_add_action_or_reset(dev, tas2557_fw_teardown, tas2557);
+ if (ret)
+ return ret;
+
+ if (client->irq) {
+ ret = devm_request_threaded_irq(dev, client->irq, NULL,
+ tas2557_irq_handler,
+ IRQF_ONESHOT | IRQF_NO_AUTOEN,
+ "tas2557", tas2557);
+ if (ret) {
+ dev_warn(dev, "IRQ %d request failed: %d\n",
+ client->irq, ret);
+ tas2557->irq = 0;
+ } else {
+ tas2557->irq = client->irq;
+ dev_dbg(dev, "IRQ %d registered\n", client->irq);
+ }
+ }
+
+ ret = devm_snd_soc_register_component(dev, &soc_component_tas2557,
+ &tas2557_dai, 1);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "component registration failed\n");
+
+ dev_dbg(dev, "TAS2557 probed (%u chip(s), primary I2C addr 0x%02x)\n",
+ tas2557->ndev, client->addr);
+ return 0;
+}
+
+static const struct i2c_device_id tas2557_i2c_id[] = {
+ { "tas2557" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, tas2557_i2c_id);
+
+static const struct of_device_id tas2557_of_match[] = {
+ { .compatible = "ti,tas2557" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, tas2557_of_match);
+
+static struct i2c_driver tas2557_i2c_driver = {
+ .driver = {
+ .name = "tas2557",
+ .of_match_table = tas2557_of_match,
+ },
+ .probe = tas2557_i2c_probe,
+ .id_table = tas2557_i2c_id,
+};
+module_i2c_driver(tas2557_i2c_driver);
+
+MODULE_AUTHOR("Texas Instruments Inc.");
+MODULE_AUTHOR("Gianluca Boiano <morf3089@gmail.com>");
+MODULE_DESCRIPTION("ASoC TAS2557 Smart Amplifier Driver");
+MODULE_FIRMWARE(TAS2557_FW_NAME);
+MODULE_FIRMWARE(TAS2557_PG1P0_FW_NAME);
+MODULE_LICENSE("GPL");
diff --git a/sound/soc/codecs/tas2557.h b/sound/soc/codecs/tas2557.h
new file mode 100644
index 000000000000..024ea2474f7d
--- /dev/null
+++ b/sound/soc/codecs/tas2557.h
@@ -0,0 +1,228 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * ALSA SoC Texas Instruments TAS2557 Smart Amplifier
+ *
+ * Copyright (C) 2016 Texas Instruments Inc.
+ * Copyright (C) 2026 Gianluca Boiano <morf3089@gmail.com>
+ */
+
+#ifndef __TAS2557_H__
+#define __TAS2557_H__
+
+#include <linux/bits.h>
+
+/*
+ * TAS2557 uses a book/page/register addressing scheme.
+ * Physical I2C address has 8-bit registers; page is selected via register 0;
+ * book is selected via register 0x7f in page 0.
+ * Encoding: 0xBBBBBBBB_PPPPPPPP_RRRRRRR (book * 256 * 128 + page * 128 + reg)
+ */
+
+/* Page Control Register - same in all pages */
+#define TAS2557_PAGE_REG 0x00
+
+/* Book Control Register - in page 0 of each book */
+#define TAS2557_BOOK_REG 0x7f
+
+/* Address encoding macros */
+#define TAS2557_REG(book, page, reg) \
+ ((((unsigned int)(book) * 256 * 128) + \
+ ((unsigned int)(page) * 128)) + (reg))
+
+#define TAS2557_BOOK_ID(reg) ((unsigned char)((reg) / (256 * 128)))
+#define TAS2557_PAGE_ID(reg) ((unsigned char)(((reg) % (256 * 128)) / 128))
+#define TAS2557_PAGE_REG_ADDR(reg) ((unsigned char)((reg) % 128))
+
+/* Book 0, Page 0 registers */
+#define TAS2557_SW_RESET_REG TAS2557_REG(0, 0, 1)
+#define TAS2557_REV_PGID_REG TAS2557_REG(0, 0, 3)
+#define TAS2557_POWER_CTRL1_REG TAS2557_REG(0, 0, 4)
+#define TAS2557_POWER_CTRL2_REG TAS2557_REG(0, 0, 5)
+#define TAS2557_SPK_CTRL_REG TAS2557_REG(0, 0, 6)
+#define TAS2557_MUTE_REG TAS2557_REG(0, 0, 7)
+#define TAS2557_SNS_CTRL_REG TAS2557_REG(0, 0, 8)
+#define TAS2557_ADC_INPUT_SEL_REG TAS2557_REG(0, 0, 9)
+#define TAS2557_DBOOST_CTL_REG TAS2557_REG(0, 0, 10)
+#define TAS2557_SAR_SAMPLING_TIME_REG TAS2557_REG(0, 0, 19)
+#define TAS2557_SAR_ADC1_REG TAS2557_REG(0, 0, 20)
+#define TAS2557_SAR_ADC2_REG TAS2557_REG(0, 0, 21)
+#define TAS2557_CRC_CHECKSUM_REG TAS2557_REG(0, 0, 32)
+#define TAS2557_CRC_RESET_REG TAS2557_REG(0, 0, 33)
+#define TAS2557_DSP_MODE_SELECT_REG TAS2557_REG(0, 0, 34)
+#define TAS2557_SAFE_GUARD_REG TAS2557_REG(0, 0, 37)
+#define TAS2557_ASI_CTL1_REG TAS2557_REG(0, 0, 42)
+#define TAS2557_CLK_ERR_CTRL TAS2557_REG(0, 0, 44)
+#define TAS2557_CLK_ERR_CTRL2 TAS2557_REG(0, 0, 45)
+#define TAS2557_CLK_ERR_CTRL3 TAS2557_REG(0, 0, 46)
+#define TAS2557_DBOOST_CFG_REG TAS2557_REG(0, 0, 52)
+#define TAS2557_POWER_UP_FLAG_REG TAS2557_REG(0, 0, 100)
+#define TAS2557_FLAGS_1 TAS2557_REG(0, 0, 104)
+#define TAS2557_FLAGS_2 TAS2557_REG(0, 0, 108)
+
+/* Book 0, Page 1 registers - ASI1 configuration */
+#define TAS2557_ASI1_DAC_FORMAT_REG TAS2557_REG(0, 1, 1)
+#define TAS2557_ASI1_ADC_FORMAT_REG TAS2557_REG(0, 1, 2)
+#define TAS2557_ASI1_OFFSET1_REG TAS2557_REG(0, 1, 3)
+#define TAS2557_ASI1_OFFSET2_REG TAS2557_REG(0, 1, 4)
+#define TAS2557_ASI1_ADC_PATH_REG TAS2557_REG(0, 1, 7)
+#define TAS2557_ASI1_DAC_BCLK_REG TAS2557_REG(0, 1, 8)
+#define TAS2557_ASI1_DAC_WCLK_REG TAS2557_REG(0, 1, 9)
+#define TAS2557_ASI1_ADC_BCLK_REG TAS2557_REG(0, 1, 10)
+#define TAS2557_ASI1_ADC_WCLK_REG TAS2557_REG(0, 1, 11)
+#define TAS2557_ASI1_DIN_DOUT_MUX_REG TAS2557_REG(0, 1, 12)
+
+/* Book 0, Page 1 - ASI2 configuration */
+#define TAS2557_ASI2_DAC_FORMAT_REG TAS2557_REG(0, 1, 21)
+#define TAS2557_ASI2_ADC_FORMAT_REG TAS2557_REG(0, 1, 22)
+#define TAS2557_ASI2_OFFSET1_REG TAS2557_REG(0, 1, 23)
+#define TAS2557_ASI2_OFFSET2_REG TAS2557_REG(0, 1, 24)
+#define TAS2557_ASI2_ADC_PATH_REG TAS2557_REG(0, 1, 27)
+#define TAS2557_ASI2_DAC_BCLK_REG TAS2557_REG(0, 1, 28)
+#define TAS2557_ASI2_DAC_WCLK_REG TAS2557_REG(0, 1, 29)
+#define TAS2557_ASI2_ADC_BCLK_REG TAS2557_REG(0, 1, 30)
+#define TAS2557_ASI2_ADC_WCLK_REG TAS2557_REG(0, 1, 31)
+#define TAS2557_ASI2_DIN_DOUT_MUX_REG TAS2557_REG(0, 1, 32)
+#define TAS2557_ASI2_BDIV_CLK_SEL_REG TAS2557_REG(0, 1, 33)
+#define TAS2557_ASI2_BDIV_CLK_RATIO_REG TAS2557_REG(0, 1, 34)
+#define TAS2557_ASI2_WDIV_CLK_RATIO_REG TAS2557_REG(0, 1, 35)
+#define TAS2557_ASI2_DAC_CLKOUT_REG TAS2557_REG(0, 1, 36)
+#define TAS2557_ASI2_ADC_CLKOUT_REG TAS2557_REG(0, 1, 37)
+
+/* Book 0, Page 1 - GPIO pin configuration */
+#define TAS2557_GPIO1_PIN_REG TAS2557_REG(0, 1, 61)
+#define TAS2557_GPIO2_PIN_REG TAS2557_REG(0, 1, 62)
+#define TAS2557_GPIO3_PIN_REG TAS2557_REG(0, 1, 63)
+#define TAS2557_GPIO4_PIN_REG TAS2557_REG(0, 1, 64)
+#define TAS2557_GPIO5_PIN_REG TAS2557_REG(0, 1, 65)
+#define TAS2557_GPIO6_PIN_REG TAS2557_REG(0, 1, 66)
+#define TAS2557_GPIO7_PIN_REG TAS2557_REG(0, 1, 67)
+#define TAS2557_GPIO8_PIN_REG TAS2557_REG(0, 1, 68)
+#define TAS2557_GPI_PIN_REG TAS2557_REG(0, 1, 77)
+
+/* Book 0, Page 1 - interrupt configuration */
+#define TAS2557_CLK_HALT_REG TAS2557_REG(0, 1, 106)
+#define TAS2557_INT_GEN1_REG TAS2557_REG(0, 1, 108)
+#define TAS2557_INT_GEN2_REG TAS2557_REG(0, 1, 109)
+#define TAS2557_INT_GEN3_REG TAS2557_REG(0, 1, 110)
+#define TAS2557_INT_GEN4_REG TAS2557_REG(0, 1, 111)
+#define TAS2557_INT_MODE_REG TAS2557_REG(0, 1, 114)
+#define TAS2557_MAIN_CLKIN_REG TAS2557_REG(0, 1, 115)
+#define TAS2557_PLL_CLKIN_REG TAS2557_REG(0, 1, 116)
+
+/* Book 0, Page 2 */
+#define TAS2557_SLEEPMODE_CTL_REG TAS2557_REG(0, 2, 7)
+
+/* Book 100, Page 0 - DSP / PLL registers */
+#define TAS2557_DAC_INTERPOL_REG TAS2557_REG(100, 0, 1)
+#define TAS2557_SOFT_MUTE_REG TAS2557_REG(100, 0, 7)
+#define TAS2557_PLL_P_VAL_REG TAS2557_REG(100, 0, 27)
+#define TAS2557_PLL_J_VAL_REG TAS2557_REG(100, 0, 28)
+#define TAS2557_PLL_D_VAL_MSB_REG TAS2557_REG(100, 0, 29)
+#define TAS2557_PLL_D_VAL_LSB_REG TAS2557_REG(100, 0, 30)
+#define TAS2557_CLK_MISC_REG TAS2557_REG(100, 0, 31)
+#define TAS2557_PLL_N_VAL_REG TAS2557_REG(100, 0, 32)
+#define TAS2557_DAC_MADC_VAL_REG TAS2557_REG(100, 0, 33)
+#define TAS2557_VBOOST_CTL_REG TAS2557_REG(100, 0, 64)
+
+/* Book 130, Page 2 - Die temperature */
+#define TAS2557_DIE_TEMP_REG TAS2557_REG(130, 2, 124)
+
+/* DSP coefficient swap register (skipped by the firmware CRC checker) */
+#define TAS2557_SA_COEFF_SWAP_REG TAS2557_REG(0, 53, 44)
+
+/* PG (Process Generation) version identifiers from REV_PGID_REG */
+#define TAS2557_PG_VERSION_1P0 0x80
+#define TAS2557_PG_VERSION_2P0 0x90
+#define TAS2557_PG_VERSION_2P1 0xa0
+
+/* B0P0R4 - TAS2557_POWER_CTRL1_REG */
+#define TAS2557_SW_SHUTDOWN BIT(0)
+#define TAS2557_MADC_POWER_UP BIT(3)
+#define TAS2557_MDAC_POWER_UP BIT(4)
+#define TAS2557_NDIV_POWER_UP BIT(5)
+#define TAS2557_PLL_POWER_UP BIT(6)
+#define TAS2557_DSP_POWER_UP BIT(7)
+
+/* B0P0R5 - TAS2557_POWER_CTRL2_REG */
+#define TAS2557_VSENSE_ENABLE BIT(0)
+#define TAS2557_ISENSE_ENABLE BIT(1)
+#define TAS2557_BOOST_ENABLE BIT(5)
+#define TAS2557_CLASSD_ENABLE BIT(7)
+
+/* B0P0R6 - TAS2557_SPK_CTRL_REG */
+#define TAS2557_DAC_GAIN_MASK (0xf << 3)
+#define TAS2557_DAC_GAIN_SHIFT 3
+#define TAS2557_DAC_GAIN_MAX 0x0f
+
+/* B0P0R8 - TAS2557_SNS_CTRL_REG */
+#define TAS2557_ISNS_SLOT_MASK (0x7 << 4)
+#define TAS2557_ISNS_SLOT_SHIFT 4
+#define TAS2557_VSNS_SLOT_MASK (0x7 << 0)
+#define TAS2557_VSNS_SLOT_SHIFT 0
+
+/* B0P0R7 - TAS2557_MUTE_REG */
+#define TAS2557_CLASSD_MUTE BIT(0)
+#define TAS2557_ISENSE_MUTE BIT(1)
+
+/* B100P0R7 - TAS2557_SOFT_MUTE_REG */
+#define TAS2557_PDM_SOFT_MUTE BIT(0)
+#define TAS2557_VSENSE_SOFT_MUTE BIT(1)
+#define TAS2557_ISENSE_SOFT_MUTE BIT(2)
+#define TAS2557_CLASSD_SOFT_MUTE BIT(3)
+
+/* ASI format field in ASIx_DAC_FORMAT_REG bits [7:5] */
+#define TAS2557_FORMAT_I2S (0x0 << 5)
+#define TAS2557_FORMAT_DSP (0x1 << 5)
+#define TAS2557_FORMAT_RIGHT_J (0x2 << 5)
+#define TAS2557_FORMAT_LEFT_J (0x3 << 5)
+#define TAS2557_FORMAT_MONO_PCM (0x4 << 5)
+#define TAS2557_FORMAT_MASK (0x7 << 5)
+
+/* ASI word length field in ASIx_DAC_FORMAT_REG bits [4:3] */
+#define TAS2557_WORDLENGTH_16BIT (0x0 << 3)
+#define TAS2557_WORDLENGTH_20BIT (0x1 << 3)
+#define TAS2557_WORDLENGTH_24BIT (0x2 << 3)
+#define TAS2557_WORDLENGTH_32BIT (0x3 << 3)
+#define TAS2557_WORDLENGTH_MASK (0x3 << 3)
+
+/* Safe guard pattern written to TAS2557_SAFE_GUARD_REG */
+#define TAS2557_SAFE_GUARD_PATTERN 0x5a
+
+/* Firmware filenames - PG version determines which file is loaded */
+#define TAS2557_FW_NAME "tas2557_uCDSP.bin"
+#define TAS2557_PG1P0_FW_NAME "tas2557_pg1p0_uCDSP.bin"
+
+/* "device" field in the firmware header */
+#define TAS2557_FW_DEVICE_MONO 2
+#define TAS2557_FW_DEVICE_STEREO 3
+
+/* Firmware block type codes */
+#define TAS2557_BLOCK_PLL 0x00
+#define TAS2557_BLOCK_PGM_ALL 0x0d
+#define TAS2557_BLOCK_PGM_DEV_A 0x01
+#define TAS2557_BLOCK_PGM_DEV_B 0x08
+#define TAS2557_BLOCK_CFG_COEFF_DEV_A 0x03
+#define TAS2557_BLOCK_CFG_COEFF_DEV_B 0x0a
+#define TAS2557_BLOCK_CFG_PRE_DEV_A 0x04
+#define TAS2557_BLOCK_CFG_PRE_DEV_B 0x0b
+#define TAS2557_BLOCK_CFG_POST 0x05
+#define TAS2557_BLOCK_CFG_POST_POWER 0x06
+
+/*
+ * One DT node manages an array of up to TAS2557_MAX_DEV physical chips
+ * (see the ti,tas2557.yaml binding's `reg` array) -- mirroring TI's own
+ * tas2781/tas2563 grouped-device architecture instead of one node per
+ * chip. The uCDSP firmware format only ever defines DEV_A/DEV_B block
+ * types (see the block type codes above); there is no third slot to
+ * extend into no matter how many entries a future DT `reg` array has.
+ */
+#define TAS2557_MAX_DEV 2
+
+/* Firmware driver version flags (driver_version field) */
+#define PPC_DRIVER_CRCCHK 0x00000200
+#define PPC_DRIVER_CONFDEV 0x00000300
+#define PPC_DRIVER_MTPLLSRC 0x00000400
+/* Present from driver_version 0x101; carries a 2-byte device field */
+#define PPC_DRIVER_CFGDEV_NONCRC 0x00000101
+
+#endif /* __TAS2557_H__ */
--
2.55.0
^ permalink raw reply related
* Re: [PATCH v3 2/2] iio: adc: add MAX40080 current-sense amplifier driver
From: Jonathan Cameron @ 2026-07-17 23:41 UTC (permalink / raw)
To: Nuno Sá
Cc: Stefan Popa, linux-iio, Andy Shevchenko, David Lechner, Nuno Sa,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Siratul Islam,
Uwe Kleine-König, Ciprian Hegbeli, devicetree, linux-kernel
In-Reply-To: <gnkthebcxlpbwvbo774hap4mphqro4gxho4zemj6p3ixa2qp7g@ncb5o7tw5wf3>
On Wed, 15 Jul 2026 16:23:11 +0200
Nuno Sá <noname.nuno@gmail.com> wrote:
> Hi Stefan,
>
> Some comments from me
>
> On Wed, Jul 15, 2026 at 09:36:17AM +0300, Stefan Popa wrote:
> > The MAX40080 is a bidirectional current-sense amplifier with an
> > integrated 12-bit ADC and an I2C/SMBus interface. It measures the
> > voltage across an external shunt resistor and the input bus voltage,
> > storing the results in an internal FIFO.
> >
> > No existing IIO driver covers this device or a register-compatible part.
> > The closest relatives target different silicon with incompatible register
> > maps and feature sets: max9611 is a unidirectional high-side sensor with a
> > die-temperature channel and MUX-selected gain and no FIFO/PEC, while
> > max34408 is an 8-bit multi-channel current monitor. The MAX40080 has a
> > device-specific register map with bidirectional 13-bit current, a 64-entry
> > FIFO, PEC, a single-measurement mode triggered by an SMBus Quick Command,
> > and two selectable input ranges, so it warrants its own driver.
> >
> > Add a direct-mode IIO driver exposing the current and voltage channels
> > with raw and scale attributes, a configurable oversampling (digital
> > averaging) ratio, and PEC-protected register access. The two selectable
> > current-sense ranges are exposed through scale/scale_available; the
> > current scale is derived from the shunt-resistor-micro-ohms device-tree
> > property.
> >
> > Link: https://www.analog.com/media/en/technical-documentation/data-sheets/MAX40080.pdf
> >
> > Co-developed-by: Ciprian Hegbeli <ciprian.hegbeli@analog.com>
> > Signed-off-by: Ciprian Hegbeli <ciprian.hegbeli@analog.com>
> > Signed-off-by: Stefan Popa <stefan.popa@analog.com>
A couple of follow ups even though v4 is on list.
That should indicate clearly why you should slow down and let people
get to earlier versions.
Jonathan
> > diff --git a/drivers/iio/adc/max40080.c b/drivers/iio/adc/max40080.c
> > new file mode 100644
> > index 0000000000000..a0c1144cfda7c
> > --- /dev/null
> > +++ b/drivers/iio/adc/max40080.c
> > @@ -0,0 +1,630 @@
> > +// SPDX-License-Identifier: GPL-2.0+
> > +/*
> > + * MAX40080 Digital Current-Sense Amplifier driver
> > + *
> > + * Copyright 2026 Analog Devices, Inc.
> > + */
> > +
> > +#include <linux/bitfield.h>
> > +#include <linux/bitops.h>
> > +#include <linux/cleanup.h>
> > +#include <linux/i2c.h>
> > +#include <linux/iopoll.h>
> > +#include <linux/math64.h>
> > +#include <linux/module.h>
>
> Typically I would say you're missing mod_devicetable.h but now we have:
>
> https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/tree/include/linux/device-id
>
> You might need to base your series on linux-next though.
I merged rc2 into the IIO togreg (and so testing as well) branches to resolve
merge conflicts with that series. Upshot is just don't bother
including mod_devicetable.h in any new code. Also don't worry
about including any of the linux/device-id headers unless you don't
get them via i2c.h, spi.h etc
>
> > +#include <linux/mutex.h>
> > +#include <linux/pm.h>
> > +#include <linux/property.h>
> > +#include <linux/time.h>
> > +static int max40080_read_iv_once(struct max40080_state *st, u32 *iv)
> > +{
> > + u8 buf[4];
> > + int ret;
> > +
> > + ret = i2c_smbus_read_i2c_block_data(st->client, MAX40080_REG_IV,
> > + sizeof(buf), buf);
>
> It's not clear to me that i2c will use safe buffer all the time (from a
> quick look). So I would say to make this DMA safe the usual way we do in
> IIO.
I2c always bounces unless you use specific dmasafe functions to indicate
that your particular buffer is DMA safe.
>
> > + if (ret < 0)
> > + return ret;
> > + if (ret != sizeof(buf))
> > + return -EIO;
> > +
> > + *iv = get_unaligned_le32(buf);
> > +
> > + return 0;
> > +}
> > +
^ permalink raw reply
* Re: [PATCH v3 2/2] ASoC: codecs: add TAS2557 mono smart amplifier driver
From: sashiko-bot @ 2026-07-17 23:47 UTC (permalink / raw)
To: Gianluca Boiano; +Cc: robh, conor+dt, devicetree
In-Reply-To: <20260717233402.414326-3-morf3089@gmail.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Unreserved secondary I2C addresses and data race on `client->addr` via single shared regmap.
--
--- Patch [2]: [PATCH v3 2/2] ASoC: codecs: add TAS2557 mono smart amplifier driver ---
commit 6ad2a6c10e4ed5ed98d85d212f482c1f0987454d
Author: Gianluca Boiano <morf3089@gmail.com>
ASoC: codecs: add TAS2557 mono smart amplifier driver
This commit adds support for the TAS2557 mono smart amplifier. It uses a
shared regmap architecture to manage up to two physical chips by dynamically
retargeting the I2C client address before each transfer.
> diff --git a/sound/soc/codecs/tas2557.c b/sound/soc/codecs/tas2557.c
> new file mode 100644
> --- /dev/null
> +++ b/sound/soc/codecs/tas2557.c
> @@ -331,12 +331,12 @@ static int tas2557_change_book_page(struct tas2557_priv *tas2557,
> unsigned int chn,
> unsigned char book, unsigned char page)
> {
> struct i2c_client *client = to_i2c_client(tas2557->dev);
> struct tas2557_dev *dv = &tas2557->devs[chn];
> int ret;
>
> if (client->addr != dv->addr) {
> client->addr = dv->addr;
>
> ret = regmap_write(tas2557->regmap, TAS2557_PAGE_REG, 0);
[Severity: High]
Can this mutation of client->addr cause a data race with concurrent regmap
accesses?
If userspace accesses regmap debugfs concurrently, it acquires the regmap's
internal mutex to perform I2C transfers. Because client->addr is modified
here under dev_lock but before acquiring the regmap lock, could this result
in debugfs I2C transactions inadvertently hitting the wrong physical device?
[ ... ]
> @@ -2875,13 +2875,13 @@ static int tas2557_parse_dt(struct tas2557_priv *tas2557, struct i2c_client *client)
> {
> struct device *dev = tas2557->dev;
> struct device_node *np = dev->of_node;
> unsigned int imon_slots[TAS2557_MAX_DEV] = { 0, 0 };
> unsigned int vmon_slots[TAS2557_MAX_DEV] = { 2, 2 };
> unsigned int i;
> int ndev = 0;
>
> if (np) {
> u64 addr;
>
> for (i = 0; i < TAS2557_MAX_DEV; i++) {
> if (of_property_read_reg(np, i, &addr, NULL))
> break;
> tas2557->devs[ndev++].addr = addr;
> }
> }
[Severity: High]
Are these secondary I2C addresses left unreserved with the I2C subsystem?
Since there is no call to i2c_new_dummy_device() or a similar API to claim
the additional addresses parsed from the reg array, is it possible that
another driver or a userspace process could accidentally bind to them and
cause bus collisions?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717233402.414326-1-morf3089@gmail.com?part=2
^ permalink raw reply
* Re: [PATCH v4 0/2] iio: adc: add MAX40080 current-sense amplifier driver
From: Jonathan Cameron @ 2026-07-17 23:50 UTC (permalink / raw)
To: Stefan Popa; +Cc: linux-iio, andriy.shevchenko, nuno.sa, devicetree
In-Reply-To: <20260717123852.1140893-1-stefan.popa@analog.com>
On Fri, 17 Jul 2026 15:38:47 +0300
Stefan Popa <stefan.popa@analog.com> wrote:
> This series adds support for the Maxim MAX40080, a bidirectional
> current-sense amplifier with an integrated 12-bit ADC and an I2C/SMBus
> interface. It measures the voltage across an external shunt resistor and
> the input bus voltage.
>
> The driver operates in direct (INDIO_DIRECT_MODE) mode. Each raw read
> triggers a single on-demand conversion (SMBus Quick Command) and reads
> back the matched current/voltage pair, so results are always fresh. It
> exposes the current and voltage channels with raw and scale attributes,
> a configurable oversampling (digital averaging) ratio, and PEC-protected
> register access. The two selectable current-sense ranges are exposed
> through scale/scale_available; the current scale is derived from the
> shunt-resistor-micro-ohms device-tree property.
>
> Tested on hardware with four MAX40080 devices on an I2C bus.
>
> Regarding regmap: it does not support SMBus PEC or Quick Command, both of
> which are required by this device. The mlx90614 driver uses the same raw
> i2c_smbus approach for similar reasons.
Why IIO rather than hwmon for this driver? For any part that is involved
in monitoring I'd expect some text on what features etc need IIO. This
will sample quickly but if you only intend to ever do DIRECT_MODE that
isn't being used. Also you'd need to support i2c modes above the standard
400kHz one. If it is remote borderline also +CC Guenter and linux-hwmon
Thanks
Jonathan
^ permalink raw reply
* Re: [PATCH v4 1/2] dt-bindings: iio: adc: add maxim,max40080
From: Jonathan Cameron @ 2026-07-17 23:52 UTC (permalink / raw)
To: Stefan Popa; +Cc: linux-iio, andriy.shevchenko, nuno.sa, devicetree
In-Reply-To: <20260717123852.1140893-2-stefan.popa@analog.com>
On Fri, 17 Jul 2026 15:38:48 +0300
Stefan Popa <stefan.popa@analog.com> wrote:
> Add device tree bindings for the Maxim MAX40080 bidirectional
> current-sense amplifier with a 12-bit ADC and an I2C/SMBus interface.
>
> Signed-off-by: Stefan Popa <stefan.popa@analog.com>
Hi Stefan,
> ---
> .../bindings/iio/adc/maxim,max40080.yaml | 64 +++++++++++++++++++
> MAINTAINERS | 8 +++
> 2 files changed, 72 insertions(+)
> create mode 100644 Documentation/devicetree/bindings/iio/adc/maxim,max40080.yaml
>
> diff --git a/Documentation/devicetree/bindings/iio/adc/maxim,max40080.yaml b/Documentation/devicetree/bindings/iio/adc/maxim,max40080.yaml
> new file mode 100644
> index 0000000000000..3b367071b5d3f
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/iio/adc/maxim,max40080.yaml
...
> +
> + shunt-resistor-micro-ohms:
> + description:
> + Value of the current-sense shunt resistor connected between the IN+ and
> + IN- inputs. Used to scale the reported current.
How useful is the device if we don't know what value this takes? For a current
sense amplifier I'd kind of expect the shunt resistance to be required.
> +
> +required:
> + - compatible
> + - reg
> + - vdd-supply
> +
> +additionalProperties: false
> +
> +examples:
> + - |
> + i2c {
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + adc@20 {
> + compatible = "maxim,max40080";
> + reg = <0x20>;
> + vdd-supply = <&vdd_3v3>;
> + #io-channel-cells = <1>;
> + shunt-resistor-micro-ohms = <100000>;
> + };
> + };
^ permalink raw reply
* [PATCH 2/2] soc/tegra: pmc: Add Tegra210B01 support
From: Aaron Kling via B4 Relay @ 2026-07-18 0:17 UTC (permalink / raw)
To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Thierry Reding,
Jonathan Hunter
Cc: Thierry Reding, devicetree, linux-tegra, linux-kernel,
Aaron Kling, Azkali Manad, Thomas Makin
In-Reply-To: <20260717-t210b01-pmc-v1-0-bf776be8b3a7@gmail.com>
From: Azkali Manad <a.ffcc7@gmail.com>
The PMC in Tegra210B01 is very similar to the one in the original
Tegra210, but has a few powergate and pad differences. Add support for
this soc.
Signed-off-by: Azkali Manad <a.ffcc7@gmail.com>
Co-developed-by: Thomas Makin <halorocker89@gmail.com>
Signed-off-by: Thomas Makin <halorocker89@gmail.com>
Co-developed-by: Aaron Kling <webgeek1234@gmail.com>
Signed-off-by: Aaron Kling <webgeek1234@gmail.com>
---
drivers/soc/tegra/pmc.c | 160 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 160 insertions(+)
diff --git a/drivers/soc/tegra/pmc.c b/drivers/soc/tegra/pmc.c
index f419a5395545f..4d77aa66d690c 100644
--- a/drivers/soc/tegra/pmc.c
+++ b/drivers/soc/tegra/pmc.c
@@ -3744,6 +3744,32 @@ static const char * const tegra210_powergates[] = {
[TEGRA_POWERGATE_VE2] = "ve2",
};
+static const char * const tegra210b01_powergates[] = {
+ [TEGRA_POWERGATE_CPU] = "crail",
+ [TEGRA_POWERGATE_3D] = "3d",
+ [TEGRA_POWERGATE_PCIE] = "pcie",
+ [TEGRA_POWERGATE_MPE] = "mpe",
+ [TEGRA_POWERGATE_SATA] = "sata",
+ [TEGRA_POWERGATE_CPU1] = "cpu1",
+ [TEGRA_POWERGATE_CPU2] = "cpu2",
+ [TEGRA_POWERGATE_CPU3] = "cpu3",
+ [TEGRA_POWERGATE_CPU0] = "cpu0",
+ [TEGRA_POWERGATE_C0NC] = "c0nc",
+ [TEGRA_POWERGATE_SOR] = "sor",
+ [TEGRA_POWERGATE_DIS] = "dis",
+ [TEGRA_POWERGATE_DISB] = "disb",
+ [TEGRA_POWERGATE_XUSBA] = "xusba",
+ [TEGRA_POWERGATE_XUSBB] = "xusbb",
+ [TEGRA_POWERGATE_XUSBC] = "xusbc",
+ [TEGRA_POWERGATE_VIC] = "vic",
+ [TEGRA_POWERGATE_IRAM] = "iram",
+ [TEGRA_POWERGATE_NVDEC] = "nvdec",
+ [TEGRA_POWERGATE_NVJPG] = "nvjpg",
+ [TEGRA_POWERGATE_AUD] = "aud",
+ [TEGRA_POWERGATE_DFD] = "dfd",
+};
+
+
static const u8 tegra210_cpu_powergates[] = {
TEGRA_POWERGATE_CPU0,
TEGRA_POWERGATE_CPU1,
@@ -3897,6 +3923,139 @@ static const struct tegra_pmc_soc tegra210_pmc_soc = {
.has_single_mmio_aperture = true,
};
+static const struct tegra_io_pad_soc tegra210b01_io_pads[] = {
+ TEGRA_IO_PAD(TEGRA_IO_PAD_AUDIO, 17, 0x1b8, 0x1bc, "audio"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_AUDIO_HV, 29, 0x1c0, 0x1c4, "audio-hv"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_CAM, 4, 0x1c0, 0x1c4, "cam"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_CSIA, 0, 0x1b8, 0x1bc, "csia"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_CSIB, 1, 0x1b8, 0x1bc, "csib"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_CSIC, 10, 0x1c0, 0x1c4, "csic"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_CSID, 11, 0x1c0, 0x1c4, "csid"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_CSIE, 12, 0x1c0, 0x1c4, "csie"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_CSIF, 13, 0x1c0, 0x1c4, "csif"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_DBG, 25, 0x1b8, 0x1bc, "dbg"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_DEBUG_NONAO, 26, 0x1b8, 0x1bc, "debug-nonao"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_DMIC, 18, 0x1c0, 0x1c4, "dmic"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_DP, 19, 0x1c0, 0x1c4, "dp"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_DSI, 2, 0x1b8, 0x1bc, "dsi"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_DSIB, 7, 0x1c0, 0x1c4, "dsib"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_DSIC, 8, 0x1c0, 0x1c4, "dsic"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_DSID, 9, 0x1c0, 0x1c4, "dsid"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_EMMC, 3, 0x1c0, 0x1c4, "emmc"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_EMMC2, 5, 0x1c0, 0x1c4, "emmc2"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_GPIO, 27, 0x1b8, 0x1bc, "gpio"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_HDMI, 28, 0x1b8, 0x1bc, "hdmi"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_HSIC, 19, 0x1b8, 0x1bc, "hsic"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_LVDS, 25, 0x1c0, 0x1c4, "lvds"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_MIPI_BIAS, 3, 0x1b8, 0x1bc, "mipi-bias"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_PEX_BIAS, 4, 0x1b8, 0x1bc, "pex-bias"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_PEX_CLK1, 5, 0x1b8, 0x1bc, "pex-clk1"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_PEX_CLK2, 6, 0x1b8, 0x1bc, "pex-clk2"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_PEX_CNTRL, UINT_MAX, UINT_MAX, UINT_MAX, "pex-cntrl"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_SDMMC1, 1, 0x1c0, 0x1c4, "sdmmc1"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_SDMMC3, 2, 0x1c0, 0x1c4, "sdmmc3"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_SPI, 14, 0x1c0, 0x1c4, "spi"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_SPI_HV, 15, 0x1c0, 0x1c4, "spi-hv"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_UART, 14, 0x1b8, 0x1bc, "uart"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_USB0, 9, 0x1b8, 0x1bc, "usb0"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_USB1, 10, 0x1b8, 0x1bc, "usb1"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_USB2, 11, 0x1b8, 0x1bc, "usb2"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_USB3, 18, 0x1b8, 0x1bc, "usb3"),
+ TEGRA_IO_PAD(TEGRA_IO_PAD_USB_BIAS, 12, 0x1b8, 0x1bc, "usb-bias"),
+};
+
+static const struct tegra_io_pad_vctrl tegra210b01_io_pad_vctrls[] = {
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_AUDIO, PMC_PWR_DET_VALUE, 5),
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_AUDIO_HV, PMC_PWR_DET_VALUE, 18),
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_CAM, PMC_PWR_DET_VALUE, 10),
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_DBG, PMC_PWR_DET_VALUE, 19),
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_DMIC, PMC_PWR_DET_VALUE, 20),
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_GPIO, PMC_PWR_DET_VALUE, 21),
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_PEX_CNTRL, PMC_PWR_DET_VALUE, 11),
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_SDMMC1, PMC_PWR_DET_VALUE, 12),
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_SDMMC3, PMC_PWR_DET_VALUE, 13),
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_SPI, PMC_PWR_DET_VALUE, 22),
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_SPI_HV, PMC_PWR_DET_VALUE, 23),
+ TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_UART, PMC_PWR_DET_VALUE, 2),
+};
+
+static const struct pinctrl_pin_desc tegra210b01_pin_descs[] = {
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_AUDIO, "audio"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_AUDIO_HV, "audio-hv"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_CAM, "cam"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_CSIA, "csia"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_CSIB, "csib"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_CSIC, "csic"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_CSID, "csid"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_CSIE, "csie"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_CSIF, "csif"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_DBG, "dbg"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_DEBUG_NONAO, "debug-nonao"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_DMIC, "dmic"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_DP, "dp"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_DSI, "dsi"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_DSIB, "dsib"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_DSIC, "dsic"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_DSID, "dsid"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_EMMC, "emmc"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_EMMC2, "emmc2"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_GPIO, "gpio"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_HDMI, "hdmi"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_HSIC, "hsic"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_LVDS, "lvds"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_MIPI_BIAS, "mipi-bias"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_PEX_BIAS, "pex-bias"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_PEX_CLK1, "pex-clk1"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_PEX_CLK2, "pex-clk2"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_PEX_CNTRL, "pex-cntrl"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_SDMMC1, "sdmmc1"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_SDMMC3, "sdmmc3"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_SPI, "spi"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_SPI_HV, "spi-hv"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_UART, "uart"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_USB0, "usb0"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_USB1, "usb1"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_USB2, "usb2"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_USB3, "usb3"),
+ TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_USB_BIAS, "usb-bias"),
+};
+
+static const struct tegra_pmc_soc tegra210b01_pmc_soc = {
+ .supports_core_domain = false,
+ .num_powergates = ARRAY_SIZE(tegra210b01_powergates),
+ .powergates = tegra210b01_powergates,
+ .num_cpu_powergates = ARRAY_SIZE(tegra210_cpu_powergates),
+ .cpu_powergates = tegra210_cpu_powergates,
+ .has_tsense_reset = true,
+ .has_gpu_clamps = true,
+ .needs_mbist_war = false,
+ .has_io_pad_wren = false,
+ .maybe_tz_only = true,
+ .num_io_pads = ARRAY_SIZE(tegra210b01_io_pads),
+ .io_pads = tegra210b01_io_pads,
+ .num_io_pad_vctrls = ARRAY_SIZE(tegra210b01_io_pad_vctrls),
+ .io_pad_vctrls = tegra210b01_io_pad_vctrls,
+ .num_pin_descs = ARRAY_SIZE(tegra210b01_pin_descs),
+ .pin_descs = tegra210b01_pin_descs,
+ .regs = &tegra20_pmc_regs,
+ .init = tegra20_pmc_init,
+ .setup_irq_polarity = tegra20_pmc_setup_irq_polarity,
+ .powergate_set = tegra114_powergate_set,
+ .irq_set_wake = tegra210_pmc_irq_set_wake,
+ .irq_set_type = tegra210_pmc_irq_set_type,
+ .reset_sources = tegra210_reset_sources,
+ .num_reset_sources = ARRAY_SIZE(tegra210_reset_sources),
+ .reset_levels = NULL,
+ .num_reset_levels = 0,
+ .num_wake_events = ARRAY_SIZE(tegra210_wake_events),
+ .wake_events = tegra210_wake_events,
+ .pmc_clks_data = tegra_pmc_clks_data,
+ .num_pmc_clks = ARRAY_SIZE(tegra_pmc_clks_data),
+ .has_blink_output = true,
+ .has_usb_sleepwalk = true,
+ .has_single_mmio_aperture = true,
+};
+
static const struct tegra_io_pad_soc tegra186_io_pads[] = {
TEGRA_IO_PAD(TEGRA_IO_PAD_CSIA, 0, 0x74, 0x78, "csia"),
TEGRA_IO_PAD(TEGRA_IO_PAD_CSIB, 1, 0x74, 0x78, "csib"),
@@ -4824,6 +4983,7 @@ static const struct of_device_id tegra_pmc_match[] = {
{ .compatible = "nvidia,tegra194-pmc", .data = &tegra194_pmc_soc },
{ .compatible = "nvidia,tegra186-pmc", .data = &tegra186_pmc_soc },
{ .compatible = "nvidia,tegra210-pmc", .data = &tegra210_pmc_soc },
+ { .compatible = "nvidia,tegra210b01-pmc", .data = &tegra210b01_pmc_soc },
{ .compatible = "nvidia,tegra132-pmc", .data = &tegra124_pmc_soc },
{ .compatible = "nvidia,tegra124-pmc", .data = &tegra124_pmc_soc },
{ .compatible = "nvidia,tegra114-pmc", .data = &tegra114_pmc_soc },
--
2.54.0
^ permalink raw reply related
* [PATCH 1/2] dt-bindings: soc: tegra: pmc: Document Tegra210B01
From: Aaron Kling via B4 Relay @ 2026-07-18 0:17 UTC (permalink / raw)
To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Thierry Reding,
Jonathan Hunter
Cc: Thierry Reding, devicetree, linux-tegra, linux-kernel,
Aaron Kling
In-Reply-To: <20260717-t210b01-pmc-v1-0-bf776be8b3a7@gmail.com>
From: Aaron Kling <webgeek1234@gmail.com>
The PMC in Tegra210B01 is very similar to the one in the original
Tegra210, but has a few powergate and pad differences. Document this
soc.
Signed-off-by: Aaron Kling <webgeek1234@gmail.com>
---
Documentation/devicetree/bindings/soc/tegra/nvidia,tegra20-pmc.yaml | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/Documentation/devicetree/bindings/soc/tegra/nvidia,tegra20-pmc.yaml b/Documentation/devicetree/bindings/soc/tegra/nvidia,tegra20-pmc.yaml
index f516960dbbefb..8ec36c49f9594 100644
--- a/Documentation/devicetree/bindings/soc/tegra/nvidia,tegra20-pmc.yaml
+++ b/Documentation/devicetree/bindings/soc/tegra/nvidia,tegra20-pmc.yaml
@@ -18,6 +18,7 @@ properties:
- nvidia,tegra114-pmc
- nvidia,tegra124-pmc
- nvidia,tegra210-pmc
+ - nvidia,tegra210b01-pmc
reg:
maxItems: 1
@@ -346,7 +347,9 @@ allOf:
properties:
compatible:
contains:
- const: nvidia,tegra210-pmc
+ enum:
+ - nvidia,tegra210-pmc
+ - nvidia,tegra210b01-pmc
then:
properties:
pinmux:
--
2.54.0
^ permalink raw reply related
* [PATCH 0/2] soc/tegra: pmc: Add Tegra210B01 support
From: Aaron Kling via B4 Relay @ 2026-07-18 0:17 UTC (permalink / raw)
To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Thierry Reding,
Jonathan Hunter
Cc: Thierry Reding, devicetree, linux-tegra, linux-kernel,
Aaron Kling, Azkali Manad, Thomas Makin
This series adds support for the Tegra210B01 SoC to the Tegra PMC driver
and documents the new compatible.
Signed-off-by: Aaron Kling <webgeek1234@gmail.com>
---
Aaron Kling (1):
dt-bindings: soc: tegra: pmc: Document Tegra210B01
Azkali Manad (1):
soc/tegra: pmc: Add Tegra210B01 support
.../bindings/soc/tegra/nvidia,tegra20-pmc.yaml | 5 +-
drivers/soc/tegra/pmc.c | 160 +++++++++++++++++++++
2 files changed, 164 insertions(+), 1 deletion(-)
---
base-commit: 1a1757b76427f6201bfe0bf1bea9f7574f332a93
change-id: 20260713-t210b01-pmc-1a3f8b908044
Best regards,
--
Aaron Kling <webgeek1234@gmail.com>
^ permalink raw reply
* Re: [PATCH 1/2] dt-bindings: soc: tegra: pmc: Document Tegra210B01
From: sashiko-bot @ 2026-07-18 0:23 UTC (permalink / raw)
To: Aaron Kling; +Cc: robh, conor+dt, devicetree
In-Reply-To: <20260717-t210b01-pmc-v1-1-bf776be8b3a7@gmail.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The schema binds `nvidia,tegra210b01-pmc` to the exact same pinmux pad constraints as `nvidia,tegra210-pmc`, despite the commit message explicitly stating there are pad differences. Furthermore, the human-readable description tables were not updated to reflect these hardware differences.
--
--- Patch [1]: [PATCH 1/2] dt-bindings: soc: tegra: pmc: Document Tegra210B01 ---
commit 4180e7276638ec7a0b3917f5607e6419625d20cc
Author: Aaron Kling <webgeek1234@gmail.com>
dt-bindings: soc: tegra: pmc: Document Tegra210B01
The PMC in Tegra210B01 is very similar to the one in the original
Tegra210, but has a few powergate and pad differences. Document this
soc.
> diff --git a/Documentation/devicetree/bindings/soc/tegra/nvidia,tegra20-pmc.yaml b/Documentation/devicetree/bindings/soc/tegra/nvidia,tegra20-pmc.yaml
> index f516960dbbefb..8ec36c49f9594 100644
> --- a/Documentation/devicetree/bindings/soc/tegra/nvidia,tegra20-pmc.yaml
> +++ b/Documentation/devicetree/bindings/soc/tegra/nvidia,tegra20-pmc.yaml
[ ... ]
> @@ -346,7 +347,9 @@ allOf:
> properties:
> compatible:
> contains:
> - const: nvidia,tegra210-pmc
> + enum:
> + - nvidia,tegra210-pmc
> + - nvidia,tegra210b01-pmc
> then:
> properties:
> pinmux:
[Severity: Medium]
The commit message states that the Tegra210B01 PMC has a few "powergate
and pad differences" from the original Tegra210. However, this groups the
Tegra210B01 variant into the exact same conditional block as Tegra210 for
pinmux validation.
Could this regression cause the schema to incorrectly enforce identical
constraints instead of modeling the pad differences?
Should the human-readable description tables for powergates and pinmux also
be updated in this binding to reflect the new Tegra210B01 hardware differences?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717-t210b01-pmc-v1-0-bf776be8b3a7@gmail.com?part=1
^ permalink raw reply
* Re: [PATCH 2/2] soc/tegra: pmc: Add Tegra210B01 support
From: sashiko-bot @ 2026-07-18 0:29 UTC (permalink / raw)
To: Aaron Kling; +Cc: conor+dt, devicetree, robh
In-Reply-To: <20260717-t210b01-pmc-v1-2-bf776be8b3a7@gmail.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Unnecessary code duplication of identical pad and pin description arrays for Tegra210B01.
--
--- Patch [2]: [PATCH 2/2] soc/tegra: pmc: Add Tegra210B01 support ---
commit 79bca764e8d02b78b29f77c96575489573ae132e
Author: Azkali Manad <a.ffcc7@gmail.com>
soc/tegra: pmc: Add Tegra210B01 support
This commit adds support for the Tegra210B01 SoC to the Tegra PMC driver.
It introduces new powergates, IO pads, voltage controls, and pin descriptions
specifically for this variant.
> diff --git a/drivers/soc/tegra/pmc.c b/drivers/soc/tegra/pmc.c
> index f419a5395545f..4d77aa66d690c 100644
> --- a/drivers/soc/tegra/pmc.c
> +++ b/drivers/soc/tegra/pmc.c
[ ... ]
> @@ -3897,6 +3923,139 @@ static const struct tegra_pmc_soc tegra210_pmc_soc = {
> .has_single_mmio_aperture = true,
> };
>
> +static const struct tegra_io_pad_soc tegra210b01_io_pads[] = {
[Severity: Low]
Are these IO pad arrays identical to the existing tegra210 versions?
Looking at tegra210_io_pads, tegra210_io_pad_vctrls, and tegra210_pin_descs,
it seems this code duplicates them exactly. Could tegra210b01_pmc_soc directly
reference the existing tegra210 arrays instead of duplicating these structures
to reduce binary size?
> + TEGRA_IO_PAD(TEGRA_IO_PAD_AUDIO, 17, 0x1b8, 0x1bc, "audio"),
> + TEGRA_IO_PAD(TEGRA_IO_PAD_AUDIO_HV, 29, 0x1c0, 0x1c4, "audio-hv"),
[ ... ]
> + TEGRA_IO_PAD(TEGRA_IO_PAD_USB_BIAS, 12, 0x1b8, 0x1bc, "usb-bias"),
> +};
> +
> +static const struct tegra_io_pad_vctrl tegra210b01_io_pad_vctrls[] = {
> + TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_AUDIO, PMC_PWR_DET_VALUE, 5),
[ ... ]
> + TEGRA_IO_PAD_VCTRL(TEGRA_IO_PAD_UART, PMC_PWR_DET_VALUE, 2),
> +};
> +
> +static const struct pinctrl_pin_desc tegra210b01_pin_descs[] = {
> + TEGRA_IO_PIN_DESC(TEGRA_IO_PAD_AUDIO, "audio"),
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260717-t210b01-pmc-v1-0-bf776be8b3a7@gmail.com?part=2
^ permalink raw reply
* Re: [PATCH v4 2/2] iio: adc: add MAX40080 current-sense amplifier driver
From: Jonathan Cameron @ 2026-07-18 0:42 UTC (permalink / raw)
To: Stefan Popa
Cc: linux-iio, andriy.shevchenko, nuno.sa, devicetree,
Ciprian Hegbeli
In-Reply-To: <20260717123852.1140893-3-stefan.popa@analog.com>
On Fri, 17 Jul 2026 15:38:49 +0300
Stefan Popa <stefan.popa@analog.com> wrote:
> The MAX40080 is a bidirectional current-sense amplifier with an
> integrated 12-bit ADC and an I2C/SMBus interface. It measures the
> voltage across an external shunt resistor and the input bus voltage,
> storing the results in an internal FIFO.
>
> No existing IIO driver covers this device or a register-compatible part.
> The closest relatives target different silicon with incompatible register
> maps and feature sets: max9611 is a unidirectional high-side sensor with a
> die-temperature channel and MUX-selected gain and no FIFO/PEC, while
> max34408 is an 8-bit multi-channel current monitor. The MAX40080 has a
> device-specific register map with bidirectional 13-bit current, a 64-entry
> FIFO, PEC, a single-measurement mode triggered by an SMBus Quick Command,
> and two selectable input ranges, so it warrants its own driver.
>
> Add a direct-mode IIO driver exposing the current and voltage channels
> with raw and scale attributes, a configurable oversampling (digital
> averaging) ratio, and PEC-protected register access. The two selectable
> current-sense ranges are exposed through scale/scale_available; the
> current scale is derived from the shunt-resistor-micro-ohms device-tree
> property.
>
> Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/MAX40080.pdf
> Co-developed-by: Ciprian Hegbeli <ciprian.hegbeli@analog.com>
> Signed-off-by: Ciprian Hegbeli <ciprian.hegbeli@analog.com>
> Signed-off-by: Stefan Popa <stefan.popa@analog.com>
For the sashiko feedback about *iv not being initialised if it exits
on first loop. I'd just set tmp to 0 before calling the polling function.
The other one is a race even with locking as the reader either gets the before
value or the after one, so up to you if you add the suggested lock.
A few other things inline
Thanks,
Jonathan
> diff --git a/drivers/iio/adc/max40080.c b/drivers/iio/adc/max40080.c
> new file mode 100644
> index 0000000000000..9ffb4e9a009c1
> --- /dev/null
> +++ b/drivers/iio/adc/max40080.c
> +struct max40080_state {
> + struct i2c_client *client;
> + /* Serializes read-modify-write access to the CFG register. */
> + struct mutex lock;
> + u32 shunt_resistor_uOhm;
> + /*
> + * Cached configuration, also used to restore the device on resume after
> + * a suspend that may have cut its power: the selected RANGE index and
> + * the oversampling ratio.
> + */
> + unsigned int range;
> + int oversampling_ratio;
> + /*
> + * Precomputed current scale (mA per code) for each RANGE setting, as
> + * {integer, nano} pairs for IIO_VAL_INT_PLUS_NANO. The range is
> + * selected by writing the corresponding scale.
> + */
> + int current_scale[ARRAY_SIZE(max40080_csa_gain)][2];
> + /* DMA-safe buffer for i2c_smbus_read_i2c_block_data(). */
> + u8 buf[4] ____cacheline_aligned;
Already came up so see that discussion. I don't mind that much if you
use __align(ARCH_DMA_MINALIGN) rather than the IIO one because this doesn't
need the 8 byte enforcement. However, it's an __le32 so better to give
it that type and cast if you need to for the i2c calls. Can then use the
aligned converters (which are probably a noop on every system you care about!)
> +};
> +
> +static const int max40080_oversampling_avail[] = { 1, 8, 16, 32, 64, 128 };
> +
> +static int __max40080_update_bits(struct max40080_state *st, u8 reg,
> + u16 mask, u16 val)
The __ is I guess to indicate that the lock should be held? There are other
functions that have that characteristic but don't have __ prefix so not
sure I'd bother.
> +{
> + int tmp;
> +
> + tmp = i2c_smbus_read_word_data(st->client, reg);
> + if (tmp < 0)
> + return tmp;
> +
> + tmp = (tmp & ~mask) | (val & mask);
> +
> + return i2c_smbus_write_word_data(st->client, reg, tmp);
> +}
> +
> +/*
> + * A single measurement holds the matched current/voltage pair in one 32-bit
> + * word (MAX40080_REG_IV). Reading all four bytes in one transaction returns
> + * both from the same conversion; reading the separate current (0x0C) and
> + * voltage (0x0E) registers would decorrelate the two channels.
> + *
> + * Unlike the word accesses used elsewhere, this is a plain I2C block read: the
> + * SMBus layer does not append or verify a PEC byte for it even when PEC is
> + * otherwise enabled for the device, so this transfer is not PEC protected.
> + */
This correlated pair thing is a pretty strong reason to add buffered support
which could ensure userspace gets a pair that match.
> +
> +/*
> + * The FILTER field selects digital averaging of N consecutive conversions
> + * (no averaging, 8, 16, 32, 64 or 128), which maps directly to the IIO
> + * oversampling ratio. Averaging reduces the effective output data rate by the
> + * same factor; the conversion rate itself is set by the separate ADC_RATE
> + * field.
> + */
> +static int max40080_get_oversampling_ratio(struct max40080_state *st, int *val)
> +{
> + u8 filter;
> + int tmp;
> +
> + tmp = i2c_smbus_read_word_data(st->client, MAX40080_REG_CFG);
> + if (tmp < 0)
> + return tmp;
> +
> + filter = FIELD_GET(MAX40080_CFG_FILTER_MSK, tmp);
> + if (filter >= ARRAY_SIZE(max40080_oversampling_avail))
> + return -EIO;
Why get this rather than returning the cached value of the oversampling_ratio
directly?
> +
> + *val = max40080_oversampling_avail[filter];
> +
> + return 0;
> +}
> +static int max40080_probe(struct i2c_client *client)
> +{
> + struct device *dev = &client->dev;
> + struct max40080_state *st;
> + struct iio_dev *indio_dev;
> + int ret;
> +
> + /*
> + * The device powers up with PEC enabled (CFG POR = 0x0060) and rejects
> + * unprotected transactions, so PEC support is mandatory, along with word
> + * access, the I2C block read used for the current/voltage pair, and the
> + * Quick Command used to trigger a conversion.
> + */
> + if (!i2c_check_functionality(client->adapter,
> + I2C_FUNC_SMBUS_WORD_DATA |
> + I2C_FUNC_SMBUS_I2C_BLOCK |
> + I2C_FUNC_SMBUS_QUICK |
> + I2C_FUNC_SMBUS_PEC))
> + return -EOPNOTSUPP;
> +
> + client->flags |= I2C_CLIENT_PEC;
> +
> + indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
> + if (!indio_dev)
> + return -ENOMEM;
> +
> + i2c_set_clientdata(client, indio_dev);
> +
> + st = iio_priv(indio_dev);
> + st->client = client;
> +
> + ret = devm_mutex_init(dev, &st->lock);
> + if (ret)
> + return ret;
> +
> + {
I'm not keen on the scope being added here just so you can declare
at the top of it. Just declare propname at the top of the function
and assign it here.
> + const char *propname = "shunt-resistor-micro-ohms";
> +
> + if (device_property_present(dev, propname)) {
> + ret = device_property_read_u32(dev, propname,
> + &st->shunt_resistor_uOhm);
> + if (ret)
> + return dev_err_probe(dev, ret,
> + "can't read %s\n", propname);
> + if (!st->shunt_resistor_uOhm)
> + return dev_err_probe(dev, -EINVAL,
> + "%s must be non-zero\n", propname);
> + } else {
> + st->shunt_resistor_uOhm = 1 * MICRO;
This default isn't in the dt-binding. I'm not sure why this value makes sense.
If there is a reason the default is useful to have, then I'd expect a specification
reference in a comment to justify it.
> + }
> + }
> +
> + max40080_calc_current_scale(st);
> +
> + /* Defaults: 50 mV range (index 0), no averaging. */
> + st->range = 0;
> + st->oversampling_ratio = 1;
> +
> + indio_dev->name = "max40080";
> + indio_dev->info = &max40080_info;
> + indio_dev->modes = INDIO_DIRECT_MODE;
> + indio_dev->channels = max40080_channels;
> + indio_dev->num_channels = ARRAY_SIZE(max40080_channels);
> +
> + ret = max40080_init(st);
> + if (ret)
> + return ret;
> +
> + return devm_iio_device_register(dev, indio_dev);
> +}
> +
> +static int max40080_resume(struct device *dev)
> +{
> + struct iio_dev *indio_dev = dev_get_drvdata(dev);
> + struct max40080_state *st = iio_priv(indio_dev);
> +
> + /*
> + * A suspend may have cut power to the device, resetting it to its
> + * power-on defaults. Reprogram it from the cached configuration.
Papering over suspend disabling power seems wrong to me.
Note that for IIO we've always allowed drivers without suspend support
because it many industrial platforms it is completely irrelevant.
If we want the driver to support that let use opt in completely and do
the supply regulator disable / enable. I.e. Let's take control
of the potential sequence. I'm going to guess that this was responding
to sashiko feedback rather than a human reviewer?
Anyone else have a strong feeling on what we should do in these circumstances?
> + */
> + return max40080_init(st);
> +}
> +
> +static DEFINE_SIMPLE_DEV_PM_OPS(max40080_pm_ops, NULL, max40080_resume);
^ permalink raw reply
* Re: [PATCH v6 1/3] iio: adc: Fix incorrect reading when datarate changed in single mode
From: Jonathan Cameron @ 2026-07-18 0:44 UTC (permalink / raw)
To: Jakub Szczudło
Cc: linux-iio, andy, antoniu.miclaus, conor+dt, devicetree, dlechner,
duje, jishnu.prakash, jorge.marques, joshua.crofts1, krzk+dt,
linusw, marcelo.schmitt, mazziesaccount, mike.looijmans, nuno.sa,
robh, sakari.ailus, wens
In-Reply-To: <20260714175645.00003812@oss.qualcomm.com>
On Tue, 14 Jul 2026 17:56:45 -0700
Jonathan Cameron <jonathan.cameron@oss.qualcomm.com> wrote:
> On Mon, 13 Jul 2026 12:53:49 +0200
> Jakub Szczudło <jakubszczudlo40@gmail.com> wrote:
>
> > > > + ret = i2c_master_recv(data->client, (char *)&buffer, sizeof(buffer));
> > >
> > > In theory this can return short (generally reflects a misbehaving device).
> > >
> > > > + if (ret < 0) {
> > > > + dev_err(&data->client->dev, "I2C read fail: %d\n", ret);
> > > > + return true;
> > > > + }
> > >
> > > if (ret < sizeof(buffer))
> > > return -EIO;
> > >
> > I have checked the source code for i2c_master_recv and it should
> > return sizeof(buffer)
> > or errno code. Also David and Andy in v4 told me to delete such check.
>
> Fair point on it not being needed. Gah, I get my i2c functions confused.
> Only the multi transfer ones can return partial success.
Turns out that we are both wrong... See Lars reply to my thread tidying
these up universally. It can return 0 :(
Anyhow, it is very unlikely to do so and hopefully that thread will lead
to us making sure it never does!
Jonathan
^ permalink raw reply
* Re: [PATCH v8 1/3] iio: adc: ti-ads1100: Fix incorrect reading when datarate changed in single mode
From: Jonathan Cameron @ 2026-07-18 0:54 UTC (permalink / raw)
To: Jakub Szczudlo
Cc: linux-iio, dlechner, nuno.sa, andy, marcelo.schmitt, robh,
krzk+dt, conor+dt, mike.looijmans, devicetree, linux-kernel,
jorge.marques, antoniu.miclaus, mazziesaccount, jishnu.prakash,
duje, wens, sakari.ailus, linusw
In-Reply-To: <20260717185033.246580-2-jakubszczudlo40@gmail.com>
On Fri, 17 Jul 2026 20:50:31 +0200
Jakub Szczudlo <jakubszczudlo40@gmail.com> wrote:
> When device is suspended and it is in single mode then changing
> datarate doesn't make it actually wait for new measurement, so to
> be sure that read after change is correct functions that changes
be sure that read after change is correct, functions that change
(to make it easier to read)
> datarate and gain will wait for a new data.
>
> Fixes: 541880542f2b ("iio: adc: Add TI ADS1100 and ADS1000")
> Signed-off-by: Jakub Szczudlo <jakubszczudlo40@gmail.com>
> ---
> drivers/iio/adc/ti-ads1100.c | 71 ++++++++++++++++++++++++++++++++++--
> 1 file changed, 67 insertions(+), 4 deletions(-)
>
> diff --git a/drivers/iio/adc/ti-ads1100.c b/drivers/iio/adc/ti-ads1100.c
> index 9fe8d54cce83..363f790ca5d5 100644
> --- a/drivers/iio/adc/ti-ads1100.c
> +++ b/drivers/iio/adc/ti-ads1100.c
> @@ -15,10 +15,12 @@
> #include <linux/module.h>
> #include <linux/init.h>
> #include <linux/i2c.h>
> +#include <linux/iopoll.h>
> #include <linux/mutex.h>
> #include <linux/property.h>
> #include <linux/pm_runtime.h>
> #include <linux/regulator/consumer.h>
> +#include <linux/time.h>
> #include <linux/units.h>
>
> #include <linux/iio/iio.h>
> @@ -43,6 +45,9 @@
> static const int ads1100_data_rate[] = { 128, 32, 16, 8 };
> static const int ads1100_data_rate_bits[] = { 12, 14, 15, 16 };
>
> +/* Timeout based on the minimum sample rate of 8 SPS (7500ms) */
> +#define ADS1100_MAX_DRDY_TIMEOUT_US (7500 * USEC_PER_MSEC)
> +
> struct ads1100_data {
> struct i2c_client *client;
> struct regulator *reg_vdd;
> @@ -123,10 +128,50 @@ static int ads1100_get_adc_result(struct ads1100_data *data, int chan, int *val)
> return 0;
> }
>
> +static int ads1100_new_data_is_ready(struct ads1100_data *data)
> +{
> + u8 buffer[3];
> + int ret;
> +
> + ret = i2c_master_recv(data->client, (char *)&buffer, sizeof(buffer));
> + if (ret < 0) {
> + dev_err(&data->client->dev, "I2C read fail: %d\n", ret);
> + return ret;
> + }
> +
> + return FIELD_GET(ADS1100_CFG_ST_BSY, buffer[2]) ? 0 : 1;
There is question from sashiko on this one. I'm kind of assuming it is wrong
but do check (link below)
> +}
> +
> +static int ads1100_poll_data_ready(struct ads1100_data *data)
> +{
> + int data_rate_Hz = ads1100_data_rate[FIELD_GET(ADS1100_DR_MASK, data->config)];
> + /* To be sure we wait 5 times more than data rate */
> + unsigned long wait_time_us = DIV_ROUND_CLOSEST(USEC_PER_SEC, 2 * data_rate_Hz);
> + int data_ready;
> + u8 buffer[3];
> + int ret;
> +
> + /* To be sure that polled value will have value after config change */
> + ret = i2c_master_recv(data->client, (char *)&buffer, sizeof(buffer));
> + if (ret < 0) {
> + dev_err(&data->client->dev, "I2C read fail: %d\n", ret);
> + return ret;
> + }
> +
> + ret = readx_poll_timeout(ads1100_new_data_is_ready, data,
> + data_ready, data_ready != 0,
> + wait_time_us, ADS1100_MAX_DRDY_TIMEOUT_US);
> + if (data_ready < 0)
> + return data_ready;
https://sashiko.dev/#/patchset/20260717185033.246580-1-jakubszczudlo40%40gmail.com
ret needs checking.
> +
> + return 0;
> +}
Otherwise this looks fine to me.
Jonathan
^ permalink raw reply
* Re: [PATCH v8 3/3] iio: adc: ti-ads1100: Add ti-ads1110 support to ti-ads1100 driver
From: Jonathan Cameron @ 2026-07-18 1:02 UTC (permalink / raw)
To: Jakub Szczudlo
Cc: linux-iio, dlechner, nuno.sa, andy, marcelo.schmitt, robh,
krzk+dt, conor+dt, mike.looijmans, devicetree, linux-kernel,
jorge.marques, antoniu.miclaus, mazziesaccount, jishnu.prakash,
duje, wens, sakari.ailus, linusw
In-Reply-To: <20260717185033.246580-4-jakubszczudlo40@gmail.com>
On Fri, 17 Jul 2026 20:50:33 +0200
Jakub Szczudlo <jakubszczudlo40@gmail.com> wrote:
> Add ADS1110 support that have faster datarate than ADS1100, it also uses
> internal voltage reference of 2.048V for measurement.
>
> Signed-off-by: Jakub Szczudlo <jakubszczudlo40@gmail.com>
Hi Jakub,
Seems some stuff in here should be in patch 1.
Thanks
Jonathan
> diff --git a/drivers/iio/adc/ti-ads1100.c b/drivers/iio/adc/ti-ads1100.c
> index 363f790ca5d5..19533667e6cd 100644
> --- a/drivers/iio/adc/ti-ads1100.c
> +++ b/drivers/iio/adc/ti-ads1100.c
...
> @@ -139,14 +177,16 @@ static int ads1100_new_data_is_ready(struct ads1100_data *data)
> return ret;
> }
>
> - return FIELD_GET(ADS1100_CFG_ST_BSY, buffer[2]) ? 0 : 1;
> + return FIELD_GET(ADS1100_CFG_ST_BSY, buffer[2]);
This change and the use below seem odd to be in this patch. I guess
they should be back in patch 1.
> }
>
> static int ads1100_poll_data_ready(struct ads1100_data *data)
> {
> - int data_rate_Hz = ads1100_data_rate[FIELD_GET(ADS1100_DR_MASK, data->config)];
> + int data_rate_index = FIELD_GET(ADS1100_DR_MASK, data->config);
> + int data_rate_Hz = data->chip_info->available_data_rate_hz[data_rate_index];
> /* To be sure we wait 5 times more than data rate */
> - unsigned long wait_time_us = DIV_ROUND_CLOSEST(USEC_PER_SEC, 2 * data_rate_Hz);
> + unsigned long period_us = DIV_ROUND_CLOSEST(USEC_PER_SEC, data_rate_Hz);
> + unsigned long wait_time_us = 5UL * period_us;
> int data_ready;
> u8 buffer[3];
> int ret;
> @@ -159,7 +199,7 @@ static int ads1100_poll_data_ready(struct ads1100_data *data)
> }
>
> ret = readx_poll_timeout(ads1100_new_data_is_ready, data,
> - data_ready, data_ready != 0,
> + data_ready, data_ready == 0,
see above. Seems to be in wrong patch.
> wait_time_us, ADS1100_MAX_DRDY_TIMEOUT_US);
Sashiko points out that the polling period is very long. Was this meant to be period_us?
> if (data_ready < 0)
> return data_ready;
^ permalink raw reply
* Re: [PATCH v4 0/4] phy: qcom: qmp-pcie: Add PCIe PHY support for Hawi
From: Matthew Leung @ 2026-07-18 1:04 UTC (permalink / raw)
To: Vinod Koul, Neil Armstrong, Rob Herring, Krzysztof Kozlowski,
Conor Dooley
Cc: linux-arm-msm, linux-phy, devicetree, linux-kernel,
Krzysztof Kozlowski
In-Reply-To: <20260707-hawi-phy-pcie-v4-0-8d9cc6324947@oss.qualcomm.com>
Gentle ping. All patches have Reviewed-by tags. Please let me know if anything is needed.
On Tue, Jul 07, 2026 at 08:32:45PM +0000, Matthew Leung wrote:
> This series adds QMP PCIe PHY support for the Qualcomm Hawi SoC. The Hawi
> platform features two PCIe PHY configurations: Gen3 x2 and Gen4 x1.
>
> The Gen3 x2 PHY uses v10 register definitions, while the Gen4 x1 PHY uses
> v10.60 register definitions.
>
> The series adds:
> - device tree bindings
> - v10 register offset headers
> - v10.60 register offset headers
> - driver support with PHY initialization tables for both configurations
>
> Overlap:
> The series has overlap with "phy: qcom: Introduce USB support for Hawi"
> by Ronak Raheja (see link [1]). Both patch series introduce a subset of
> v10 registers (this series for PCIe and Ronak's for USB). I have
> coordinated with Ronak regarding the overlap, and we can update the
> series to resolve any overlap based on the order of merging.
>
> Link: https://lore.kernel.org/all/20260508213234.4643-1-ronak.raheja@oss.qualcomm.com/ [1]
>
> Signed-off-by: Matthew Leung <matthew.leung@oss.qualcomm.com>
> ---
> Changes in v4:
> - Rebased onto v7.2-rc2
> - Link to v3: https://patch.msgid.link/20260618-hawi-phy-pcie-v3-0-3fa42ca45ea4@oss.qualcomm.com
>
> Changes in v3:
> - Squashed v10 register offsets into a single change
> - Squashed v10.60 register offsets into a single change
> - Removed USB mentions from header comments; offsets are PCIe-specific
> - Reused the tx offset for the v10.60 combined txrx module instead of
> introducing a separate txrx offset
> - Link to v2: https://patch.msgid.link/20260604-hawi-phy-pcie-v2-0-be908d3560db@oss.qualcomm.com
>
> Changes in v2:
> - Rebased onto v7.1-rc6
> - Patch 1: no change (Reviewed-by carried forward)
> - Patch 9: rename QPHY_PCIE_V10_60_PCS_PCS_TX_RX_CONFIG to
> QPHY_PCIE_V10_60_PCS_TX_RX_CONFIG to be consistent with the
> naming convention used in previous pcs-pcie headers
> - Patch 10: update usage of renamed macro
> - Link to v1: https://patch.msgid.link/20260508-hawi-phy-pcie-v1-0-237b894353fc@oss.qualcomm.com
>
> To: Vinod Koul <vkoul@kernel.org>
> To: Neil Armstrong <neil.armstrong@linaro.org>
> To: Rob Herring <robh@kernel.org>
> To: Krzysztof Kozlowski <krzk+dt@kernel.org>
> To: Conor Dooley <conor+dt@kernel.org>
> Cc: linux-arm-msm@vger.kernel.org
> Cc: linux-phy@lists.infradead.org
> Cc: devicetree@vger.kernel.org
> Cc: linux-kernel@vger.kernel.org
>
> ---
> Matthew Leung (4):
> dt-bindings: phy: qcom,sc8280xp-qmp-pcie-phy: Add Hawi compatibles
> phy: qcom-qmp: Add v10 register offsets
> phy: qcom-qmp: Add v10.60 register offsets
> phy: qcom: qmp-pcie: Add QMP PCIe PHY support for Hawi
>
> .../bindings/phy/qcom,sc8280xp-qmp-pcie-phy.yaml | 6 +
> drivers/phy/qualcomm/phy-qcom-qmp-pcie.c | 375 +++++++++++++++++++++
> drivers/phy/qualcomm/phy-qcom-qmp-pcs-pcie-v10.h | 18 +
> .../phy/qualcomm/phy-qcom-qmp-pcs-pcie-v10_60.h | 26 ++
> drivers/phy/qualcomm/phy-qcom-qmp-pcs-v10.h | 22 ++
> drivers/phy/qualcomm/phy-qcom-qmp-pcs-v10_60.h | 23 ++
> .../phy/qualcomm/phy-qcom-qmp-qserdes-com-v10.h | 49 +++
> .../phy/qualcomm/phy-qcom-qmp-qserdes-com-v10_60.h | 55 +++
> .../phy/qualcomm/phy-qcom-qmp-qserdes-txrx-v10.h | 47 +++
> .../qualcomm/phy-qcom-qmp-qserdes-txrx-v10_60.h | 109 ++++++
> drivers/phy/qualcomm/phy-qcom-qmp.h | 10 +
> 11 files changed, 740 insertions(+)
> ---
> base-commit: 8cdeaa50eae8dad34885515f62559ee83e7e8dda
> change-id: 20260506-hawi-phy-pcie-283933b4113e
>
> Best regards,
> --
> Matthew Leung <matthew.leung@oss.qualcomm.com>
>
^ permalink raw reply
* Re: [PATCH v4 2/2] iio: adc: add Axiado SARADC driver
From: Jonathan Cameron @ 2026-07-18 1:40 UTC (permalink / raw)
To: Andy Shevchenko
Cc: Petar Stepanovic, Akhila Kavi, Prasad Bolisetty, David Lechner,
Nuno Sá, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Harshit Shah, linux-iio, devicetree,
linux-arm-kernel, linux-kernel
In-Reply-To: <alnpMFK6_ZkdWlLi@ashevche-desk.local>
On Fri, 17 Jul 2026 11:34:56 +0300
Andy Shevchenko <andriy.shevchenko@intel.com> wrote:
> On Thu, Jul 16, 2026 at 10:53:02PM -0700, Petar Stepanovic wrote:
> > Add support for the SARADC controller found on Axiado AX3000 and
> > AX3005 SoCs.
> >
> > The driver supports single-shot voltage reads through the IIO
> > subsystem. The number of available input channels is selected from
> > the SoC match data, allowing AX3000 and AX3005 variants to use the
> > same driver.
>
Andy already commented everywhere I'd have said things, so just
a few places where I'd take a different approach to resolving stuff.
Thanks,
Jonathan
>
> ...
>
> > +/* GLOBAL_CTRL register values */
> > +#define AX_SARADC_GLOBAL_CTRL_SAMPLE_16 \
> > + FIELD_PREP(AX_SARADC_GLOBAL_CTRL_SAMPLE_MASK, 0)
> > +
> > +#define AX_SARADC_GLOBAL_CTRL_MODE_MANUAL \
> > + FIELD_PREP(AX_SARADC_GLOBAL_CTRL_MODE_MASK, 1)
>
> FIELD_PREP_CONST() in both cases.
>
For this second one I'd rather see defines for the values the
field takes and then FIELD_PREP only used inline.
#define AX_SARADC_GLOBAL_CTL_MODE_something 0
#define AX_SARADC_GLOBAL_CTL_MODE_MANUAL 1
To me that acts as clearer documentation of what is
going on at the point of the write.
Similar applies for the SAMPLE_MASK field where I'd define the
value 0 with a name then have the FIELD_PREP() inline.
> ...
>
> > +#define AX_SARADC_MANUAL_CTRL_EN(ch) \
> > + (AX_SARADC_MANUAL_CTRL_ENABLE | \
> > + FIELD_PREP(AX_SARADC_MANUAL_CTRL_CH_SEL_MASK, ch))
>
> Missing parentheses for ch. Also wrong indentation of the second line, missing
> one space.
>
I wonder if it would be more readable to just push that inline rather
than having this define at all.
^ permalink raw reply
* Re: [PATCH v7 2/4] iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs
From: Jonathan Cameron @ 2026-07-18 1:46 UTC (permalink / raw)
To: Marcelo Schmitt
Cc: sashiko-reviews, Marcelo Schmitt, conor+dt, devicetree, robh,
linux-iio
In-Reply-To: <alpab2gBs4nPDU5g@debian-BULLSEYE-live-builder-AMD64>
On Fri, 17 Jul 2026 13:38:07 -0300
Marcelo Schmitt <marcelo.schmitt1@gmail.com> wrote:
> On 07/16, sashiko-bot@kernel.org wrote:
> > Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> > - [Medium] The `LTC2378_DIFF_CHANNEL` macro defines an IIO channel that is intended to be differential but lacks the `.differential = 1`, `.indexed = 1`, `.channel = 0`, and `.channel2 = 1` attributes.
> > --
> >
> > --- Patch [2]: [PATCH v7 2/4] iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs ---
> > commit 5710d5f25b45906977e2522567c1964b1d3219f8
> > Author: Marcelo Schmitt <marcelo.schmitt@analog.com>
> >
> > iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs
> >
> > This commit adds a new driver for the Analog Devices LTC2378-20 and
> > similar analog-to-digital converters.
> >
> > > diff --git a/drivers/iio/adc/ltc2378.c b/drivers/iio/adc/ltc2378.c
> > > new file mode 100644
> > > index 0000000000000..5600c820c7f45
> > > --- /dev/null
> > > +++ b/drivers/iio/adc/ltc2378.c
> > > @@ -0,0 +1,416 @@
> > [ ... ]
> > > +#define LTC2378_DIFF_CHANNEL(_sign, _real_bits, _storage_bits) \
> > > +{ \
> > > + .type = IIO_VOLTAGE, \
> > > + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
> > > + BIT(IIO_CHAN_INFO_SCALE), \
> > > + .scan_index = 0, \
> >
> > [Severity: Medium]
> > Does this macro need to set .differential = 1, .indexed = 1, .channel = 0,
> > and .channel2 = 1? The name suggests these are differential channels, but
> > without these attributes, the IIO ABI will expose them as single-ended
> > (e.g., in_voltage_raw instead of in_voltage0-voltage1_raw).
>
> For parts with multiple inputs and configurable differential channels (e.g.
> AD4130-8, MCP3561/2/4R, NAFE13388 [1, 2, 3]), the voltageY-voltageZ ABI helps
> clarifying what physical inputs make up a channel. For differential only parts,
> the ABI seems to be flexible by only requiring a 'consistent labeling' (either
> voltageY or voltageY-voltageZ IIUC). Will set the channel with the differential
> ABI if maintainers believe that's the best way to have it. Might slightly tweak
> the channel macros to keep .differential = 0 for pseudo-differential chips
> (e.g.LTC2369-18).
Whilst I get your point on differential only cases not really requiring the
specification of two inputs, I think that does provide a simple indication
to users that they are differential channel.
I would prefer that we make them 'look' like differential channels so make up
a suitable number for the negative side.
I vaguely recall we did once consider handling these as inX-inX but it seemed
a little too weird. Hohum. ABI was designed against the max1363 and I didn't
think much about device that always did differential. What can I say, I was
young and foolish ;)
Jonathan
>
> [1]: https://www.analog.com/media/en/technical-documentation/data-sheets/ad4130-8.pdf
> [2]: https://ww1.microchip.com/downloads/aemDocuments/documents/APID/ProductDocuments/DataSheets/MCP3561_2_4R-Data-Sheet-DS200006391C.pdf
> [3]: https://www.nxp.com/docs/en/data-sheet/NAFE13388.pdf
>
> >
> > > + .scan_type = { \
> > > + .format = _sign ? IIO_SCAN_FORMAT_SIGNED_INT : \
> > > + IIO_SCAN_FORMAT_UNSIGNED_INT, \
> > > + .realbits = _real_bits, \
> > > + .storagebits = _storage_bits, \
> > > + .shift = _storage_bits - _real_bits, \
> > > + .endianness = IIO_BE, \
> > > + }, \
> > > +}
> > > +
> > > +#define LTC2378_BIPOLAR_DIFF_CHANNEL(_real_bits) \
> > > + LTC2378_DIFF_CHANNEL(1, _real_bits, (((_real_bits) > 16) ? 32 : 16))
> > > +
> > > +#define LTC2378_UNIPOLAR_DIFF_CHANNEL(_real_bits) \
> > > + LTC2378_DIFF_CHANNEL(0, _real_bits, (((_real_bits) > 16) ? 32 : 16))
> > > +
> > [ ... ]
> >
> > --
> > Sashiko AI review · https://sashiko.dev/#/patchset/cover.1784235595.git.marcelo.schmitt@analog.com?part=2
>
^ permalink raw reply
* Re: [PATCH v7 2/4] iio: adc: ltc2378: Add support for LTC2378-20 and similar ADCs
From: Jonathan Cameron @ 2026-07-18 1:51 UTC (permalink / raw)
To: Marcelo Schmitt
Cc: linux-iio, devicetree, linux-kernel, nuno.sa, Michael.Hennerich,
dlechner, andy, robh, krzk+dt, conor+dt, julianbraha,
marcelo.schmitt1
In-Reply-To: <5108c89986ae8723139419058db1b2f2e74daff6.1784235595.git.marcelo.schmitt@analog.com>
On Thu, 16 Jul 2026 18:09:36 -0300
Marcelo Schmitt <marcelo.schmitt@analog.com> wrote:
> Support for LTC2378-20 and similar analog-to-digital converters.
>
> Reviewed-by: David Lechner <dlechner@baylibre.com>
> Signed-off-by: Marcelo Schmitt <marcelo.schmitt@analog.com>
One entirely trivial comment.
Jonathan
> diff --git a/drivers/iio/adc/ltc2378.c b/drivers/iio/adc/ltc2378.c
> new file mode 100644
> index 000000000000..5600c820c7f4
> --- /dev/null
> +++ b/drivers/iio/adc/ltc2378.c
> +static int ltc2378_refin_setup(struct device *dev, struct ltc2378_state *st)
> +{
> + int ret;
> +
> + /*
> + * The internal reference buffer amplifies both the internal reference
> + * and REFIN by a factor of 2.
> + */
> + ret = devm_regulator_get_enable_read_voltage(dev, "refin");
> + if (ret == -ENODEV) { /* refin is optional */
> + st->ref_uV = st->info->internal_ref_uV * 2;
> + return 0;
> + } else if (ret < 0) {
if (ret < 0)
> + return dev_err_probe(dev, ret, "failed to read refin regulator\n");
> + }
> +
> + st->ref_uV = ret * 2;
> +
> + return 0;
> +}
^ permalink raw reply
* [PATCH v10 0/2] phy: qcom-mipi-csi2: Add a CSI2 MIPI DPHY driver
From: Bryan O'Donoghue @ 2026-07-18 2:38 UTC (permalink / raw)
To: Vinod Koul, Kishon Vijay Abraham I, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Neil Armstrong
Cc: Bryan O'Donoghue, Vladimir Zapolskiy, linux-arm-msm,
linux-phy, linux-media, devicetree, linux-kernel,
Bryan O'Donoghue
This driver is very critical to several SoCs being merged and CPHY being
added to Qualcomm SoCs.
It would be very beneficial therefore to get this merged in this cycle. A
lot of other enablement code depends on it.
Vinod, Kishon what do you need to take this for 7.2 ?
Changes in v10:
- Rewords commit log of yaml schema to read more organically - Krzysztof
- title: Qualcomm X1E80100 SoC CSI2 PHY - Krzysztof
- opp-table: object - Krzysztof
- Example opp-table in PHY node - Krzysztof
- port@1 uses stock graph.yaml - Krzysztof
- Dropped redundant remote-endpoint declaration - Krzysztof
- vdda-0p9 - SoC pins are vdda-0p9 rail is l2c_0p8. Named as with pin - Vlad, Konrad
- Retaining #phy-cells = 1.
Rob gave a steer to have the consumer configure this via DT. Not the way
Vlad suggests but I'm sticking to Rob's guidance for now - Rob/Vlad.
- data-lanes - retained as per CAMSS existing.
Nihal suggested, I agreed and Loic added his assent too. Vlad disagrees
with this. I accept Nihal's suggestion though we should remain
consistent. - Nihal, Loic, Vlad.
- pd_data = {} - Sashiko, Wengmeng
- lanes_enable() failure unwinds - Sashiko
- power_off and error paths drop OPP rate before performance state - Sashiko
- probe returns -ENODEV when pd_list is NULL with ret == 0 - Sashiko
- Move to devm_clk_bulk_get_all(), timer clock taken from the bulk,
clk_names indirection removed from soc_cfg - Frank
- Drop static CSI_COMMON_CTRL5 (0x1014) table row - Nihal
- Link to v9: https://patch.msgid.link/20260708-x1e-csi2-phy-v9-0-0210b90c04cf@linaro.org
Changes in v9:
Key advantages of implementing this logic as drivers/phy:
a) Per-PHY power rails become declarable and enforceable on a per-PHY
basis.
b) With future adaptations of CAMSS routing the PHY output to something
other than the CSI Decoders becomes possible.
c) Standard framework contract. phy_configure(mipi_dphy opts) / power_on / of_xlate.
d) Introduction of CPHY in this driver will elaborate CPHY for Linux in
general as there is currently no mipi_cphy_opts structure.
- TITAN_TOP_GDSC retained. Further elaboration in below link. - Vlad
Link: https://lore.kernel.org/all/d5407ab1-1af7-4678-ae67-5cf30ce8fa4b@kernel.org/
- Consumer selects PHY mode - Rob Herring
Link: https://lore.kernel.org/linux-media/20250710230846.GA44483-robh@kernel.org/
- Incorporates two comments by Krzysztof I missed previously - krzk
- Changes 0p9 rail name to 0p8 - Konrad, Sashiko
- Uses port@0 and port@1 to denote input/output - Vladimir
- Label phy@ instead of csiphy@ in example - Sashiko
- Drops additional entries in example opp one is enough - bod
- return 0 on timer_period_ps < 6. - Sashiko
- Use power-domain names and a "scaled" bool to flag if
a power-domain needs to be scaled or not. Allowing the code to
attached to all PDs, GDSC and RPMPD alike but only scale the
RPMPD. - Sashiko
- Fixes error cleanup flagged by Sashiko.
- Checks args->args_count. Sashiko
- Performs checks on pd_list == NULL. Sashiko
- Makes opp-table required. Sashiko
- Uses const "core" and "timer" when getting clocks. - Loic
- Adds AHB clock - Vijay
- Revert v7 lane-enable change, BIT(pos * 2) is correct: data-lanes
are logical indices, CSI_COMMON_CTRL5 is a physical bitmap - Nihal
- Adds a comment to explain clock-lane - Nihal
- Link to v8: https://patch.msgid.link/20260523-x1e-csi2-phy-v8-0-a85668459521@linaro.org
Changes in v8:
- Fixes two dt verification splats I missed by passing the wrong yaml file
to my checking script :( - Rob's bot
- Fixes the polarity offset error - thanks Sashiko.
- CONFIG_PM implies CONFIG_GENERIC_PM_DOMAINS no change - Sashiko, Bryan
- Implemented suggested unwinding by Sashiko
- Leaving the flagged settle_cnt alone. Requires invalid DT settings and in
addition the result is just a long settle count. Not real bug - Sashiko
- Link to v7: https://patch.msgid.link/20260522-x1e-csi2-phy-v7-0-79cb1280fad6@linaro.org
Changes in v7:
- Made CONFIG_PM a dependency. Sashiko commented on the pd_list being NULL
and suggested I check the pointer but, we need to ramp the rails when
switching clock rate so we need CONFIG_PM full stop. - Sashiko.ai, Bryan
- Added unwind operation for performance state error path - Sashiko
- Made clock-lanes genuinely optional for the supported use-case. - Sashiko
- Fixed the enable of lanes. Thus far we have had forever it seems
val |= BIT(lane.pos * 2) which I've never looked at much because it
has always worked. But looking at how to switch on polarities I realised
the relevant register is a linear bitmask without gaps so the correct
method is `val |= BIT(lane.pos)`.
This needs an update in the legacy PHY too in another series - Bryan
- I opted not to do any of the "but if DT send junk into your driver" fixes
from Sashiko since TBH I think the code would be Spaghetti afterwards.
We trust DT and if DT is wrong we fix it, we don't try to graph its
relative (in)sanity.
- Fixed my example in the yaml. Sashiko
- Link to v6: https://patch.msgid.link/20260521-x1e-csi2-phy-v6-0-9d73d9bd7d20@linaro.org
Changes in v6:
- Taking feedback from lively debate added ports and
endpoints to the PHY - Neil, Vlad
- Detection of split mode by way of which ports are declared.
port@0 is always a sensor input.
port@1 is optional and if present implies split-mode
port@2 is always the output. - Dmitry, Neil, Vlad.
- Split mode is left as -ENOTSUPP unless/until someone with the appropriate
hardware can take on responsibility to drive to completion.
- Extending phy_config_opts dropped.
I think this is a worthwhile extension but this series no longer depends
on it so dropped. - Bryan
- MX/MXC.
Two OPP tables one for CSIPHY0/1/2 scaling MXC one for CSIPHY4 keeping
MXA at LOWSVS_D1 - to be implemented in DT not here. Taniya, Konrad, Bryan
- Changed MAINTAINERS from Supported to Maintained.
Hobby time for me right now. - Bryan
- Link to v5: https://lore.kernel.org/r/20260326-x1e-csi2-phy-v5-0-0c0fc7f5c01b@linaro.org
v5:
- Adds support to apply passed parameters for clock/data position/polarity - Neil
- Drops GEN1/GEN2 differentiation this can be reconstituted if GEN1 ever
gets supported in this driver - Dmitry
- Drops camnoc_axi, cpas_ahb - Konrad
- Renames csiphy->core csiphy_timer->timer - Konrad
- Renames rail from 0p8 to 0p9 schematics say VDD_A_CSI_n_0P9 - Konrad
- TITAN_TOP_GDSC dropped - Konrad
- Passes PHY_QCOM_CSI2_MODE_{DPHY|CPHY|SPLIT_DPHY} with the controller
selecting the mode. Only DPHY mode is supported but the method to pass
CPHY or split-mode DPHY configuration is there.
Since split-mode is a Qualcomm specific mode the PHY modes are defined in
our binding instead of adding a new type to include/linux/phy/phy.h - bod
- Depends-on: https://lore.kernel.org/r/20260325-dphy-params-extension-v1-0-c6df5599284a@linaro.org
- Link to v4: https://lore.kernel.org/r/20260315-x1e-csi2-phy-v4-0-90c09203888d@linaro.org
v4:
- MMCX, MCX and MX/MXA power-domains added - Dmitry, Vijay, Konrad
- power-domain-names added as required - bod
- opp-tables amended to capture RPMHPD deps - Dmitry, Vijay
- Switched to dev_pm_opp_set_rate, dev_pm_domain_attach_by_name etc
dropped inherited CAMSS code - Dmitry
- Amended parameters structure to specify power-domain name list - bod
- Removed dead defines - Dmitry
- Noted in CSIPHY commit log intention to rework patterns of
PHY lane configs into loops/defines/bit-fields later - Dmitry, bod
- Lowercase hex throughout - Dmitry
- The yaml and code in this driver doesn't care if the node is a
sibling or a sub-node of CAMSS confirmed to work both ways - Dmitry, bod
- Link to v3: https://lore.kernel.org/r/20260226-x1e-csi2-phy-v3-0-11e608759410@linaro.org
v3:
- Resending this to make clear this submission is additive to x1e/Hamoa
The existing bindings and code will continue to work
Bindings are added only, nothing is subtracted from existing ABI.
- Link to v2: https://lore.kernel.org/r/20260225-x1e-csi2-phy-v2-0-7756edb67ea9@linaro.org
v2:
In this updated version
- Added operating-point support
The csiphy clock sets the OPP prior to setting the rate
for csiphy and csiphy_timer - Konrad
- Combo mode
Combo mode in CAMSS yaml has been added. Right now
no code has been changed in the PHY driver to support it as
I don't have hardware to test. In principle though it can
be supported. - Vladimir
- CSIPHY init sequences
I left these as their "magic number formats". With my diminished
status as a non-qcom VPN person - I can no longer see what the bits
map to. Moreover this is the situation any non-VPN community member
will be in when submitting CSIPHY sequences derived from downstream.
I think it is perfectly reasonable to take public CSIPHY init sequences
as magic numbers. If someone with bit-level access wants to enumerate
the bits that's fine but, it shouldn't gate in the interim. - Konrad/bod
- Sensor endpoints
I've stuck to the format used by every other CSIPHY in upstream.
Sensor endpoints hit the CAMSS/CSID endpoint not a endpoint in the PHY.
Given the proposed changes to CAMSS though to support "combo mode" I
think this should achieve the same outcome - multiple sensors on the one
PHY without introducing endpoints into the PHY that no other CSIPHY in
upstream currently has.
- Bitmask of enabled lanes
Work needs to be done in the v4l2 layer to really support this.
I propose making a separate series dedicated to non-linear bit
interpretation after merging this so as to contain the scope of the
series to something more bite (byte haha) sized. - Konrad/bod
- Link to v1: https://lore.kernel.org/r/20250710-x1e-csi2-phy-v1-0-74acbb5b162b@linaro.org
v1:
This short series adds a CSI2 MIPI PHY driver, initially supporting D-PHY
mode. The core logic and init sequences come directly from CAMSS and are
working on at least five separate x1e devices.
The rationale to instantiate CSI2 PHYs as standalone devices instead of as
sub-nodes of CAMSS is as follows.
1. Precedence
CAMSS has a dedicated I2C bus called CCI Camera Control Interface.
We model this controller as its own separate device in devicetree.
This makes sense and CCI/I2C is a well defined bus type already modelled
in Linux.
MIPI CSI2 PHY devices similarly fit into a well defined separate
bus/device structure.
Contrast to another CAMSS component such as VFE, CSID or TPG these
components only interact with other CAMSS inputs/outputs unlike CSIPHY
which interacts with non-SoC components.
2. Hardware pinouts and rails
The CSI2 PHY has its own data/clock lanes out from the SoC and indeed
has its own incoming power-rails.
3. Other devicetree schemas
There are several examples throughout the kernel of CSI PHYs modeled as
standalone devices which one assumes follows the same reasoning as given
above.
I've been working on this on-and-off since the end of April:
Link: https://lore.kernel.org/linux-media/c5cf0155-f839-4db9-b865-d39b56bb1e0a@linaro.org
There is another proposal to have the PHYs be subdevices of CAMSS but, I
believe we should go with a "full fat" PHY to match best practices in
drivers/phy/qualcomm/*.
Using the standard PHY API and the parameter passing that goes with it
allows us to move away from custom interfaces in CAMSS and to conform more
clearly to established PHY paradigms such as the QMP combo PHY.
Looking at existing compat strings I settled on
"qcom,x1e80100-mipi-csi2-combo-phy" deliberately omitting reference to the
fact the PHY is built on a four nano-meter process node, which seems to
match recent submissions to QMP PHY.
My first pass at this driver included support for the old two phase
devices:
Link: https://git.codelinaro.org/bryan.odonoghue/kernel/-/commit/a504c28d109296c93470340cfe7281231f573bcb#b6e59ed7db94c9da22e492bb03fcda6a4300983c
I realised that the device tree schema changes required to support a
comprehensive conversion of all CAMSS to this driver would be an
almost certainly be unacceptable ABI break or at the very least an enormous
amount of work and verification so I instead aimed to support just one new
SoC in the submission.
I've retained the callback indirections give us scope to add in another type of
future PHY including potentially adding in the 2PH later on.
This driver is tested and working on x1e/Hamoa and has been tested as not
breaking sc8280xp/Makena and sm8250/Kona.
Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
---
Bryan O'Donoghue (2):
dt-bindings: phy: qcom: Add CSI2 C-PHY/DPHY schema
phy: qcom-mipi-csi2: Add a CSI2 MIPI DPHY driver
.../bindings/phy/qcom,x1e80100-csi2-phy.yaml | 195 +++++++++
MAINTAINERS | 10 +
drivers/phy/qualcomm/Kconfig | 14 +
drivers/phy/qualcomm/Makefile | 5 +
drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c | 386 ++++++++++++++++++
drivers/phy/qualcomm/phy-qcom-mipi-csi2-core.c | 443 +++++++++++++++++++++
drivers/phy/qualcomm/phy-qcom-mipi-csi2.h | 98 +++++
7 files changed, 1151 insertions(+)
---
base-commit: 8dac27bfa2f994ecb11f01a63641527d17d48fc1
change-id: 20250710-x1e-csi2-phy-f6434b651d3a
Best regards,
--
Bryan O'Donoghue <bryan.odonoghue@linaro.org>
^ permalink raw reply
* [PATCH v10 1/2] dt-bindings: phy: qcom: Add CSI2 C-PHY/DPHY schema
From: Bryan O'Donoghue @ 2026-07-18 2:38 UTC (permalink / raw)
To: Vinod Koul, Kishon Vijay Abraham I, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Neil Armstrong
Cc: Bryan O'Donoghue, Vladimir Zapolskiy, linux-arm-msm,
linux-phy, linux-media, devicetree, linux-kernel,
Bryan O'Donoghue
In-Reply-To: <20260718-x1e-csi2-phy-v10-0-5720a7888953@linaro.org>
Add a base schema for the MIPI CSI2 PHYs on Qualcomm SoCs. This PHY
supports both DPHY and CPHY operation. A special mode of DPHY operation -
called variously split-mode or combo-mode also allows for two sensors to be
connected to one PHY.
The submitted binding here describes the DPHY modes of operation only. CPHY
is left to future work.
Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
---
.../bindings/phy/qcom,x1e80100-csi2-phy.yaml | 195 +++++++++++++++++++++
1 file changed, 195 insertions(+)
diff --git a/Documentation/devicetree/bindings/phy/qcom,x1e80100-csi2-phy.yaml b/Documentation/devicetree/bindings/phy/qcom,x1e80100-csi2-phy.yaml
new file mode 100644
index 0000000000000..880fe602945cb
--- /dev/null
+++ b/Documentation/devicetree/bindings/phy/qcom,x1e80100-csi2-phy.yaml
@@ -0,0 +1,195 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/phy/qcom,x1e80100-csi2-phy.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Qualcomm X1E80100 SoC CSI2 PHY
+
+maintainers:
+ - Bryan O'Donoghue <bod@kernel.org>
+
+description:
+ Qualcomm MIPI CSI2 C-PHY/D-PHY combination PHY. Connects MIPI CSI2 sensors
+ to Qualcomm's Camera CSI Decoder. The PHY supports both C-PHY and D-PHY
+ modes.
+
+properties:
+ compatible:
+ const: qcom,x1e80100-csi2-phy
+
+ reg:
+ maxItems: 1
+
+ "#phy-cells":
+ const: 1
+ description:
+ The single cell specifies the PHY operating mode.
+
+ clocks:
+ maxItems: 3
+
+ clock-names:
+ items:
+ - const: core
+ - const: timer
+ - const: ahb
+
+ interrupts:
+ maxItems: 1
+
+ operating-points-v2: true
+
+ opp-table:
+ type: object
+
+ power-domains:
+ items:
+ - description: Titan Top GDSC - Titan ISP Block, Global Distributed Switch Controller.
+ - description: MMCX voltage rail
+ - description: MXC or MXA voltage rail
+
+ power-domain-names:
+ items:
+ - const: top
+ - const: mmcx
+ - const: mx
+
+ vdda-0p9-supply:
+ description: Phandle to a 0.9V regulator supply to a PHY.
+
+ vdda-1p2-supply:
+ description: Phandle to 1.2V regulator supply to a PHY.
+
+ ports:
+ $ref: /schemas/graph.yaml#/properties/ports
+
+ properties:
+ port@0:
+ $ref: /schemas/graph.yaml#/$defs/port-base
+ description:
+ Sensor input. Always present. A single sensor is described by a
+ single endpoint with one to four data lanes. DPHY split mode,
+ where two independent sensors share the same PHY, is described
+ by two endpoints; endpoint@0 with exactly two data-lanes and
+ endpoint@1 with exactly one data-lane.
+ unevaluatedProperties: false
+
+ patternProperties:
+ "^endpoint(@[0-9a-f]+)?$":
+ $ref: /schemas/media/video-interfaces.yaml#
+ unevaluatedProperties: false
+ properties:
+ data-lanes:
+ minItems: 1
+ maxItems: 4
+
+ required:
+ - data-lanes
+ - remote-endpoint
+
+ allOf:
+ - if:
+ required:
+ - endpoint@1
+ then:
+ properties:
+ endpoint@0:
+ properties:
+ data-lanes:
+ minItems: 2
+ maxItems: 2
+ endpoint@1:
+ properties:
+ data-lanes:
+ maxItems: 1
+ required:
+ - endpoint@0
+
+ port@1:
+ $ref: /schemas/graph.yaml#/properties/port
+ description: Output to the CAMSS CSID controller.
+
+ required:
+ - port@0
+ - port@1
+
+required:
+ - compatible
+ - reg
+ - "#phy-cells"
+ - clocks
+ - clock-names
+ - interrupts
+ - operating-points-v2
+ - power-domains
+ - power-domain-names
+ - vdda-0p9-supply
+ - vdda-1p2-supply
+ - ports
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ #include <dt-bindings/clock/qcom,x1e80100-camcc.h>
+ #include <dt-bindings/clock/qcom,x1e80100-gcc.h>
+ #include <dt-bindings/power/qcom,rpmhpd.h>
+
+ phy@ace4000 {
+ compatible = "qcom,x1e80100-csi2-phy";
+ reg = <0x0ace4000 0x2000>;
+ #phy-cells = <1>;
+
+ clocks = <&camcc CAM_CC_CSIPHY0_CLK>,
+ <&camcc CAM_CC_CSI0PHYTIMER_CLK>,
+ <&camcc CAM_CC_CORE_AHB_CLK>;
+ clock-names = "core",
+ "timer",
+ "ahb";
+
+ interrupts = <GIC_SPI 477 IRQ_TYPE_EDGE_RISING>;
+
+ operating-points-v2 = <&csiphy_opp_table>;
+
+ power-domains = <&camcc CAM_CC_TITAN_TOP_GDSC>,
+ <&rpmhpd RPMHPD_MMCX>,
+ <&rpmhpd RPMHPD_MX>;
+ power-domain-names = "top",
+ "mmcx",
+ "mx";
+
+ vdda-0p9-supply = <&vreg_l2c_0p9>;
+ vdda-1p2-supply = <&vreg_l1c_1p2>;
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+ csiphy0_in: endpoint {
+ data-lanes = <0 1 2 3>;
+ remote-endpoint = <&sensor_out>;
+ };
+ };
+
+ port@1 {
+ reg = <1>;
+ csiphy0_out: endpoint {
+ remote-endpoint = <&csid_in>;
+ };
+ };
+ };
+
+ csiphy_opp_table: opp-table {
+ compatible = "operating-points-v2";
+
+ opp-300000000 {
+ opp-hz = /bits/ 64 <300000000>;
+ required-opps = <&rpmhpd_opp_low_svs_d1>,
+ <&rpmhpd_opp_low_svs_d1>;
+ };
+ };
+ };
--
2.54.0
^ permalink raw reply related
* [PATCH v10 2/2] phy: qcom-mipi-csi2: Add a CSI2 MIPI DPHY driver
From: Bryan O'Donoghue @ 2026-07-18 2:38 UTC (permalink / raw)
To: Vinod Koul, Kishon Vijay Abraham I, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Neil Armstrong
Cc: Bryan O'Donoghue, Vladimir Zapolskiy, linux-arm-msm,
linux-phy, linux-media, devicetree, linux-kernel,
Bryan O'Donoghue
In-Reply-To: <20260718-x1e-csi2-phy-v10-0-5720a7888953@linaro.org>
Add a new MIPI CSI2 driver in DPHY mode initially. The entire set of
existing CAMSS CSI PHY init sequences are imported in order to save time
and effort in later patches.
The following devices are supported in this drop:
"qcom,x1e80100-csi2-phy"
In-line with other PHY drivers the process node is included in the name.
Data-lane and clock lane positioning and polarity selection via newly
amended struct phy_configure_opts_mipi_dphy{} is supported.
The Qualcomm 3PH class of PHYs can do both DPHY and CPHY mode. For now only
DPHY is supported.
In porting some of the logic over from camss-csiphy*.c to here its also
possible to rationalise some of the code.
In particular use of regulator_bulk and clk_bulk as well as dropping the
seemingly useless and unused interrupt handler.
The PHY sequences and a lot of the logic that goes with them are well
proven in CAMSS and mature so the main thing to watch out for here is how
to get the right sequencing of regulators, clocks and register-writes.
The register init sequence table is imported verbatim from the existing
CAMSS csiphy driver. A follow-up series will rework the table to extract
the repetitive per-lane pattern into a loop.
Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
---
MAINTAINERS | 10 +
drivers/phy/qualcomm/Kconfig | 14 +
drivers/phy/qualcomm/Makefile | 5 +
drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c | 386 ++++++++++++++++++
drivers/phy/qualcomm/phy-qcom-mipi-csi2-core.c | 443 +++++++++++++++++++++
drivers/phy/qualcomm/phy-qcom-mipi-csi2.h | 98 +++++
6 files changed, 956 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 15011f5752a99..a203b41475ea4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22296,6 +22296,16 @@ S: Maintained
F: Documentation/devicetree/bindings/media/qcom,*-iris.yaml
F: drivers/media/platform/qcom/iris/
+QUALCOMM MIPI CSI2 PHY DRIVER
+M: Bryan O'Donoghue <bod@kernel.org>
+L: linux-phy@lists.infradead.org
+L: linux-media@vger.kernel.org
+L: linux-arm-msm@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/phy/qcom,*-csi2-phy.yaml
+F: drivers/phy/qualcomm/phy-qcom-mipi-csi2*.c
+F: drivers/phy/qualcomm/phy-qcom-mipi-csi2*.h
+
QUALCOMM NAND CONTROLLER DRIVER
M: Manivannan Sadhasivam <mani@kernel.org>
L: linux-mtd@lists.infradead.org
diff --git a/drivers/phy/qualcomm/Kconfig b/drivers/phy/qualcomm/Kconfig
index 60a0ead127fa9..779a3511ba852 100644
--- a/drivers/phy/qualcomm/Kconfig
+++ b/drivers/phy/qualcomm/Kconfig
@@ -28,6 +28,20 @@ config PHY_QCOM_EDP
Enable this driver to support the Qualcomm eDP PHY found in various
Qualcomm chipsets.
+config PHY_QCOM_MIPI_CSI2
+ tristate "Qualcomm MIPI CSI2 PHY driver"
+ depends on ARCH_QCOM || COMPILE_TEST
+ depends on OF
+ depends on PM
+ depends on COMMON_CLK
+ select GENERIC_PHY
+ select GENERIC_PHY_MIPI_DPHY
+ help
+ Enable this to support the MIPI CSI2 PHY driver found in various
+ Qualcomm chipsets. This PHY is used to connect MIPI CSI2
+ camera sensors to the CSI Decoder in the Qualcomm Camera Subsystem
+ CAMSS.
+
config PHY_QCOM_IPQ4019_USB
tristate "Qualcomm IPQ4019 USB PHY driver"
depends on OF && (ARCH_QCOM || COMPILE_TEST)
diff --git a/drivers/phy/qualcomm/Makefile b/drivers/phy/qualcomm/Makefile
index b71a6a0bed3f1..382cb594b06b6 100644
--- a/drivers/phy/qualcomm/Makefile
+++ b/drivers/phy/qualcomm/Makefile
@@ -6,6 +6,11 @@ obj-$(CONFIG_PHY_QCOM_IPQ4019_USB) += phy-qcom-ipq4019-usb.o
obj-$(CONFIG_PHY_QCOM_IPQ806X_SATA) += phy-qcom-ipq806x-sata.o
obj-$(CONFIG_PHY_QCOM_M31_USB) += phy-qcom-m31.o
obj-$(CONFIG_PHY_QCOM_M31_EUSB) += phy-qcom-m31-eusb2.o
+
+phy-qcom-mipi-csi2-objs += phy-qcom-mipi-csi2-core.o \
+ phy-qcom-mipi-csi2-3ph-dphy.o
+obj-$(CONFIG_PHY_QCOM_MIPI_CSI2) += phy-qcom-mipi-csi2.o
+
obj-$(CONFIG_PHY_QCOM_PCIE2) += phy-qcom-pcie2.o
obj-$(CONFIG_PHY_QCOM_QMP_COMBO) += phy-qcom-qmp-combo.o phy-qcom-qmp-usbc.o
diff --git a/drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c b/drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c
new file mode 100644
index 0000000000000..5ac0d041b36b7
--- /dev/null
+++ b/drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c
@@ -0,0 +1,386 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Qualcomm MSM Camera Subsystem - CSIPHY Module 3phase v1.0
+ *
+ * Copyright (c) 2011-2015, The Linux Foundation. All rights reserved.
+ * Copyright (C) 2016-2026 Linaro Ltd.
+ */
+
+#include <linux/delay.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/time64.h>
+
+#include "phy-qcom-mipi-csi2.h"
+
+#define CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(offset, n) ((offset) + 0x4 * (n))
+#define CSIPHY_3PH_CMN_CSI_COMMON_CTRL0_PHY_SW_RESET BIT(0)
+#define CSIPHY_3PH_CMN_CSI_COMMON_CTRL5_CLK_ENABLE BIT(7)
+#define CSIPHY_3PH_CMN_CSI_COMMON_CTRL6_COMMON_PWRDN_B BIT(0)
+#define CSIPHY_3PH_CMN_CSI_COMMON_CTRL6_SHOW_REV_ID BIT(1)
+#define CSIPHY_3PH_CMN_CSI_COMMON_CTRL10_IRQ_CLEAR_CMD BIT(0)
+#define CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(offset, n) ((offset) + 0xb0 + 0x4 * (n))
+
+#define CSIPHY_2PH_LN_CSI_2PHASE_CTRL9n(n) ((0x200 * (n)) + 0x24)
+
+/*
+ * 3 phase CSI has 19 common status regs with only 0-10 being used
+ * and 11-18 being reserved.
+ */
+#define CSI_COMMON_STATUS_NUM 11
+/*
+ * There are a number of common control registers
+ * The offset to clear the CSIPHY IRQ status starts @ 22
+ * So to clear CSI_COMMON_STATUS0 this is CSI_COMMON_CONTROL22, STATUS1 is
+ * CONTROL23 and so on
+ */
+#define CSI_CTRL_STATUS_INDEX 22
+
+/*
+ * There are 43 COMMON_CTRL registers with regs after # 33 being reserved
+ */
+#define CSI_CTRL_MAX 33
+
+#define CSIPHY_DEFAULT_PARAMS 0
+#define CSIPHY_SETTLE_CNT_LOWER_BYTE 2
+#define CSIPHY_SKEW_CAL 7
+
+/* 4nm 2PH v 2.1.2 2p5Gbps 4 lane DPHY mode */
+static const struct
+mipi_csi2phy_lane_regs lane_regs_x1e80100[] = {
+ /* Power up lanes 2ph mode */
+ {.reg_addr = 0x101c, .reg_data = 0x7a, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x1018, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
+
+ {.reg_addr = 0x0094, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x00a0, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0090, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0098, .reg_data = 0x08, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0094, .reg_data = 0x07, .delay_us = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0030, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0000, .reg_data = 0x8e, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0038, .reg_data = 0xfe, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x002c, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0034, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x001c, .reg_data = 0x0a, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0014, .reg_data = 0x60, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x003c, .reg_data = 0xb8, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0004, .reg_data = 0x0c, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0020, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0008, .reg_data = 0x10, .param_type = CSIPHY_SETTLE_CNT_LOWER_BYTE},
+ {.reg_addr = 0x0010, .reg_data = 0x52, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0094, .reg_data = 0xd7, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x005c, .reg_data = 0x00, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x0060, .reg_data = 0xbd, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x0064, .reg_data = 0x7f, .param_type = CSIPHY_SKEW_CAL},
+
+ {.reg_addr = 0x0e94, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0ea0, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e90, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e98, .reg_data = 0x08, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e94, .reg_data = 0x07, .delay_us = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e30, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e28, .reg_data = 0x04, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e00, .reg_data = 0x80, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e0c, .reg_data = 0xff, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e38, .reg_data = 0x1f, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e2c, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e34, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e1c, .reg_data = 0x0a, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e14, .reg_data = 0x60, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e3c, .reg_data = 0xb8, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e04, .reg_data = 0x0c, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e20, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0e08, .reg_data = 0x10, .param_type = CSIPHY_SETTLE_CNT_LOWER_BYTE},
+ {.reg_addr = 0x0e10, .reg_data = 0x52, .param_type = CSIPHY_DEFAULT_PARAMS},
+
+ {.reg_addr = 0x0494, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x04a0, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0490, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0498, .reg_data = 0x08, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0494, .reg_data = 0x07, .delay_us = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0430, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0400, .reg_data = 0x8e, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0438, .reg_data = 0xfe, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x042c, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0434, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x041c, .reg_data = 0x0a, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0414, .reg_data = 0x60, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x043c, .reg_data = 0xb8, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0404, .reg_data = 0x0c, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0420, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0408, .reg_data = 0x10, .param_type = CSIPHY_SETTLE_CNT_LOWER_BYTE},
+ {.reg_addr = 0x0410, .reg_data = 0x52, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0494, .reg_data = 0xd7, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x045c, .reg_data = 0x00, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x0460, .reg_data = 0xbd, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x0464, .reg_data = 0x7f, .param_type = CSIPHY_SKEW_CAL},
+
+ {.reg_addr = 0x0894, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x08a0, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0890, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0898, .reg_data = 0x08, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0894, .reg_data = 0x07, .delay_us = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0830, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0800, .reg_data = 0x8e, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0838, .reg_data = 0xfe, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x082c, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0834, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x081c, .reg_data = 0x0a, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0814, .reg_data = 0x60, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x083c, .reg_data = 0xb8, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0804, .reg_data = 0x0c, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0820, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0808, .reg_data = 0x10, .param_type = CSIPHY_SETTLE_CNT_LOWER_BYTE},
+ {.reg_addr = 0x0810, .reg_data = 0x52, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0894, .reg_data = 0xd7, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x085c, .reg_data = 0x00, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x0860, .reg_data = 0xbd, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x0864, .reg_data = 0x7f, .param_type = CSIPHY_SKEW_CAL},
+
+ {.reg_addr = 0x0c94, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0ca0, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c90, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c98, .reg_data = 0x08, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c94, .reg_data = 0x07, .delay_us = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c30, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c00, .reg_data = 0x8e, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c38, .reg_data = 0xfe, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c2c, .reg_data = 0x01, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c34, .reg_data = 0x0f, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c1c, .reg_data = 0x0a, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c14, .reg_data = 0x60, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c3c, .reg_data = 0xb8, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c04, .reg_data = 0x0c, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c20, .reg_data = 0x00, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c08, .reg_data = 0x10, .param_type = CSIPHY_SETTLE_CNT_LOWER_BYTE},
+ {.reg_addr = 0x0c10, .reg_data = 0x52, .param_type = CSIPHY_DEFAULT_PARAMS},
+ {.reg_addr = 0x0c94, .reg_data = 0xd7, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x0c5c, .reg_data = 0x00, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x0c60, .reg_data = 0xbd, .param_type = CSIPHY_SKEW_CAL},
+ {.reg_addr = 0x0c64, .reg_data = 0x7f, .param_type = CSIPHY_SKEW_CAL},
+};
+
+static inline const struct mipi_csi2phy_device_regs *
+csi2phy_dev_to_regs(struct mipi_csi2phy_device *csi2phy)
+{
+ return &csi2phy->soc_cfg->reg_info;
+}
+
+static void phy_qcom_mipi_csi2_hw_version_read(struct mipi_csi2phy_device *csi2phy)
+{
+ const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
+ u32 tmp;
+
+ writel(CSIPHY_3PH_CMN_CSI_COMMON_CTRL6_SHOW_REV_ID, csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 6));
+
+ tmp = readl_relaxed(csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(regs->common_regs_offset, 12));
+ csi2phy->hw_version = tmp;
+
+ tmp = readl_relaxed(csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(regs->common_regs_offset, 13));
+ csi2phy->hw_version |= (tmp << 8) & 0xFF00;
+
+ tmp = readl_relaxed(csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(regs->common_regs_offset, 14));
+ csi2phy->hw_version |= (tmp << 16) & 0xFF0000;
+
+ tmp = readl_relaxed(csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(regs->common_regs_offset, 15));
+ csi2phy->hw_version |= (tmp << 24) & 0xFF000000;
+
+ dev_dbg_once(csi2phy->dev, "CSIPHY 3PH HW Version = 0x%08x\n", csi2phy->hw_version);
+}
+
+/*
+ * phy_qcom_mipi_csi2_reset - Perform software reset on CSIPHY module
+ * @phy_qcom_mipi_csi2: CSIPHY device
+ */
+static void phy_qcom_mipi_csi2_reset(struct mipi_csi2phy_device *csi2phy)
+{
+ const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
+
+ writel(CSIPHY_3PH_CMN_CSI_COMMON_CTRL0_PHY_SW_RESET,
+ csi2phy->base + CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 0));
+ usleep_range(5000, 8000);
+ writel(0x0, csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 0));
+}
+
+/*
+ * phy_qcom_mipi_csi2_settle_cnt_calc - Calculate settle count value
+ *
+ * Helper function to calculate settle count value. This is
+ * based on the CSI2 T_hs_settle parameter which in turn
+ * is calculated based on the CSI2 transmitter link frequency.
+ *
+ * Return settle count value or 0 if the CSI2 link frequency
+ * is not available
+ */
+static u8 phy_qcom_mipi_csi2_settle_cnt_calc(s64 link_freq, u32 timer_clk_rate)
+{
+ u32 t_hs_prepare_max_ps;
+ u32 timer_period_ps;
+ u32 t_hs_settle_ps;
+ u8 settle_cnt;
+ u32 ui_ps;
+
+ if (link_freq <= 0)
+ return 0;
+
+ ui_ps = div_u64(PSEC_PER_SEC, link_freq);
+ ui_ps /= 2;
+ t_hs_prepare_max_ps = 85000 + 6 * ui_ps;
+ t_hs_settle_ps = t_hs_prepare_max_ps;
+
+ timer_period_ps = div_u64(PSEC_PER_SEC, timer_clk_rate);
+
+ if ((t_hs_settle_ps / timer_period_ps) < 6)
+ return 0;
+
+ settle_cnt = t_hs_settle_ps / timer_period_ps - 6;
+
+ return settle_cnt;
+}
+
+static void
+phy_qcom_mipi_csi2_gen2_config_lanes(struct mipi_csi2phy_device *csi2phy,
+ u8 settle_cnt)
+{
+ const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
+ const struct mipi_csi2phy_lane_regs *r = regs->init_seq;
+ int i, array_size = regs->lane_array_size;
+ u32 val;
+
+ for (i = 0; i < array_size; i++, r++) {
+ switch (r->param_type) {
+ case CSIPHY_SETTLE_CNT_LOWER_BYTE:
+ val = settle_cnt & 0xff;
+ break;
+ case CSIPHY_SKEW_CAL:
+ /* TODO: support application of skew from dt flag */
+ continue;
+ default:
+ val = r->reg_data;
+ break;
+ }
+ writel(val, csi2phy->base + r->reg_addr);
+ if (r->delay_us)
+ udelay(r->delay_us);
+ }
+}
+
+static int phy_qcom_mipi_csi2_lanes_enable(struct mipi_csi2phy_device *csi2phy,
+ struct mipi_csi2phy_stream_cfg *cfg)
+{
+ const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
+ struct mipi_csi2phy_lanes_cfg *lane_cfg = &cfg->lane_cfg;
+ u8 settle_cnt;
+ u8 val;
+ int i;
+
+ settle_cnt = phy_qcom_mipi_csi2_settle_cnt_calc(cfg->link_freq, csi2phy->timer_clk_rate);
+ if (!settle_cnt)
+ return -EINVAL;
+
+ /*
+ * CSI_COMMON_CTRL5 is a physical lane power-up bitmap:
+ * - Bits [0,2,4,6] → D-PHY data lanes(LN0, LN2, LN4, LN6)
+ * - Bits [1,3,5] → C-PHY trio lanes(LN1, LN3, LN5)
+ * - Bit [7] → D-PHY clock lane(LNCK) dedicated clock enable
+ */
+ val = BIT(lane_cfg->clk.pos);
+ for (i = 0; i < cfg->num_data_lanes; i++)
+ val |= BIT(lane_cfg->data[i].pos * 2);
+
+ writel(val, csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 5));
+
+ /* Lane configuration for polarity @ CSIPHY-base + CTRL9 */
+ for (i = 0; i < cfg->num_data_lanes; i++) {
+ if (lane_cfg->data[i].pol) {
+ u8 pos = lane_cfg->data[i].pos;
+
+ writel(BIT(2), csi2phy->base + CSIPHY_2PH_LN_CSI_2PHASE_CTRL9n(pos));
+ }
+ }
+
+ if (lane_cfg->clk.pol)
+ writel(BIT(2), csi2phy->base + CSIPHY_2PH_LN_CSI_2PHASE_CTRL9n(lane_cfg->clk.pos));
+
+ val = CSIPHY_3PH_CMN_CSI_COMMON_CTRL6_COMMON_PWRDN_B;
+ writel(val, csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 6));
+
+ val = 0x02;
+ writel(val, csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 7));
+
+ val = 0x00;
+ writel(val, csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 0));
+
+ phy_qcom_mipi_csi2_gen2_config_lanes(csi2phy, settle_cnt);
+
+ /* IRQ_MASK registers - disable all interrupts */
+ for (i = CSI_COMMON_STATUS_NUM; i < CSI_CTRL_STATUS_INDEX; i++) {
+ writel(0, csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, i));
+ }
+
+ return 0;
+}
+
+static void
+phy_qcom_mipi_csi2_lanes_disable(struct mipi_csi2phy_device *csi2phy,
+ struct mipi_csi2phy_stream_cfg *cfg)
+{
+ const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
+
+ writel(0, csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 5));
+
+ writel(0, csi2phy->base +
+ CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 6));
+}
+
+static const struct mipi_csi2phy_hw_ops phy_qcom_mipi_csi2_ops_3ph_1_0 = {
+ .hw_version_read = phy_qcom_mipi_csi2_hw_version_read,
+ .reset = phy_qcom_mipi_csi2_reset,
+ .lanes_enable = phy_qcom_mipi_csi2_lanes_enable,
+ .lanes_disable = phy_qcom_mipi_csi2_lanes_disable,
+};
+
+static const char * const x1e_clks[] = {
+ "core",
+ "timer",
+ "ahb"
+};
+
+static const char * const x1e_supplies[] = {
+ "vdda-0p9",
+ "vdda-1p2"
+};
+
+static struct mipi_csi2_genpd x1e_genpds[] = {
+ { .name = "top", .scaled = false },
+ { .name = "mmcx", .scaled = true },
+ { .name = "mx", .scaled = true },
+};
+
+const struct mipi_csi2phy_soc_cfg mipi_csi2_dphy_4nm_x1e = {
+ .ops = &phy_qcom_mipi_csi2_ops_3ph_1_0,
+ .reg_info = {
+ .init_seq = lane_regs_x1e80100,
+ .lane_array_size = ARRAY_SIZE(lane_regs_x1e80100),
+ .common_regs_offset = 0x1000,
+ },
+ .supply_names = (const char **)x1e_supplies,
+ .num_supplies = ARRAY_SIZE(x1e_supplies),
+ .clk_names = (const char **)x1e_clks,
+ .num_clk = ARRAY_SIZE(x1e_clks),
+ .genpds = x1e_genpds,
+ .num_genpds = ARRAY_SIZE(x1e_genpds),
+};
diff --git a/drivers/phy/qualcomm/phy-qcom-mipi-csi2-core.c b/drivers/phy/qualcomm/phy-qcom-mipi-csi2-core.c
new file mode 100644
index 0000000000000..8dc8c9f4f2cf0
--- /dev/null
+++ b/drivers/phy/qualcomm/phy-qcom-mipi-csi2-core.c
@@ -0,0 +1,443 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2026, Linaro Ltd.
+ */
+
+#include <dt-bindings/phy/phy.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/io.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/pm_opp.h>
+#include <linux/phy/phy.h>
+#include <linux/phy/phy-mipi-dphy.h>
+#include <linux/platform_device.h>
+#include <linux/pm_domain.h>
+#include <linux/pm_runtime.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/reset.h>
+#include <linux/slab.h>
+
+#include "phy-qcom-mipi-csi2.h"
+
+static int
+phy_qcom_mipi_csi2_set_clock_rates(struct mipi_csi2phy_device *csi2phy,
+ s64 link_freq)
+{
+ struct device *dev = csi2phy->dev;
+ unsigned long opp_rate = link_freq / 4;
+ struct dev_pm_opp *opp;
+ long timer_rate;
+ int i, pstate;
+ int ret;
+
+ opp = dev_pm_opp_find_freq_ceil(dev, &opp_rate);
+ if (IS_ERR(opp)) {
+ dev_err(csi2phy->dev, "Couldn't find ceiling for %lld Hz\n",
+ link_freq);
+ return PTR_ERR(opp);
+ }
+
+ pstate = 0;
+ for (i = 0; i < csi2phy->pd_list->num_pds; i++) {
+ unsigned int perf;
+
+ if (!csi2phy->soc_cfg->genpds[i].scaled)
+ continue;
+
+ perf = dev_pm_opp_get_required_pstate(opp, pstate);
+ pstate += 1;
+
+ ret = dev_pm_genpd_set_performance_state(csi2phy->pd_list->pd_devs[i], perf);
+ if (ret) {
+ dev_err(csi2phy->dev, "Couldn't set perf state %u\n",
+ perf);
+ dev_pm_opp_put(opp);
+ goto unset_pstate;
+ }
+ }
+ dev_pm_opp_put(opp);
+
+ ret = dev_pm_opp_set_rate(dev, opp_rate);
+ if (ret) {
+ dev_err(csi2phy->dev, "dev_pm_opp_set_rate() fail\n");
+ goto unset_opp_rate;
+ }
+
+ timer_rate = clk_round_rate(csi2phy->timer_clk, link_freq / 4);
+ if (timer_rate <= 0) {
+ ret = -ENODEV;
+ goto unset_opp_rate;
+ }
+
+ ret = clk_set_rate(csi2phy->timer_clk, timer_rate);
+ if (ret)
+ goto unset_opp_rate;
+
+ csi2phy->timer_clk_rate = timer_rate;
+
+ return 0;
+
+unset_opp_rate:
+ dev_pm_opp_set_rate(dev, 0);
+
+unset_pstate:
+ while (i--) {
+ if (!csi2phy->soc_cfg->genpds[i].scaled)
+ continue;
+
+ dev_pm_genpd_set_performance_state(csi2phy->pd_list->pd_devs[i], 0);
+ }
+
+ return ret;
+}
+
+static int phy_qcom_mipi_csi2_configure(struct phy *phy,
+ union phy_configure_opts *opts)
+{
+ struct mipi_csi2phy_device *csi2phy = phy_get_drvdata(phy);
+ struct phy_configure_opts_mipi_dphy *dphy_cfg = &opts->mipi_dphy;
+ struct mipi_csi2phy_stream_cfg *stream_cfg = &csi2phy->stream_cfg;
+ int ret;
+
+ ret = phy_mipi_dphy_config_validate(dphy_cfg);
+ if (ret)
+ return ret;
+
+ if (dphy_cfg->lanes < 1 || dphy_cfg->lanes > CSI2_MAX_DATA_LANES)
+ return -EINVAL;
+
+ stream_cfg->link_freq = dphy_cfg->hs_clk_rate;
+
+ return 0;
+}
+
+static int phy_qcom_mipi_csi2_power_on(struct phy *phy)
+{
+ struct mipi_csi2phy_device *csi2phy = phy_get_drvdata(phy);
+ const struct mipi_csi2phy_hw_ops *ops = csi2phy->soc_cfg->ops;
+ int i, ret;
+
+ ret = regulator_bulk_enable(csi2phy->soc_cfg->num_supplies,
+ csi2phy->supplies);
+ if (ret)
+ return ret;
+
+ ret = pm_runtime_resume_and_get(csi2phy->dev);
+ if (ret < 0)
+ goto disable_regulators;
+
+ ret = phy_qcom_mipi_csi2_set_clock_rates(csi2phy, csi2phy->stream_cfg.link_freq);
+ if (ret)
+ goto poweroff_phy;
+
+ ret = clk_bulk_prepare_enable(csi2phy->soc_cfg->num_clk,
+ csi2phy->clks);
+ if (ret) {
+ dev_err(csi2phy->dev, "failed to enable clocks, %d\n", ret);
+ goto unset_rate;
+ }
+
+ ops->reset(csi2phy);
+
+ ops->hw_version_read(csi2phy);
+
+ ret = ops->lanes_enable(csi2phy, &csi2phy->stream_cfg);
+ if (ret)
+ goto unset_clocks;
+
+ return 0;
+
+unset_clocks:
+ clk_bulk_disable_unprepare(csi2phy->soc_cfg->num_clk,
+ csi2phy->clks);
+
+unset_rate:
+ dev_pm_opp_set_rate(csi2phy->dev, 0);
+
+ for (i = 0; i < csi2phy->pd_list->num_pds; i++) {
+ if (!csi2phy->soc_cfg->genpds[i].scaled)
+ continue;
+
+ dev_pm_genpd_set_performance_state(csi2phy->pd_list->pd_devs[i], 0);
+ }
+
+poweroff_phy:
+ pm_runtime_put_sync(csi2phy->dev);
+
+disable_regulators:
+ regulator_bulk_disable(csi2phy->soc_cfg->num_supplies,
+ csi2phy->supplies);
+
+ return ret;
+}
+
+static int phy_qcom_mipi_csi2_power_off(struct phy *phy)
+{
+ struct mipi_csi2phy_device *csi2phy = phy_get_drvdata(phy);
+ const struct mipi_csi2phy_hw_ops *ops = csi2phy->soc_cfg->ops;
+ int i;
+
+ ops->lanes_disable(csi2phy, &csi2phy->stream_cfg);
+
+ clk_bulk_disable_unprepare(csi2phy->soc_cfg->num_clk,
+ csi2phy->clks);
+
+ dev_pm_opp_set_rate(csi2phy->dev, 0);
+
+ for (i = 0; i < csi2phy->pd_list->num_pds; i++) {
+ if (!csi2phy->soc_cfg->genpds[i].scaled)
+ continue;
+
+ dev_pm_genpd_set_performance_state(csi2phy->pd_list->pd_devs[i], 0);
+ }
+
+ pm_runtime_put_sync(csi2phy->dev);
+
+ regulator_bulk_disable(csi2phy->soc_cfg->num_supplies,
+ csi2phy->supplies);
+
+ return 0;
+}
+
+static const struct phy_ops phy_qcom_mipi_csi2_ops = {
+ .configure = phy_qcom_mipi_csi2_configure,
+ .power_on = phy_qcom_mipi_csi2_power_on,
+ .power_off = phy_qcom_mipi_csi2_power_off,
+ .owner = THIS_MODULE,
+};
+
+static struct phy *qcom_csi2_phy_xlate(struct device *dev,
+ const struct of_phandle_args *args)
+{
+ struct mipi_csi2phy_device *csi2phy = dev_get_drvdata(dev);
+
+ if (args->args_count < 1 || args->args[0] != PHY_TYPE_DPHY) {
+ dev_err(csi2phy->dev, "invalid phy mode in DTB\n");
+ return ERR_PTR(-EOPNOTSUPP);
+ }
+
+ csi2phy->phy_mode = args->args[0];
+
+ return csi2phy->phy;
+}
+
+static int phy_qcom_mipi_csi2_attach_pm_domains(struct mipi_csi2phy_device *csi2phy)
+{
+ struct dev_pm_domain_attach_data pd_data = { 0 };
+ const char **pd_names;
+ int i;
+
+ pd_names = devm_kzalloc(csi2phy->dev,
+ sizeof(char *) * csi2phy->soc_cfg->num_genpds,
+ GFP_KERNEL);
+ if (!pd_names)
+ return -ENOMEM;
+
+ for (i = 0; i < csi2phy->soc_cfg->num_genpds; i++)
+ pd_names[i] = csi2phy->soc_cfg->genpds[i].name;
+
+ pd_data.pd_names = pd_names;
+ pd_data.num_pd_names = csi2phy->soc_cfg->num_genpds;
+
+ return devm_pm_domain_attach_list(csi2phy->dev, &pd_data,
+ &csi2phy->pd_list);
+}
+
+static int phy_qcom_mipi_csi2_parse_routing(struct mipi_csi2phy_device *csi2phy)
+{
+ struct mipi_csi2phy_stream_cfg *stream_cfg = &csi2phy->stream_cfg;
+ u32 lane_polarities[CSI2_MAX_DATA_LANES + 1];
+ u32 data_lanes[CSI2_MAX_DATA_LANES];
+ struct device *dev = csi2phy->dev;
+ struct fwnode_handle *ep;
+ int num_polarities;
+ int num_data_lanes;
+ int i, ret;
+
+ ep = fwnode_graph_get_endpoint_by_id(dev_fwnode(dev), 0, 1, 0);
+ if (ep) {
+ fwnode_handle_put(ep);
+ dev_err(dev, "DPHY split mode is not supported\n");
+ return -EOPNOTSUPP;
+ }
+
+ ep = fwnode_graph_get_endpoint_by_id(dev_fwnode(dev), 0, 0, 0);
+ if (!ep) {
+ dev_err(dev, "Missing port@0\n");
+ return -ENODEV;
+ }
+
+ num_data_lanes = fwnode_property_count_u32(ep, "data-lanes");
+ if (num_data_lanes < 1 || num_data_lanes > CSI2_MAX_DATA_LANES) {
+ ret = -EINVAL;
+ dev_err(dev, "Invalid data-lanes count: %d\n", num_data_lanes);
+ goto out_put;
+ }
+ stream_cfg->num_data_lanes = num_data_lanes;
+
+ ret = fwnode_property_read_u32_array(ep, "data-lanes", data_lanes,
+ stream_cfg->num_data_lanes);
+ if (ret) {
+ dev_err(dev, "Failed to read data-lanes: %d\n", ret);
+ goto out_put;
+ }
+
+ /* lane-polarities: optional, up to num_data_lanes + 1 entries */
+ memset(lane_polarities, 0x00, sizeof(lane_polarities));
+ num_polarities = fwnode_property_count_u32(ep, "lane-polarities");
+ if (num_polarities > 0) {
+ if (num_polarities != stream_cfg->num_data_lanes + 1) {
+ ret = -EINVAL;
+ dev_err(dev, "clock+data-lane %d/polarities %d mismatch\n",
+ stream_cfg->num_data_lanes + 1, num_polarities);
+ goto out_put;
+ }
+
+ ret = fwnode_property_read_u32_array(ep, "lane-polarities", lane_polarities,
+ num_polarities);
+ if (ret) {
+ dev_err(dev, "Failed to read lane-polarities: %d\n", ret);
+ goto out_put;
+ }
+ }
+
+ csi2phy->stream_cfg.lane_cfg.clk.pos = CSI2_DEFAULT_CLK_LANE;
+ csi2phy->stream_cfg.lane_cfg.clk.pol = lane_polarities[0];
+
+ for (i = 0; i < csi2phy->stream_cfg.num_data_lanes; i++) {
+ if (data_lanes[i] > CSI2_MAX_LANE_NUM) {
+ dev_err(dev, "Invalid lane %d\n", data_lanes[i]);
+ ret = -EINVAL;
+ goto out_put;
+ }
+ csi2phy->stream_cfg.lane_cfg.data[i].pos = data_lanes[i];
+ csi2phy->stream_cfg.lane_cfg.data[i].pol = lane_polarities[i + 1];
+ }
+
+ ret = 0;
+
+out_put:
+ fwnode_handle_put(ep);
+
+ return ret;
+}
+
+static int phy_qcom_mipi_csi2_probe(struct platform_device *pdev)
+{
+ unsigned int i, num_clk, num_supplies;
+ struct mipi_csi2phy_device *csi2phy;
+ struct phy_provider *phy_provider;
+ struct device *dev = &pdev->dev;
+ struct phy *generic_phy;
+ int ret;
+
+ csi2phy = devm_kzalloc(dev, sizeof(*csi2phy), GFP_KERNEL);
+ if (!csi2phy)
+ return -ENOMEM;
+
+ csi2phy->dev = dev;
+ dev_set_drvdata(dev, csi2phy);
+
+ csi2phy->soc_cfg = device_get_match_data(&pdev->dev);
+
+ if (!csi2phy->soc_cfg)
+ return -EINVAL;
+
+ num_clk = csi2phy->soc_cfg->num_clk;
+
+ ret = phy_qcom_mipi_csi2_parse_routing(csi2phy);
+ if (ret)
+ return ret;
+
+ ret = phy_qcom_mipi_csi2_attach_pm_domains(csi2phy);
+ if (ret < 0 || csi2phy->pd_list == NULL) {
+ if (ret == 0)
+ ret = -ENODEV;
+ return dev_err_probe(dev, ret, "Failed to attach power-domain list\n");
+ }
+ devm_pm_runtime_enable(dev);
+
+ ret = devm_clk_bulk_get_all(dev, &csi2phy->clks);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "Failed to get clocks\n");
+
+ if (num_clk != ret) {
+ return dev_err_probe(dev, -ENODEV, "clock count %d expected %d\n",
+ ret, num_clk);
+ }
+
+ for (i = 0; i < num_clk; i++) {
+ if (!strcmp(csi2phy->clks[i].id, "timer")) {
+ csi2phy->timer_clk = csi2phy->clks[i].clk;
+ break;
+ }
+ }
+ if (!csi2phy->timer_clk)
+ return dev_err_probe(dev, -ENODEV, "no timer clock\n");
+
+ ret = devm_pm_opp_set_clkname(dev, "core");
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to set opp clkname\n");
+
+ ret = devm_pm_opp_of_add_table(dev);
+ if (ret)
+ return dev_err_probe(dev, ret, "invalid OPP table in device tree\n");
+
+ num_supplies = csi2phy->soc_cfg->num_supplies;
+ csi2phy->supplies = devm_kzalloc(dev, sizeof(*csi2phy->supplies) * num_supplies,
+ GFP_KERNEL);
+ if (!csi2phy->supplies)
+ return -ENOMEM;
+
+ for (i = 0; i < num_supplies; i++)
+ csi2phy->supplies[i].supply = csi2phy->soc_cfg->supply_names[i];
+
+ ret = devm_regulator_bulk_get(dev, num_supplies, csi2phy->supplies);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to get regulator supplies\n");
+
+ csi2phy->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(csi2phy->base))
+ return PTR_ERR(csi2phy->base);
+
+ generic_phy = devm_phy_create(dev, NULL, &phy_qcom_mipi_csi2_ops);
+ if (IS_ERR(generic_phy)) {
+ ret = PTR_ERR(generic_phy);
+ return dev_err_probe(dev, ret, "failed to create phy\n");
+ }
+ csi2phy->phy = generic_phy;
+
+ phy_set_drvdata(generic_phy, csi2phy);
+
+ phy_provider = devm_of_phy_provider_register(dev, qcom_csi2_phy_xlate);
+ if (!IS_ERR(phy_provider))
+ dev_dbg(dev, "Registered MIPI CSI2 PHY device\n");
+
+ return PTR_ERR_OR_ZERO(phy_provider);
+}
+
+static const struct of_device_id phy_qcom_mipi_csi2_of_match_table[] = {
+ { .compatible = "qcom,x1e80100-csi2-phy", .data = &mipi_csi2_dphy_4nm_x1e },
+ { }
+};
+MODULE_DEVICE_TABLE(of, phy_qcom_mipi_csi2_of_match_table);
+
+static struct platform_driver phy_qcom_mipi_csi2_driver = {
+ .probe = phy_qcom_mipi_csi2_probe,
+ .driver = {
+ .name = "qcom-mipi-csi2-phy",
+ .of_match_table = phy_qcom_mipi_csi2_of_match_table,
+ },
+};
+
+module_platform_driver(phy_qcom_mipi_csi2_driver);
+
+MODULE_DESCRIPTION("Qualcomm MIPI CSI2 PHY driver");
+MODULE_AUTHOR("Bryan O'Donoghue <bod@kernel.org>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/phy/qualcomm/phy-qcom-mipi-csi2.h b/drivers/phy/qualcomm/phy-qcom-mipi-csi2.h
new file mode 100644
index 0000000000000..17b8eb505b7bd
--- /dev/null
+++ b/drivers/phy/qualcomm/phy-qcom-mipi-csi2.h
@@ -0,0 +1,98 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ *
+ * Qualcomm MIPI CSI2 CPHY/DPHY driver
+ *
+ * Copyright (C) 2025 Linaro Ltd.
+ */
+#ifndef __PHY_QCOM_MIPI_CSI2_H__
+#define __PHY_QCOM_MIPI_CSI2_H__
+
+#include <linux/phy/phy.h>
+
+#define CSI2_MAX_DATA_LANES 4
+#define CSI2_DEFAULT_CLK_LANE 7
+#define CSI2_MAX_LANE_NUM 7
+
+struct mipi_csi2phy_lane {
+ u8 pos;
+ u8 pol;
+};
+
+struct mipi_csi2phy_lanes_cfg {
+ struct mipi_csi2phy_lane data[CSI2_MAX_DATA_LANES];
+ struct mipi_csi2phy_lane clk;
+};
+
+struct mipi_csi2phy_stream_cfg {
+ s64 link_freq;
+ u8 num_data_lanes;
+ struct mipi_csi2phy_lanes_cfg lane_cfg;
+};
+
+struct mipi_csi2phy_device;
+
+struct mipi_csi2phy_hw_ops {
+ void (*hw_version_read)(struct mipi_csi2phy_device *csi2phy_dev);
+ void (*reset)(struct mipi_csi2phy_device *csi2phy_dev);
+ int (*lanes_enable)(struct mipi_csi2phy_device *csi2phy_dev,
+ struct mipi_csi2phy_stream_cfg *cfg);
+ void (*lanes_disable)(struct mipi_csi2phy_device *csi2phy_dev,
+ struct mipi_csi2phy_stream_cfg *cfg);
+};
+
+struct mipi_csi2phy_lane_regs {
+ const s32 reg_addr;
+ const s32 reg_data;
+ const u32 delay_us;
+ const u32 param_type;
+};
+
+struct mipi_csi2phy_device_regs {
+ const struct mipi_csi2phy_lane_regs *init_seq;
+ const int lane_array_size;
+ const u32 common_regs_offset;
+};
+
+struct mipi_csi2_genpd {
+ const char *name;
+ bool scaled;
+};
+
+struct mipi_csi2phy_soc_cfg {
+ const struct mipi_csi2phy_hw_ops *ops;
+ const struct mipi_csi2phy_device_regs reg_info;
+
+ const char ** const supply_names;
+ const unsigned int num_supplies;
+
+ const char ** const clk_names;
+ const unsigned int num_clk;
+
+ const struct mipi_csi2_genpd *genpds;
+ const unsigned int num_genpds;
+};
+
+struct mipi_csi2phy_device {
+ struct device *dev;
+ u8 phy_mode;
+
+ struct phy *phy;
+ void __iomem *base;
+
+ struct clk_bulk_data *clks;
+ struct clk *timer_clk;
+ u32 timer_clk_rate;
+
+ struct regulator_bulk_data *supplies;
+ struct dev_pm_domain_list *pd_list;
+
+ const struct mipi_csi2phy_soc_cfg *soc_cfg;
+ struct mipi_csi2phy_stream_cfg stream_cfg;
+
+ u32 hw_version;
+};
+
+extern const struct mipi_csi2phy_soc_cfg mipi_csi2_dphy_4nm_x1e;
+
+#endif /* __PHY_QCOM_MIPI_CSI2_H__ */
--
2.54.0
^ permalink raw reply related
* Re: [PATCH v10 2/2] phy: qcom-mipi-csi2: Add a CSI2 MIPI DPHY driver
From: sashiko-bot @ 2026-07-18 2:51 UTC (permalink / raw)
To: Bryan O'Donoghue
Cc: neil.armstrong, linux-phy, olteanv, devicetree, robh, conor+dt,
media-ci, vkoul
In-Reply-To: <20260718-x1e-csi2-phy-v10-2-5720a7888953@linaro.org>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Incorrect translation of logical to physical lanes causes register offset errors and bitwise truncation.
- [High] Integer truncation in division leads to potential division-by-zero panic in settle count calculation.
- [Low] Hardware version register read does not mask upper bits, potentially corrupting the version struct.
--
--- Patch [2]: [PATCH v10 2/2] phy: qcom-mipi-csi2: Add a CSI2 MIPI DPHY driver ---
commit 3a4ef513bbeefe5185b0f08d343f98883cbe66ca
Author: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
phy: qcom-mipi-csi2: Add a CSI2 MIPI DPHY driver
This commit adds a new MIPI CSI2 driver in DPHY mode initially. It supports
devices like "qcom,x1e80100-csi2-phy" and imports CAMSS CSI PHY init sequences.
> diff --git a/drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c b/drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c
> index 0000000000000..5ac0d041b36b7
> --- /dev/null
> +++ b/drivers/phy/qualcomm/phy-qcom-mipi-csi2-3ph-dphy.c
[ ... ]
> +static void phy_qcom_mipi_csi2_hw_version_read(struct mipi_csi2phy_device *csi2phy)
> +{
> + const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
> + u32 tmp;
> +
> + writel(CSIPHY_3PH_CMN_CSI_COMMON_CTRL6_SHOW_REV_ID, csi2phy->base +
> + CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 6));
> +
> + tmp = readl_relaxed(csi2phy->base +
> + CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(regs->common_regs_offset, 12));
> + csi2phy->hw_version = tmp;
[Severity: Low]
Is it possible for the hardware register to return garbage or reserved bits in
the upper bytes here? Since this first read is assigned directly without
masking (e.g., tmp & 0xFF), any upper garbage bits would remain in hw_version
and wouldn't be cleared by the subsequent bitwise OR operations.
> +
> + tmp = readl_relaxed(csi2phy->base +
> + CSIPHY_3PH_CMN_CSI_COMMON_STATUSn(regs->common_regs_offset, 13));
> + csi2phy->hw_version |= (tmp << 8) & 0xFF00;
[ ... ]
> +static u8 phy_qcom_mipi_csi2_settle_cnt_calc(s64 link_freq, u32 timer_clk_rate)
> +{
> + u32 t_hs_prepare_max_ps;
> + u32 timer_period_ps;
> + u32 t_hs_settle_ps;
> + u8 settle_cnt;
> + u32 ui_ps;
> +
> + if (link_freq <= 0)
> + return 0;
> +
> + ui_ps = div_u64(PSEC_PER_SEC, link_freq);
[Severity: High]
Does this code silently truncate link_freq?
The div_u64() macro expects a u32 divisor, but link_freq is an s64. This will
implicitly truncate the 64-bit link_freq value to 32 bits.
If link_freq happens to be an exact multiple of 2^32, the truncated divisor
becomes exactly 0, which would cause a division-by-zero panic. Frequencies
exceeding 4.29 GHz would also be silently corrupted.
Could this be updated to use div64_u64() or similar to avoid truncation?
> + ui_ps /= 2;
> + t_hs_prepare_max_ps = 85000 + 6 * ui_ps;
[ ... ]
> +static int phy_qcom_mipi_csi2_lanes_enable(struct mipi_csi2phy_device *csi2phy,
> + struct mipi_csi2phy_stream_cfg *cfg)
> +{
> + const struct mipi_csi2phy_device_regs *regs = csi2phy_dev_to_regs(csi2phy);
> + struct mipi_csi2phy_lanes_cfg *lane_cfg = &cfg->lane_cfg;
> + u8 settle_cnt;
> + u8 val;
> + int i;
> +
> + settle_cnt = phy_qcom_mipi_csi2_settle_cnt_calc(cfg->link_freq, csi2phy->timer_clk_rate);
> + if (!settle_cnt)
> + return -EINVAL;
> +
> + /*
> + * CSI_COMMON_CTRL5 is a physical lane power-up bitmap:
> + * - Bits [0,2,4,6] → D-PHY data lanes(LN0, LN2, LN4, LN6)
> + * - Bits [1,3,5] → C-PHY trio lanes(LN1, LN3, LN5)
> + * - Bit [7] → D-PHY clock lane(LNCK) dedicated clock enable
> + */
> + val = BIT(lane_cfg->clk.pos);
> + for (i = 0; i < cfg->num_data_lanes; i++)
> + val |= BIT(lane_cfg->data[i].pos * 2);
[Severity: High]
Can this bitmask overflow the u8 val?
If the device tree uses physical indices (e.g., 4 or 6) for pos, pos * 2
becomes 8 or 12. Since val is a u8, using BIT(8) or BIT(12) will overflow
and silently truncate to 0, failing to enable the corresponding hardware lane.
> +
> + writel(val, csi2phy->base +
> + CSIPHY_3PH_CMN_CSI_COMMON_CTRLn(regs->common_regs_offset, 5));
> +
> + /* Lane configuration for polarity @ CSIPHY-base + CTRL9 */
> + for (i = 0; i < cfg->num_data_lanes; i++) {
> + if (lane_cfg->data[i].pol) {
> + u8 pos = lane_cfg->data[i].pos;
> +
> + writel(BIT(2), csi2phy->base + CSIPHY_2PH_LN_CSI_2PHASE_CTRL9n(pos));
[Severity: High]
Does this correctly map logical to physical lanes?
The CSIPHY_2PH_LN_CSI_2PHASE_CTRL9n(pos) macro computes 0x200 * pos + 0x24.
However, D-PHY data lane blocks are spaced at offsets 0, 0x400, 0x800, and
0xC00.
If pos is a logical index like 1, this would access offset 0x200 (which is a
C-PHY lane) instead of the intended 0x400.
> + }
> + }
> +
> + if (lane_cfg->clk.pol)
> + writel(BIT(2), csi2phy->base + CSIPHY_2PH_LN_CSI_2PHASE_CTRL9n(lane_cfg->clk.pos));
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260718-x1e-csi2-phy-v10-0-5720a7888953@linaro.org?part=2
^ permalink raw reply
* RE: [PATCH v28 3/7] firmware: imx: add driver for NXP EdgeLock Enclave
From: Pankaj Gupta (OSS) @ 2026-07-18 3:04 UTC (permalink / raw)
To: Frank Li (OSS), sashiko-reviews@lists.linux.dev
Cc: Pankaj Gupta (OSS), Frank.Li@kernel.org, imx@lists.linux.dev,
devicetree@vger.kernel.org, conor+dt@kernel.org, robh@kernel.org
In-Reply-To: <alpUTTH1VA9hvQva@SMW015318>
> On Fri, Jul 17, 2026 at 11:10:09AM +0000, sashiko-bot@kernel.org wrote:
> > Thank you for your contribution! Sashiko AI review found 6 potential issue(s)
> to consider:
> > - [High] The mailbox receive callback `se_if_rx_callback` dereferences `msg`
> without checking `IS_ERR()`, leading to a kernel panic if the mailbox controller
> returns an error.
> > - [High] `ele_msg_rcv()` uses `MAX_SCHEDULE_TIMEOUT` causing an infinite
> uninterruptible wait and subsystem deadlock if the firmware hangs.
> > - [Medium] Global state `var_se_info.soc_rev` prevents SoC device
> registration and per-device IMEM state initialization on driver rebind or
> secondary device instances.
> > - [Medium] Casting a 1-byte aligned stack array to `struct ele_dev_info` and
> `u32` pointers causes undefined behavior and potential unaligned memory
> access faults.
> > - [Medium] The driver fails to expose the file descriptor interfaces claimed in
> the commit message and Kconfig.
> > - [Low] The SoC revision is incorrectly formatted as minor.major instead of
> major.minor.
> > --
> >
> > --- Patch [3]: [PATCH v28 3/7] firmware: imx: add driver for NXP
> > EdgeLock Enclave --- commit a06a0c9a5464457c1ae77bb60350c41fff331ed9
> > Author: Pankaj Gupta <pankaj.gupta@nxp.com>
> > Subject: firmware: imx: add driver for NXP EdgeLock Enclave
> >
> > Add MU-based communication interface for secure enclave.
> > The secure enclave FW communicates with Linux over single or multiple
> > dedicated messaging unit(MU) based interface(s).
> >
> > Link:
> > https://sashiko.dev/#/patchset/20260715-imx-se-if-v27-0-bb7c45952f06@n
> > xp.com?part=3
> >
> > > diff --git a/drivers/firmware/imx/Kconfig
> > > b/drivers/firmware/imx/Kconfig index 127ad752acf86..e3cb7f965e70e
> > > 100644
> > > --- a/drivers/firmware/imx/Kconfig
> > > +++ b/drivers/firmware/imx/Kconfig
> >
> > [ ... ]
> >
> > > @@ -55,3 +55,16 @@ config IMX_SCMI_MISC_DRV
> > > core that could provide misc functions such as board control.
> > >
> > > This driver can also be built as a module.
> > > +
> > > +config IMX_SEC_ENCLAVE
> > > + tristate "i.MX Embedded Secure Enclave - EdgeLock Enclave Firmware
> driver."
> > > + depends on MAILBOX && ((IMX_MBOX && ARCH_MXC && ARM64) ||
> COMPILE_TEST)
> > > + select FW_LOADER
> > > + default m if ARCH_MXC
> > > +
> > > + help
> > > + Exposes APIs supported by the iMX Secure Enclave HW IP called:
> > > + - EdgeLock Enclave Firmware (for i.MX8ULP, i.MX93),
> > > + like base, HSM, V2X & SHE using the SAB protocol via the shared
> Messaging
> > > + Unit. This driver exposes these interfaces via a set of file descriptors
> > > + allowing to configure shared memory, send and receive messages.
> >
> > [Severity: Medium]
> > Is this Kconfig description accurate? It mentions exposing interfaces
> > via a set of file descriptors allowing to configure shared memory,
> > send and receive messages. However, there is no character device
> > registration or file operations implemented in this patch.
>
> please check this
Patch 3/7 only adds the base firmware communication path. The
misc-device file descriptor interface is added later in patch 5/7.
Will remove the file-descriptor wording from the Kconfig help in patch 3/7.
And add it with the miscdev patch 5/7.
>
> >
> > > diff --git a/drivers/firmware/imx/ele_base_msg.h
> > > b/drivers/firmware/imx/ele_base_msg.h
> > > new file mode 100644
> > > index 0000000000000..4c3699543e879
> > > --- /dev/null
> > > +++ b/drivers/firmware/imx/ele_base_msg.h
> >
> > [ ... ]
> >
> > > @@ -0,0 +1,98 @@
> > > +/* SPDX-License-Identifier: GPL-2.0+ */
> > > +/*
> > > + * Copyright 2025 NXP
> > > + *
> > > + * Header file for the EdgeLock Enclave Base API(s).
> > > + */
> >
> > [ ... ]
> >
> > > +#define GET_SERIAL_NUM_FROM_UID(x, uid_word_sz) ({\
> > > + const u32 *__x = (const u32 *)(x); \
> > > + size_t __sz = (uid_word_sz); \
> > > + ((u64)__x[__sz - 1] << 32) | __x[0]; \
> > > + })
> >
> > [Severity: Medium]
> > Can this macro cause undefined behavior? When called from
> > get_se_soc_info(), the x parameter points into a 1-byte aligned stack
> > array (u8 data). Casting this to a u32 pointer and dereferencing it
> > could trigger unaligned memory access faults on architectures with strict
> alignment requirements.
> >
> > > diff --git a/drivers/firmware/imx/ele_common.c
> > > b/drivers/firmware/imx/ele_common.c
> > > new file mode 100644
> > > index 0000000000000..ca8a32a82689b
> > > --- /dev/null
> > > +++ b/drivers/firmware/imx/ele_common.c
> >
> > [ ... ]
> >
> > > @@ -0,0 +1,470 @@
> > > +// SPDX-License-Identifier: GPL-2.0+
> > > +/*
> > > + * Copyright 2025 NXP
> > > + */
> >
> > [ ... ]
> >
> > > +int ele_msg_rcv(struct se_if_priv *priv, struct se_clbk_handle
> > > +*se_clbk_hdl) {
> > > + bool wait_uninterruptible = false;
> > > + unsigned long remaining_jiffies;
> > > + unsigned long flags;
> > > + int ret;
> > > +
> > > + remaining_jiffies = MAX_SCHEDULE_TIMEOUT;
> > > + do {
> > > + if (wait_uninterruptible)
> > > + ret = wait_for_completion_timeout(&se_clbk_hdl-
> >done,
> > > + remaining_jiffies);
> >
> > [Severity: High]
> > Can this loop cause a subsystem deadlock? Since remaining_jiffies is
> > set to MAX_SCHEDULE_TIMEOUT, wait_for_completion_timeout() blocks
> > indefinitely. If the firmware hangs and wait_uninterruptible is set to
> > true after a signal, the thread will enter an infinite uninterruptible
> > sleep while holding the se_if_cmd_lock mutex. The timeout fallback and
> > circuit breaker logic become unreachable.
>
> Can you use reasonable timeout value to avoid deadlock?
Accepted. Will fix in v29. Though it will get corrected in patch 5/7
>
> >
> > > + else
> > > + ret =
> wait_for_completion_interruptible_timeout(&se_clbk_hdl->done,
> > > +
> remaining_jiffies);
> >
> > [ ... ]
> >
> > > +void se_if_rx_callback(struct mbox_client *mbox_cl, void *msg) {
> > > + struct se_clbk_handle *se_clbk_hdl;
> > > + struct device *dev = mbox_cl->dev;
> > > + struct se_msg_hdr *header;
> > > + bool sz_mismatch = false;
> > > + struct se_if_priv *priv;
> > > + unsigned long flags;
> > > + u32 rx_msg_sz;
> > > +
> > > + priv = dev_get_drvdata(dev);
> > > +
> > > + /* The function can be called with NULL msg */
> > > + if (!msg) {
> > > + dev_err(dev, "Message is invalid\n");
> > > + return;
> > > + }
> > > +
> > > + header = msg;
> > > + rx_msg_sz = header->size << 2;
> >
> > [Severity: High]
> > Does this code safely handle mailbox errors? If the mailbox controller
> > returns an error (like a timeout), it typically passes ERR_PTR(err) to
> > the callback. The check for !msg allows ERR_PTR to bypass the check,
> > which would lead to a kernel panic when msg is cast to struct
> > se_msg_hdr and dereferenced here.
>
> Please double check this ? if yes, should use IS_ERR()
Accepted will fix in v29.
>
> >
> > > diff --git a/drivers/firmware/imx/se_ctrl.c
> > > b/drivers/firmware/imx/se_ctrl.c new file mode 100644 index
> > > 0000000000000..9bb59c200911b
> > > --- /dev/null
> > > +++ b/drivers/firmware/imx/se_ctrl.c
> >
> > [ ... ]
> >
> > > @@ -0,0 +1,417 @@
> > > +// SPDX-License-Identifier: GPL-2.0+
> > > +/*
> > > + * Copyright 2026 NXP
> > > + */
> >
> > [ ... ]
> >
> > > +static int get_se_soc_info(struct se_if_priv *priv, const struct
> > > +se_soc_info *se_info) {
> > > + struct se_fw_load_info *load_fw = get_load_fw_instance(priv);
> > > + struct soc_device_attribute *attr;
> > > + u8 data[MAX_SOC_INFO_DATA_SZ];
> > > + struct ele_dev_info *s_info;
> > > + struct soc_device *sdev;
> > > + int err = 0;
> > > +
> > > + /*
> > > + * This function should be called once.
> > > + * Check if the se_soc_rev is zero to continue.
> > > + */
> > > + guard(mutex)(&var_se_info.se_var_info_lock);
> > > + if (var_se_info.soc_rev)
> > > + return err;
> >
> > [Severity: Medium]
> > Will this early return prevent initialization on driver rebind? When
> > the driver is unbound, soc_device_unregister() removes the SoC device,
> > but the global var_se_info.soc_rev is never cleared. On a subsequent
> > bind, a non-zero soc_rev causes an early return, bypassing
> > soc_device_register() and leaving load_fw->imem.state uninitialized.
> >
> > > +
> > > + err = ele_fetch_soc_info(priv, &data);
> >
> > [ ... ]
> >
> > > + if (FIELD_GET(DEV_GETINFO_MIN_VER_MASK, var_se_info.soc_rev))
> > > + attr->revision = devm_kasprintf(priv->dev, GFP_KERNEL,
> "%x.%x",
> > > +
> FIELD_GET(DEV_GETINFO_MIN_VER_MASK,
> > > + var_se_info.soc_rev),
> > > +
> FIELD_GET(DEV_GETINFO_MAJ_VER_MASK,
> > > +
> var_se_info.soc_rev));
> >
> > [Severity: Low]
> > Is this string format correct? It prints MIN_VER_MASK before
> > MAJ_VER_MASK, resulting in minor.major instead of major.minor. This
> > might break userspace parsing expectations.
>
> Make sense, please fix it.
>
> Frank
[Accepted] Will correct the order. Will fix this in v29
>
> >
> > --
> > Sashiko AI review *
> > https://sashiko.dev/#/patchset/20260717-imx-se-if-v28-0-0a9659c7e69d@n
> > xp.com?part=3
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox