ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
tas58xx.cpp
Go to the documentation of this file.
1#include "tas58xx.h"
2
3#include <cmath>
4
5#include "esphome/core/hal.h"
7#include "esphome/core/log.h"
8
9namespace esphome::tas58xx {
10
11ESPHOME_LOG_TAG(TAG, "tas58xx");
12
13static constexpr uint8_t TAS58XX_PAGE_SELECT = 0x00; // Page Select, in every book
14static constexpr uint8_t TAS58XX_BOOK_SELECT = 0x7F; // Book Select, on page 0 of every book
15static constexpr uint8_t TAS58XX_BOOK_CONTROL = 0x00;
16static constexpr uint8_t TAS58XX_PAGE_0 = 0x00;
17
18/* BOOK 0x00, PAGE 0x00 */
19static constexpr uint8_t TAS58XX_RESET_CTRL = 0x01;
20static constexpr uint8_t TAS58XX_RESET_CTRL_ALL = 0x11; // Reset DSP/control port and registers
21static constexpr uint8_t TAS58XX_DEVICE_CTRL_1 = 0x02;
22static constexpr uint8_t TAS58XX_DEVICE_CTRL_1_PBTL = (1 << 2);
23static constexpr uint8_t TAS58XX_DEVICE_CTRL_2 = 0x03;
24static constexpr uint8_t TAS58XX_DEVICE_CTRL_2_MUTE = (1 << 3);
25static constexpr uint8_t TAS58XX_CTRL_STATE_MASK = 0x03; // DEVICE_CTRL_2 and POWER_STATE
26static constexpr uint8_t TAS58XX_CTRL_STATE_DEEP_SLEEP = 0x00;
27static constexpr uint8_t TAS58XX_CTRL_STATE_SLEEP = 0x01;
28static constexpr uint8_t TAS58XX_CTRL_STATE_HIZ = 0x02;
29static constexpr uint8_t TAS58XX_CTRL_STATE_PLAY = 0x03;
30static constexpr uint8_t TAS58XX_CTRL_STATE_UNKNOWN = 0xFF; // Not a device value, forces the next update to act
31static constexpr uint8_t TAS58XX_DIG_VOL = 0x4C; // 0x00 = +24 dB to 0xFE = -103 dB in 0.5 dB steps, 0xFF = mute
32static constexpr uint8_t TAS58XX_DIG_VOL_0DB = 0x30;
33static constexpr uint8_t TAS58XX_DIG_VOL_MINUS_103DB = 0xFE;
34static constexpr uint8_t TAS58XX_AGAIN = 0x54; // 0x00 = 0 dB to 0x1F = -15.5 dB in 0.5 dB steps
35static constexpr uint8_t TAS58XX_AGAIN_MINUS_15_5DB = 0x1F;
36static constexpr uint8_t TAS58XX_POWER_STATE = 0x68;
37static constexpr uint8_t TAS58XX_CHAN_FAULT = 0x70; // GLOBAL_FAULT1, GLOBAL_FAULT2 and OT_WARNING follow
38static constexpr uint8_t TAS58XX_FAULT_REGISTER_COUNT = 4;
39static constexpr uint8_t TAS58XX_FAULT_CLEAR = 0x78;
40static constexpr uint8_t TAS58XX_FAULT_CLEAR_ANALOG = 0x80;
41
42/* Input mixer, four 9.23 fixed point big endian coefficients at a location that differs per model */
43static constexpr uint8_t TAS58XX_MIXER_COEFFICIENT_SIZE = 4;
44// Only the second byte differs between mute, -6 dB and 0 dB
45static constexpr uint8_t TAS58XX_MIXER_MUTE = 0x00;
46static constexpr uint8_t TAS58XX_MIXER_MINUS_6DB = 0x40;
47static constexpr uint8_t TAS58XX_MIXER_0DB = 0x80;
48
49static constexpr uint32_t PDN_LOW_MS = 1;
50static constexpr uint32_t PDN_TO_I2C_MS = 5; // Minimum time from PDN high to I2C access
51static constexpr uint32_t RESET_SETTLE_MS = 5;
52
53static const LogString *power_state_name(uint8_t state) {
54 if (state == TAS58XX_CTRL_STATE_DEEP_SLEEP)
55 return LOG_STR("Deep sleep");
56 if (state == TAS58XX_CTRL_STATE_SLEEP)
57 return LOG_STR("Sleep");
58 if (state == TAS58XX_CTRL_STATE_HIZ)
59 return LOG_STR("Hi-Z");
60 return LOG_STR("Play");
61}
62
64 if (this->enable_pin_ != nullptr) {
65 this->enable_pin_->setup();
66 this->enable_pin_->digital_write(false);
67 delay(PDN_LOW_MS);
68 this->enable_pin_->digital_write(true);
69 delay(PDN_TO_I2C_MS);
70 }
71 if (!this->init_()) {
72 this->mark_failed();
73 }
74}
75
76bool TAS58xx::select_book_page_(uint8_t book, uint8_t page) {
77 // The book can only be changed from page 0
78 return this->write_byte(TAS58XX_PAGE_SELECT, TAS58XX_PAGE_0) && this->write_byte(TAS58XX_BOOK_SELECT, book) &&
79 this->write_byte(TAS58XX_PAGE_SELECT, page);
80}
81
83 // Header of every TI PurePath Console export: select book 0 in case the MCU restarted without a PDN toggle,
84 // silence the output with Hi-Z, reset the DSP and the control registers, then return to Hi-Z
85 if (!this->select_book_page_(TAS58XX_BOOK_CONTROL, TAS58XX_PAGE_0) ||
86 !this->write_byte(TAS58XX_DEVICE_CTRL_2, TAS58XX_CTRL_STATE_HIZ) ||
87 !this->write_byte(TAS58XX_RESET_CTRL, TAS58XX_RESET_CTRL_ALL) ||
88 !this->write_byte(TAS58XX_DEVICE_CTRL_2, TAS58XX_CTRL_STATE_HIZ)) {
89 ESP_LOGE(TAG, "I2C write failed during reset");
90 return false;
91 }
92 delay(RESET_SETTLE_MS);
93
94 for (uint8_t i = 0; i < this->model_->startup_sequence_length; i++) {
95 const uint8_t *entry = this->model_->startup_sequence[i];
96 if (!this->write_byte(progmem_read_byte(&entry[0]), progmem_read_byte(&entry[1]))) {
97 ESP_LOGE(TAG, "I2C write failed during init");
98 return false;
99 }
100 }
101
102 // Setters are public, so keep the value inside the register range
103 const uint8_t again =
104 static_cast<uint8_t>(lroundf(clamp(-this->analog_gain_db_ * 2.0f, 0.0f, float{TAS58XX_AGAIN_MINUS_15_5DB})));
105 if (!this->write_byte(TAS58XX_DEVICE_CTRL_1, this->dac_mode_ == DAC_MODE_PBTL ? TAS58XX_DEVICE_CTRL_1_PBTL : 0) ||
106 !this->write_byte(TAS58XX_AGAIN, again) || !this->write_volume_() ||
107 !this->write_ctrl_state_(TAS58XX_CTRL_STATE_PLAY, this->is_muted_) ||
108 !this->write_byte(TAS58XX_FAULT_CLEAR, TAS58XX_FAULT_CLEAR_ANALOG)) {
109 ESP_LOGE(TAG, "I2C write failed during init");
110 return false;
111 }
112 // The mixer is written once the I2S clocks run, see on_audio_started() and update()
113 this->power_state_ = TAS58XX_CTRL_STATE_UNKNOWN;
114 this->mixer_written_ = false;
115 return true;
116}
117
119 if (this->is_failed())
120 return;
121 ESP_LOGD(TAG, "[0x%02X] Activating", this->address_);
122 // A failed mixer write can leave the device in another book
123 if (!this->select_book_page_(TAS58XX_BOOK_CONTROL, TAS58XX_PAGE_0)) {
124 ESP_LOGE(TAG, "Failed to select the control port");
125 return;
126 }
127 // Also the way to restart the output after a DC or over current fault
128 if (!this->write_byte(TAS58XX_FAULT_CLEAR, TAS58XX_FAULT_CLEAR_ANALOG)) {
129 ESP_LOGW(TAG, "Failed to clear faults");
130 }
131 const bool muted = this->is_muted_;
132 // Leaving deep sleep needs this sequence to reset the internal state machine, see datasheet 7.4.5
133 if (this->ctrl_state_ == TAS58XX_CTRL_STATE_DEEP_SLEEP &&
134 !(this->write_ctrl_state_(TAS58XX_CTRL_STATE_HIZ, muted) &&
135 this->write_ctrl_state_(TAS58XX_CTRL_STATE_DEEP_SLEEP, muted) &&
136 this->write_ctrl_state_(TAS58XX_CTRL_STATE_HIZ, muted))) {
137 return;
138 }
139 this->write_ctrl_state_(TAS58XX_CTRL_STATE_PLAY, muted);
140}
141
143 if (this->is_failed())
144 return;
145 ESP_LOGD(TAG, "[0x%02X] Deactivating", this->address_);
146 if (!this->select_book_page_(TAS58XX_BOOK_CONTROL, TAS58XX_PAGE_0)) {
147 ESP_LOGE(TAG, "Failed to select the control port");
148 return;
149 }
150 // The DSP, and with it the mixer, stays active in deep sleep
151 this->write_ctrl_state_(TAS58XX_CTRL_STATE_DEEP_SLEEP, this->is_muted_);
152}
153
154bool TAS58xx::write_ctrl_state_(uint8_t state, bool muted) {
155 if (!this->write_byte(TAS58XX_DEVICE_CTRL_2, state | (muted ? TAS58XX_DEVICE_CTRL_2_MUTE : 0))) {
156 ESP_LOGE(TAG, "Failed to write DEVICE_CTRL_2");
157 return false;
158 }
159 this->ctrl_state_ = state;
160 return true;
161}
162
163// Returns false if the fault registers could not be read
165 uint8_t fault_registers[TAS58XX_FAULT_REGISTER_COUNT];
166 if (!this->read_bytes(TAS58XX_CHAN_FAULT, fault_registers, sizeof(fault_registers)))
167 return false;
168 uint32_t faults = 0;
169 for (uint8_t reg = 0; reg < TAS58XX_FAULT_REGISTER_COUNT; reg++)
170 faults |= uint32_t{fault_registers[reg]} << (reg * 8);
171 const ModelInfo &model = *this->model_;
172 const uint32_t active = faults & (model.fault_error_mask | model.fault_warning_mask);
173
174 // Clearing makes a lasting condition latch again on every poll, so only log changes
175 const uint32_t changed = active ^ this->logged_faults_;
176 for (uint8_t index = 0; index < 32; index++) {
177 const uint32_t mask = uint32_t{1} << index;
178 if (!(changed & mask))
179 continue;
180 const LogString *name = model.fault_name(index);
181 if (!(active & mask)) {
182 ESP_LOGI(TAG, "[0x%02X] %s cleared", this->address_, LOG_STR_ARG(name));
183 } else if (model.fault_error_mask & mask) {
184 ESP_LOGE(TAG, "[0x%02X] %s", this->address_, LOG_STR_ARG(name));
185 } else {
186 ESP_LOGW(TAG, "[0x%02X] %s", this->address_, LOG_STR_ARG(name));
187 }
188 }
189 if (changed & active & model.fault_output_off_mask) {
190 ESP_LOGW(TAG, "[0x%02X] Output stays off: fix the cause, then call tas58xx.activate or power cycle the amplifier",
191 this->address_);
192 }
193 this->logged_faults_ = active;
194
195#ifdef USE_BINARY_SENSOR
196 if (this->have_fault_binary_sensor_ != nullptr)
197 this->have_fault_binary_sensor_->publish_state((active & model.fault_error_mask) != 0);
198 for (uint8_t fault = 0; fault < FAULT_SENSOR_COUNT; fault++) {
199 const uint8_t bit = model.fault_sensor_bits[fault];
200 if (this->fault_binary_sensors_[fault] != nullptr && bit != NO_BIT)
201 this->fault_binary_sensors_[fault]->publish_state(faults & (uint32_t{1} << bit));
202 }
203#endif
204
205 // Latched fault bits stay set after the condition is gone and only report it; clear those so the bits, and the
206 // sensors, follow the current state. The clear register resets all faults at once, so hold off while the output
207 // is off.
208 if ((faults & model.fault_latched_mask) && !(active & model.fault_output_off_mask) &&
209 !this->write_byte(TAS58XX_FAULT_CLEAR, TAS58XX_FAULT_CLEAR_ANALOG)) {
210 ESP_LOGW(TAG, "Failed to clear faults");
211 }
212 return true;
213}
214
216 // DSP coefficients can only be written with a running I2S clock (datasheet 7.5.3.1) and are kept when it stops
217 if (this->is_failed() || this->mixer_written_)
218 return;
219 if (!this->write_mixer_()) {
220 ESP_LOGW(TAG, "Failed to write mixer");
221 }
222}
223
225 uint8_t power_state;
226 if (!this->read_faults_() || !this->read_byte(TAS58XX_POWER_STATE, &power_state)) {
227 this->status_set_warning(LOG_STR("Failed to read status"));
228 return;
229 }
230 this->status_clear_warning();
231
232 power_state &= TAS58XX_CTRL_STATE_MASK;
233 if (power_state != this->power_state_) {
234 ESP_LOGD(TAG, "[0x%02X] Power state: %s", this->address_, LOG_STR_ARG(power_state_name(power_state)));
235 this->power_state_ = power_state;
236 }
237 // The device only plays with a running I2S clock. Fallback for speakers that do not call on_audio_started();
238 // a failed write is retried on the next update.
239 if (power_state == TAS58XX_CTRL_STATE_PLAY && !this->mixer_written_ && !this->write_mixer_()) {
240 ESP_LOGW(TAG, "Failed to write mixer");
241 }
242}
243
245 const LogString *mixer_mode;
247 mixer_mode = LOG_STR("Stereo inverse");
248 } else if (this->mixer_mode_ == MIXER_MODE_MONO) {
249 mixer_mode = LOG_STR("Mono");
250 } else if (this->mixer_mode_ == MIXER_MODE_LEFT) {
251 mixer_mode = LOG_STR("Left");
252 } else if (this->mixer_mode_ == MIXER_MODE_RIGHT) {
253 mixer_mode = LOG_STR("Right");
254 } else {
255 mixer_mode = LOG_STR("Stereo");
256 }
257 ESP_LOGCONFIG(TAG,
258 "Audio Amplifier:\n"
259 " Model: %s",
260 LOG_STR_ARG(this->model_->name()));
261 LOG_I2C_DEVICE(this);
262 LOG_PIN(" Enable Pin: ", this->enable_pin_);
263 LOG_UPDATE_INTERVAL(this);
264 ESP_LOGCONFIG(TAG,
265 " Analog Gain: %.1f dB\n"
266 " DAC Mode: %s\n"
267 " Mixer Mode: %s\n"
268 " Volume Range: %.1f dB to %.1f dB",
269 this->analog_gain_db_,
270 this->dac_mode_ == DAC_MODE_PBTL ? LOG_STR_LITERAL("PBTL") : LOG_STR_LITERAL("BTL"),
271 LOG_STR_ARG(mixer_mode), this->volume_min_db_, this->volume_max_db_);
272#ifdef USE_BINARY_SENSOR
273 LOG_BINARY_SENSOR(" ", "Any Fault", this->have_fault_binary_sensor_);
274 for (auto *sensor : this->fault_binary_sensors_) {
275 LOG_BINARY_SENSOR(" ", "Fault", sensor);
276 }
277#endif
278}
279
280bool TAS58xx::set_mute_(bool muted) {
281 if (!this->write_ctrl_state_(this->ctrl_state_, muted))
282 return false;
283 this->is_muted_ = muted;
284 return true;
285}
286
287bool TAS58xx::set_volume(float volume) {
288 float previous = this->volume_;
289 this->volume_ = clamp(volume, 0.0f, 1.0f);
290 if (!this->write_volume_()) {
291 this->volume_ = previous;
292 return false;
293 }
294 return true;
295}
296
298 // volume 0.0 maps to volume_min_db_, which is only close to silence at -103 dB
299 const float volume_db = std::lerp(this->volume_min_db_, this->volume_max_db_, this->volume_);
300 const uint8_t dig_vol = static_cast<uint8_t>(
301 lroundf(clamp(TAS58XX_DIG_VOL_0DB - volume_db * 2.0f, 0.0f, float{TAS58XX_DIG_VOL_MINUS_103DB})));
302 ESP_LOGV(TAG, "Setting volume to 0x%02X", dig_vol);
303 return this->write_byte(TAS58XX_DIG_VOL, dig_vol);
304}
305
307 uint8_t left_to_left = TAS58XX_MIXER_0DB;
308 uint8_t right_to_left = TAS58XX_MIXER_MUTE;
309 uint8_t left_to_right = TAS58XX_MIXER_MUTE;
310 uint8_t right_to_right = TAS58XX_MIXER_0DB;
312 left_to_left = TAS58XX_MIXER_MUTE;
313 right_to_left = TAS58XX_MIXER_0DB;
314 left_to_right = TAS58XX_MIXER_0DB;
315 right_to_right = TAS58XX_MIXER_MUTE;
316 } else if (this->mixer_mode_ == MIXER_MODE_MONO) {
317 left_to_left = TAS58XX_MIXER_MINUS_6DB;
318 right_to_left = TAS58XX_MIXER_MINUS_6DB;
319 left_to_right = TAS58XX_MIXER_MINUS_6DB;
320 right_to_right = TAS58XX_MIXER_MINUS_6DB;
321 } else if (this->mixer_mode_ == MIXER_MODE_LEFT) {
322 left_to_right = TAS58XX_MIXER_0DB;
323 right_to_right = TAS58XX_MIXER_MUTE;
324 } else if (this->mixer_mode_ == MIXER_MODE_RIGHT) {
325 left_to_left = TAS58XX_MIXER_MUTE;
326 right_to_left = TAS58XX_MIXER_0DB;
327 }
328 const uint8_t coefficients[4 * TAS58XX_MIXER_COEFFICIENT_SIZE] = {0, left_to_left, 0, 0, 0, right_to_left, 0, 0,
329 0, left_to_right, 0, 0, 0, right_to_right, 0, 0};
330 bool ok = this->select_book_page_(this->model_->mixer_book, this->model_->mixer_page) &&
331 this->write_bytes(this->model_->mixer_register, coefficients, sizeof(coefficients));
332 // Always return to the control port, even after a failed write
333 ok = this->select_book_page_(TAS58XX_BOOK_CONTROL, TAS58XX_PAGE_0) && ok;
334 this->mixer_written_ = ok;
335 return ok;
336}
337
338} // namespace esphome::tas58xx
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
void status_clear_warning()
Definition component.h:291
virtual void setup()=0
virtual void digital_write(bool value)=0
uint8_t address_
store the address of the device on the bus
Definition i2c.h:270
bool write_byte(uint8_t a_register, uint8_t data) const
Definition i2c.h:265
bool read_byte(uint8_t a_register, uint8_t *data)
Definition i2c.h:240
I2CRegister reg(uint8_t a_register)
calls the I2CRegister constructor
Definition i2c.h:152
bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) const
Definition i2c.h:251
bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len)
Compat APIs All methods below have been added for compatibility reasons.
Definition i2c.h:217
binary_sensor::BinarySensor * sensor
Definition tas58xx.h:133
bool set_mute_(bool muted)
Definition tas58xx.cpp:280
const ModelInfo * model_
Definition tas58xx.h:147
std::array< binary_sensor::BinarySensor *, FAULT_SENSOR_COUNT > fault_binary_sensors_
Definition tas58xx.h:150
void setup() override
Definition tas58xx.cpp:63
void update() override
Definition tas58xx.cpp:224
void dump_config() override
Definition tas58xx.cpp:244
bool set_volume(float volume) override
Definition tas58xx.cpp:287
bool write_ctrl_state_(uint8_t state, bool muted)
Definition tas58xx.cpp:154
void activate()
Clear faults, leave deep sleep and switch to play; the device waits in Hi-Z until an I2S clock is pre...
Definition tas58xx.cpp:118
void deactivate()
Switch to deep sleep, the lowest power state that keeps I2C and the DSP running.
Definition tas58xx.cpp:142
float volume() override
Definition tas58xx.h:120
void on_audio_started() override
Definition tas58xx.cpp:215
bool select_book_page_(uint8_t book, uint8_t page)
Definition tas58xx.cpp:76
bool state
Definition fan.h:2
ESPHOME_LOG_TAG(TAG, "tas58xx")
@ MIXER_MODE_STEREO_INVERSE
Definition tas58xx.h:24
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint8_t progmem_read_byte(const uint8_t *addr)
Definition hal.h:51
static void uint32_t
Everything that differs between models of the family.
Definition tas58xx.h:70
uint8_t fault_sensor_bits[FAULT_SENSOR_COUNT]
Bit index for each FaultSensor, NO_BIT used where the model does not have that fault bit.
Definition tas58xx.h:90
uint32_t fault_output_off_mask
Faults that keep the output off until cleared by activate()
Definition tas58xx.h:85
const LogString *(* fault_name)(uint8_t index)
Definition tas58xx.h:88
uint32_t fault_warning_mask
Faults logged as warnings.
Definition tas58xx.h:83
uint8_t mixer_book
Location of the four 9.23 fixed point input mixer coefficients: LEFT_TO_LEFT, RIGHT_TO_LEFT,...
Definition tas58xx.h:77
const LogString *(* name)()
Definition tas58xx.h:71
uint32_t fault_latched_mask
Faults that stay set after the condition is gone and are cleared after each read.
Definition tas58xx.h:87
const uint8_t(* startup_sequence)[2]
Remainder of the startup sequence, run after the reset, as {register, value} pairs in PROGMEM.
Definition tas58xx.h:73
uint32_t fault_error_mask
Faults logged as errors and reported by have_fault.
Definition tas58xx.h:81