mirror of
https://github.com/esphome/esphome.git
synced 2026-09-28 07:20:24 +00:00
[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:
co-authored by
Claude Sonnet 4.6
pre-commit-ci-lite[bot]
J. Nick Koston
parent
f1f83cda07
commit
d2fefe2299
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@remcom"]
|
||||
@@ -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]))
|
||||
@@ -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, ¤t)) {
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user