ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
mcp4461.cpp
Go to the documentation of this file.
1#include "mcp4461.h"
2
3#include "esphome/core/hal.h"
5
6namespace esphome::mcp4461 {
7
8static const char *const TAG = "mcp4461";
9constexpr uint8_t EEPROM_WRITE_TIMEOUT_MS = 10;
10
12 auto err = this->write(nullptr, 0);
13 if (err != i2c::ERROR_OK) {
14 this->error_code_ = MCP4461_STATUS_I2C_ERROR;
15 this->mark_failed();
16 return;
17 }
18 // save WP/WL status
20 for (uint8_t i = 0; i < 8; i++) {
21 auto init_val = this->reg_[i].initial_value;
22 if (init_val.has_value()) {
23 uint16_t initial_state = static_cast<uint16_t>(*init_val * 256.0f);
24 if (i > 3) {
25 // NV wiper: an unconditional write would cost one EEPROM erase/write cycle on EVERY
26 // boot. Only write when the stored value actually differs — and always write when
27 // the read itself failed (a failed read returns 0, which would silently skip the
28 // write whenever initial_value is 0).
29 bool read_ok = false;
30 if (this->read_wiper_level_(i, &read_ok) != initial_state || !read_ok) {
31 this->write_wiper_level_(i, initial_state);
32 }
33 } else {
34 this->write_wiper_level_(i, initial_state);
35 }
36 }
37 if (this->reg_[i].enabled) {
38 this->reg_[i].state = this->read_wiper_level_(i);
39 } else {
40 // only volatile wipers can be set disabled on hw level
41 if (i < 4) {
42 this->reg_[i].state = 0;
43 Mcp4461WiperIdx wiper_idx = static_cast<Mcp4461WiperIdx>(i);
44 this->disable_wiper_(wiper_idx);
45 }
46 }
47 }
48 // Push the YAML terminal configuration to the TCON registers. TCON is volatile — on POR
49 // the chip restores wiper levels from the NV registers but resets TCON to "all terminals
50 // connected", so any terminal_a/b/w disables from the config MUST be written here.
51 for (uint8_t t = 0; t < 2; t++) {
52 Mcp4461TerminalIdx terminal_connector = static_cast<Mcp4461TerminalIdx>(t);
53 uint8_t terminal_byte = this->calc_terminal_connector_byte_(terminal_connector);
54 this->set_terminal_register_(terminal_connector, terminal_byte);
55 }
56}
57
59 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
60 if (wiper_idx > 3) {
61 return; // NV channels E-H are the persistence target themselves
62 }
63 this->reg_[wiper_idx].nonvolatile = true;
64 this->reg_[wiper_idx].nonvolatile_write_delay_ms = write_delay_ms;
65}
66
67void Mcp4461Component::set_initial_value(Mcp4461WiperIdx wiper, float initial_value) {
68 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
69 this->reg_[wiper_idx].initial_value = initial_value;
70}
71
73 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
74 switch (terminal) {
75 case 'a':
76 this->reg_[wiper_idx].terminal_a = false;
77 break;
78 case 'b':
79 this->reg_[wiper_idx].terminal_b = false;
80 break;
81 case 'w':
82 this->reg_[wiper_idx].terminal_w = false;
83 break;
84 }
85}
86
88 uint8_t status_register_value = this->get_status_register_();
89 this->write_protected_ = static_cast<bool>((status_register_value >> 0) & 0x01);
90 this->reg_[0].wiper_lock_active = static_cast<bool>((status_register_value >> 2) & 0x01);
91 this->reg_[1].wiper_lock_active = static_cast<bool>((status_register_value >> 3) & 0x01);
92 this->reg_[2].wiper_lock_active = static_cast<bool>((status_register_value >> 5) & 0x01);
93 this->reg_[3].wiper_lock_active = static_cast<bool>((status_register_value >> 6) & 0x01);
94}
95
97 ESP_LOGCONFIG(TAG, "mcp4461:");
98 LOG_I2C_DEVICE(this);
99 if (this->is_failed()) {
100 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
101 }
102 // log wiper status
103 for (uint8_t i = 0; i < 8; ++i) {
104 // terminals only valid for volatile wipers 0-3 - enable/disable is terminal hw
105 // so also invalid for nonvolatile. For these, only print current level.
106 // reworked to be a one-line intentionally, as output would not be in order
107 if (i < 4) {
108 ESP_LOGCONFIG(TAG,
109 " ├── Volatile wiper [%u] level: %u, Status: %s, HW: %s, "
110 "A: %s, B: %s, W: %s, NV: %s",
111 i, this->reg_[i].state, ONOFF(this->reg_[i].enabled), ONOFF(this->reg_[i].terminal_hw),
112 ONOFF(this->reg_[i].terminal_a), ONOFF(this->reg_[i].terminal_b), ONOFF(this->reg_[i].terminal_w),
113 ONOFF(this->reg_[i].nonvolatile));
114 } else {
115 ESP_LOGCONFIG(TAG, " ├── Nonvolatile wiper [%u] level: %u", i, this->reg_[i].state);
116 }
117 }
118}
119
121 if (this->status_has_warning()) {
122 this->get_status_register_();
123 }
124 for (uint8_t i = 0; i < 8; i++) {
125 if (this->reg_[i].update_level) {
126 // set wiper i state if changed — a failed read (returns 0) must not suppress the
127 // write when the target state is 0, same hardening as the NV read-compare paths
128 bool read_ok = false;
129 if (this->reg_[i].state != this->read_wiper_level_(i, &read_ok) || !read_ok) {
130 this->write_wiper_level_(i, this->reg_[i].state);
131 }
132 }
133 this->reg_[i].update_level = false;
134 // can be true only for wipers 0-3
135 // setting changes for terminals of nonvolatile wipers
136 // is prohibited in public methods
137 if (this->reg_[i].update_terminal) {
138 // set terminal register changes
139 Mcp4461TerminalIdx terminal_connector =
141 uint8_t new_terminal_value = this->calc_terminal_connector_byte_(terminal_connector);
142 ESP_LOGV(TAG, "updating terminal %u to new value %u", static_cast<uint8_t>(terminal_connector),
143 new_terminal_value);
144 this->set_terminal_register_(terminal_connector, new_terminal_value);
145 }
146 this->reg_[i].update_terminal = false;
147 }
149}
150
152 const uint32_t now = millis();
153 for (uint8_t i = 0; i < 4; i++) {
154 if (!this->reg_[i].nonvolatile || !this->reg_[i].nonvolatile_dirty) {
155 continue;
156 }
157 if ((now - this->reg_[i].last_level_change_ms) < this->reg_[i].nonvolatile_write_delay_ms) {
158 continue; // still settling — debounce window not over yet
159 }
160 // Never block the loop on a still-running EEPROM cycle (t_WC up to 10 ms); datasheet:
161 // during an EEPROM write only volatile commands are accepted. Retry on the next loop.
162 if (this->is_writing_()) {
163 continue;
164 }
165 // Clear the dirty flag on success — and equally when WP or WiperLock block the write
166 // permanently, instead of retrying forever.
167 if (this->store_level_nonvolatile_(static_cast<Mcp4461WiperIdx>(i)) || this->write_protected_ ||
168 this->reg_[i].wiper_lock_active) {
169 this->reg_[i].nonvolatile_dirty = false;
170 } else {
171 // Transient failure (e.g. I2C error): without this, the retry fires on every single
172 // loop() iteration, spamming a warning each time. Re-arming the timestamp reuses the
173 // stability delay as a natural retry backoff.
174 this->reg_[i].last_level_change_ms = now;
175 }
176 }
177}
178
180 if (this->is_failed()) {
181 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
182 return false;
183 }
184 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
185 if (wiper_idx > 3) {
186 // E-H ARE the nonvolatile registers — keep this consistent with the other guards
187 // instead of failing silently (reachable via the store_nonvolatile action).
188 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_PROHIBITED_FOR_NONVOLATILE)));
189 return false;
190 }
191 if (this->reg_[wiper_idx].wiper_lock_active) {
192 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED)));
193 return false;
194 }
195 const uint16_t level = this->reg_[wiper_idx].state;
196 // Skip the EEPROM cycle entirely when the NV register already holds the value. A failed
197 // read must NOT count as a match (it returns 0): fall through to the write instead — if
198 // the bus is really down, the write fails too and the dirty flag stays set for a retry.
199 bool read_ok = false;
200 if (this->read_wiper_level_(wiper_idx + 4, &read_ok) == level && read_ok) {
201 return true;
202 }
203 ESP_LOGV(TAG, "Persisting wiper %u level %u to nonvolatile register", wiper_idx, level);
204 if (!this->mcp4461_write_(this->get_wiper_address_(wiper_idx + 4), level, true)) {
205 ESP_LOGW(TAG, "Error persisting wiper %u level %u", wiper_idx, level);
206 return false;
207 }
208 return true;
209}
210
212 if (this->is_failed()) {
213 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
214 return 0;
215 }
216 uint8_t addr = static_cast<uint8_t>(Mcp4461Addresses::MCP4461_STATUS);
217 uint8_t reg = addr | static_cast<uint8_t>(Mcp4461Commands::READ);
218 uint16_t buf;
219 if (!this->read_16_(reg, &buf)) {
220 this->error_code_ = MCP4461_STATUS_REGISTER_ERROR;
221 this->mark_failed();
222 return 0;
223 }
224 uint8_t msb = buf >> 8;
225 uint8_t lsb = static_cast<uint8_t>(buf & 0x00ff);
226 if (msb != 1 || ((lsb >> 7) & 0x01) != 1 || ((lsb >> 1) & 0x01) != 1) {
227 // D8, D7 and R1 bits are hardlocked to 1 -> a status msb bit 0 (bit 9 of status register) of 0 or lsb bit 1/7 = 0
228 // indicate device/communication issues, therefore mark component failed
229 this->error_code_ = MCP4461_STATUS_REGISTER_INVALID;
230 this->mark_failed();
231 return 0;
232 }
233 this->status_clear_warning();
234 return lsb;
235}
236
238 uint8_t status_register_value = this->get_status_register_();
239 ESP_LOGI(TAG, "D7: %u, WL3: %u, WL2: %u, EEWA: %u, WL1: %u, WL0: %u, R1: %u, WP: %u",
240 ((status_register_value >> 7) & 0x01), ((status_register_value >> 6) & 0x01),
241 ((status_register_value >> 5) & 0x01), ((status_register_value >> 4) & 0x01),
242 ((status_register_value >> 3) & 0x01), ((status_register_value >> 2) & 0x01),
243 ((status_register_value >> 1) & 0x01), ((status_register_value >> 0) & 0x01));
244}
245bool Mcp4461Component::read_16_(uint8_t address, uint16_t *buf) {
246 // read 16 bits and convert from big endian to host,
247 // Do this as two separate operations to ensure a stop condition between the write and read
248 i2c::ErrorCode err = this->write(&address, 1);
249 if (err != i2c::ERROR_OK) {
250 return false;
251 }
252 err = this->read(reinterpret_cast<uint8_t *>(buf), 2);
253 if (err != i2c::ERROR_OK) {
254 return false;
255 }
256 *buf = convert_big_endian(*buf);
257 return true;
258}
259
261 uint8_t addr;
262 bool nonvolatile = false;
263 if (wiper > 3) {
264 nonvolatile = true;
265 wiper = wiper - 4;
266 }
267 switch (wiper) {
268 case 0:
269 addr = static_cast<uint8_t>(Mcp4461Addresses::MCP4461_VW0);
270 break;
271 case 1:
272 addr = static_cast<uint8_t>(Mcp4461Addresses::MCP4461_VW1);
273 break;
274 case 2:
275 addr = static_cast<uint8_t>(Mcp4461Addresses::MCP4461_VW2);
276 break;
277 case 3:
278 addr = static_cast<uint8_t>(Mcp4461Addresses::MCP4461_VW3);
279 break;
280 default:
281 ESP_LOGW(TAG, "unknown wiper specified");
282 return 0;
283 }
284 if (nonvolatile) {
285 addr = addr + 0x20;
286 }
287 return addr;
288}
289
291 if (this->is_failed()) {
292 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
293 return 0;
294 }
295 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
296 if (!(this->reg_[wiper_idx].enabled)) {
297 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED)));
298 return 0;
299 }
300 if (!(this->reg_[wiper_idx].enabled)) {
301 ESP_LOGW(TAG, "reading from disabled volatile wiper %u, returning 0", wiper_idx);
302 return 0;
303 }
304 return this->read_wiper_level_(wiper_idx);
305}
306
307uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx, bool *ok) {
308 if (ok != nullptr) {
309 *ok = false;
310 }
311 uint8_t addr = this->get_wiper_address_(wiper_idx);
312 uint8_t reg = addr | static_cast<uint8_t>(Mcp4461Commands::READ);
313 if (wiper_idx > 3) {
314 if (!this->is_eeprom_ready_for_writing_(true)) {
315 return 0;
316 }
317 }
318 uint16_t buf = 0;
319 if (!(this->read_16_(reg, &buf))) {
320 this->error_code_ = MCP4461_STATUS_I2C_ERROR;
321 this->status_set_warning();
322 ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? LOG_STR_LITERAL("nonvolatile ") : "", wiper_idx);
323 return 0;
324 }
325 if (ok != nullptr) {
326 *ok = true;
327 }
328 return buf;
329}
330
332 if (this->is_failed()) {
333 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
334 return false;
335 }
336 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
337 if (!(this->reg_[wiper_idx].enabled)) {
338 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED)));
339 return false;
340 }
341 uint16_t data = this->get_wiper_level_(wiper);
342 ESP_LOGV(TAG, "Got value %u from wiper %u", data, wiper_idx);
343 this->reg_[wiper_idx].state = data;
344 return true;
345}
346
348 if (this->is_failed()) {
349 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
350 return false;
351 }
352 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
353 if (value > 0x100) {
354 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_VALUE_INVALID)));
355 return false;
356 }
357 if (!(this->reg_[wiper_idx].enabled)) {
358 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED)));
359 return false;
360 }
361 if (this->reg_[wiper_idx].wiper_lock_active) {
362 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED)));
363 return false;
364 }
365 ESP_LOGV(TAG, "Setting MCP4461 wiper %u to %u", wiper_idx, value);
366 this->reg_[wiper_idx].state = value;
367 this->reg_[wiper_idx].update_level = true;
368 if (this->reg_[wiper_idx].nonvolatile) {
369 this->reg_[wiper_idx].nonvolatile_dirty = true;
370 this->reg_[wiper_idx].last_level_change_ms = millis();
371 }
372 return true;
373}
374
375void Mcp4461Component::write_wiper_level_(uint8_t wiper, uint16_t value) {
376 bool nonvolatile = wiper > 3;
377 if (!(this->mcp4461_write_(this->get_wiper_address_(wiper), value, nonvolatile))) {
378 this->error_code_ = MCP4461_STATUS_I2C_ERROR;
379 this->status_set_warning();
380 ESP_LOGW(TAG, "Error writing %swiper %u level %u", (wiper > 3) ? LOG_STR_LITERAL("nonvolatile ") : "", wiper,
381 value);
382 }
383}
384
386 if (this->is_failed()) {
387 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
388 return;
389 }
390 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
391 if ((this->reg_[wiper_idx].enabled)) {
392 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_ENABLED)));
393 return;
394 }
395 if (this->reg_[wiper_idx].wiper_lock_active) {
396 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED)));
397 return;
398 }
399 ESP_LOGV(TAG, "Enabling wiper %u", wiper_idx);
400 this->reg_[wiper_idx].enabled = true;
401 if (wiper_idx < 4) {
402 this->reg_[wiper_idx].terminal_hw = true;
403 this->reg_[wiper_idx].update_terminal = true;
404 }
405}
406
408 if (this->is_failed()) {
409 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
410 return;
411 }
412 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
413 if (!(this->reg_[wiper_idx].enabled)) {
414 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED)));
415 return;
416 }
417 if (this->reg_[wiper_idx].wiper_lock_active) {
418 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED)));
419 return;
420 }
421 ESP_LOGV(TAG, "Disabling wiper %u", wiper_idx);
422 this->reg_[wiper_idx].enabled = false;
423 if (wiper_idx < 4) {
424 this->reg_[wiper_idx].terminal_hw = false;
425 this->reg_[wiper_idx].update_terminal = true;
426 }
427}
428
430 if (this->is_failed()) {
431 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
432 return false;
433 }
434 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
435 if (!(this->reg_[wiper_idx].enabled)) {
436 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED)));
437 return false;
438 }
439 if (this->reg_[wiper_idx].wiper_lock_active) {
440 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED)));
441 return false;
442 }
443 if (wiper_idx > 3) {
444 // Datasheet: increment commands are only valid for the volatile wiper registers —
445 // the chip NACKs them on nonvolatile addresses.
446 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_PROHIBITED_FOR_NONVOLATILE)));
447 return false;
448 }
449 if (this->reg_[wiper_idx].state == 256) {
450 ESP_LOGV(TAG, "Maximum wiper level reached, further increase of wiper %u prohibited", wiper_idx);
451 return false;
452 }
453 ESP_LOGV(TAG, "Increasing wiper %u", wiper_idx);
454 uint8_t addr = this->get_wiper_address_(wiper_idx);
455 uint8_t reg = addr | static_cast<uint8_t>(Mcp4461Commands::INCREMENT);
456 auto err = this->write(&reg, 1);
457 if (err != i2c::ERROR_OK) {
458 this->error_code_ = MCP4461_STATUS_I2C_ERROR;
459 this->status_set_warning();
460 return false;
461 }
462 this->reg_[wiper_idx].state++;
463 if (this->reg_[wiper_idx].nonvolatile) {
464 this->reg_[wiper_idx].nonvolatile_dirty = true;
465 this->reg_[wiper_idx].last_level_change_ms = millis();
466 }
467 return true;
468}
469
471 if (this->is_failed()) {
472 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
473 return false;
474 }
475 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
476 if (!(this->reg_[wiper_idx].enabled)) {
477 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED)));
478 return false;
479 }
480 if (this->reg_[wiper_idx].wiper_lock_active) {
481 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED)));
482 return false;
483 }
484 if (wiper_idx > 3) {
485 // Datasheet: decrement commands are only valid for the volatile wiper registers —
486 // the chip NACKs them on nonvolatile addresses.
487 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_PROHIBITED_FOR_NONVOLATILE)));
488 return false;
489 }
490 if (this->reg_[wiper_idx].state == 0) {
491 ESP_LOGV(TAG, "Minimum wiper level reached, further decrease of wiper %u prohibited", wiper_idx);
492 return false;
493 }
494 ESP_LOGV(TAG, "Decreasing wiper %u", wiper_idx);
495 uint8_t addr = this->get_wiper_address_(wiper_idx);
496 uint8_t reg = addr | static_cast<uint8_t>(Mcp4461Commands::DECREMENT);
497 auto err = this->write(&reg, 1);
498 if (err != i2c::ERROR_OK) {
499 this->error_code_ = MCP4461_STATUS_I2C_ERROR;
500 this->status_set_warning();
501 return false;
502 }
503 this->reg_[wiper_idx].state--;
504 if (this->reg_[wiper_idx].nonvolatile) {
505 this->reg_[wiper_idx].nonvolatile_dirty = true;
506 this->reg_[wiper_idx].last_level_change_ms = millis();
507 }
508 return true;
509}
510
512 // TCON0 covers wipers 0/1 (A/B), TCON1 covers wipers 2/3 (C/D). The enum only holds
513 // 0 and 1, so the old `<= 1 ? 0 : 2` collapsed to always-0 and built TCON1 from
514 // channels A/B's flags — mirror the (correct) read path in update_terminal_register_().
515 uint8_t i = static_cast<uint8_t>(terminal_connector) == 0 ? 0 : 2;
516 uint8_t new_value_byte = 0;
517 new_value_byte += static_cast<uint8_t>(this->reg_[i].terminal_b);
518 new_value_byte += static_cast<uint8_t>(this->reg_[i].terminal_w) << 1;
519 new_value_byte += static_cast<uint8_t>(this->reg_[i].terminal_a) << 2;
520 new_value_byte += static_cast<uint8_t>(this->reg_[i].terminal_hw) << 3;
521 new_value_byte += static_cast<uint8_t>(this->reg_[(i + 1)].terminal_b) << 4;
522 new_value_byte += static_cast<uint8_t>(this->reg_[(i + 1)].terminal_w) << 5;
523 new_value_byte += static_cast<uint8_t>(this->reg_[(i + 1)].terminal_a) << 6;
524 new_value_byte += static_cast<uint8_t>(this->reg_[(i + 1)].terminal_hw) << 7;
525 return new_value_byte;
526}
527
529 if (this->is_failed()) {
530 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
531 return 0;
532 }
533 uint8_t reg = static_cast<uint8_t>(terminal_connector) == 0 ? static_cast<uint8_t>(Mcp4461Addresses::MCP4461_TCON0)
534 : static_cast<uint8_t>(Mcp4461Addresses::MCP4461_TCON1);
535 reg |= static_cast<uint8_t>(Mcp4461Commands::READ);
536 uint16_t buf;
537 if (this->read_16_(reg, &buf)) {
538 return static_cast<uint8_t>(buf & 0x00ff);
539 } else {
540 this->error_code_ = MCP4461_STATUS_I2C_ERROR;
541 this->status_set_warning();
542 ESP_LOGW(TAG, "Error fetching terminal register value");
543 return 0;
544 }
545}
546
548 if (this->is_failed()) {
549 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
550 return;
551 }
552 if ((static_cast<uint8_t>(terminal_connector) != 0 && static_cast<uint8_t>(terminal_connector) != 1)) {
553 return;
554 }
555 uint8_t terminal_data = this->get_terminal_register_(terminal_connector);
556 if (terminal_data == 0) {
557 return;
558 }
559 ESP_LOGV(TAG, "Got terminal register %u data 0x%02X", static_cast<uint8_t>(terminal_connector), terminal_data);
560 uint8_t wiper_index = 0;
561 if (static_cast<uint8_t>(terminal_connector) == 1) {
562 wiper_index = 2;
563 }
564 this->reg_[wiper_index].terminal_b = ((terminal_data >> 0) & 0x01);
565 this->reg_[wiper_index].terminal_w = ((terminal_data >> 1) & 0x01);
566 this->reg_[wiper_index].terminal_a = ((terminal_data >> 2) & 0x01);
567 this->reg_[wiper_index].terminal_hw = ((terminal_data >> 3) & 0x01);
568 this->reg_[(wiper_index + 1)].terminal_b = ((terminal_data >> 4) & 0x01);
569 this->reg_[(wiper_index + 1)].terminal_w = ((terminal_data >> 5) & 0x01);
570 this->reg_[(wiper_index + 1)].terminal_a = ((terminal_data >> 6) & 0x01);
571 this->reg_[(wiper_index + 1)].terminal_hw = ((terminal_data >> 7) & 0x01);
572}
573
574bool Mcp4461Component::set_terminal_register_(Mcp4461TerminalIdx terminal_connector, uint8_t data) {
575 if (this->is_failed()) {
576 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
577 return false;
578 }
579 uint8_t addr;
580 if (static_cast<uint8_t>(terminal_connector) == 0) {
581 addr = static_cast<uint8_t>(Mcp4461Addresses::MCP4461_TCON0);
582 } else if (static_cast<uint8_t>(terminal_connector) == 1) {
583 addr = static_cast<uint8_t>(Mcp4461Addresses::MCP4461_TCON1);
584 } else {
585 ESP_LOGW(TAG, "Invalid terminal connector id %u specified", static_cast<uint8_t>(terminal_connector));
586 return false;
587 }
588 if (!(this->mcp4461_write_(addr, data))) {
589 this->error_code_ = MCP4461_STATUS_I2C_ERROR;
590 this->status_set_warning();
591 return false;
592 }
593 return true;
594}
595
597 if (this->is_failed()) {
598 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
599 return;
600 }
601 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
602 if (wiper_idx > 3) {
603 // Terminal control only exists for the volatile wipers; loop() would otherwise emit
604 // an unrelated TCON write and silently drop the request.
605 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_PROHIBITED_FOR_NONVOLATILE)));
606 return;
607 }
608 ESP_LOGV(TAG, "Enabling terminal %c of wiper %u", terminal, wiper_idx);
609 switch (terminal) {
610 case 'h':
611 this->reg_[wiper_idx].terminal_hw = true;
612 break;
613 case 'a':
614 this->reg_[wiper_idx].terminal_a = true;
615 break;
616 case 'b':
617 this->reg_[wiper_idx].terminal_b = true;
618 break;
619 case 'w':
620 this->reg_[wiper_idx].terminal_w = true;
621 break;
622 default:
623 ESP_LOGW(TAG, "Unknown terminal %c specified", terminal);
624 return;
625 }
626 this->reg_[wiper_idx].update_terminal = true;
627}
628
630 if (this->is_failed()) {
631 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
632 return;
633 }
634 uint8_t wiper_idx = static_cast<uint8_t>(wiper);
635 if (wiper_idx > 3) {
636 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_PROHIBITED_FOR_NONVOLATILE)));
637 return;
638 }
639 ESP_LOGV(TAG, "Disabling terminal %c of wiper %u", terminal, wiper_idx);
640 switch (terminal) {
641 case 'h':
642 this->reg_[wiper_idx].terminal_hw = false;
643 break;
644 case 'a':
645 this->reg_[wiper_idx].terminal_a = false;
646 break;
647 case 'b':
648 this->reg_[wiper_idx].terminal_b = false;
649 break;
650 case 'w':
651 this->reg_[wiper_idx].terminal_w = false;
652 break;
653 default:
654 ESP_LOGW(TAG, "Unknown terminal %c specified", terminal);
655 return;
656 }
657 this->reg_[wiper_idx].update_terminal = true;
658}
659
661 if (this->is_failed()) {
662 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
663 return 0;
664 }
665 uint8_t reg = 0;
666 reg |= static_cast<uint8_t>(Mcp4461EepromLocation::MCP4461_EEPROM_1) + (static_cast<uint8_t>(location) * 0x10);
667 reg |= static_cast<uint8_t>(Mcp4461Commands::READ);
668 uint16_t buf;
669 if (!this->is_eeprom_ready_for_writing_(true)) {
670 return 0;
671 }
672 if (!this->read_16_(reg, &buf)) {
673 this->error_code_ = MCP4461_STATUS_I2C_ERROR;
674 this->status_set_warning();
675 ESP_LOGW(TAG, "Error fetching EEPROM location value");
676 return 0;
677 }
678 return buf;
679}
680
682 if (this->is_failed()) {
683 ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_)));
684 return false;
685 }
686 uint8_t addr = 0;
687 if (value > 511) {
688 return false;
689 }
690 if (value > 256) {
691 addr = 1;
692 }
693 addr |= static_cast<uint8_t>(Mcp4461EepromLocation::MCP4461_EEPROM_1) + (static_cast<uint8_t>(location) * 0x10);
694 if (!(this->mcp4461_write_(addr, value, true))) {
695 this->error_code_ = MCP4461_STATUS_I2C_ERROR;
696 this->status_set_warning();
697 ESP_LOGW(TAG, "Error writing EEPROM value");
698 return false;
699 }
700 return true;
701}
702
704 /* Read the EEPROM write-active status from the status register */
705 bool writing = static_cast<bool>((this->get_status_register_() >> 4) & 0x01);
706
707 /* If EEPROM is no longer writing, reset the timeout flag */
708 if (!writing) {
709 /* This is protected boolean flag in Mcp4461Component class */
710 this->last_eeprom_write_timed_out_ = false;
711 }
712
713 return writing;
714}
715
717 /* Check initial write status */
718 bool ready_for_write = !this->is_writing_();
719
720 /* Return early if no waiting is required or EEPROM is already ready */
721 if (ready_for_write || !wait_if_not_ready || this->last_eeprom_write_timed_out_) {
722 return ready_for_write;
723 }
724
725 /* Timestamp before starting the loop */
726 const uint32_t start_millis = millis();
727
728 ESP_LOGV(TAG, "Waiting until EEPROM is ready for write, start_millis = %" PRIu32, start_millis);
729
730 /* Loop until EEPROM is ready or timeout is reached */
731 while (!ready_for_write && ((millis() - start_millis) < EEPROM_WRITE_TIMEOUT_MS)) {
732 ready_for_write = !this->is_writing_();
733
734 /* If ready, exit early */
735 if (ready_for_write) {
736 ESP_LOGV(TAG, "EEPROM is ready for new write, elapsed_millis = %" PRIu32, millis() - start_millis);
737 return true;
738 }
739
740 /* Not ready yet, yield before checking again */
741 yield();
742 }
743
744 /* If still not ready after timeout, log error and mark the timeout */
745 ESP_LOGE(TAG, "EEPROM write timeout exceeded (%u ms)", EEPROM_WRITE_TIMEOUT_MS);
746 this->last_eeprom_write_timed_out_ = true;
747
748 return false;
749}
750
751bool Mcp4461Component::mcp4461_write_(uint8_t addr, uint16_t data, bool nonvolatile) {
752 uint8_t reg = data > 0xff ? 1 : 0;
753 uint8_t value_byte = static_cast<uint8_t>(data & 0x00ff);
754 ESP_LOGV(TAG, "Writing value %u to address %u", data, addr);
755 reg |= addr;
756 reg |= static_cast<uint8_t>(Mcp4461Commands::WRITE);
757 if (nonvolatile) {
758 if (this->write_protected_) {
759 ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WRITE_PROTECTED)));
760 return false;
761 }
762 if (!this->is_eeprom_ready_for_writing_(true)) {
763 return false;
764 }
765 }
766 return this->write_byte(reg, value_byte);
767}
768} // namespace esphome::mcp4461
uint8_t address
Definition bl0906.h:4
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:272
bool status_has_warning() const
Definition component.h:278
void status_clear_warning()
Definition component.h:289
ErrorCode write(const uint8_t *data, size_t len) const
writes an array of bytes to a device using an I2CBus
Definition i2c.h:183
ErrorCode read(uint8_t *data, size_t len) const
reads an array of bytes from the device using an I2CBus
Definition i2c.h:163
bool write_byte(uint8_t a_register, uint8_t data) const
Definition i2c.h:265
I2CRegister reg(uint8_t a_register)
calls the I2CRegister constructor
Definition i2c.h:152
void set_initial_value(Mcp4461WiperIdx wiper, float initial_value)
public function used to set initial value
Definition mcp4461.cpp:67
bool increase_wiper_(Mcp4461WiperIdx wiper)
Definition mcp4461.cpp:429
uint8_t calc_terminal_connector_byte_(Mcp4461TerminalIdx terminal_connector)
Definition mcp4461.cpp:511
uint8_t get_terminal_register_(Mcp4461TerminalIdx terminal_connector)
Definition mcp4461.cpp:528
void update_terminal_register_(Mcp4461TerminalIdx terminal_connector)
Definition mcp4461.cpp:547
uint16_t get_eeprom_value(Mcp4461EepromLocation location)
get eeprom value from location
Definition mcp4461.cpp:660
void process_nonvolatile_dirty_()
Deferred NV mirroring driven from loop() — see WiperState::nonvolatile.
Definition mcp4461.cpp:151
bool set_eeprom_value(Mcp4461EepromLocation location, uint16_t value)
set eeprom value at specified location
Definition mcp4461.cpp:681
void disable_wiper_(Mcp4461WiperIdx wiper)
Definition mcp4461.cpp:407
void initialize_terminal_disabled(Mcp4461WiperIdx wiper, char terminal)
public function used to set disable terminal config
Definition mcp4461.cpp:72
void enable_wiper_(Mcp4461WiperIdx wiper)
Definition mcp4461.cpp:385
bool mcp4461_write_(uint8_t addr, uint16_t data, bool nonvolatile=false)
Definition mcp4461.cpp:751
bool read_16_(uint8_t address, uint16_t *buf)
Definition mcp4461.cpp:245
void enable_terminal_(Mcp4461WiperIdx wiper, char terminal)
Definition mcp4461.cpp:596
bool is_eeprom_ready_for_writing_(bool wait_if_not_ready)
Definition mcp4461.cpp:716
uint16_t read_wiper_level_(uint8_t wiper, bool *ok=nullptr)
Read a wiper register.
Definition mcp4461.cpp:307
uint16_t get_wiper_level_(Mcp4461WiperIdx wiper)
Definition mcp4461.cpp:290
static const LogString * get_message_string(int status)
Definition mcp4461.h:145
bool update_wiper_level_(Mcp4461WiperIdx wiper)
Definition mcp4461.cpp:331
uint8_t get_wiper_address_(uint8_t wiper)
Definition mcp4461.cpp:260
void disable_terminal_(Mcp4461WiperIdx, char terminal)
Definition mcp4461.cpp:629
bool store_level_nonvolatile_(Mcp4461WiperIdx wiper)
Copy the current volatile level of wiper 0-3 into its NV register (immediate, blocking only for a pen...
Definition mcp4461.cpp:179
void write_wiper_level_(uint8_t wiper, uint16_t value)
Definition mcp4461.cpp:375
bool decrease_wiper_(Mcp4461WiperIdx wiper)
Definition mcp4461.cpp:470
bool set_terminal_register_(Mcp4461TerminalIdx terminal_connector, uint8_t data)
Definition mcp4461.cpp:574
bool set_wiper_level_(Mcp4461WiperIdx wiper, uint16_t value)
Definition mcp4461.cpp:347
void read_status_register_to_log()
read status register to log
Definition mcp4461.cpp:237
void set_nonvolatile(Mcp4461WiperIdx wiper, uint32_t write_delay_ms)
enable nonvolatile persistence for a volatile wiper (0-3): every level change is mirrored to the corr...
Definition mcp4461.cpp:58
void yield(void)
bool state
Definition fan.h:2
ErrorCode
Error codes returned by I2CBus and I2CDevice methods.
Definition i2c_bus.h:12
@ ERROR_OK
No error found during execution of method.
Definition i2c_bus.h:14
constexpr uint8_t EEPROM_WRITE_TIMEOUT_MS
Definition mcp4461.cpp:9
constexpr T convert_big_endian(T val)
Convert a value between host byte order and big endian (most significant byte first) order.
Definition helpers.h:940
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t
optional< float > initial_value
Definition mcp4461.h:12
uint32_t nonvolatile_write_delay_ms
Definition mcp4461.h:27