mirror of
https://github.com/esphome/esphome.git
synced 2026-10-01 08:50:22 +00:00
[tas58xx] Add TAS58xx audio amplifier (TAS5805M) (#19595)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@mrtoy-me", "@remcom"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user