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