* [PATCH v2 0/2] ASoC: codecs: add Texas Instruments TAS2557 smart amplifier driver
@ 2026-07-11 14:57 Gianluca Boiano
2026-07-11 14:57 ` [PATCH v2 1/2] dt-bindings: sound: add Texas Instruments TAS2557 Gianluca Boiano
2026-07-11 14:57 ` [PATCH v2 2/2] ASoC: codecs: add TAS2557 mono smart amplifier driver Gianluca Boiano
0 siblings, 2 replies; 4+ messages in thread
From: Gianluca Boiano @ 2026-07-11 14:57 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: mono and dual-device stereo operation, per-device ti,channel
tuning, and fault-interrupt recovery that reloads the firmware
configuration on clock loss, over-current, brownout or
over-temperature.
Tested 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
configuration on Xiaomi Mi Pad 4 (SDM660, clover).
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
- corrected a comment implying the ASI2/GPIO5-8 wiring in
tas2557_startup_data comes from a TI reference sequence; it does
not (TI's own driver never touches ASI2)
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 | 127 +
MAINTAINERS | 1 +
sound/soc/codecs/Kconfig | 11 +
sound/soc/codecs/Makefile | 2 +
sound/soc/codecs/tas2557.c | 2648 +++++++++++++++++
sound/soc/codecs/tas2557.h | 218 ++
6 files changed, 3007 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: dc59e4fea9d83f03bad6bddf3fa2e52491777482
--
2.55.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v2 1/2] dt-bindings: sound: add Texas Instruments TAS2557
2026-07-11 14:57 [PATCH v2 0/2] ASoC: codecs: add Texas Instruments TAS2557 smart amplifier driver Gianluca Boiano
@ 2026-07-11 14:57 ` Gianluca Boiano
2026-07-11 14:57 ` [PATCH v2 2/2] ASoC: codecs: add TAS2557 mono smart amplifier driver Gianluca Boiano
1 sibling, 0 replies; 4+ messages in thread
From: Gianluca Boiano @ 2026-07-11 14:57 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
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). It can drive a single mono speaker
or, as two devices, a stereo pair; in that case the ti,channel
property selects the audio slot each device reproduces and the
per-device tuning it applies.
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.
Signed-off-by: Gianluca Boiano <morf3089@gmail.com>
---
v2: add the four mandatory supplies to the "required" list (Wang Sen)
.../devicetree/bindings/sound/ti,tas2557.yaml | 127 ++++++++++++++++++
MAINTAINERS | 1 +
2 files changed, 128 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..862ea4036cb8
--- /dev/null
+++ b/Documentation/devicetree/bindings/sound/ti,tas2557.yaml
@@ -0,0 +1,127 @@
+# 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.
+
+ Stereo playback is built from two devices, each reproducing one audio
+ slot selected through the ti,channel property and applying the
+ matching per-device tuning contained in a stereo-tuned firmware
+ image.
+
+ Datasheet: https://www.ti.com/lit/gpn/tas2557
+
+allOf:
+ - $ref: dai-common.yaml#
+
+properties:
+ compatible:
+ const: ti,tas2557
+
+ reg:
+ maxItems: 1
+
+ '#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-no:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ description: TDM TX time slot for current-sense data.
+ default: 0
+ minimum: 0
+ maximum: 7
+
+ ti,vmon-slot-no:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ description: TDM TX time slot for voltage-sense data.
+ default: 2
+ minimum: 0
+ maximum: 7
+
+ ti,channel:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ description: |
+ Audio slot of the stereo stream reproduced by this device (0 =
+ left, 1 = right). With stereo firmware this also selects which
+ per-device tuning set is applied.
+ enum: [0, 1]
+ default: 0
+
+ firmware-name:
+ $ref: /schemas/types.yaml#/definitions/string
+ description: |
+ Name of the DSP firmware image to load. Defaults to a name
+ derived from the silicon revision. Stereo systems typically use
+ a stereo-tuned image shared by both devices.
+
+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-no = <0>;
+ ti,vmon-slot-no = <2>;
+ ti,channel = <0>;
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 15011f5752a9..8a824da3979e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -26677,6 +26677,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 [flat|nested] 4+ messages in thread
* [PATCH v2 2/2] ASoC: codecs: add TAS2557 mono smart amplifier driver
2026-07-11 14:57 [PATCH v2 0/2] ASoC: codecs: add Texas Instruments TAS2557 smart amplifier driver Gianluca Boiano
2026-07-11 14:57 ` [PATCH v2 1/2] dt-bindings: sound: add Texas Instruments TAS2557 Gianluca Boiano
@ 2026-07-11 14:57 ` Gianluca Boiano
2026-07-11 15:10 ` sashiko-bot
1 sibling, 1 reply; 4+ messages in thread
From: Gianluca Boiano @ 2026-07-11 14:57 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. 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.
Amplifier power is managed through DAPM. A fault interrupt handler
recovers the device 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.
Stereo is supported as two cooperating instances using stereo
(device 3) firmware, with per-instance DEV_A/DEV_B tuning selected by
the ti,channel property.
Signed-off-by: Gianluca Boiano <morf3089@gmail.com>
---
v2:
- propagate register-write failures out of hw_params() instead of
only logging them (Wang Sen)
- return IRQ_NONE instead of IRQ_HANDLED when the device isn't in a
state to have raised the interrupt (Mark Brown)
- re-apply cached runtime state (DAI format, sense enables, DAC gain,
ASI slot offsets) after every reset, including fault recovery,
instead of a hardcoded sequence silently overriding set_dai_fmt()
and the ISENSE/VSENSE mixer controls (Mark Brown)
- honour explicit snd_soc_dai_set_tdm_slot() configuration in
hw_params, 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 doesn't mute a live capture
- corrected a comment implying ASI2/GPIO5-8 wiring in
tas2557_startup_data comes from a TI reference sequence
sound/soc/codecs/Kconfig | 11 +
sound/soc/codecs/Makefile | 2 +
sound/soc/codecs/tas2557.c | 2648 ++++++++++++++++++++++++++++++++++++
sound/soc/codecs/tas2557.h | 218 +++
4 files changed, 2879 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..f32320351561
--- /dev/null
+++ b/sound/soc/codecs/tas2557.c
@@ -0,0 +1,2648 @@
+// 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.
+
+#include <linux/module.h>
+#include <linux/i2c.h>
+#include <linux/gpio/consumer.h>
+#include <linux/regmap.h>
+#include <linux/of.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_priv - per-device driver state */
+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;
+
+ /* Book/page tracking for book-paged regmap */
+ unsigned char current_book;
+ unsigned char current_page;
+
+ /* Hardware revision */
+ int pg_id;
+
+ /* DT-configured audio parameters */
+ unsigned int channel; /* 0=left, 1=right from ti,channel */
+ unsigned int imon_slot; /* from ti,imon-slot-no */
+ unsigned int vmon_slot; /* from ti,vmon-slot-no */
+ 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 */
+ 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 */
+ unsigned int current_program;
+ unsigned int current_config;
+
+ /* Runtime audio configuration */
+ unsigned int sample_rate;
+ int dai_fmt; /* ASI format from set_dai_fmt(), -1 = never set */
+
+ /*
+ * Cached ASI data-slot offsets in BCLK cycles. 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 */
+ int tdm_slot_width;
+ bool tdm_configured; /* true once set_tdm_slot() set an RX slot */
+
+ /* Health tracking */
+ unsigned int restart_count;
+
+ /* 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_DAC_FORMAT_REG, 0x18, /* ASI2: 32-bit I2S */
+ 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
+ */
+static int tas2557_change_book_page(struct tas2557_priv *tas2557,
+ unsigned char book, unsigned char page)
+{
+ int ret = 0;
+
+ if (tas2557->current_book == book && tas2557->current_page == page)
+ return 0;
+
+ if (tas2557->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, "page-0 switch failed: %d\n", ret);
+ return ret;
+ }
+ tas2557->current_page = 0;
+
+ ret = regmap_write(tas2557->regmap, TAS2557_BOOK_REG, book);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "book switch to %u failed: %d\n",
+ book, ret);
+ return ret;
+ }
+ tas2557->current_book = book;
+ }
+
+ if (tas2557->current_page != page) {
+ ret = regmap_write(tas2557->regmap, TAS2557_PAGE_REG, page);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "page switch to %u failed: %d\n",
+ page, ret);
+ return ret;
+ }
+ tas2557->current_page = page;
+ }
+
+ return 0;
+}
+
+static int tas2557_dev_read(struct tas2557_priv *tas2557,
+ unsigned int reg, unsigned int *value)
+{
+ int ret;
+
+ mutex_lock(&tas2557->dev_lock);
+ ret = tas2557_change_book_page(tas2557, 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, "read reg 0x%06x failed: %d\n", reg, ret);
+out:
+ mutex_unlock(&tas2557->dev_lock);
+ return ret;
+}
+
+static int tas2557_dev_write(struct tas2557_priv *tas2557,
+ unsigned int reg, unsigned int value)
+{
+ int ret;
+
+ mutex_lock(&tas2557->dev_lock);
+ ret = tas2557_change_book_page(tas2557, 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, "write reg 0x%06x = 0x%02x failed: %d\n",
+ reg, value, ret);
+out:
+ mutex_unlock(&tas2557->dev_lock);
+ return ret;
+}
+
+static int tas2557_dev_update_bits(struct tas2557_priv *tas2557,
+ unsigned int reg, unsigned int mask,
+ unsigned int value)
+{
+ int ret;
+
+ mutex_lock(&tas2557->dev_lock);
+ ret = tas2557_change_book_page(tas2557, 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, "update_bits reg 0x%06x failed: %d\n",
+ reg, ret);
+out:
+ mutex_unlock(&tas2557->dev_lock);
+ return ret;
+}
+
+static int tas2557_dev_bulk_write(struct tas2557_priv *tas2557,
+ 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, 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, "bulk_write reg 0x%06x failed: %d\n",
+ reg, ret);
+out:
+ mutex_unlock(&tas2557->dev_lock);
+ return ret;
+}
+
+/*
+ * tas2557_load_data - walk a null-terminated {reg,val} sequence table
+ *
+ * Special reg values TAS2557_UDELAY and TAS2557_MDELAY insert delays;
+ * 0xFFFFFFFF terminates.
+ */
+static int tas2557_load_data(struct tas2557_priv *tas2557,
+ 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, reg, val);
+ if (ret < 0)
+ break;
+ }
+ i++;
+ }
+
+ return ret;
+}
+
+/* =========================================================================
+ * 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)
+{
+ const u8 *data = block->data;
+ unsigned int i;
+ u8 book, page, offset, value;
+ u16 sleep_time, bulk_len;
+ int ret;
+
+ dev_dbg(tas2557->dev, "load block type=0x%x cmds=%u\n",
+ 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,
+ 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,
+ TAS2557_REG(book, page, offset),
+ &data[i * 4 + 3], bulk_len);
+ else
+ ret = tas2557_dev_write(tas2557,
+ 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_wanted - should a firmware block be applied to this instance?
+ *
+ * Stereo (device == 3) firmware carries tuning for two amplifiers: DEV_A
+ * blocks for the left channel and DEV_B blocks for the right. An instance
+ * applies the device-independent blocks (PGM_ALL, CFG_POST*) plus the blocks
+ * for its own channel. Mono (device == 2) firmware has no DEV_B blocks, so
+ * @want_dev_b is always false and the DEV_A blocks are applied. Any other
+ * (unknown) block type is rejected rather than applied to both devices.
+ */
+static bool tas2557_block_wanted(unsigned int type, bool want_dev_b)
+{
+ switch (type) {
+ case TAS2557_BLOCK_PGM_DEV_A:
+ case TAS2557_BLOCK_CFG_COEFF_DEV_A:
+ case TAS2557_BLOCK_CFG_PRE_DEV_A:
+ return !want_dev_b;
+ case TAS2557_BLOCK_PGM_DEV_B:
+ case TAS2557_BLOCK_CFG_COEFF_DEV_B:
+ case TAS2557_BLOCK_CFG_PRE_DEV_B:
+ return want_dev_b;
+ case TAS2557_BLOCK_PGM_ALL:
+ case TAS2557_BLOCK_CFG_POST:
+ case TAS2557_BLOCK_CFG_POST_POWER:
+ /* device-independent blocks: apply to either instance */
+ return true;
+ default:
+ /* unknown type in program data: do not apply blindly */
+ return false;
+ }
+}
+
+/*
+ * tas2557_load_fw_data - apply the blocks of a data set for this channel
+ * @want_dev_b: apply DEV_B blocks (right channel of stereo firmware) rather
+ * than DEV_A; device-independent blocks are applied either way
+ */
+static int tas2557_load_fw_data(struct tas2557_priv *tas2557,
+ struct tas2557_data *img_data,
+ bool want_dev_b)
+{
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < img_data->num_blocks; i++) {
+ if (!tas2557_block_wanted(img_data->blocks[i].type, want_dev_b))
+ continue;
+
+ ret = tas2557_load_block(tas2557, &img_data->blocks[i]);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+/*
+ * tas2557_load_fw_data_type - apply only the blocks of a single type.
+ */
+static int tas2557_load_fw_data_type(struct tas2557_priv *tas2557,
+ struct tas2557_data *img_data,
+ unsigned int type)
+{
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < img_data->num_blocks; i++) {
+ if (img_data->blocks[i].type != type)
+ continue;
+
+ ret = tas2557_load_block(tas2557, &img_data->blocks[i]);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+/*
+ * tas2557_load_config_data - apply a configuration's pre-power blocks in the
+ * order the hardware expects: the pre-coefficient block, the coefficient
+ * block, 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.
+ * @want_dev_b selects the DEV_B (right) blocks of stereo firmware instead of
+ * DEV_A (left); blocks are applied by type, not storage order (per TI).
+ */
+static int tas2557_load_config_data(struct tas2557_priv *tas2557,
+ struct tas2557_data *img_data,
+ bool want_dev_b)
+{
+ unsigned int pre, coeff;
+ int ret;
+
+ if (want_dev_b) {
+ pre = TAS2557_BLOCK_CFG_PRE_DEV_B;
+ coeff = TAS2557_BLOCK_CFG_COEFF_DEV_B;
+ } else {
+ pre = TAS2557_BLOCK_CFG_PRE_DEV_A;
+ coeff = TAS2557_BLOCK_CFG_COEFF_DEV_A;
+ }
+
+ ret = tas2557_load_fw_data_type(tas2557, img_data, pre);
+ if (ret < 0)
+ return ret;
+
+ ret = tas2557_load_fw_data_type(tas2557, img_data, coeff);
+ if (ret < 0)
+ return ret;
+
+ return tas2557_load_fw_data_type(tas2557, img_data,
+ TAS2557_BLOCK_CFG_POST);
+}
+
+/*
+ * 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.
+ */
+static int tas2557_load_config_post_power(struct tas2557_priv *tas2557)
+{
+ struct tas2557_config *config;
+
+ if (!tas2557->fw || tas2557->current_config >= tas2557->fw->num_configs)
+ return 0;
+
+ config = &tas2557->fw->configs[tas2557->current_config];
+
+ return tas2557_load_fw_data_type(tas2557, &config->data,
+ TAS2557_BLOCK_CFG_POST_POWER);
+}
+
+static int tas2557_load_pll(struct tas2557_priv *tas2557, unsigned int pll_idx)
+{
+ 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);
+
+ return tas2557_load_block(tas2557, &tas2557->fw->plls[pll_idx].block);
+}
+
+/*
+ * tas2557_set_program - reset device 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 ->
+ * program blocks (DSP coefficients) -> PLL block for config ->
+ * config blocks (sample-rate-specific settings)
+ *
+ * It does not re-power the device: callers that need the amplifier
+ * 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;
+ int ret;
+ bool want_dev_b;
+
+ 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(tas2557, tas2557_shutdown_data);
+ if (ret < 0)
+ return ret;
+ }
+
+ /* Hard + soft reset, then baseline register setup */
+ tas2557_hw_reset(tas2557);
+
+ ret = tas2557_dev_write(tas2557, TAS2557_SW_RESET_REG, 0x01);
+ if (ret < 0)
+ return ret;
+ usleep_range(1000, 2000);
+
+ ret = tas2557_load_data(tas2557, tas2557_default_data);
+ if (ret < 0)
+ return ret;
+
+ /* Restore IRQ configuration lost by the reset */
+ ret = tas2557_load_data(tas2557, tas2557_irq_config);
+ if (ret < 0)
+ dev_warn(tas2557->dev, "IRQ config reload failed: %d\n", ret);
+
+ /*
+ * Stereo firmware (device == 3) carries DEV_A (left) and DEV_B (right)
+ * tuning; select this instance's channel. Mono firmware has only
+ * DEV_A blocks, so this stays false.
+ */
+ want_dev_b = fw->device == TAS2557_FW_DEVICE_STEREO &&
+ tas2557->channel == 1;
+
+ /* Load DSP program blocks */
+ dev_dbg(tas2557->dev, "loading program %u (%s)\n",
+ prog_idx, program->name);
+ ret = tas2557_load_fw_data(tas2557, &program->data, want_dev_b);
+ if (ret < 0)
+ return ret;
+
+ tas2557->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, want_dev_b);
+ if (ret < 0)
+ return ret;
+
+ tas2557->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 %s ready: fw '%s' ver=0x%x drv=0x%x (%u prog, %u cfg)\n",
+ tas2557_pg_name(tas2557->pg_id),
+ tas2557->channel ? "right" : "left", 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)
+{
+ if (!tas2557->reset_gpio) {
+ mutex_lock(&tas2557->dev_lock);
+ tas2557->current_book = 0xff;
+ tas2557->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 */
+ gpiod_set_value_cansleep(tas2557->reset_gpio, 0);
+ usleep_range(10000, 11000);
+
+ mutex_lock(&tas2557->dev_lock);
+ tas2557->current_book = 0xff;
+ tas2557->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); the
+ * chip drives it active-high and holds it until FLAGS_1/FLAGS_2 are read.
+ * The chip's INT output is gated off while diagnosing the fault and
+ * re-enabled once the condition has been read/handled.
+ */
+static irqreturn_t tas2557_irq_handler(int irq, void *data)
+{
+ struct tas2557_priv *tas2557 = data;
+ unsigned int int1 = 0, int2 = 0, pwr_flag = 0;
+ int ret;
+
+ mutex_lock(&tas2557->lock);
+
+ if (!tas2557->fw || !tas2557->powered) {
+ mutex_unlock(&tas2557->lock);
+ return IRQ_NONE;
+ }
+
+ /* Gate the chip's INT output while diagnosing the fault */
+ tas2557_dev_write(tas2557, TAS2557_GPIO4_PIN_REG, 0x00);
+
+ ret = tas2557_dev_read(tas2557, TAS2557_FLAGS_1, &int1);
+ if (ret >= 0)
+ ret = tas2557_dev_read(tas2557, TAS2557_FLAGS_2, &int2);
+
+ if (ret < 0) {
+ dev_err(tas2557->dev, "IRQ: failed to read FLAGS\n");
+ goto reset;
+ }
+
+ 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: clock stop while muted (FLAGS_1=0x%02x FLAGS_2=0x%02x)\n",
+ int1, int2);
+ goto out;
+ }
+ dev_err(tas2557->dev, "IRQ: FLAGS_1=0x%02x FLAGS_2=0x%02x\n",
+ int1, int2);
+ goto reset;
+ }
+
+ if (!tas2557->dac_muted) {
+ ret = tas2557_dev_read(tas2557, TAS2557_POWER_UP_FLAG_REG,
+ &pwr_flag);
+ if (ret < 0)
+ goto reset;
+
+ if ((pwr_flag & 0xc0) != 0xc0) {
+ dev_err(tas2557->dev, "IRQ: power-up flag=0x%02x\n",
+ pwr_flag);
+ goto reset;
+ }
+ }
+
+ /* No fault (or expected clock stop): re-enable the INT output */
+out:
+ tas2557_dev_write(tas2557, TAS2557_GPIO4_PIN_REG, 0x07);
+ mutex_unlock(&tas2557->lock);
+ return IRQ_HANDLED;
+
+reset:
+ if (tas2557_failsafe_recovery(tas2557) == 0)
+ tas2557_dev_write(tas2557, TAS2557_GPIO4_PIN_REG, 0x07);
+
+ mutex_unlock(&tas2557->lock);
+ return IRQ_HANDLED;
+}
+
+/*
+ * tas2557_clk_err_detect - gate the chip's 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_dev_write(tas2557, TAS2557_CLK_ERR_CTRL,
+ enable ? 0x2b : 0x00);
+}
+
+/*
+ * tas2557_failsafe_recovery - re-initialise the device 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.
+ */
+static int tas2557_failsafe_recovery(struct tas2557_priv *tas2557)
+{
+ int ret;
+
+ lockdep_assert_held(&tas2557->lock);
+
+ tas2557->restart_count++;
+ dev_info(tas2557->dev, "failsafe recovery attempt %u\n",
+ tas2557->restart_count);
+
+ if (tas2557->restart_count > 5) {
+ dev_err(tas2557->dev, "too many recovery attempts, giving up\n");
+ return -EIO;
+ }
+
+ tas2557_hw_reset(tas2557);
+
+ ret = tas2557_load_data(tas2557, tas2557_default_data);
+ if (ret < 0)
+ return ret;
+
+ if (tas2557->fw) {
+ ret = tas2557_set_program(tas2557, tas2557->current_program,
+ (int)tas2557->current_config);
+ if (ret < 0)
+ return ret;
+ }
+
+ if (tas2557->powered) {
+ ret = tas2557_load_data(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(tas2557, tas2557_unmute_data);
+ if (ret < 0)
+ return ret;
+ } else {
+ /* Muted: clocks not guaranteed, keep detection off */
+ ret = tas2557_clk_err_detect(tas2557, false);
+ 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
+ *
+ * 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 flag = 0, f1 = 0, f2 = 0;
+ int i;
+
+ /*
+ * 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 (i = 0; i < 20; i++) {
+ tas2557_dev_read(tas2557, TAS2557_POWER_UP_FLAG_REG, &flag);
+ if ((flag & 0xc0) == 0xc0)
+ break;
+ usleep_range(2000, 2500);
+ }
+
+ tas2557_dev_read(tas2557, TAS2557_FLAGS_1, &f1);
+ tas2557_dev_read(tas2557, TAS2557_FLAGS_2, &f2);
+
+ if ((flag & 0xc0) == 0xc0)
+ dev_dbg(tas2557->dev,
+ "PLL locked, power-up OK (FLAG=0x%02x F1=0x%02x F2=0x%02x)\n",
+ flag, f1, f2);
+ else
+ dev_warn(tas2557->dev,
+ "PLL or clock may not have locked (FLAG=0x%02x F1=0x%02x F2=0x%02x)\n",
+ flag, f1, f2);
+
+ if (f1 & 0x04)
+ dev_warn(tas2557->dev, "clock error detected (FLAGS_1=0x%02x)\n",
+ 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; 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)
+{
+ int ret;
+
+ lockdep_assert_held(&tas2557->lock);
+
+ /* Configure current/voltage sense output slots */
+ if (tas2557->isense_enabled || tas2557->vsense_enabled) {
+ unsigned int sns = 0;
+
+ if (tas2557->isense_enabled)
+ sns |= (tas2557->imon_slot & 0x7) <<
+ TAS2557_ISNS_SLOT_SHIFT;
+ if (tas2557->vsense_enabled)
+ sns |= (tas2557->vmon_slot & 0x7) <<
+ TAS2557_VSNS_SLOT_SHIFT;
+ ret = tas2557_dev_write(tas2557, TAS2557_SNS_CTRL_REG, sns);
+ if (ret < 0)
+ return ret;
+ }
+
+ /*
+ * tas2557_startup_data hardcodes both sense-enable bits on;
+ * make the cached ALSA switch state win instead.
+ */
+ ret = tas2557_dev_update_bits(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_dev_update_bits(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 cleared by the SW reset, if ever set */
+ if (tas2557->dai_fmt >= 0) {
+ ret = tas2557_dev_update_bits(tas2557,
+ TAS2557_ASI1_DAC_FORMAT_REG,
+ TAS2557_FORMAT_MASK,
+ tas2557->dai_fmt);
+ if (ret < 0)
+ return ret;
+
+ ret = tas2557_dev_update_bits(tas2557,
+ TAS2557_ASI2_DAC_FORMAT_REG,
+ TAS2557_FORMAT_MASK,
+ tas2557->dai_fmt);
+ if (ret < 0)
+ return ret;
+ }
+
+ /*
+ * Re-apply the cached ASI data-slot offset (BCLK cycles).
+ * 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.
+ */
+ ret = tas2557_dev_write(tas2557, TAS2557_ASI2_OFFSET1_REG,
+ tas2557->asi_offset);
+ if (ret < 0)
+ return ret;
+
+ ret = tas2557_dev_write(tas2557, TAS2557_ASI1_OFFSET1_REG,
+ tas2557->asi_offset);
+ if (ret < 0)
+ return ret;
+
+ /* Re-apply the cached TX (capture) slot offset, if ever set */
+ if (tas2557->asi_tx_offset >= 0) {
+ ret = tas2557_dev_write(tas2557, TAS2557_ASI1_OFFSET2_REG,
+ tas2557->asi_tx_offset);
+ if (ret < 0)
+ return ret;
+
+ ret = tas2557_dev_write(tas2557, TAS2557_ASI2_OFFSET2_REG,
+ tas2557->asi_tx_offset);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+/*
+ * tas2557_enable - power the amplifier 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)
+{
+ 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->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 */
+ ret = tas2557_load_data(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(tas2557, tas2557_unmute_data);
+ if (ret >= 0 && tas2557->sense_muted)
+ ret = tas2557_dev_update_bits(tas2557,
+ TAS2557_MUTE_REG,
+ TAS2557_ISENSE_MUTE,
+ TAS2557_ISENSE_MUTE);
+ if (ret >= 0 && tas2557->sense_muted)
+ ret = tas2557_dev_update_bits(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_dev_update_bits(tas2557,
+ TAS2557_MUTE_REG,
+ TAS2557_ISENSE_MUTE,
+ 0);
+ if (ret >= 0 && !tas2557->sense_muted)
+ ret = tas2557_dev_update_bits(tas2557,
+ TAS2557_SOFT_MUTE_REG,
+ sense_soft_mask,
+ 0);
+ }
+ if (ret < 0)
+ return ret;
+
+ /* Verify PLL lock and clock health */
+ tas2557_check_pll_lock(tas2557);
+
+ tas2557->powered = true;
+ tas2557->restart_count = 0;
+
+ dev_dbg(tas2557->dev, "amplifier powered on (prog=%u cfg=%u)\n",
+ tas2557->current_program, tas2557->current_config);
+
+ } else if (!enable && tas2557->powered) {
+ ret = tas2557_load_data(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 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 width, offset;
+ int ret;
+
+ mutex_lock(&tas2557->lock);
+
+ dev_dbg(tas2557->dev, "hw_params: rate=%u width=%d\n",
+ rate, params_physical_width(params));
+
+ /* Store the rate; tas2557_enable() uses it to pick the config */
+ tas2557->sample_rate = rate;
+
+ /*
+ * The ASI word length is fixed by tas2557_startup_data to match
+ * what the DSP firmware expects; it does not depend on the stream
+ * format, so no WORDLENGTH bits are written here. Only the
+ * channel data-slot offset (in BCLK cycles) depends on the
+ * stream: channel 0 sits at offset 1 (the validated hardware
+ * configuration); later channels follow, width*2 apart. An
+ * explicit snd_soc_dai_set_tdm_slot() configuration takes
+ * precedence over this default and leaves asi_offset untouched.
+ */
+ if (!tas2557->tdm_configured) {
+ width = tas2557->tdm_slot_width > 0 ?
+ tas2557->tdm_slot_width :
+ params_physical_width(params);
+ offset = (tas2557->channel == 0) ? 1 : 1 + width * 2;
+ tas2557->asi_offset = offset;
+ }
+ offset = tas2557->asi_offset;
+
+ /* Playback runs over ASI2; ASI1 is programmed too (per TI driver) */
+ ret = tas2557_dev_write(tas2557, TAS2557_ASI2_OFFSET1_REG, offset);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "ASI2 offset write failed: %d\n", ret);
+ mutex_unlock(&tas2557->lock);
+ return ret;
+ }
+
+ ret = tas2557_dev_write(tas2557, TAS2557_ASI1_OFFSET1_REG, offset);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "ASI1 offset write failed: %d\n", ret);
+ mutex_unlock(&tas2557->lock);
+ return ret;
+ }
+
+ dev_dbg(tas2557->dev, "ASI: %s channel, offset=%u BCLK cycles\n",
+ tas2557->channel ? "right" : "left", 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().
+ */
+
+ 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_dev_update_bits(tas2557, TAS2557_ASI1_DAC_FORMAT_REG,
+ TAS2557_FORMAT_MASK, asi_fmt);
+ if (ret >= 0)
+ ret = tas2557_dev_update_bits(tas2557,
+ TAS2557_ASI2_DAC_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;
+ 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;
+ /* RX (playback) data-slot offset in BCLK cycles */
+ tas2557->asi_offset = rx_slot * slot_width;
+ ret = tas2557_dev_write(tas2557, TAS2557_ASI1_OFFSET1_REG,
+ tas2557->asi_offset);
+ if (ret >= 0)
+ ret = tas2557_dev_write(tas2557,
+ TAS2557_ASI2_OFFSET1_REG,
+ tas2557->asi_offset);
+ }
+
+ if (ret >= 0 && tx_slot >= 0) {
+ unsigned int tx_offset = tx_slot * slot_width;
+
+ tas2557->asi_tx_offset = tx_offset;
+ ret = tas2557_dev_write(tas2557, TAS2557_ASI1_OFFSET2_REG,
+ tx_offset);
+ if (ret >= 0)
+ ret = tas2557_dev_write(tas2557,
+ TAS2557_ASI2_OFFSET2_REG,
+ tx_offset);
+ }
+
+ 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_dev_update_bits(tas2557, TAS2557_MUTE_REG,
+ TAS2557_ISENSE_MUTE,
+ mute ? TAS2557_ISENSE_MUTE : 0);
+ if (ret >= 0)
+ ret = tas2557_dev_update_bits(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_dev_update_bits(tas2557,
+ TAS2557_SOFT_MUTE_REG,
+ TAS2557_PDM_SOFT_MUTE,
+ TAS2557_PDM_SOFT_MUTE);
+ if (ret >= 0) {
+ usleep_range(10000, 11000);
+ ret = tas2557_dev_update_bits(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(tas2557, tas2557_unmute_data);
+ /* unmute_data clears sense mutes too; re-assert if needed */
+ if (ret >= 0 && tas2557->sense_muted)
+ ret = tas2557_dev_update_bits(tas2557,
+ TAS2557_MUTE_REG,
+ TAS2557_ISENSE_MUTE,
+ TAS2557_ISENSE_MUTE);
+ if (ret >= 0 && tas2557->sense_muted)
+ ret = tas2557_dev_update_bits(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,
+};
+
+/* =========================================================================
+ * 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;
+ }
+
+ tas2557->dac_gain = gain;
+
+ if (tas2557->powered)
+ ret = tas2557_dev_update_bits(tas2557, TAS2557_SPK_CTRL_REG,
+ TAS2557_DAC_GAIN_MASK,
+ gain << TAS2557_DAC_GAIN_SHIFT);
+
+ mutex_unlock(&tas2557->lock);
+
+ if (ret < 0)
+ return ret;
+
+ 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;
+ }
+
+ tas2557->isense_enabled = en;
+
+ if (tas2557->powered)
+ ret = tas2557_dev_update_bits(tas2557, TAS2557_POWER_CTRL2_REG,
+ TAS2557_ISENSE_ENABLE,
+ en ? TAS2557_ISENSE_ENABLE : 0);
+
+ mutex_unlock(&tas2557->lock);
+
+ if (ret < 0)
+ return ret;
+
+ 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;
+ }
+
+ tas2557->vsense_enabled = en;
+
+ if (tas2557->powered)
+ ret = tas2557_dev_update_bits(tas2557, TAS2557_POWER_CTRL2_REG,
+ TAS2557_VSENSE_ENABLE,
+ en ? TAS2557_VSENSE_ENABLE : 0);
+
+ mutex_unlock(&tas2557->lock);
+
+ if (ret < 0)
+ return ret;
+
+ 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);
+ unsigned int pg_id;
+ const char *default_fw;
+ int ret;
+
+ tas2557_hw_reset(tas2557);
+
+ ret = tas2557_dev_write(tas2557, TAS2557_SW_RESET_REG, 0x01);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "software reset failed: %d\n", ret);
+ return ret;
+ }
+ usleep_range(1000, 2000);
+
+ ret = tas2557_dev_read(tas2557, TAS2557_REV_PGID_REG, &pg_id);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "failed to read REV_PGID: %d\n", ret);
+ return ret;
+ }
+
+ switch (pg_id) {
+ case TAS2557_PG_VERSION_1P0:
+ dev_dbg(tas2557->dev, "silicon: PG1.0 (0x%02x)\n", pg_id);
+ default_fw = TAS2557_PG1P0_FW_NAME;
+ break;
+ case TAS2557_PG_VERSION_2P0:
+ dev_dbg(tas2557->dev, "silicon: PG2.0 (0x%02x)\n", pg_id);
+ default_fw = TAS2557_FW_NAME;
+ break;
+ case TAS2557_PG_VERSION_2P1:
+ dev_dbg(tas2557->dev, "silicon: PG2.1 (0x%02x)\n", pg_id);
+ default_fw = TAS2557_FW_NAME;
+ break;
+ default:
+ dev_warn(tas2557->dev,
+ "unknown silicon version 0x%02x, assuming PG2.x fw\n",
+ pg_id);
+ default_fw = TAS2557_FW_NAME;
+ break;
+ }
+
+ mutex_lock(&tas2557->lock);
+ tas2557->pg_id = pg_id;
+ /*
+ * 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(tas2557, tas2557_default_data);
+ if (ret < 0) {
+ dev_err(tas2557->dev, "default data load failed: %d\n", ret);
+ return ret;
+ }
+
+ ret = tas2557_load_data(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, requesting firmware '%s'\n",
+ 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(tas2557, tas2557_default_data);
+ if (ret < 0)
+ dev_err(tas2557->dev, "defaults reload on resume failed: %d\n",
+ ret);
+ else if (tas2557_load_data(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
+ * =========================================================================
+ */
+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;
+
+ /* Default gain (0 dB) */
+ tas2557->dac_gain = TAS2557_DAC_GAIN_MAX;
+
+ /* The device 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->asi_tx_offset = -1;
+
+ /* DT properties with defaults per the pinned contract */
+ if (of_property_read_u32(dev->of_node, "ti,channel", &tas2557->channel))
+ tas2557->channel = 0; /* left */
+ else if (tas2557->channel > 1)
+ return dev_err_probe(dev, -EINVAL,
+ "ti,channel must be 0 or 1\n");
+
+ if (of_property_read_u32(dev->of_node, "ti,imon-slot-no",
+ &tas2557->imon_slot))
+ tas2557->imon_slot = 0;
+ else if (tas2557->imon_slot > 7)
+ return dev_err_probe(dev, -EINVAL,
+ "ti,imon-slot-no must be 0-7\n");
+
+ if (of_property_read_u32(dev->of_node, "ti,vmon-slot-no",
+ &tas2557->vmon_slot))
+ tas2557->vmon_slot = 2;
+ else if (tas2557->vmon_slot > 7)
+ return dev_err_probe(dev, -EINVAL,
+ "ti,vmon-slot-no must be 0-7\n");
+
+ /* 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));
+
+ /*
+ * Seed the cached ASI data-slot offset before any hw_params() or
+ * set_tdm_slot() call arrives; 32 is the fixed ASI word length
+ * used by tas2557_startup_data.
+ */
+ tas2557->asi_offset = (tas2557->channel == 0) ? 1 : 1 + 32 * 2;
+
+ 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.
+ */
+ 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");
+
+ /* Mark book/page as unknown so the first access triggers a switch */
+ tas2557->current_book = 0xff;
+ tas2557->current_page = 0xff;
+
+ /*
+ * 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 (I2C addr 0x%02x, %s channel)\n",
+ client->addr, tas2557->channel ? "right" : "left");
+ 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..69b14a0bc3c5
--- /dev/null
+++ b/sound/soc/codecs/tas2557.h
@@ -0,0 +1,218 @@
+/* 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
+
+/* 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 [flat|nested] 4+ messages in thread
* Re: [PATCH v2 2/2] ASoC: codecs: add TAS2557 mono smart amplifier driver
2026-07-11 14:57 ` [PATCH v2 2/2] ASoC: codecs: add TAS2557 mono smart amplifier driver Gianluca Boiano
@ 2026-07-11 15:10 ` sashiko-bot
0 siblings, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-07-11 15:10 UTC (permalink / raw)
To: Gianluca Boiano; +Cc: robh, conor+dt, devicetree
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Incorrect ASI data-slot offset calculation for the right channel skips the correct audio slot.
- [Medium] Missing SNS_CTRL_REG configuration when enabling ISENSE/VSENSE ALSA controls at runtime.
- [Medium] `tas2557_failsafe_recovery` unintentionally unmutes the capture path upon fault recovery.
- [Medium] `tas2557_set_dai_fmt()` updates DAC format registers but fails to update ADC format registers.
- [Medium] `tas2557_startup_data` hardcodes ASI2 DAC format but omits ASI1 DAC format configuration.
--
--- Patch [2]: [PATCH v2 2/2] ASoC: codecs: add TAS2557 mono smart amplifier driver ---
commit d1471370278591a7f73ca18bb2ac75981e6b8a92
Author: Gianluca Boiano <morf3089@gmail.com>
Subject: ASoC: codecs: add TAS2557 mono smart amplifier driver
The TAS2557 is a mono Class-D smart amplifier with an on-chip DSP for
speaker protection. It uses I2S/TDM and requires board-specific firmware
loaded per speaker design. Amplifier power is managed through DAPM and
includes fault interrupt recovery and volume controls.
> 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
[ ... ]
> +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_DAC_FORMAT_REG, 0x18, /* ASI2: 32-bit I2S */
[Severity: Medium]
Is it intentional to omit the equivalent formatting register for ASI1 here?
The comment above mentions both ASI interfaces are enabled and configured
identically, but if the board wiring uses ASI1, it looks like it would be left
at its reset default word length while ASI2 is explicitly set to 32-bit.
> + TAS2557_ASI2_BDIV_CLK_SEL_REG, 0x01,
[ ... ]
> +static int tas2557_failsafe_recovery(struct tas2557_priv *tas2557)
> +{
[ ... ]
> + if (tas2557->powered) {
> + ret = tas2557_load_data(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(tas2557, tas2557_unmute_data);
> + if (ret < 0)
> + return ret;
> + } else {
[Severity: Medium]
Does this sequence inadvertently unmute the capture path upon fault recovery?
When the DAC is unmuted but the capture stream is muted, calling
tas2557_load_data with tas2557_unmute_data clears the mute bits for both the
DAC and ISENSE paths.
Should this check tas2557->sense_muted and re-assert the capture mutes like
it does in tas2557_enable?
> + /* Muted: clocks not guaranteed, keep detection off */
> + ret = tas2557_clk_err_detect(tas2557, false);
[ ... ]
> +static int tas2557_hw_params(struct snd_pcm_substream *substream,
> + struct snd_pcm_hw_params *params,
> + struct snd_soc_dai *dai)
> +{
[ ... ]
> + if (!tas2557->tdm_configured) {
> + width = tas2557->tdm_slot_width > 0 ?
> + tas2557->tdm_slot_width :
> + params_physical_width(params);
> + offset = (tas2557->channel == 0) ? 1 : 1 + width * 2;
[Severity: High]
Can this offset calculation skip the correct audio slot for the right channel?
For a 32-bit word, 1 + width * 2 evaluates to 65 BCLKs, which falls completely
outside the standard 64-bit stereo I2S frame where the right channel is
expected at offset 33.
> + tas2557->asi_offset = offset;
> + }
[ ... ]
> +static int tas2557_set_dai_fmt(struct snd_soc_dai *dai, unsigned int fmt)
> +{
[ ... ]
> + mutex_lock(&tas2557->lock);
> + tas2557->dai_fmt = asi_fmt;
> + ret = tas2557_dev_update_bits(tas2557, TAS2557_ASI1_DAC_FORMAT_REG,
> + TAS2557_FORMAT_MASK, asi_fmt);
> + if (ret >= 0)
> + ret = tas2557_dev_update_bits(tas2557,
> + TAS2557_ASI2_DAC_FORMAT_REG,
> + TAS2557_FORMAT_MASK, asi_fmt);
[Severity: Medium]
Should the ADC format registers be updated here as well?
This currently updates the DAC format registers (TAS2557_ASI1_DAC_FORMAT_REG
and TAS2557_ASI2_DAC_FORMAT_REG) but leaves the corresponding ADC format
registers unmodified at their reset defaults. Could this cause the capture
stream to use the wrong ASI data format?
> + mutex_unlock(&tas2557->lock);
[ ... ]
> +static int tas2557_isense_put(struct snd_kcontrol *kc,
> + struct snd_ctl_elem_value *ucontrol)
> +{
[ ... ]
> + tas2557->isense_enabled = en;
> +
> + if (tas2557->powered)
> + ret = tas2557_dev_update_bits(tas2557, TAS2557_POWER_CTRL2_REG,
> + TAS2557_ISENSE_ENABLE,
> + en ? TAS2557_ISENSE_ENABLE : 0);
[Severity: Medium]
If the amplifier powers up with ISENSE and VSENSE disabled, does enabling
them later via this control miss updating TAS2557_SNS_CTRL_REG?
In tas2557_apply_runtime_state, the slot routing is only written if at least
one sense line is already enabled. If the ALSA switches are turned on later,
the hardware might transmit on the default slot instead of the DT-configured
slot.
> +
> + mutex_unlock(&tas2557->lock);
[ ... ]
> +static int tas2557_vsense_put(struct snd_kcontrol *kc,
> + struct snd_ctl_elem_value *ucontrol)
> +{
[ ... ]
> + tas2557->vsense_enabled = en;
> +
> + if (tas2557->powered)
> + ret = tas2557_dev_update_bits(tas2557, TAS2557_POWER_CTRL2_REG,
> + TAS2557_VSENSE_ENABLE,
> + en ? TAS2557_VSENSE_ENABLE : 0);
[Severity: Medium]
Similarly, does toggling VSENSE power at runtime need to configure the
SNS_CTRL_REG routing if it wasn't done during startup?
> +
> + mutex_unlock(&tas2557->lock);
[ ... ]
> +static int tas2557_i2c_probe(struct i2c_client *client)
> +{
[ ... ]
> + /*
> + * Seed the cached ASI data-slot offset before any hw_params() or
> + * set_tdm_slot() call arrives; 32 is the fixed ASI word length
> + * used by tas2557_startup_data.
> + */
> + tas2557->asi_offset = (tas2557->channel == 0) ? 1 : 1 + 32 * 2;
[Severity: High]
Does this seed the same incorrect right channel offset mentioned earlier in
tas2557_hw_params?
> +
> + ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(tas2557_supplies),
> + tas2557_supplies);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260711145722.188556-1-morf3089@gmail.com?part=2
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-07-11 15:10 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-11 14:57 [PATCH v2 0/2] ASoC: codecs: add Texas Instruments TAS2557 smart amplifier driver Gianluca Boiano
2026-07-11 14:57 ` [PATCH v2 1/2] dt-bindings: sound: add Texas Instruments TAS2557 Gianluca Boiano
2026-07-11 14:57 ` [PATCH v2 2/2] ASoC: codecs: add TAS2557 mono smart amplifier driver Gianluca Boiano
2026-07-11 15:10 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox