[TAS2780] Add TAS2780 audio amplifier (#15708)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Remco van Essen
2026-09-24 15:45:51 +01:00
committed by GitHub
co-authored by Claude Sonnet 4.6 pre-commit-ci-lite[bot] J. Nick Koston
parent f1f83cda07
commit d2fefe2299
9 changed files with 621 additions and 0 deletions
+1
View File
@@ -549,6 +549,7 @@ esphome/components/sx127x/* @swoboda1337
esphome/components/sy6970/* @linkedupbits
esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes
esphome/components/tas2780/* @remcom
esphome/components/tc74/* @sethgirvan
esphome/components/tca9548a/* @andreashergert1984
esphome/components/tca9555/* @mobrembski
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@remcom"]
+108
View File
@@ -0,0 +1,108 @@
from esphome import automation
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_CHANNEL, CONF_ID, CONF_POWER_MODE
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
tas2780_ns = cg.esphome_ns.namespace("tas2780")
TAS2780 = tas2780_ns.class_("TAS2780", AudioDac, cg.PollingComponent, i2c.I2CDevice)
ChannelSelect = tas2780_ns.enum("ChannelSelect")
CHANNELS = {
"mono": ChannelSelect.MONO_DWN_MIX,
"left": ChannelSelect.LEFT_CHANNEL,
"right": ChannelSelect.RIGHT_CHANNEL,
}
CONF_VOL_RANGE_MIN = "vol_range_min"
CONF_VOL_RANGE_MAX = "vol_range_max"
CONF_AMP_LEVEL = "amp_level"
_AMP_LEVEL = cv.int_range(min=0, max=20)
_POWER_MODE = cv.int_range(min=0, max=3)
def _validate_vol_range(config: ConfigType) -> ConfigType:
if config[CONF_VOL_RANGE_MIN] >= config[CONF_VOL_RANGE_MAX]:
raise cv.Invalid(f"{CONF_VOL_RANGE_MIN} must be less than {CONF_VOL_RANGE_MAX}")
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(TAS2780),
cv.Optional(CONF_AMP_LEVEL, default=8): _AMP_LEVEL,
cv.Optional(CONF_POWER_MODE, default=2): _POWER_MODE,
cv.Optional(CONF_VOL_RANGE_MIN, default=0.3): cv.percentage,
cv.Optional(CONF_VOL_RANGE_MAX, default=1.0): cv.percentage,
cv.Optional(CONF_CHANNEL, default="mono"): cv.enum(CHANNELS),
}
)
.extend(cv.polling_component_schema("5s"))
.extend(i2c.i2c_device_schema(0x38)),
_validate_vol_range,
)
TAS2780_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(TAS2780)})
for _name, _call in (
("tas2780.deactivate", "deactivate()"),
("tas2780.reset", "reset()"),
):
automation.register_apply_action(
_name, TAS2780_ACTION_SCHEMA, automation.ApplyCall(_call)
)
# Without power_mode the configured mode stays; activate() re-initializes only on a change.
automation.register_apply_action(
"tas2780.activate",
maybe_simple_id(
{
cv.GenerateID(): cv.use_id(TAS2780),
cv.Optional(CONF_POWER_MODE): cv.templatable(_POWER_MODE),
}
),
automation.ApplyField(CONF_POWER_MODE, "set_power_mode", cg.uint8),
automation.ApplyCall("activate()"),
)
automation.register_apply_action(
"tas2780.update_config",
cv.Schema(
{
cv.GenerateID(): cv.use_id(TAS2780),
cv.Optional(CONF_VOL_RANGE_MIN): cv.templatable(cv.percentage),
cv.Optional(CONF_VOL_RANGE_MAX): cv.templatable(cv.percentage),
cv.Optional(CONF_AMP_LEVEL): cv.templatable(_AMP_LEVEL),
cv.Optional(CONF_CHANNEL): cv.templatable(cv.enum(CHANNELS)),
}
).add_extra(
cv.has_at_least_one_key(
CONF_VOL_RANGE_MIN, CONF_VOL_RANGE_MAX, CONF_AMP_LEVEL, CONF_CHANNEL
)
),
automation.ApplyField(CONF_AMP_LEVEL, "set_amp_level", cg.uint8),
automation.ApplyField(CONF_VOL_RANGE_MIN, "set_vol_range_min", cg.float_),
automation.ApplyField(CONF_VOL_RANGE_MAX, "set_vol_range_max", cg.float_),
automation.ApplyField(CONF_CHANNEL, "set_selected_channel", ChannelSelect),
automation.ApplyCall("apply_config()"),
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_amp_level(config[CONF_AMP_LEVEL]))
cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
cg.add(var.set_vol_range_min(config[CONF_VOL_RANGE_MIN]))
cg.add(var.set_vol_range_max(config[CONF_VOL_RANGE_MAX]))
cg.add(var.set_selected_channel(config[CONF_CHANNEL]))
+406
View File
@@ -0,0 +1,406 @@
#include "tas2780.h"
#include <cmath>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::tas2780 {
static const char *const TAG = "tas2780";
static constexpr uint8_t TAS2780_PAGE_SELECT = 0x00; // Page Select
static constexpr uint8_t TAS2780_PAGE_0 = 0x00; // Page 0
static constexpr uint8_t TAS2780_PAGE_1 = 0x01; // Page 1
static constexpr uint8_t TAS2780_PAGE_FD = 0xFD; // Page 0xFD
/* PAGE 0 */
static constexpr uint8_t TAS2780_SW_RESET = 0x01; // Software Reset
static constexpr uint8_t TAS2780_SW_RESET_CMD = 0x01; // Trigger software reset
static constexpr uint8_t TAS2780_MODE_CTRL = 0x02; // Device operational mode
static constexpr uint8_t TAS2780_MODE_CTRL_MODE_MASK = 0x07;
static constexpr uint8_t TAS2780_MODE_CTRL_MODE_ACTIVE = 0x00;
static constexpr uint8_t TAS2780_MODE_CTRL_MODE_ACTIVE_MUTED = 0x01;
static constexpr uint8_t TAS2780_MODE_CTRL_MODE_SFTW_SHTDWN = 0x02;
static constexpr uint8_t TAS2780_CHNL_0 = 0x03; // Y Bridge and Channel settings
static constexpr uint8_t TAS2780_CHNL_0_CDS_MODE_SHIFT = 6;
static constexpr uint8_t TAS2780_CHNL_0_CDS_MODE_MASK = (0x03 << TAS2780_CHNL_0_CDS_MODE_SHIFT);
static constexpr uint8_t TAS2780_CHNL_0_AMP_LEVEL_SHIFT = 1;
static constexpr uint8_t TAS2780_CHNL_0_AMP_LEVEL_MASK = (0x1F) << TAS2780_CHNL_0_AMP_LEVEL_SHIFT;
static constexpr uint8_t TAS2780_AMP_LEVEL_MAX = 0x14; // Codes above 20 are reserved
static constexpr uint8_t TAS2780_DC_BLK0 = 0x04; // SAR Filter and DC Path Blocker
static constexpr uint8_t TAS2780_DC_BLK0_VBAT1S_MODE_MASK = (1 << 7);
static constexpr uint8_t TAS2780_DC_BLK1 = 0x05; // Record DC Blocker
static constexpr uint8_t TAS2780_DC_BLK1_RESET_VAL = 0x41; // Presence check, there is no WHO_AM_I register
static constexpr uint8_t TAS2780_TDM_CFG2 = 0x0A; // TDM Configuration 2
static constexpr uint8_t TAS2780_TDM_CFG2_RX_SCFG_SHIFT = 4;
static constexpr uint8_t TAS2780_TDM_CFG2_RX_SCFG_MASK = (3 << TAS2780_TDM_CFG2_RX_SCFG_SHIFT);
static constexpr uint8_t TAS2780_TDM_CFG2_RX_WLEN_SHIFT = 2;
static constexpr uint8_t TAS2780_TDM_CFG2_RX_WLEN_MASK = (3 << TAS2780_TDM_CFG2_RX_WLEN_SHIFT);
static constexpr uint8_t TAS2780_TDM_CFG2_RX_WLEN_32BIT = (3 << TAS2780_TDM_CFG2_RX_WLEN_SHIFT);
static constexpr uint8_t TAS2780_TDM_CFG2_RX_SLEN_MASK = (3 << 0);
static constexpr uint8_t TAS2780_TDM_CFG2_RX_SLEN_32BIT = 2;
static constexpr uint8_t TAS2780_TDM_CFG5 = 0x0E; // TDM Configuration 5
static constexpr uint8_t TAS2780_TDM_CFG5_TX_VSNS_EN_SLOT4 = 0x44; // vsns TX enable, slot 4
static constexpr uint8_t TAS2780_TDM_CFG6 = 0x0F; // TDM Configuration 6
static constexpr uint8_t TAS2780_TDM_CFG6_TX_ISNS_EN_SLOT0 = 0x40; // isns TX enable, slot 0
static constexpr uint8_t TAS2780_DVC = 0x1A; // Digital Volume Control
static constexpr uint8_t TAS2780_DVC_MAX_ATTEN = 200; // 0 dB (0x00) to -100 dB (0xC8) in 0.5 dB steps
/* Interrupts */
static constexpr uint8_t TAS2780_INT_MASK_ALL = 0xFF; // Mask all interrupts
static constexpr uint8_t TAS2780_INT_MASK0 = 0x3B; // Interrupt Mask 0
static constexpr uint8_t TAS2780_INT_MASK1 = 0x3C; // Interrupt Mask 1
static constexpr uint8_t TAS2780_INT_MASK1_0 = 0x3D; // Interrupt Mask 1_0 (INT_LTCH1_0 group)
static constexpr uint8_t TAS2780_INT_MASK2 = 0x40; // Interrupt Mask 2
static constexpr uint8_t TAS2780_INT_MASK3 = 0x41; // Interrupt Mask 3
static constexpr uint8_t TAS2780_INT_LTCH0 = 0x49; // Latched Interrupt Read-back 0, 1 and 1_0 follow
static constexpr uint8_t TAS2780_INT_LTCH2 = 0x4F; // Latched Interrupt Read-back 2
static constexpr uint8_t TAS2780_INT_CLK_CFG = 0x5C; // Clock Setting and IRQZ
static constexpr uint8_t TAS2780_INT_CLK_CFG_CLR_LATCH = (1 << 2); // Clear interrupt latches
static constexpr uint8_t TAS2780_INT_CLK_CFG_MODE_MASK = 0x03; // Trigger mode field mask
static constexpr uint8_t TAS2780_INT_CLK_CFG_MODE_LIVE = 0x00; // Trigger on any unmasked live interrupt
static constexpr uint8_t TAS2780_PVDD_UVLO = 0x71; // UVLO Threshold
static constexpr uint8_t TAS2780_PVDD_UVLO_2V76 = 0x03; // PVDD UVLO threshold = 2.76V
/* PAGE 0x01 */
static constexpr uint8_t TAS2780_INIT_0 = 0x17; // Initialization
static constexpr uint8_t TAS2780_INIT_0_VAL = 0xC8; // SARBurstMask=0, CMP_HYST_LP=1
static constexpr uint8_t TAS2780_LSR = 0x19; // Modulation
static constexpr uint8_t TAS2780_LSR_PWM_MODE = 0x00; // PWM modulation mode
static constexpr uint8_t TAS2780_INIT_1 = 0x21; // Initialization
static constexpr uint8_t TAS2780_INIT_1_VAL = 0x00; // Disable comparator hysteresis
static constexpr uint8_t TAS2780_INIT_2 = 0x35; // Initialization
static constexpr uint8_t TAS2780_INIT_2_VAL = 0x74; // Noise minimized
/* PAGE 0xFD */
static constexpr uint8_t TAS2780_PAGE_FD_ACCESS = 0x0D; // Page 0xFD access unlock/lock register
static constexpr uint8_t TAS2780_PAGE_FD_ACCESS_UNLOCK = 0x0D; // Unlock page 0xFD access
static constexpr uint8_t TAS2780_PAGE_FD_ACCESS_LOCK = 0x00; // Lock page 0xFD access
static constexpr uint8_t TAS2780_INIT_3 = 0x3E; // Initialization
static constexpr uint8_t TAS2780_INIT_3_VAL = 0x4A; // Optimal Dmin
// CDS_MODE (2 bits) and VBAT1S_MODE (1 bit) per power mode 0..3, packed so nothing lands in RAM on ESP8266:
// PWR_MODE0: 2/0, PWR_MODE1: 0/0, PWR_MODE2: 3/1, PWR_MODE3: 1/0
static constexpr uint8_t POWER_MODE_CDS_MODES = 0x72;
static constexpr uint8_t POWER_MODE_VBAT1S_MODES = 0x04;
// Latched interrupt bits per register (INT_LTCH0, 1, 1_0, 2, one byte each, low to high), split into faults
// and informational events; bits outside both masks are undefined.
static constexpr uint32_t TAS2780_INT_LTCH_ERROR_MASKS = 0x0FA058C7;
static constexpr uint32_t TAS2780_INT_LTCH_INFO_MASKS = 0x00002138;
// 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 reg, uint8_t bit) {
const uint8_t key = (reg << 3) | bit;
if (key == 0x00)
return LOG_STR("Over temperature error");
if (key == 0x01)
return LOG_STR("Over current error");
if (key == 0x02)
return LOG_STR("TDM Clock Error");
if (key == 0x03)
return LOG_STR("Limiter active");
if (key == 0x04)
return LOG_STR("PVDD below limiter inflection point");
if (key == 0x05)
return LOG_STR("Limiter max attenuation");
if (key == 0x06)
return LOG_STR("BOP infinite hold");
if (key == 0x07)
return LOG_STR("BOP Mute");
if (key == 0x08)
return LOG_STR("Gain limiter active");
if (key == 0x0B)
return LOG_STR("Load Diagnostic mode fault status");
if (key == 0x0D)
return LOG_STR("Load diagnostic complete");
if (key == 0x0E)
return LOG_STR("OTP CRC error flag");
if (key == 0x15)
return LOG_STR("VBAT1S Under Voltage");
if (key == 0x17)
return LOG_STR("Internal PLL Clock Error");
if (key == 0x18)
return LOG_STR("PVDD UVLO");
if (key == 0x19)
return LOG_STR("Internal VBAT1S LDO Over Load");
if (key == 0x1A)
return LOG_STR("Internal VBAT1S LDO Over Voltage");
if (key == 0x1B)
return LOG_STR("Internal VBAT1S LDO Under Voltage");
return nullptr;
}
void TAS2780::setup() {
if (!this->init_()) {
this->mark_failed();
return;
}
this->write_mode_ctrl_(TAS2780_MODE_CTRL_MODE_SFTW_SHTDWN);
}
bool TAS2780::select_page_(uint8_t page) {
if (this->current_page_ == page)
return true;
if (!this->write_byte(TAS2780_PAGE_SELECT, page)) {
this->current_page_ = 0xFF;
return false;
}
this->current_page_ = page;
return true;
}
bool TAS2780::update_bits_(uint8_t reg, uint8_t mask, uint8_t value) {
uint8_t current;
if (!this->select_page_(TAS2780_PAGE_0) || !this->read_byte(reg, &current)) {
ESP_LOGE(TAG, "Failed to read register 0x%02X", reg);
return false;
}
return this->write_byte(reg, (current & ~mask) | (value & mask));
}
bool TAS2780::init_() {
// Software reset (must select page 0 first; reset invalidates page cache)
if (!this->select_page_(TAS2780_PAGE_0)) {
ESP_LOGE(TAG, "I2C write failed during init");
return false;
}
this->current_page_ = 0xFF;
this->applied_power_mode_ = 0xFF; // only a complete init makes the chip trustworthy again
this->reg(TAS2780_SW_RESET) = TAS2780_SW_RESET_CMD;
delay(1);
uint8_t chd1;
if (!this->read_byte(TAS2780_DC_BLK1, &chd1)) {
ESP_LOGE(TAG, "I2C read failed during init");
return false;
}
if (chd1 != TAS2780_DC_BLK1_RESET_VAL) {
ESP_LOGE(TAG, "Init failed (DC_BLK1=0x%02X, expected 0x%02X)", chd1, TAS2780_DC_BLK1_RESET_VAL);
return false;
}
if (!this->select_page_(TAS2780_PAGE_0)) {
return false;
}
this->reg(TAS2780_TDM_CFG5) = TAS2780_TDM_CFG5_TX_VSNS_EN_SLOT4;
this->reg(TAS2780_TDM_CFG6) = TAS2780_TDM_CFG6_TX_ISNS_EN_SLOT0;
if (!this->select_page_(TAS2780_PAGE_1)) {
return false;
}
this->reg(TAS2780_LSR) = TAS2780_LSR_PWM_MODE;
this->reg(TAS2780_INIT_0) = TAS2780_INIT_0_VAL;
this->reg(TAS2780_INIT_1) = TAS2780_INIT_1_VAL;
this->reg(TAS2780_INIT_2) = TAS2780_INIT_2_VAL;
if (!this->select_page_(TAS2780_PAGE_FD)) {
return false;
}
this->reg(TAS2780_PAGE_FD_ACCESS) = TAS2780_PAGE_FD_ACCESS_UNLOCK;
this->reg(TAS2780_INIT_3) = TAS2780_INIT_3_VAL;
this->reg(TAS2780_PAGE_FD_ACCESS) = TAS2780_PAGE_FD_ACCESS_LOCK;
if (!this->select_page_(TAS2780_PAGE_0)) {
return false;
}
if (!this->set_power_mode_(this->power_mode_))
return false;
// When Y bridge is used (eg. PWR_MODE1) PVDD UVLO threshold needs to be set 2.5 V above VBAT1S level.
// UVLO = 1.753V + val * 0.332V
this->reg(TAS2780_PVDD_UVLO) = TAS2780_PVDD_UVLO_2V76;
// Mask all interrupt groups on the IRQZ pin, events are polled via update()
this->reg(TAS2780_INT_MASK0) = TAS2780_INT_MASK_ALL;
this->reg(TAS2780_INT_MASK1) = TAS2780_INT_MASK_ALL;
this->reg(TAS2780_INT_MASK1_0) = TAS2780_INT_MASK_ALL;
this->reg(TAS2780_INT_MASK2) = TAS2780_INT_MASK_ALL;
this->reg(TAS2780_INT_MASK3) = TAS2780_INT_MASK_ALL;
if (!this->update_bits_(TAS2780_INT_CLK_CFG, TAS2780_INT_CLK_CFG_MODE_MASK, TAS2780_INT_CLK_CFG_MODE_LIVE))
return false;
// Software reset sets DVC back to 0 dB (full volume)
if (!this->apply_config())
return false;
this->applied_power_mode_ = this->power_mode_;
return true;
}
void TAS2780::activate() {
if (this->is_failed())
return;
ESP_LOGD(TAG, "Activating (PWR_MODE:%u)", this->power_mode_);
this->clear_latches_();
if (this->power_mode_ != this->applied_power_mode_ && !this->reinit_())
return;
this->active_ = this->write_mode_ctrl_(this->active_mode_());
}
void TAS2780::deactivate() {
if (this->is_failed())
return;
ESP_LOGD(TAG, "Deactivating");
this->active_ = false;
this->write_mode_ctrl_(TAS2780_MODE_CTRL_MODE_SFTW_SHTDWN);
}
void TAS2780::reset() {
if (this->is_failed())
return;
if (this->reinit_() && this->active_)
this->activate();
}
bool TAS2780::reinit_() {
if (!this->init_()) {
ESP_LOGE(TAG, "Re-initialization failed");
this->status_set_error(LOG_STR("Init failed"));
return false;
}
this->status_clear_error();
return true;
}
void TAS2780::set_power_mode(uint8_t power_mode) {
// Lambda-supplied values bypass schema validation; refuse before anything touches the chip
if (power_mode >= 4) {
ESP_LOGE(TAG, "Invalid power mode %u, must be 0-3", power_mode);
return;
}
this->power_mode_ = power_mode;
}
bool TAS2780::set_power_mode_(uint8_t power_mode) {
uint8_t cds_mode = (POWER_MODE_CDS_MODES >> (power_mode * 2)) & 0x03;
uint8_t vbat1s_mode = (POWER_MODE_VBAT1S_MODES >> power_mode) & 0x01;
return this->update_bits_(TAS2780_CHNL_0, TAS2780_CHNL_0_CDS_MODE_MASK, cds_mode << TAS2780_CHNL_0_CDS_MODE_SHIFT) &&
this->update_bits_(TAS2780_DC_BLK0, TAS2780_DC_BLK0_VBAT1S_MODE_MASK, vbat1s_mode ? 0xFF : 0);
}
void TAS2780::clear_latches_() {
this->update_bits_(TAS2780_INT_CLK_CFG, TAS2780_INT_CLK_CFG_CLR_LATCH, TAS2780_INT_CLK_CFG_CLR_LATCH);
}
// Returns true if any latched interrupt flag is set
bool TAS2780::log_error_states_() {
uint8_t latched[4];
if (!this->select_page_(TAS2780_PAGE_0) || !this->read_bytes(TAS2780_INT_LTCH0, latched, 3) ||
!this->read_byte(TAS2780_INT_LTCH2, &latched[3])) {
return false;
}
// LDMODE is a two-bit field in INT_LTCH1; report it once
if (latched[1] & (1 << 4))
latched[1] = (latched[1] & ~(1 << 4)) | (1 << 3);
for (uint8_t reg = 0; reg < 4; reg++) {
uint8_t errors = latched[reg] & (TAS2780_INT_LTCH_ERROR_MASKS >> (reg * 8));
uint8_t infos = latched[reg] & (TAS2780_INT_LTCH_INFO_MASKS >> (reg * 8));
for (uint8_t bit = 0; bit < 8; bit++) {
if (errors & (1 << bit)) {
ESP_LOGE(TAG, "%s", LOG_STR_ARG(fault_name(reg, bit)));
} else if (infos & (1 << bit)) {
ESP_LOGD(TAG, "%s", LOG_STR_ARG(fault_name(reg, bit)));
}
}
}
return (latched[0] | latched[1] | latched[2] | latched[3]) != 0;
}
void TAS2780::update() {
// Latches hold until cleared; without this the same events are logged on every update
if (this->log_error_states_())
this->clear_latches_();
}
void TAS2780::dump_config() {
ESP_LOGCONFIG(TAG, "Audio Amplifier:");
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG,
" Power Mode: %u\n"
" Amp Level: %u\n"
" Volume Range: %.2f - %.2f\n"
" Channel: %s",
this->power_mode_, this->amp_level_, this->vol_range_min_, this->vol_range_max_,
this->selected_channel_ == LEFT_CHANNEL ? LOG_STR_LITERAL("Left")
: this->selected_channel_ == RIGHT_CHANNEL ? LOG_STR_LITERAL("Right")
: LOG_STR_LITERAL("Mono Downmix"));
}
bool TAS2780::write_mode_ctrl_(uint8_t mode) {
return this->update_bits_(TAS2780_MODE_CTRL, TAS2780_MODE_CTRL_MODE_MASK, mode);
}
uint8_t TAS2780::active_mode_() const {
return this->is_muted_ ? TAS2780_MODE_CTRL_MODE_ACTIVE_MUTED : TAS2780_MODE_CTRL_MODE_ACTIVE;
}
bool TAS2780::set_mute_(bool muted) {
bool previous = this->is_muted_;
this->is_muted_ = muted;
uint8_t mode_ctrl;
if (!this->select_page_(TAS2780_PAGE_0) || !this->read_byte(TAS2780_MODE_CTRL, &mode_ctrl)) {
ESP_LOGE(TAG, "Failed to read MODE_CTRL");
this->is_muted_ = previous;
return false;
}
uint8_t current_mode = mode_ctrl & TAS2780_MODE_CTRL_MODE_MASK;
// Only switch between active/muted if device is active; don't wake from shutdown
if ((current_mode == TAS2780_MODE_CTRL_MODE_ACTIVE || current_mode == TAS2780_MODE_CTRL_MODE_ACTIVE_MUTED) &&
!this->write_byte(TAS2780_MODE_CTRL, (mode_ctrl & ~TAS2780_MODE_CTRL_MODE_MASK) | this->active_mode_())) {
ESP_LOGE(TAG, "Failed to write MODE_CTRL");
this->is_muted_ = previous;
return false;
}
return true;
}
bool TAS2780::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 TAS2780::write_volume_() {
// Lambda-supplied volume ranges are not bounded or ordered
float range_min = std::min(this->vol_range_min_, this->vol_range_max_);
float range_max = std::max(this->vol_range_min_, this->vol_range_max_);
float volume = clamp(std::lerp(range_min, range_max, this->volume_), 0.0f, 1.0f);
uint8_t dvc = remap<uint8_t, float>(volume, 0.0f, 1.0f, TAS2780_DVC_MAX_ATTEN, 0);
ESP_LOGD(TAG, "Setting attenuation to: %u", dvc);
return this->select_page_(TAS2780_PAGE_0) && this->write_byte(TAS2780_DVC, dvc);
}
bool TAS2780::apply_config() { return this->apply_amp_and_channel_config_() && this->write_volume_(); }
bool TAS2780::apply_amp_and_channel_config_() {
// Lambda-supplied values bypass schema validation
if (this->amp_level_ > TAS2780_AMP_LEVEL_MAX) {
ESP_LOGW(TAG, "Amp level %u out of range, using %u", this->amp_level_, TAS2780_AMP_LEVEL_MAX);
this->amp_level_ = TAS2780_AMP_LEVEL_MAX;
}
ESP_LOGD(TAG, "Update amp to level idx: %u", this->amp_level_);
if (!this->update_bits_(TAS2780_CHNL_0, TAS2780_CHNL_0_AMP_LEVEL_MASK,
this->amp_level_ << TAS2780_CHNL_0_AMP_LEVEL_SHIFT)) {
return false;
}
return this->update_bits_(
TAS2780_TDM_CFG2, TAS2780_TDM_CFG2_RX_SCFG_MASK | TAS2780_TDM_CFG2_RX_WLEN_MASK | TAS2780_TDM_CFG2_RX_SLEN_MASK,
(this->selected_channel_ << TAS2780_TDM_CFG2_RX_SCFG_SHIFT) | TAS2780_TDM_CFG2_RX_WLEN_32BIT |
TAS2780_TDM_CFG2_RX_SLEN_32BIT);
}
} // namespace esphome::tas2780
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include "esphome/components/audio_dac/audio_dac.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
namespace esphome::tas2780 {
// Values are the TDM_CFG2 RX_SCFG field.
enum ChannelSelect : uint8_t { LEFT_CHANNEL = 1, RIGHT_CHANNEL = 2, MONO_DWN_MIX = 3 };
class TAS2780 : public audio_dac::AudioDac, public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::IO; }
void update() override;
/// Software reset and re-initialization, leaving the amplifier on or off as last requested.
void reset();
/// Activate with the configured power mode; re-initializes first when the mode changed.
void activate();
void deactivate();
/// Write the amp level, channel and volume range to the device.
bool apply_config();
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 set_amp_level(uint8_t amp_level) { this->amp_level_ = amp_level; }
void set_power_mode(uint8_t power_mode);
void set_vol_range_min(float min_val) { this->vol_range_min_ = min_val; }
void set_vol_range_max(float max_val) { this->vol_range_max_ = max_val; }
void set_selected_channel(ChannelSelect channel) { this->selected_channel_ = channel; }
protected:
bool select_page_(uint8_t page);
bool update_bits_(uint8_t reg, uint8_t mask, uint8_t value);
bool init_();
bool reinit_();
bool set_power_mode_(uint8_t power_mode);
bool apply_amp_and_channel_config_();
bool write_mode_ctrl_(uint8_t mode);
uint8_t active_mode_() const;
bool set_mute_(bool muted);
bool write_volume_();
bool log_error_states_();
void clear_latches_();
float volume_{0};
float vol_range_min_{0.3f};
float vol_range_max_{1.0f};
uint8_t current_page_{0xFF};
uint8_t power_mode_{2};
uint8_t applied_power_mode_{0xFF};
uint8_t amp_level_{8};
ChannelSelect selected_channel_{MONO_DWN_MIX};
bool active_{false};
};
} // namespace esphome::tas2780
+31
View File
@@ -0,0 +1,31 @@
audio_dac:
- platform: tas2780
id: tas2780_amp
address: 0x38
i2c_id: i2c_bus
amp_level: 8
power_mode: 2
vol_range_min: 0.3
vol_range_max: 1.0
channel: mono
esphome:
on_boot:
- tas2780.activate:
id: tas2780_amp
- tas2780.activate:
id: tas2780_amp
power_mode: 1
- tas2780.activate:
id: tas2780_amp
power_mode: !lambda "return id(tas2780_amp).volume() > 0.5f ? 3 : 1;"
- tas2780.update_config:
id: tas2780_amp
amp_level: 10
vol_range_min: 0.2
vol_range_max: 0.9
channel: left
- tas2780.deactivate:
id: tas2780_amp
- tas2780.reset:
id: tas2780_amp
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
tas2780: !include common.yaml
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
tas2780: !include common.yaml
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
tas2780: !include common.yaml