[tas58xx] Add TAS58xx audio amplifier (TAS5805M) (#19595)

This commit is contained in:
Remco van Essen
2026-09-30 20:19:23 +02:00
committed by GitHub
parent e742e64fb0
commit 1d69ddc09e
13 changed files with 880 additions and 0 deletions
+1
View File
@@ -554,6 +554,7 @@ esphome/components/syslog/* @clydebarrow
esphome/components/systa_bus/* @Mat931
esphome/components/t6615/* @tylermenezes
esphome/components/tas2780/* @remcom
esphome/components/tas58xx/* @mrtoy-me @remcom
esphome/components/tc74/* @sethgirvan
esphome/components/tca9548a/* @andreashergert1984
esphome/components/tca9555/* @mobrembski
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@mrtoy-me", "@remcom"]
+176
View File
@@ -0,0 +1,176 @@
from dataclasses import dataclass
import logging
from esphome import automation, pins
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.audio_dac import AudioDac
import esphome.config_validation as cv
from esphome.const import CONF_ENABLE_PIN, CONF_ID, CONF_MODEL
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["i2c"]
CONF_ANALOG_GAIN = "analog_gain"
CONF_DAC_MODE = "dac_mode"
CONF_IGNORE_ENABLE_PIN_WARNING = "ignore_enable_pin_warning"
CONF_MIXER_MODE = "mixer_mode"
CONF_VOLUME_MIN_DB = "volume_min_db"
CONF_VOLUME_MAX_DB = "volume_max_db"
CONF_TAS58XX_ID = "tas58xx_id"
tas58xx_ns = cg.esphome_ns.namespace("tas58xx")
TAS58xx = tas58xx_ns.class_("TAS58xx", AudioDac, cg.PollingComponent, i2c.I2CDevice)
DacMode = tas58xx_ns.enum("DacMode")
DAC_MODES = {
"btl": DacMode.DAC_MODE_BTL,
"pbtl": DacMode.DAC_MODE_PBTL,
}
MixerMode = tas58xx_ns.enum("MixerMode")
MIXER_MODES = {
"stereo": MixerMode.MIXER_MODE_STEREO,
"stereo_inverse": MixerMode.MIXER_MODE_STEREO_INVERSE,
"mono": MixerMode.MIXER_MODE_MONO,
"left": MixerMode.MIXER_MODE_LEFT,
"right": MixerMode.MIXER_MODE_RIGHT,
}
@dataclass(frozen=True)
class Model:
"""Limits of one model. The C++ side of a model is its ModelInfo constant."""
model_info: MockObj
analog_gain_min_db: float
volume_min_db: float
volume_max_db: float
default_address: int
MODELS: dict[str, Model] = {
"tas5805m": Model(
model_info=tas58xx_ns.TAS5805M_MODEL,
analog_gain_min_db=-15.5,
volume_min_db=-103.0,
volume_max_db=24.0,
default_address=0x2D,
),
}
def _analog_gain_validator(model: Model):
range_validator = cv.All(
cv.decibel, cv.float_range(min=model.analog_gain_min_db, max=0.0)
)
def validator(value: float) -> float:
value = range_validator(value)
if value * 2 != int(value * 2):
raise cv.Invalid("analog_gain must be a multiple of 0.5 dB")
return value
return validator
def _model_schema(model: Model) -> cv.Schema:
volume_validator = cv.All(
cv.decibel,
cv.float_range(min=model.volume_min_db, max=model.volume_max_db),
)
return (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(TAS58xx),
cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_IGNORE_ENABLE_PIN_WARNING, default=False): cv.boolean,
cv.Optional(
CONF_ANALOG_GAIN, default=model.analog_gain_min_db
): _analog_gain_validator(model),
cv.Optional(CONF_DAC_MODE, default="btl"): cv.enum(
DAC_MODES, lower=True
),
cv.Optional(CONF_MIXER_MODE, default="stereo"): cv.enum(
MIXER_MODES, lower=True
),
cv.Optional(
CONF_VOLUME_MIN_DB, default=model.volume_min_db
): volume_validator,
cv.Optional(
CONF_VOLUME_MAX_DB, default=model.volume_max_db
): volume_validator,
}
)
.extend(cv.polling_component_schema("1s"))
.extend(i2c.i2c_device_schema(model.default_address))
)
def _validate_config(config: ConfigType) -> ConfigType:
if config[CONF_VOLUME_MIN_DB] >= config[CONF_VOLUME_MAX_DB]:
raise cv.Invalid(f"{CONF_VOLUME_MIN_DB} must be less than {CONF_VOLUME_MAX_DB}")
if config[CONF_DAC_MODE] == "pbtl" and config[CONF_MIXER_MODE] in (
"stereo",
"stereo_inverse",
):
raise cv.Invalid(
f"{CONF_DAC_MODE} 'pbtl' drives a single speaker; use {CONF_MIXER_MODE} 'mono', 'left' or 'right'"
)
if CONF_ENABLE_PIN in config:
if config[CONF_IGNORE_ENABLE_PIN_WARNING]:
raise cv.Invalid(
f"{CONF_IGNORE_ENABLE_PIN_WARNING} only applies when {CONF_ENABLE_PIN} is not set"
)
elif not config[CONF_IGNORE_ENABLE_PIN_WARNING]:
# Without PDN high the device does not answer on I2C, and setup only reports an I2C failure
_LOGGER.warning(
"%s: %s not configured - if PDN (power down) is not hardwired high then add %s. "
"Set %s: true to hide this warning",
config[CONF_ID],
CONF_ENABLE_PIN,
CONF_ENABLE_PIN,
CONF_IGNORE_ENABLE_PIN_WARNING,
)
return config
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{name: _model_schema(model) for name, model in MODELS.items()},
key=CONF_MODEL,
lower=True,
),
_validate_config,
)
TAS58XX_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(TAS58xx)})
for _name, _call in (
("tas58xx.activate", "activate()"),
("tas58xx.deactivate", "deactivate()"),
):
automation.register_apply_action(
_name, TAS58XX_ACTION_SCHEMA, automation.ApplyCall(_call)
)
async def to_code(config: ConfigType) -> None:
model = MODELS[config[CONF_MODEL]]
var = cg.new_Pvariable(config[CONF_ID], cg.RawExpression(f"&{model.model_info}"))
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_analog_gain(config[CONF_ANALOG_GAIN]))
cg.add(var.set_dac_mode(config[CONF_DAC_MODE]))
cg.add(var.set_mixer_mode(config[CONF_MIXER_MODE]))
cg.add(var.set_volume_min_db(config[CONF_VOLUME_MIN_DB]))
cg.add(var.set_volume_max_db(config[CONF_VOLUME_MAX_DB]))
if enable_pin_config := config.get(CONF_ENABLE_PIN):
enable_pin = await cg.gpio_pin_expression(enable_pin_config)
cg.add(var.set_enable_pin(enable_pin))
@@ -0,0 +1,51 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_PROBLEM, ENTITY_CATEGORY_DIAGNOSTIC
from esphome.types import ConfigType
from .audio_dac import CONF_TAS58XX_ID, TAS58xx, tas58xx_ns
CONF_HAVE_FAULT = "have_fault"
# Each name matches a FaultSensor value in tas58xx.h
FAULT_SENSORS = (
"left_channel_dc_fault",
"right_channel_dc_fault",
"left_channel_over_current",
"right_channel_over_current",
"otp_crc_check",
"bq_write_failed",
"clock_fault",
"pvdd_over_voltage",
"pvdd_under_voltage",
"over_temp_shutdown",
"over_temp_warning",
)
FaultSensor = tas58xx_ns.enum("FaultSensor")
_FAULT_SCHEMA = binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_PROBLEM,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_TAS58XX_ID): cv.use_id(TAS58xx),
cv.Optional(CONF_HAVE_FAULT): _FAULT_SCHEMA,
**{cv.Optional(key): _FAULT_SCHEMA for key in FAULT_SENSORS},
}
)
async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_TAS58XX_ID])
if sensor_config := config.get(CONF_HAVE_FAULT):
sens = await binary_sensor.new_binary_sensor(sensor_config)
cg.add(parent.set_have_fault_binary_sensor(sens))
for key in FAULT_SENSORS:
if sensor_config := config.get(key):
sens = await binary_sensor.new_binary_sensor(sensor_config)
fault = getattr(FaultSensor, f"FAULT_SENSOR_{key.upper()}")
cg.add(parent.set_fault_binary_sensor(fault, sens))
@@ -0,0 +1,84 @@
#include "tas58xx.h"
#include "esphome/core/hal.h"
namespace esphome::tas58xx {
// Remainder of the startup sequence from TI PurePath Console, run after the reset. Register 0x00 selects the page.
// Registers 0x46, 0x7D, 0x7E and page 1 register 0x51 are not documented in the datasheet.
static const uint8_t STARTUP_SEQUENCE[][2] PROGMEM = {
{0x03, 0x00}, // DEVICE_CTRL_2: deep sleep
{0x46, 0x01},
{0x03, 0x02}, // DEVICE_CTRL_2: Hi-Z
// The I2C address is latched at power up, after which the ADR pin can report faults
{0x61, 0x0B}, // ADR_PIN_CONFIG: FAULTZ
{0x60, 0x01}, // ADR_PIN_CTRL: output
{0x7D, 0x11},
{0x7E, 0xFF},
{0x00, 0x01},
{0x51, 0x05},
{0x00, 0x00},
};
static const LogString *model_name() { return LOG_STR("TAS5805M"); }
// An if chain rather than a switch: a switch table would land in rodata, which is RAM on ESP8266.
static const LogString *fault_name(uint8_t index) {
if (index == 0)
return LOG_STR("Right channel over current");
if (index == 1)
return LOG_STR("Left channel over current");
if (index == 2)
return LOG_STR("Right channel DC fault");
if (index == 3)
return LOG_STR("Left channel DC fault");
if (index == 8)
return LOG_STR("PVDD under voltage");
if (index == 9)
return LOG_STR("PVDD over voltage");
if (index == 10)
return LOG_STR("Clock fault");
if (index == 14)
return LOG_STR("BQ write failed");
if (index == 15)
return LOG_STR("OTP CRC check error");
if (index == 16)
return LOG_STR("Over temperature shutdown");
if (index == 26)
return LOG_STR("Over temperature warning");
return LOG_STR("Unknown fault");
}
const ModelInfo TAS5805M_MODEL = {
.name = model_name,
.startup_sequence = STARTUP_SEQUENCE,
.startup_sequence_length = sizeof(STARTUP_SEQUENCE) / sizeof(STARTUP_SEQUENCE[0]),
.mixer_book = 0x8C,
.mixer_page = 0x29,
.mixer_register = 0x18,
// The clock fault is left out of the log and have_fault: it is set whenever the I2S clock stops, which is normal
.fault_error_mask = 0x0001C30F,
.fault_warning_mask = 0x04000000,
// DC and over current faults keep the output off until cleared (datasheet 7.5.3.3.1, 7.5.3.3.2). They are not
// cleared automatically: a DC fault re-trips only after 570 ms, so a clear on every poll would pass DC to the
// speaker.
.fault_output_off_mask = 0x0000000F,
.fault_latched_mask = 0x0401C70F,
.fault_name = fault_name,
.fault_sensor_bits =
{
3, // FAULT_SENSOR_LEFT_CHANNEL_DC_FAULT
2, // FAULT_SENSOR_RIGHT_CHANNEL_DC_FAULT
1, // FAULT_SENSOR_LEFT_CHANNEL_OVER_CURRENT
0, // FAULT_SENSOR_RIGHT_CHANNEL_OVER_CURRENT
15, // FAULT_SENSOR_OTP_CRC_CHECK
14, // FAULT_SENSOR_BQ_WRITE_FAILED
10, // FAULT_SENSOR_CLOCK_FAULT
9, // FAULT_SENSOR_PVDD_OVER_VOLTAGE
8, // FAULT_SENSOR_PVDD_UNDER_VOLTAGE
16, // FAULT_SENSOR_OVER_TEMP_SHUTDOWN
26, // FAULT_SENSOR_OVER_TEMP_WARNING
},
};
} // namespace esphome::tas58xx
+337
View File
@@ -0,0 +1,337 @@
#include "tas58xx.h"
#include <cmath>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::tas58xx {
static const char *const TAG = "tas58xx";
static constexpr uint8_t TAS58XX_PAGE_SELECT = 0x00; // Page Select, in every book
static constexpr uint8_t TAS58XX_BOOK_SELECT = 0x7F; // Book Select, on page 0 of every book
static constexpr uint8_t TAS58XX_BOOK_CONTROL = 0x00;
static constexpr uint8_t TAS58XX_PAGE_0 = 0x00;
/* BOOK 0x00, PAGE 0x00 */
static constexpr uint8_t TAS58XX_RESET_CTRL = 0x01;
static constexpr uint8_t TAS58XX_RESET_CTRL_ALL = 0x11; // Reset DSP/control port and registers
static constexpr uint8_t TAS58XX_DEVICE_CTRL_1 = 0x02;
static constexpr uint8_t TAS58XX_DEVICE_CTRL_1_PBTL = (1 << 2);
static constexpr uint8_t TAS58XX_DEVICE_CTRL_2 = 0x03;
static constexpr uint8_t TAS58XX_DEVICE_CTRL_2_MUTE = (1 << 3);
static constexpr uint8_t TAS58XX_CTRL_STATE_MASK = 0x03; // DEVICE_CTRL_2 and POWER_STATE
static constexpr uint8_t TAS58XX_CTRL_STATE_DEEP_SLEEP = 0x00;
static constexpr uint8_t TAS58XX_CTRL_STATE_SLEEP = 0x01;
static constexpr uint8_t TAS58XX_CTRL_STATE_HIZ = 0x02;
static constexpr uint8_t TAS58XX_CTRL_STATE_PLAY = 0x03;
static constexpr uint8_t TAS58XX_CTRL_STATE_UNKNOWN = 0xFF; // Not a device value, forces the next update to act
static constexpr uint8_t TAS58XX_DIG_VOL = 0x4C; // 0x00 = +24 dB to 0xFE = -103 dB in 0.5 dB steps, 0xFF = mute
static constexpr uint8_t TAS58XX_DIG_VOL_0DB = 0x30;
static constexpr uint8_t TAS58XX_DIG_VOL_MINUS_103DB = 0xFE;
static constexpr uint8_t TAS58XX_AGAIN = 0x54; // 0x00 = 0 dB to 0x1F = -15.5 dB in 0.5 dB steps
static constexpr uint8_t TAS58XX_AGAIN_MINUS_15_5DB = 0x1F;
static constexpr uint8_t TAS58XX_POWER_STATE = 0x68;
static constexpr uint8_t TAS58XX_CHAN_FAULT = 0x70; // GLOBAL_FAULT1, GLOBAL_FAULT2 and OT_WARNING follow
static constexpr uint8_t TAS58XX_FAULT_REGISTER_COUNT = 4;
static constexpr uint8_t TAS58XX_FAULT_CLEAR = 0x78;
static constexpr uint8_t TAS58XX_FAULT_CLEAR_ANALOG = 0x80;
/* Input mixer, four 9.23 fixed point big endian coefficients at a location that differs per model */
static constexpr uint8_t TAS58XX_MIXER_COEFFICIENT_SIZE = 4;
// Only the second byte differs between mute, -6 dB and 0 dB
static constexpr uint8_t TAS58XX_MIXER_MUTE = 0x00;
static constexpr uint8_t TAS58XX_MIXER_MINUS_6DB = 0x40;
static constexpr uint8_t TAS58XX_MIXER_0DB = 0x80;
static constexpr uint32_t PDN_LOW_MS = 1;
static constexpr uint32_t PDN_TO_I2C_MS = 5; // Minimum time from PDN high to I2C access
static constexpr uint32_t RESET_SETTLE_MS = 5;
static const LogString *power_state_name(uint8_t state) {
if (state == TAS58XX_CTRL_STATE_DEEP_SLEEP)
return LOG_STR("Deep sleep");
if (state == TAS58XX_CTRL_STATE_SLEEP)
return LOG_STR("Sleep");
if (state == TAS58XX_CTRL_STATE_HIZ)
return LOG_STR("Hi-Z");
return LOG_STR("Play");
}
void TAS58xx::setup() {
if (this->enable_pin_ != nullptr) {
this->enable_pin_->setup();
this->enable_pin_->digital_write(false);
delay(PDN_LOW_MS);
this->enable_pin_->digital_write(true);
delay(PDN_TO_I2C_MS);
}
if (!this->init_()) {
this->mark_failed();
}
}
bool TAS58xx::select_book_page_(uint8_t book, uint8_t page) {
// The book can only be changed from page 0
return this->write_byte(TAS58XX_PAGE_SELECT, TAS58XX_PAGE_0) && this->write_byte(TAS58XX_BOOK_SELECT, book) &&
this->write_byte(TAS58XX_PAGE_SELECT, page);
}
bool TAS58xx::init_() {
// Header of every TI PurePath Console export: select book 0 in case the MCU restarted without a PDN toggle,
// silence the output with Hi-Z, reset the DSP and the control registers, then return to Hi-Z
if (!this->select_book_page_(TAS58XX_BOOK_CONTROL, TAS58XX_PAGE_0) ||
!this->write_byte(TAS58XX_DEVICE_CTRL_2, TAS58XX_CTRL_STATE_HIZ) ||
!this->write_byte(TAS58XX_RESET_CTRL, TAS58XX_RESET_CTRL_ALL) ||
!this->write_byte(TAS58XX_DEVICE_CTRL_2, TAS58XX_CTRL_STATE_HIZ)) {
ESP_LOGE(TAG, "I2C write failed during reset");
return false;
}
delay(RESET_SETTLE_MS);
for (uint8_t i = 0; i < this->model_->startup_sequence_length; i++) {
const uint8_t *entry = this->model_->startup_sequence[i];
if (!this->write_byte(progmem_read_byte(&entry[0]), progmem_read_byte(&entry[1]))) {
ESP_LOGE(TAG, "I2C write failed during init");
return false;
}
}
// Setters are public, so keep the value inside the register range
const uint8_t again =
static_cast<uint8_t>(lroundf(clamp(-this->analog_gain_db_ * 2.0f, 0.0f, float{TAS58XX_AGAIN_MINUS_15_5DB})));
if (!this->write_byte(TAS58XX_DEVICE_CTRL_1, this->dac_mode_ == DAC_MODE_PBTL ? TAS58XX_DEVICE_CTRL_1_PBTL : 0) ||
!this->write_byte(TAS58XX_AGAIN, again) || !this->write_volume_() ||
!this->write_ctrl_state_(TAS58XX_CTRL_STATE_PLAY, this->is_muted_) ||
!this->write_byte(TAS58XX_FAULT_CLEAR, TAS58XX_FAULT_CLEAR_ANALOG)) {
ESP_LOGE(TAG, "I2C write failed during init");
return false;
}
// The mixer is written once the I2S clocks run, see on_audio_started() and update()
this->power_state_ = TAS58XX_CTRL_STATE_UNKNOWN;
this->mixer_written_ = false;
return true;
}
void TAS58xx::activate() {
if (this->is_failed())
return;
ESP_LOGD(TAG, "[0x%02X] Activating", this->address_);
// A failed mixer write can leave the device in another book
if (!this->select_book_page_(TAS58XX_BOOK_CONTROL, TAS58XX_PAGE_0)) {
ESP_LOGE(TAG, "Failed to select the control port");
return;
}
// Also the way to restart the output after a DC or over current fault
if (!this->write_byte(TAS58XX_FAULT_CLEAR, TAS58XX_FAULT_CLEAR_ANALOG)) {
ESP_LOGW(TAG, "Failed to clear faults");
}
const bool muted = this->is_muted_;
// Leaving deep sleep needs this sequence to reset the internal state machine, see datasheet 7.4.5
if (this->ctrl_state_ == TAS58XX_CTRL_STATE_DEEP_SLEEP &&
!(this->write_ctrl_state_(TAS58XX_CTRL_STATE_HIZ, muted) &&
this->write_ctrl_state_(TAS58XX_CTRL_STATE_DEEP_SLEEP, muted) &&
this->write_ctrl_state_(TAS58XX_CTRL_STATE_HIZ, muted))) {
return;
}
this->write_ctrl_state_(TAS58XX_CTRL_STATE_PLAY, muted);
}
void TAS58xx::deactivate() {
if (this->is_failed())
return;
ESP_LOGD(TAG, "[0x%02X] Deactivating", this->address_);
if (!this->select_book_page_(TAS58XX_BOOK_CONTROL, TAS58XX_PAGE_0)) {
ESP_LOGE(TAG, "Failed to select the control port");
return;
}
// The DSP, and with it the mixer, stays active in deep sleep
this->write_ctrl_state_(TAS58XX_CTRL_STATE_DEEP_SLEEP, this->is_muted_);
}
bool TAS58xx::write_ctrl_state_(uint8_t state, bool muted) {
if (!this->write_byte(TAS58XX_DEVICE_CTRL_2, state | (muted ? TAS58XX_DEVICE_CTRL_2_MUTE : 0))) {
ESP_LOGE(TAG, "Failed to write DEVICE_CTRL_2");
return false;
}
this->ctrl_state_ = state;
return true;
}
// Returns false if the fault registers could not be read
bool TAS58xx::read_faults_() {
uint8_t fault_registers[TAS58XX_FAULT_REGISTER_COUNT];
if (!this->read_bytes(TAS58XX_CHAN_FAULT, fault_registers, sizeof(fault_registers)))
return false;
uint32_t faults = 0;
for (uint8_t reg = 0; reg < TAS58XX_FAULT_REGISTER_COUNT; reg++)
faults |= uint32_t{fault_registers[reg]} << (reg * 8);
const ModelInfo &model = *this->model_;
const uint32_t active = faults & (model.fault_error_mask | model.fault_warning_mask);
// Clearing makes a lasting condition latch again on every poll, so only log changes
const uint32_t changed = active ^ this->logged_faults_;
for (uint8_t index = 0; index < 32; index++) {
const uint32_t mask = uint32_t{1} << index;
if (!(changed & mask))
continue;
const LogString *name = model.fault_name(index);
if (!(active & mask)) {
ESP_LOGI(TAG, "[0x%02X] %s cleared", this->address_, LOG_STR_ARG(name));
} else if (model.fault_error_mask & mask) {
ESP_LOGE(TAG, "[0x%02X] %s", this->address_, LOG_STR_ARG(name));
} else {
ESP_LOGW(TAG, "[0x%02X] %s", this->address_, LOG_STR_ARG(name));
}
}
if (changed & active & model.fault_output_off_mask) {
ESP_LOGW(TAG, "[0x%02X] Output stays off: fix the cause, then call tas58xx.activate or power cycle the amplifier",
this->address_);
}
this->logged_faults_ = active;
#ifdef USE_BINARY_SENSOR
if (this->have_fault_binary_sensor_ != nullptr)
this->have_fault_binary_sensor_->publish_state((active & model.fault_error_mask) != 0);
for (uint8_t fault = 0; fault < FAULT_SENSOR_COUNT; fault++) {
if (this->fault_binary_sensors_[fault] != nullptr)
this->fault_binary_sensors_[fault]->publish_state(faults & (uint32_t{1} << model.fault_sensor_bits[fault]));
}
#endif
// Latched fault bits stay set after the condition is gone and only report it; clear those so the bits, and the
// sensors, follow the current state. The clear register resets all faults at once, so hold off while the output
// is off.
if ((faults & model.fault_latched_mask) && !(active & model.fault_output_off_mask) &&
!this->write_byte(TAS58XX_FAULT_CLEAR, TAS58XX_FAULT_CLEAR_ANALOG)) {
ESP_LOGW(TAG, "Failed to clear faults");
}
return true;
}
void TAS58xx::on_audio_started() {
// DSP coefficients can only be written with a running I2S clock (datasheet 7.5.3.1) and are kept when it stops
if (this->is_failed() || this->mixer_written_)
return;
if (!this->write_mixer_()) {
ESP_LOGW(TAG, "Failed to write mixer");
}
}
void TAS58xx::update() {
uint8_t power_state;
if (!this->read_faults_() || !this->read_byte(TAS58XX_POWER_STATE, &power_state)) {
this->status_set_warning(LOG_STR("Failed to read status"));
return;
}
this->status_clear_warning();
power_state &= TAS58XX_CTRL_STATE_MASK;
if (power_state != this->power_state_) {
ESP_LOGD(TAG, "[0x%02X] Power state: %s", this->address_, LOG_STR_ARG(power_state_name(power_state)));
this->power_state_ = power_state;
}
// The device only plays with a running I2S clock. Fallback for speakers that do not call on_audio_started();
// a failed write is retried on the next update.
if (power_state == TAS58XX_CTRL_STATE_PLAY && !this->mixer_written_ && !this->write_mixer_()) {
ESP_LOGW(TAG, "Failed to write mixer");
}
}
void TAS58xx::dump_config() {
const LogString *mixer_mode;
if (this->mixer_mode_ == MIXER_MODE_STEREO_INVERSE) {
mixer_mode = LOG_STR("Stereo inverse");
} else if (this->mixer_mode_ == MIXER_MODE_MONO) {
mixer_mode = LOG_STR("Mono");
} else if (this->mixer_mode_ == MIXER_MODE_LEFT) {
mixer_mode = LOG_STR("Left");
} else if (this->mixer_mode_ == MIXER_MODE_RIGHT) {
mixer_mode = LOG_STR("Right");
} else {
mixer_mode = LOG_STR("Stereo");
}
ESP_LOGCONFIG(TAG,
"Audio Amplifier:\n"
" Model: %s",
LOG_STR_ARG(this->model_->name()));
LOG_I2C_DEVICE(this);
LOG_PIN(" Enable Pin: ", this->enable_pin_);
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG,
" Analog Gain: %.1f dB\n"
" DAC Mode: %s\n"
" Mixer Mode: %s\n"
" Volume Range: %.1f dB - %.1f dB",
this->analog_gain_db_,
this->dac_mode_ == DAC_MODE_PBTL ? LOG_STR_LITERAL("PBTL") : LOG_STR_LITERAL("BTL"),
LOG_STR_ARG(mixer_mode), this->volume_min_db_, this->volume_max_db_);
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Any Fault", this->have_fault_binary_sensor_);
for (auto *sensor : this->fault_binary_sensors_) {
LOG_BINARY_SENSOR(" ", "Fault", sensor);
}
#endif
}
bool TAS58xx::set_mute_(bool muted) {
if (!this->write_ctrl_state_(this->ctrl_state_, muted))
return false;
this->is_muted_ = muted;
return true;
}
bool TAS58xx::set_volume(float volume) {
float previous = this->volume_;
this->volume_ = clamp(volume, 0.0f, 1.0f);
if (!this->write_volume_()) {
this->volume_ = previous;
return false;
}
return true;
}
bool TAS58xx::write_volume_() {
// volume 0.0 maps to volume_min_db_, which is only close to silence at -103 dB
const float volume_db = std::lerp(this->volume_min_db_, this->volume_max_db_, this->volume_);
const uint8_t dig_vol = static_cast<uint8_t>(
lroundf(clamp(TAS58XX_DIG_VOL_0DB - volume_db * 2.0f, 0.0f, float{TAS58XX_DIG_VOL_MINUS_103DB})));
ESP_LOGV(TAG, "Setting volume to 0x%02X", dig_vol);
return this->write_byte(TAS58XX_DIG_VOL, dig_vol);
}
bool TAS58xx::write_mixer_() {
uint8_t left_to_left = TAS58XX_MIXER_0DB;
uint8_t right_to_left = TAS58XX_MIXER_MUTE;
uint8_t left_to_right = TAS58XX_MIXER_MUTE;
uint8_t right_to_right = TAS58XX_MIXER_0DB;
if (this->mixer_mode_ == MIXER_MODE_STEREO_INVERSE) {
left_to_left = TAS58XX_MIXER_MUTE;
right_to_left = TAS58XX_MIXER_0DB;
left_to_right = TAS58XX_MIXER_0DB;
right_to_right = TAS58XX_MIXER_MUTE;
} else if (this->mixer_mode_ == MIXER_MODE_MONO) {
left_to_left = TAS58XX_MIXER_MINUS_6DB;
right_to_left = TAS58XX_MIXER_MINUS_6DB;
left_to_right = TAS58XX_MIXER_MINUS_6DB;
right_to_right = TAS58XX_MIXER_MINUS_6DB;
} else if (this->mixer_mode_ == MIXER_MODE_LEFT) {
left_to_right = TAS58XX_MIXER_0DB;
right_to_right = TAS58XX_MIXER_MUTE;
} else if (this->mixer_mode_ == MIXER_MODE_RIGHT) {
left_to_left = TAS58XX_MIXER_MUTE;
right_to_left = TAS58XX_MIXER_0DB;
}
const uint8_t coefficients[4 * TAS58XX_MIXER_COEFFICIENT_SIZE] = {0, left_to_left, 0, 0, 0, right_to_left, 0, 0,
0, left_to_right, 0, 0, 0, right_to_right, 0, 0};
bool ok = this->select_book_page_(this->model_->mixer_book, this->model_->mixer_page) &&
this->write_bytes(this->model_->mixer_register, coefficients, sizeof(coefficients));
// Always return to the control port, even after a failed write
ok = this->select_book_page_(TAS58XX_BOOK_CONTROL, TAS58XX_PAGE_0) && ok;
this->mixer_written_ = ok;
return ok;
}
} // namespace esphome::tas58xx
+140
View File
@@ -0,0 +1,140 @@
#pragma once
#include <array>
#include "esphome/components/audio_dac/audio_dac.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
namespace esphome::tas58xx {
enum DacMode : uint8_t {
DAC_MODE_BTL = 0, // Bridge tied load, two speakers
DAC_MODE_PBTL = 1, // Parallel bridge tied load, one speaker
};
enum MixerMode : uint8_t {
MIXER_MODE_STEREO = 0,
MIXER_MODE_STEREO_INVERSE,
MIXER_MODE_MONO,
MIXER_MODE_LEFT,
MIXER_MODE_RIGHT,
};
/// Fault binary sensors that map to a single fault bit. The Python FAULT_SENSORS list uses the same names.
enum FaultSensor : uint8_t {
FAULT_SENSOR_LEFT_CHANNEL_DC_FAULT = 0,
FAULT_SENSOR_RIGHT_CHANNEL_DC_FAULT,
FAULT_SENSOR_LEFT_CHANNEL_OVER_CURRENT,
FAULT_SENSOR_RIGHT_CHANNEL_OVER_CURRENT,
FAULT_SENSOR_OTP_CRC_CHECK,
FAULT_SENSOR_BQ_WRITE_FAILED,
FAULT_SENSOR_CLOCK_FAULT,
FAULT_SENSOR_PVDD_OVER_VOLTAGE,
FAULT_SENSOR_PVDD_UNDER_VOLTAGE,
FAULT_SENSOR_OVER_TEMP_SHUTDOWN,
FAULT_SENSOR_OVER_TEMP_WARNING,
FAULT_SENSOR_COUNT,
};
/// Everything that differs between models of the family. One constant instance exists per model, see
/// model_*.cpp, and each TAS58xx instance points to the one for its model.
///
/// Fault bits are packed into a 32 bit word: CHAN_FAULT, GLOBAL_FAULT1, GLOBAL_FAULT2 and OT_WARNING, one byte
/// each, from low to high. A bit index is register * 8 + bit.
struct ModelInfo {
const LogString *(*name)();
/// Remainder of the startup sequence, run after the reset, as {register, value} pairs in PROGMEM
const uint8_t (*startup_sequence)[2];
uint8_t startup_sequence_length;
/// Location of the four 9.23 fixed point input mixer coefficients: LEFT_TO_LEFT, RIGHT_TO_LEFT, LEFT_TO_RIGHT,
/// RIGHT_TO_RIGHT
uint8_t mixer_book;
uint8_t mixer_page;
uint8_t mixer_register;
/// Faults logged as errors and reported by have_fault
uint32_t fault_error_mask;
/// Faults logged as warnings
uint32_t fault_warning_mask;
/// Faults that keep the output off until cleared by activate()
uint32_t fault_output_off_mask;
/// Faults that stay set after the condition is gone and are cleared after each read
uint32_t fault_latched_mask;
const LogString *(*fault_name)(uint8_t index);
/// Bit index for each FaultSensor
uint8_t fault_sensor_bits[FAULT_SENSOR_COUNT];
};
extern const ModelInfo TAS5805M_MODEL;
class TAS58xx : public audio_dac::AudioDac, public PollingComponent, public i2c::I2CDevice {
public:
explicit TAS58xx(const ModelInfo *model) : model_(model) {}
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::IO; }
void update() override;
/// Clear faults, leave deep sleep and switch to play; the device waits in Hi-Z until an I2S clock is present.
/// This is also how the output is restarted after a DC or over current fault.
void activate();
/// Switch to deep sleep, the lowest power state that keeps I2C and the DSP running.
void deactivate();
bool set_mute_off() override { return this->set_mute_(false); }
bool set_mute_on() override { return this->set_mute_(true); }
bool set_volume(float volume) override;
bool is_muted() override { return this->is_muted_; }
float volume() override { return this->volume_; }
void on_audio_started() override;
void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; }
void set_analog_gain(float analog_gain_db) { this->analog_gain_db_ = analog_gain_db; }
void set_dac_mode(DacMode dac_mode) { this->dac_mode_ = dac_mode; }
void set_mixer_mode(MixerMode mixer_mode) { this->mixer_mode_ = mixer_mode; }
void set_volume_min_db(float volume_min_db) { this->volume_min_db_ = volume_min_db; }
void set_volume_max_db(float volume_max_db) { this->volume_max_db_ = volume_max_db; }
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(have_fault)
void set_fault_binary_sensor(FaultSensor fault, binary_sensor::BinarySensor *sensor) {
this->fault_binary_sensors_[fault] = sensor;
}
#endif
protected:
bool select_book_page_(uint8_t book, uint8_t page);
bool init_();
bool write_ctrl_state_(uint8_t state, bool muted);
bool set_mute_(bool muted);
bool write_volume_();
bool write_mixer_();
bool read_faults_();
const ModelInfo *model_;
GPIOPin *enable_pin_{nullptr};
#ifdef USE_BINARY_SENSOR
std::array<binary_sensor::BinarySensor *, FAULT_SENSOR_COUNT> fault_binary_sensors_{};
#endif
float volume_{0};
float analog_gain_db_{-15.5f};
float volume_min_db_{-103.0f};
float volume_max_db_{24.0f};
uint32_t logged_faults_{0}; // Fault bits as last logged, packed like the fault masks
DacMode dac_mode_{DAC_MODE_BTL};
MixerMode mixer_mode_{MIXER_MODE_STEREO};
uint8_t ctrl_state_{0};
uint8_t power_state_{0xFF}; // Last POWER_STATE seen by update(), 0xFF until the first read
bool mixer_written_{false}; // Mixer written since the last reset
};
} // namespace esphome::tas58xx
@@ -0,0 +1,7 @@
speaker:
- platform: i2s_audio
id: tas58xx_speaker
i2s_audio_id: i2s_audio_bus
audio_dac: tas58xx_amp
dac_type: external
i2s_dout_pin: GPIO13
+61
View File
@@ -0,0 +1,61 @@
audio_dac:
- platform: tas58xx
model: tas5805m
id: tas58xx_amp
i2c_id: i2c_bus
address: 0x2D
enable_pin: GPIO12
analog_gain: -15.5dB
dac_mode: btl
mixer_mode: stereo
volume_min_db: -60dB
volume_max_db: 0dB
update_interval: 1s
- platform: tas58xx
model: TAS5805M
id: tas58xx_amp_2
i2c_id: i2c_bus
address: 0x2C
ignore_enable_pin_warning: true
esphome:
on_boot:
- tas58xx.deactivate: tas58xx_amp
- tas58xx.activate:
id: tas58xx_amp
- tas58xx.activate: tas58xx_amp_2
binary_sensor:
- platform: tas58xx
tas58xx_id: tas58xx_amp
have_fault:
name: Any Fault
left_channel_dc_fault:
name: Left Channel DC Fault
right_channel_dc_fault:
name: Right Channel DC Fault
left_channel_over_current:
name: Left Channel Over Current
right_channel_over_current:
name: Right Channel Over Current
otp_crc_check:
name: OTP CRC Check
bq_write_failed:
name: BQ Write Failed
clock_fault:
name: Clock Fault
pvdd_over_voltage:
name: PVDD Over Voltage
pvdd_under_voltage:
name: PVDD Under Voltage
over_temp_shutdown:
name: Over Temperature Shutdown
over_temp_warning:
name: Over Temperature Warning
- platform: tas58xx
tas58xx_id: tas58xx_amp_2
have_fault:
name: Second Amp Any Fault
left_channel_dc_fault:
name: Second Amp Left Channel DC Fault
@@ -0,0 +1,5 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
i2s_audio: !include ../../test_build_components/common/i2s_audio/esp32-idf.yaml
tas58xx: !include common.yaml
tas58xx_speaker: !include common-speaker.yaml
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
tas58xx: !include common.yaml
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
tas58xx: !include common.yaml
@@ -0,0 +1,11 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
audio_dac:
- platform: tas58xx
model: tas5805m
id: tas58xx_pbtl
i2c_id: i2c_bus
dac_mode: PBTL
mixer_mode: mono
analog_gain: -3.5dB