From 8bff9d3f37c10ca29da9c2263768c586f3ce1d2f Mon Sep 17 00:00:00 2001 From: Gianluca Boiano Date: Wed, 17 Jun 2026 15:29:38 +0200 Subject: [PATCH 1/6] dt-bindings: sound: add Texas Instruments TAS2557 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 --- .../devicetree/bindings/sound/ti,tas2557.yaml | 123 ++++++++++++++++++ MAINTAINERS | 1 + 2 files changed, 124 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 00000000000000..b1d1183c4a79d9 --- /dev/null +++ b/Documentation/devicetree/bindings/sound/ti,tas2557.yaml @@ -0,0 +1,123 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +# Copyright (C) 2026 Gianluca Boiano +%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 + +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' + +unevaluatedProperties: false + +examples: + - | + #include + #include + + 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 1a5292fa826c0a..fb76857081a947 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -26112,6 +26112,7 @@ M: Baojun Xu 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 From 4ec10d4576689908b0684f7ed28770c64eedca24 Mon Sep 17 00:00:00 2001 From: Gianluca Boiano Date: Wed, 17 Jun 2026 15:29:38 +0200 Subject: [PATCH 2/6] 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. 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 --- sound/soc/codecs/Kconfig | 11 + sound/soc/codecs/Makefile | 2 + sound/soc/codecs/tas2557.c | 2476 ++++++++++++++++++++++++++++++++++++ sound/soc/codecs/tas2557.h | 218 ++++ 4 files changed, 2707 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 adb3fb923be394..5d16ab6d27f60b 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -265,6 +265,7 @@ config SND_SOC_ALL_CODECS imply SND_SOC_STAC9766 imply SND_SOC_STI_SAS imply SND_SOC_TAS2552 + imply SND_SOC_TAS2557 imply SND_SOC_TAS2562 imply SND_SOC_TAS2764 imply SND_SOC_TAS2770 @@ -2134,6 +2135,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 3ddee529872197..a30dc201b2a7d8 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -428,6 +428,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 @@ -746,6 +747,7 @@ obj-$(CONFIG_SND_SOC_STA529) += snd-soc-sta529.o obj-$(CONFIG_SND_SOC_STAC9766) += snd-soc-stac9766.o obj-$(CONFIG_SND_SOC_STI_SAS) += snd-soc-sti-sas.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 00000000000000..08ab437b63091c --- /dev/null +++ b/sound/soc/codecs/tas2557.c @@ -0,0 +1,2476 @@ +// 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 +// +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 muted; + + /* 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; + + /* + * Cached ASI data-slot offset in BCLK cycles. Computed in + * hw_params()/set_tdm_slot() and re-applied in tas2557_enable() + * after the SW reset in tas2557_set_program() wipes the ASI + * offset registers. + */ + unsigned int asi_offset; + int tdm_slot_width; + + /* 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); + +/* + * 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 the pin configuration from the + * TI 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(&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_HANDLED; + } + + /* 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->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->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_load_config_post_power(tas2557); + if (ret < 0) + return ret; + + if (!tas2557->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, 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_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) { + 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; + } + + /* 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; + tas2557_dev_write(tas2557, TAS2557_SNS_CTRL_REG, sns); + } + + /* Apply DAC gain */ + tas2557_dev_update_bits(tas2557, TAS2557_SPK_CTRL_REG, + TAS2557_DAC_GAIN_MASK, + tas2557->dac_gain << TAS2557_DAC_GAIN_SHIFT); + + /* + * 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. + */ + tas2557_dev_write(tas2557, TAS2557_ASI2_OFFSET1_REG, + tas2557->asi_offset); + tas2557_dev_write(tas2557, TAS2557_ASI1_OFFSET1_REG, + tas2557->asi_offset); + + /* 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 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. When 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->muted) { + ret = tas2557_clk_err_detect(tas2557, false); + } else { + ret = tas2557_load_data(tas2557, tas2557_unmute_data); + if (ret >= 0) + ret = tas2557_clk_err_detect(tas2557, true); + } + 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->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. + */ + 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; + + /* 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_warn(tas2557->dev, "ASI2 offset write failed: %d\n", ret); + + ret = tas2557_dev_write(tas2557, TAS2557_ASI1_OFFSET1_REG, offset); + if (ret < 0) + dev_warn(tas2557->dev, "ASI1 offset write failed: %d\n", 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); + 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) { + /* 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; + + 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); + int ret = 0; + + mutex_lock(&tas2557->lock); + + if (!tas2557->powered) { + tas2557->muted = mute; + mutex_unlock(&tas2557->lock); + return 0; + } + + 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_write(tas2557, + TAS2557_SOFT_MUTE_REG, 0x01); + if (ret >= 0) { + usleep_range(10000, 11000); + ret = tas2557_dev_write(tas2557, + TAS2557_MUTE_REG, 0x03); + } + } else { + /* Same register values as tas2557_unmute_data, same order */ + ret = tas2557_load_data(tas2557, tas2557_unmute_data); + /* Clocks are running now: arm clock error detection */ + if (ret >= 0) + ret = tas2557_clk_err_detect(tas2557, true); + } + + tas2557->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, + .no_capture_mute = 1, +}; + +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: + 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->muted = true; + + /* 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 "); +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 00000000000000..69b14a0bc3c522 --- /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 + */ + +#ifndef __TAS2557_H__ +#define __TAS2557_H__ + +#include + +/* + * 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__ */ From 9f3da0a589f3084a4477e8d530ad4277626c9cf8 Mon Sep 17 00:00:00 2001 From: Gianluca Boiano Date: Sun, 21 Jun 2026 18:36:15 +0200 Subject: [PATCH 3/6] ASoC: qcom: sdm660-internal: add Primary MI2S support The startup/shutdown clock setup only handled the internal codec MI2S ports (INT0/INT3) and the TDM ports, returning -ENOSYS for any other AFE DAI. External speaker amplifiers such as the TAS2557 on the Xiaomi tulip are wired to the Primary MI2S port, so streams to that backend failed at startup with -ENOSYS. Handle PRIMARY_MI2S_RX: on the first user enable the PRI_MI2S_IBIT clock at the I2S bit-clock rate and put the AFE into I2S master mode (CBP_CFP), releasing the clock on the last user. Also force S16_LE on the MI2S/TDM backends in the hw_params fixup: snd_mask_set_format() only adds to the format mask, so a front-end negotiating S24_LE (e.g. PipeWire) would run the Primary MI2S/TAS2557 path at 24-bit and emit silence. Reset the mask with snd_mask_none() first. Signed-off-by: Gianluca Boiano --- sound/soc/qcom/sdm660-internal.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/sound/soc/qcom/sdm660-internal.c b/sound/soc/qcom/sdm660-internal.c index 2486279dc9b074..232c8a09158425 100644 --- a/sound/soc/qcom/sdm660-internal.c +++ b/sound/soc/qcom/sdm660-internal.c @@ -32,6 +32,7 @@ struct sdm660_int_snd_data { uint32_t sec_tdm_clk_count; uint32_t int0_mi2s_clk_count; uint32_t int3_mi2s_clk_count; + u32 pri_mi2s_clk_count; }; static int snd_sdm660_int_startup(struct snd_pcm_substream *stream) @@ -112,6 +113,18 @@ static int snd_sdm660_int_startup(struct snd_pcm_substream *stream) */ snd_soc_dai_set_fmt(cpu, SND_SOC_DAIFMT_CBP_CFP); + break; + case PRIMARY_MI2S_RX: + data->pri_mi2s_clk_count++; + if (data->pri_mi2s_clk_count == 1) + snd_soc_dai_set_sysclk(cpu, + Q6AFE_LPASS_CLK_ID_PRI_MI2S_IBIT, + MI2S_BCLK_RATE, + SNDRV_PCM_STREAM_PLAYBACK); + + /* AFE drives the I2S bit/frame clock for the external amp. */ + snd_soc_dai_set_fmt(cpu, SND_SOC_DAIFMT_CBP_CFP); + break; default: dev_err(cpu->dev, "unimplemented afe dai\n"); @@ -173,6 +186,14 @@ static void snd_sdm660_int_shutdown(struct snd_pcm_substream *stream) Q6AFE_LPASS_CLK_ID_INT3_MI2S_IBIT, 0, SNDRV_PCM_STREAM_PLAYBACK); + break; + case PRIMARY_MI2S_RX: + data->pri_mi2s_clk_count--; + if (data->pri_mi2s_clk_count == 0) + snd_soc_dai_set_sysclk(cpu, + Q6AFE_LPASS_CLK_ID_PRI_MI2S_IBIT, + 0, SNDRV_PCM_STREAM_PLAYBACK); + break; default: dev_err(cpu->dev, "unimplemented afe dai\n"); @@ -273,6 +294,7 @@ static int sdm660_int_be_hw_params_fixup(struct snd_soc_pcm_runtime *rtd, struct snd_soc_dai *cpu = snd_soc_rtd_to_cpu(rtd, 0); rate->min = rate->max = DEFAULT_SAMPLE_RATE_48K; + snd_mask_none(fmt); snd_mask_set_format(fmt, SNDRV_PCM_FORMAT_S16_LE); if (cpu->id == PRIMARY_TDM_TX_0) From a37a0796d5f492ab36dcde2e3427304347892b41 Mon Sep 17 00:00:00 2001 From: Gianluca Boiano Date: Sun, 21 Jun 2026 18:37:09 +0200 Subject: [PATCH 4/6] fixup! sdm660_defconfig --- arch/arm64/configs/sdm660_defconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/arch/arm64/configs/sdm660_defconfig b/arch/arm64/configs/sdm660_defconfig index dc767060877242..482d310cdf62ad 100644 --- a/arch/arm64/configs/sdm660_defconfig +++ b/arch/arm64/configs/sdm660_defconfig @@ -525,6 +525,7 @@ CONFIG_SND_SOC_QDSP6_USB=m CONFIG_SND_SOC_SDM660_INT=m CONFIG_SND_SOC_MSM8916_WCD_ANALOG=m CONFIG_SND_SOC_MSM8916_WCD_DIGITAL=m +CONFIG_SND_SOC_TAS2557=m CONFIG_HID_BATTERY_STRENGTH=y CONFIG_HIDRAW=y CONFIG_UHID=m From 69ce52349d42786476985a1b33dec3bf17dc6926 Mon Sep 17 00:00:00 2001 From: Gianluca Boiano Date: Fri, 3 Jul 2026 22:57:01 +0200 Subject: [PATCH 5/6] fixup! arm64: dts: qcom: add device-tree for Redmi Note 6 Pro (tulip) --- .../boot/dts/qcom/sdm636-xiaomi-tulip.dts | 99 +++++++++++++++++-- 1 file changed, 93 insertions(+), 6 deletions(-) diff --git a/arch/arm64/boot/dts/qcom/sdm636-xiaomi-tulip.dts b/arch/arm64/boot/dts/qcom/sdm636-xiaomi-tulip.dts index c8ad621347892d..481de0aecec07a 100644 --- a/arch/arm64/boot/dts/qcom/sdm636-xiaomi-tulip.dts +++ b/arch/arm64/boot/dts/qcom/sdm636-xiaomi-tulip.dts @@ -173,12 +173,24 @@ }; }; -&blsp_i2c2 { - status = "okay"; -}; - &blsp_i2c6 { status = "okay"; + + tas2557: codec@4c { + compatible = "ti,tas2557"; + reg = <0x4c>; + #sound-dai-cells = <0>; + + reset-gpios = <&tlmm 77 GPIO_ACTIVE_LOW>; + interrupts-extended = <&tlmm 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,channel = <0>; + }; }; &blsp1_uart2 { @@ -328,6 +340,11 @@ }; &q6afedai { + dai@16 { + reg = ; + qcom,sd-lines = <0>; + }; + dai@81 { reg = ; qcom,sd-lines = <0 1>; @@ -446,6 +463,12 @@ regulator-allow-set-load; }; + vreg_l6b_1p8: l6 { + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + regulator-enable-ramp-delay = <250>; + }; + vreg_l7b_3p125: l7 { regulator-min-microvolt = <2700000>; regulator-max-microvolt = <3125000>; @@ -661,7 +684,8 @@ model = "Xiaomi Redmi Note 6 Pro"; pinctrl-0 = <&cdc_pdm_default>, - <&cdc_dmic_default>; + <&cdc_dmic_default>, + <&pri_mi2s_active>; pinctrl-names = "default"; audio-routing = "RX_BIAS", "Digital MCLK", @@ -676,7 +700,8 @@ "PDM_RX3", "Digital PDM_RX3", "Digital LPASS_PDM_TX", "PDM_TX", "AMIC1", "MIC BIAS External1", - "AMIC2", "MIC BIAS External2"; + "AMIC2", "MIC BIAS External2", + "ASI2", "Primary MI2S Playback"; mm1-dai-link { link-name = "MultiMedia1"; @@ -711,6 +736,22 @@ }; }; + pri-mi2s-dai-link { + link-name = "Primary MI2S Playback"; + + cpu { + sound-dai = <&q6afedai PRIMARY_MI2S_RX>; + }; + + platform { + sound-dai = <&q6routing>; + }; + + codec { + sound-dai = <&tas2557>; + }; + }; + int3-mi2s-dai-link { link-name = "Internal MI2S Capture"; @@ -780,6 +821,52 @@ drive-strength = <2>; bias-disable; }; + + pri_mi2s_active: pri-mi2s-active-state { + sck-pins { + pins = "gpio12"; + function = "pri_mi2s"; + drive-strength = <8>; + bias-disable; + }; + + ws-pins { + pins = "gpio13"; + function = "pri_mi2s_ws"; + drive-strength = <8>; + bias-disable; + }; + + data-pins { + pins = "gpio14", "gpio15"; + function = "pri_mi2s"; + drive-strength = <8>; + bias-disable; + }; + }; + + pri_mi2s_sleep: pri-mi2s-sleep-state { + sck-pins { + pins = "gpio12"; + function = "pri_mi2s"; + drive-strength = <2>; + bias-pull-down; + }; + + ws-pins { + pins = "gpio13"; + function = "pri_mi2s_ws"; + drive-strength = <2>; + bias-pull-down; + }; + + data-pins { + pins = "gpio14", "gpio15"; + function = "pri_mi2s"; + drive-strength = <2>; + bias-pull-down; + }; + }; }; &usb3 { From a2981b98fe587c65b353dcbbf690180374313b66 Mon Sep 17 00:00:00 2001 From: Gianluca Boiano Date: Fri, 3 Jul 2026 22:57:57 +0200 Subject: [PATCH 6/6] fixup! arm64: dts: qcom: sdm660: Add device tree for Xiaomi Mi A2 (jasmine) --- .../boot/dts/qcom/sdm660-xiaomi-jasmine.dts | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/arch/arm64/boot/dts/qcom/sdm660-xiaomi-jasmine.dts b/arch/arm64/boot/dts/qcom/sdm660-xiaomi-jasmine.dts index 36aeec154797fb..94054bd0b52572 100644 --- a/arch/arm64/boot/dts/qcom/sdm660-xiaomi-jasmine.dts +++ b/arch/arm64/boot/dts/qcom/sdm660-xiaomi-jasmine.dts @@ -11,6 +11,8 @@ #include #include #include +#include +#include / { model = "Xiaomi Mi A2"; @@ -170,6 +172,30 @@ }; }; +&blsp_i2c6 { + status = "okay"; + + tas2557: codec@4c { + compatible = "ti,tas2557"; + reg = <0x4c>; + #sound-dai-cells = <0>; + + reset-gpios = <&tlmm 77 GPIO_ACTIVE_LOW>; + interrupts-extended = <&tlmm 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,channel = <0>; + }; +}; + +&lpass_codec { + status = "okay"; +}; + &mdss { status = "okay"; }; @@ -242,6 +268,17 @@ status = "okay"; }; +&pm660l_codec { + vdd-cdc-io-supply = <&vreg_s4a_2p04>; + vdd-cdc-tx-rx-cx-supply = <&vreg_s4a_2p04>; + vdd-micbias-supply = <&vreg_l7b_3p125>; + + qcom,micbias-lvl = <2600>; + qcom,micbias1-ext-cap; + + status = "okay"; +}; + &pm660l_gpios { vol_up_default: vol-up-default-state { pins = "gpio7"; @@ -270,6 +307,33 @@ status = "okay"; }; +&q6afedai { + dai@16 { + reg = ; + qcom,sd-lines = <0>; + }; + + dai@81 { + reg = ; + qcom,sd-lines = <0 1>; + }; + + dai@88 { + reg = ; + qcom,sd-lines = <0 1>; + }; +}; + +&q6asmdai { + dai@0 { + reg = <0>; + }; + + dai@1 { + reg = <1>; + }; +}; + &qusb2phy0 { vdd-supply = <&vreg_l1b_0p925>; vdda-pll-supply = <&vreg_l10a_1p8>; @@ -368,6 +432,12 @@ regulator-system-load = <800000>; }; + vreg_l6b_1p8: l6 { + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + regulator-enable-ramp-delay = <250>; + }; + vreg_l7b_3p125: l7 { regulator-min-microvolt = <2700000>; regulator-max-microvolt = <3125000>; @@ -570,6 +640,97 @@ status = "okay"; }; +&sound { + compatible = "qcom,sdm660-internal-sndcard"; + model = "Xiaomi Mi A2"; + + pinctrl-0 = <&cdc_pdm_default>, + <&cdc_dmic_default>, + <&pri_mi2s_active>; + pinctrl-names = "default"; + + audio-routing = "RX_BIAS", "Digital MCLK", + "INT_LDO_H", "Digital MCLK", + "Digital RX_I2S_CLK", "Digital MCLK", + "Digital TX_I2S_CLK", "Digital MCLK", + "Digital ADC1", "ADC1", + "Digital ADC2", "ADC2", + "Digital ADC3", "ADC3", + "PDM_RX1", "Digital PDM_RX1", + "PDM_RX2", "Digital PDM_RX2", + "PDM_RX3", "Digital PDM_RX3", + "Digital LPASS_PDM_TX", "PDM_TX", + "AMIC1", "MIC BIAS External1", + "AMIC2", "MIC BIAS External2", + "ASI2", "Primary MI2S Playback"; + + mm1-dai-link { + link-name = "MultiMedia1"; + + cpu { + sound-dai = <&q6asmdai MSM_FRONTEND_DAI_MULTIMEDIA1>; + }; + }; + + mm2-dai-link { + link-name = "MultiMedia2"; + + cpu { + sound-dai = <&q6asmdai MSM_FRONTEND_DAI_MULTIMEDIA2>; + }; + }; + + int0-mi2s-dai-link { + link-name = "Internal MI2S Playback"; + + cpu { + sound-dai = <&q6afedai INT0_MI2S_RX>; + }; + + platform { + sound-dai = <&q6routing>; + }; + + codec { + sound-dai = <&pm660l_codec 0>, + <&lpass_codec 0>; + }; + }; + + pri-mi2s-dai-link { + link-name = "Primary MI2S Playback"; + + cpu { + sound-dai = <&q6afedai PRIMARY_MI2S_RX>; + }; + + platform { + sound-dai = <&q6routing>; + }; + + codec { + sound-dai = <&tas2557>; + }; + }; + + int3-mi2s-dai-link { + link-name = "Internal MI2S Capture"; + + cpu { + sound-dai = <&q6afedai INT3_MI2S_TX>; + }; + + platform { + sound-dai = <&q6routing>; + }; + + codec { + sound-dai = <&pm660l_codec 1>, + <&lpass_codec 1>; + }; + }; +}; + &tlmm { gpio-reserved-ranges = <8 4>; @@ -614,6 +775,52 @@ drive-strength = <2>; bias-pull-down; }; + + pri_mi2s_active: pri-mi2s-active-state { + sck-pins { + pins = "gpio12"; + function = "pri_mi2s"; + drive-strength = <8>; + bias-disable; + }; + + ws-pins { + pins = "gpio13"; + function = "pri_mi2s_ws"; + drive-strength = <8>; + bias-disable; + }; + + data-pins { + pins = "gpio14", "gpio15"; + function = "pri_mi2s"; + drive-strength = <8>; + bias-disable; + }; + }; + + pri_mi2s_sleep: pri-mi2s-sleep-state { + sck-pins { + pins = "gpio12"; + function = "pri_mi2s"; + drive-strength = <2>; + bias-pull-down; + }; + + ws-pins { + pins = "gpio13"; + function = "pri_mi2s_ws"; + drive-strength = <2>; + bias-pull-down; + }; + + data-pins { + pins = "gpio14", "gpio15"; + function = "pri_mi2s"; + drive-strength = <2>; + bias-pull-down; + }; + }; }; &usb3 {