ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
it8951.cpp
Go to the documentation of this file.
1#include "it8951.h"
2
3#include <algorithm>
4#include <cstring>
5
7#include "esphome/core/hal.h"
9#include "esphome/core/log.h"
10
11namespace esphome::it8951 {
12
13ESPHOME_LOG_TAG(TAG, "it8951");
14
15// Soft cap for time spent in a single XFER_ROWS Op so we yield back to the
16// loop within one tick budget.
17static constexpr uint32_t MAX_TRANSFER_TIME_MS = 20;
18
19// --- Loop / scheduling -------------------------------------------------------
20
21void IT8951Display::enqueue_(OpType type, uint16_t a, uint16_t b) {
22 if (!this->queue_.push_back(Op{type, a, b})) {
23 ESP_LOGE(TAG, "Op queue overflow (cap=%u); dropping op type=%u", static_cast<unsigned>(OP_QUEUE_SIZE),
24 static_cast<unsigned>(type));
25 }
26}
27
28void IT8951Display::prepend_(OpType type, uint16_t a, uint16_t b) {
29 if (!this->queue_.push_front(Op{type, a, b})) {
30 ESP_LOGE(TAG, "Op queue overflow (cap=%u); dropping op type=%u", static_cast<unsigned>(OP_QUEUE_SIZE),
31 static_cast<unsigned>(type));
32 }
33}
34
36 // IT8951 Hardware Ready (HW_RDY): HIGH = ready, LOW = busy.
37 return !this->busy_pin_->digital_read();
38}
39
41 const uint32_t now = millis();
42 if (static_cast<int32_t>(now - this->delay_until_) < 0)
43 return;
44
45 // Nothing queued — either the current phase has more work to enqueue, or
46 // we're done.
47 if (this->queue_.empty()) {
48 if (this->phase_ == Phase::IDLE) {
49 this->disable_loop();
50 return;
51 }
52 this->advance_phase_();
53 if (this->queue_.empty())
54 return;
55 }
56
57 // Gate SPI ops on HW_RDY. GPIO/DELAY ops run unconditionally — they're how
58 // we get the controller out of a stuck-busy state in the first place
59 // (e.g. during reset, HW_RDY is undefined/low until ROM boot completes).
60 Op queued_op = this->queue_.front();
61 const bool needs_hardware_ready = queued_op.type != OpType::GPIO_RESET_LOW &&
62 queued_op.type != OpType::GPIO_RESET_HIGH && queued_op.type != OpType::DELAY_MS;
63 if (needs_hardware_ready && this->is_busy_()) {
64 // Signed elapsed: any pending DELAY_MS or scheduled work in the near
65 // future shows up as <= 0 elapsed and won't trigger a false timeout.
66 const int32_t elapsed = static_cast<int32_t>(now - this->phase_started_at_);
67 ESP_LOGV(TAG, "HW_RDY is LOW (busy) in phase %u, elapsed=%" PRId32 "ms", static_cast<unsigned>(this->phase_),
68 elapsed);
69 if (elapsed > static_cast<int32_t>(BUSY_TIMEOUT_MS)) {
70 ESP_LOGW(TAG, "Busy timeout (%" PRIu32 "ms) in phase %u, recovering", elapsed,
71 static_cast<unsigned>(this->phase_));
72 this->recover_();
73 }
74 return;
75 }
76
77 this->queue_.pop_front();
78 this->process_op_(queued_op);
79}
80
82 ESP_LOGV(TAG, "Processing op type=%u a=0x%04X b=0x%04X", static_cast<unsigned>(op.type), op.a, op.b);
83 switch (op.type) {
84 case OpType::CMD:
85 this->spi_cmd_(op.a);
86 break;
87 case OpType::WRITE_W:
88 this->spi_write_word_(op.a);
89 break;
91 this->spi_write_reg_(op.a, op.b);
92 break;
94 this->spi_read_dev_info_();
95 break;
97 this->read_result_ = this->spi_read_word_();
98 break;
100 this->op_check_lut_idle_();
101 break;
102 case OpType::SET_1BPP:
103 this->op_set_1bpp_();
104 break;
106 this->op_xfer_lisar_();
107 break;
109 this->spi_cmd_(TCON_LD_IMG_AREA);
110 break;
112 this->op_xfer_area_args_();
113 break;
115 // Stream rows into the single open LD_IMG_AREA load. The load stays open
116 // across loop iterations (CS toggles between bursts, matching the
117 // reference driver), so a partial slice just re-queues another XFER_ROWS
118 // pass to resume; only when all rows are sent do we close it with one
119 // LD_IMG_END. This avoids an LD_IMG_END / LD_IMG_AREA round-trip per slice.
120 if (this->op_xfer_rows_()) {
122 } else {
124 }
125 break;
127 this->op_xfer_area_end_();
128 break;
130 // Some panel firmwares (notably Seeed reTerminal E1003) silently drop
131 // I80_CMD_DPY_BUF_AREA (0x0037) — the LUT engine never starts and the
132 // host eventually times out after ~12s. Fall back to the basic
133 // I80_CMD_DPY_AREA (0x0034) for those panels; the buffer address is
134 // already programmed via LISAR during the transfer phase.
135 this->spi_cmd_(this->use_legacy_dpy_area_ ? I80_CMD_DPY_AREA : I80_CMD_DPY_BUF_AREA);
136 break;
138 this->op_dpy_buf_args_();
139 break;
141 if (this->reset_pin_ != nullptr)
142 this->reset_pin_->digital_write(false);
143 break;
145 if (this->reset_pin_ != nullptr)
146 this->reset_pin_->digital_write(true);
147 break;
148 case OpType::DELAY_MS:
149 this->delay_until_ = millis() + op.a;
150 break;
151 }
152}
153
155 ESP_LOGV(TAG, "Phase %u -> %u", static_cast<unsigned>(this->phase_), static_cast<unsigned>(next));
156 // Run the loop continuously for the whole active sequence, returning to normal
157 // throttling only at IDLE. Each queued op is processed one per loop iteration,
158 // so at the default ~16ms loop interval the dozens of small ops in the refresh
159 // and restore phases (register polls, 1bpp enable/restore, DPY) would dominate
160 // a partial update's latency. The LUT-idle polls are DELAY_MS-paced, so this
161 // doesn't hammer SPI — it only spends a little extra CPU during the (short,
162 // infrequent) update instead of sleeping between ops. start()/stop() are
163 // idempotent, so driving them off the transition is safe.
164 if (next == Phase::IDLE) {
165 this->high_freq_.stop();
166 } else {
167 this->high_freq_.start();
168 }
169 this->phase_ = next;
170 this->phase_started_at_ = millis();
171}
172
174 switch (this->phase_) {
175 case Phase::IDLE:
176 if (this->initialised_ && this->update_pending_) {
177 this->update_pending_ = false;
179 this->update_started_at_ = millis();
181 this->advance_phase_();
182 } else {
183 this->disable_loop();
184 }
185 break;
186
190 break;
191
193 if (this->dev_info_.panel_width == 0 || this->dev_info_.panel_width > 2048 || this->dev_info_.panel_height == 0 ||
194 this->dev_info_.panel_height > 2048 || this->dev_info_.panel_width == 0xFFFF ||
195 this->dev_info_.panel_height == 0xFFFF) {
196 if (++this->dev_info_attempts_ < 5) {
197 ESP_LOGW(TAG, "DevInfo attempt %u returned invalid data (W=%u H=%u), retrying...", this->dev_info_attempts_,
198 this->dev_info_.panel_width, this->dev_info_.panel_height);
199 // Give the controller more time, then re-read.
200 this->enqueue_(OpType::DELAY_MS, 100);
202 return;
203 }
204 ESP_LOGE(TAG, "DevInfo invalid after %u attempts (W=%u H=%u)", this->dev_info_attempts_,
205 this->dev_info_.panel_width, this->dev_info_.panel_height);
206 this->mark_failed(LOG_STR("Failed to read IT8951 device info"));
207 this->set_phase_(Phase::IDLE);
208 return;
209 }
210
211 if (this->dev_info_.panel_width != this->width_ || this->dev_info_.panel_height != this->height_) {
212 ESP_LOGE(TAG, "Panel dimension mismatch: configured=%ux%u, DevInfo=%ux%u. Check model/dimensions settings.",
213 this->width_, this->height_, this->dev_info_.panel_width, this->dev_info_.panel_height);
214 this->mark_failed(LOG_STR("IT8951 panel dimensions do not match DevInfo"));
215 this->set_phase_(Phase::IDLE);
216 return;
217 }
218
219 this->dev_info_attempts_ = 0;
220 this->row_width_ = this->compute_row_width_();
221 this->buffer_length_ = static_cast<size_t>(this->row_width_) * static_cast<size_t>(this->height_);
224 ESP_LOGI(TAG, "DevInfo: %ux%u, ImgBuf 0x%04X%04X", this->width_, this->height_, this->img_buf_addr_h_,
225 this->img_buf_addr_l_);
227 this->enqueue_init_vcom_();
228 break;
229
230 case Phase::INIT_VCOM:
232 if (this->force_temperature_set_) {
233 this->enqueue_init_temp_();
234 } else {
235 this->advance_phase_();
236 }
237 break;
238
239 case Phase::INIT_TEMP:
241 this->advance_phase_();
242 break;
243
244 case Phase::INIT_DONE:
245 if (this->configured_data_rate_ != 0 && this->configured_data_rate_ != this->data_rate_) {
246 this->spi_teardown();
248 this->spi_setup();
249 }
250 this->initialised_ = true;
251 this->recovery_attempts_ = 0;
252 ESP_LOGCONFIG(TAG, "IT8951 setup complete");
253 this->set_phase_(Phase::IDLE);
254 this->advance_phase_();
255 break;
256
258 this->do_update_();
260 if (!this->prepare_update_region_(mode)) {
261 ESP_LOGD(TAG, "Nothing to update");
262 this->set_phase_(Phase::IDLE);
263 this->advance_phase_();
264 return;
265 }
266 this->active_mode_ = mode;
269 break;
270 }
271
275 break;
276
278 // Fire-and-forget: don't block here waiting for the refresh to complete.
279 // The next update's pre-display LUT-idle poll (and the HW_RDY-gated
280 // TCON_SLEEP) wait as needed, so the refresh time stays off this update's
281 // critical path. The 1bpp display mode is left enabled rather than
282 // restored after every update: on a monochrome display every update
283 // (DU partials and the periodic GC16 cleans) runs in 1bpp mode, so the
284 // bit never needs clearing — and clearing it required a full
285 // refresh-length LUT-idle wait.
287 this->enqueue_update_sleep_();
288 break;
289
291 ESP_LOGV(TAG, "Update took %" PRIu32 "ms (mode=%u area=%ux%u@%u,%u)", millis() - this->update_started_at_,
292 static_cast<unsigned>(this->active_mode_), this->area_w_, this->area_h_, this->area_x_, this->area_y_);
293 this->set_phase_(Phase::IDLE);
294 this->advance_phase_();
295 break;
296 }
297}
298
299// --- Setup -------------------------------------------------------------------
300
302 ESP_LOGCONFIG(TAG, "Setting up IT8951...");
303 this->configured_data_rate_ = this->data_rate_;
304 this->data_rate_ = SPI_PROBE_FREQUENCY;
305 this->spi_setup();
306
307 // Power on the panel before reset and the init handshake.
308 for (auto *pin : this->enable_pins_) {
309 pin->setup();
310 pin->digital_write(true);
311 }
312
313 if (this->reset_pin_ != nullptr) {
314 this->reset_pin_->setup();
315 this->reset_pin_->digital_write(true);
316 }
317 if (this->busy_pin_ != nullptr) {
318 this->busy_pin_->setup();
319 }
320
322 this->reset_dirty_region_();
323
324 // Allocate the framebuffer now: its size is fixed by the configured pixel
325 // format and dimensions, so there's no need to defer to the async controller
326 // init. LVGL (and other writers) can push pixels via draw_pixels_at as soon
327 // as the component is set up — before init completes — and without a buffer
328 // those writes would dereference a null pointer and crash.
329 this->row_width_ = this->compute_row_width_();
330 this->buffer_length_ = static_cast<size_t>(this->row_width_) * static_cast<size_t>(this->height_);
331 RAMAllocator<uint8_t> allocator{};
332 this->buffer_ = allocator.allocate(this->buffer_length_);
333 if (this->buffer_ == nullptr) {
334 this->mark_failed(LOG_STR("Failed to allocate IT8951 framebuffer"));
335 return;
336 }
337 // The allocator does not zero memory; start blank (white) so undrawn regions
338 // (e.g. with auto_clear disabled) don't show garbage on the first update.
339 this->fill(Color::WHITE);
340
341 // Kick off async init via the queue. Reset pulse + boot delay + wake +
342 // packed-write enable; everything blocking lives as DELAY_MS Ops gated by
343 // the loop scheduler.
345 this->enqueue_init_reset_();
346 this->enable_loop();
347}
348
350 // Best-effort synchronous sleep — runs during shutdown so we don't queue.
351 this->spi_cmd_(TCON_SLEEP);
352}
353
355 for (auto *pin : this->enable_pins_) {
356 pin->digital_write(false);
357 }
358}
359
360// --- Init op enqueuers -------------------------------------------------------
361
363 // A reset (including recovery) re-runs SYS_RUN below, so the controller is
364 // awake once this sequence completes.
365 this->asleep_ = false;
366 // Reset pulse: high -> low (reset_duration) -> high -> wait for ROM boot.
369 this->enqueue_(OpType::DELAY_MS, static_cast<uint16_t>(this->reset_duration_));
371 // SPI ROM boot. HW_RDY gating in loop() handles the actual wait, but a small
372 // floor avoids hammering SPI before HW_RDY has settled high. 300ms matches
373 // what most IT8951 reference drivers use for safety.
374 this->enqueue_(OpType::DELAY_MS, 300);
375 this->enqueue_(OpType::CMD, TCON_SYS_RUN);
376 this->enqueue_(OpType::DELAY_MS, 10); // clocks settle after SYS_RUN
377 this->enqueue_(OpType::CMD, TCON_REG_WR); // packed write mode
378 this->enqueue_(OpType::WRITE_REG, I80CPCR, 0x0001);
379}
380
382 // CMD triggers the controller to prepare DevInfo. HW_RDY drops while it works.
383 // The loop-level HW_RDY gate non-blockingly waits before dispatching READ_DEV_INFO.
384 this->enqueue_(OpType::CMD, I80_CMD_GET_DEV_INFO);
386}
387
389 // Always write configured VCOM. The IT8951 stores it in OTP-backed RAM;
390 // rewriting the same value is harmless. The VCOM SET selector is
391 // panel-specific (see I80_CMD_VCOM_WRITE / I80_CMD_VCOM_WRITE_ALT in
392 // it8951_defs.h) and is supplied via the model preset.
393 this->enqueue_(OpType::CMD, I80_CMD_VCOM);
395 this->enqueue_(OpType::WRITE_W, this->vcom_);
396}
397
399 // Force panel temperature (in degrees C) so the controller selects the
400 // correct waveform LUT. Some panels (e.g. Seeed reTerminal E1003) ship
401 // with auto-temperature disabled and rely on the host to declare the
402 // operating temperature; without this, grayscale waveforms run against
403 // a mismatched LUT and pixels do not visibly change even though the LUT
404 // engine completes a full cycle.
405 this->enqueue_(OpType::CMD, I80_CMD_FORCE_TEMP);
406 this->enqueue_(OpType::WRITE_W, I80_CMD_FORCE_TEMP_WRITE);
407 this->enqueue_(OpType::WRITE_W, static_cast<uint16_t>(this->force_temperature_));
408}
409
410// --- Update op enqueuers -----------------------------------------------------
411
413 // If the controller was put to sleep after the previous update, wake it
414 // before touching the display engine. TCON_SLEEP gates off all clocks; a
415 // register read (e.g. the LUTAFSR poll in UPDATE_REFRESH) returns a frozen
416 // value while asleep, so without this the next update stalls forever in
417 // op_check_lut_idle_(). SRAM/registers (packed-write mode, VCOM, LUT) are
418 // retained across sleep, so SYS_RUN + a short settle is all that's needed.
419 if (this->asleep_) {
420 this->enqueue_(OpType::CMD, TCON_SYS_RUN);
421 this->enqueue_(OpType::DELAY_MS, 10); // clocks settle after SYS_RUN
422 this->asleep_ = false;
423 }
424 this->transfer_row_ = 0;
425 // Open a single LD_IMG_AREA load for the whole region. XFER_ROWS streams into
426 // it across as many time-sliced passes as needed and emits the one matching
427 // LD_IMG_END when the last row is sent (see the XFER_ROWS handler).
432}
433
435 ESP_LOGV(TAG, "Enqueueing refresh ops: grayscale=%u", this->grayscale_);
436 // Poll LUT idle: CMD(REG_RD) → WRITE_W(LUTAFSR) → READ_WORD → CHECK_LUT_IDLE
437 this->enqueue_(OpType::CMD, TCON_REG_RD);
438 this->enqueue_(OpType::WRITE_W, LUTAFSR);
441 if (!this->grayscale_) {
442 // Read UP1SR+2: CMD(REG_RD) → WRITE_W(UP1SR+2) → READ_WORD → SET_1BPP
443 this->enqueue_(OpType::CMD, TCON_REG_RD);
444 this->enqueue_(OpType::WRITE_W, static_cast<uint16_t>(UP1SR + 2));
447 }
450}
451
453 if (this->sleep_when_done_) {
454 this->enqueue_(OpType::CMD, TCON_SLEEP);
455 // Remember that the controller is now asleep so the next update wakes it
456 // (see enqueue_update_transfer_) before polling any register.
457 this->asleep_ = true;
458 }
459}
460
461// --- SPI primitives ----------------------------------------------------------
462//
463// IT8951 SPI protocol: no DC pin. 16-bit preamble word identifies whether
464// the transaction is command (0x6000), write-data (0x0000), or read-data
465// (0x1000).
466//
467// All ops are fully non-blocking at the loop level. The loop-level HW_RDY gate
468// guarantees the controller is ready before any op is dispatched.
469//
470// Within a single CS-asserted transaction, the IT8951 requires HW_RDY to be
471// checked after the preamble word before sending the first data word. This
472// is a hardware protocol requirement — the controller needs a few clock
473// cycles to latch the preamble and configure its internal bus direction.
474// In practice this completes in <1µs for write ops; we use a short spin
475// (max ~50µs) that never triggers under normal operation.
476
477static constexpr uint32_t INTRA_CS_READY_TIMEOUT_US = 50;
478
479static inline void wait_for_hardware_ready(GPIOPin *busy_pin) {
480 if (busy_pin == nullptr)
481 return;
482 uint32_t waited = 0;
483 while (!busy_pin->digital_read()) {
484 if (waited >= INTRA_CS_READY_TIMEOUT_US)
485 return;
487 waited += 1;
488 }
489}
490
491void IT8951Display::spi_cmd_(uint16_t cmd) {
492 this->enable();
493 this->write_byte16(PACKET_TYPE_CMD);
494 wait_for_hardware_ready(this->busy_pin_);
495 this->write_byte16(cmd);
496 this->disable();
497}
498
499void IT8951Display::spi_write_word_(uint16_t value) {
500 this->enable();
501 this->write_byte16(PACKET_TYPE_WRITE);
502 wait_for_hardware_ready(this->busy_pin_);
503 this->write_byte16(value);
504 this->disable();
505}
506
507void IT8951Display::spi_write_reg_(uint16_t addr, uint16_t value) {
508 // Single CS transaction: WRITE preamble + addr + value.
509 // Caller must have already sent CMD(TCON_REG_WR) as a prior op.
510 this->enable();
511 this->write_byte16(PACKET_TYPE_WRITE);
512 wait_for_hardware_ready(this->busy_pin_);
513 this->write_byte16(addr);
514 this->write_byte16(value);
515 this->disable();
516}
517
518void IT8951Display::spi_write_args_(const uint16_t *args, uint16_t count) {
519 // Single CS transaction: WRITE preamble + N data words.
520 this->enable();
521 this->write_byte16(PACKET_TYPE_WRITE);
522 wait_for_hardware_ready(this->busy_pin_);
523 for (uint16_t i = 0; i < count; i++)
524 this->write_byte16(args[i]);
525 this->disable();
526}
527
529 // Single CS read transaction. HW_RDY was confirmed HIGH by the loop gate
530 // before this op was dispatched, so data is ready.
531 this->enable();
532 this->write_byte16(PACKET_TYPE_READ);
533 wait_for_hardware_ready(this->busy_pin_);
534 this->write_byte16(0x0000); // dummy — provides clock cycles for controller
535 wait_for_hardware_ready(this->busy_pin_);
536 // Read byte-by-byte: a 2-byte transfer_array can lose the low byte on
537 // ESP-IDF SPI DMA due to 4-byte alignment requirements.
538 const uint8_t hi = this->transfer_byte(0);
539 const uint8_t lo = this->transfer_byte(0);
540 this->disable();
541 return encode_uint16(hi, lo);
542}
543
545 // Read DevInfo struct. The CMD(GET_DEV_INFO) was already sent as a prior op,
546 // and the loop HW_RDY gate waited for the controller to prepare data.
547 std::memset(&this->dev_info_, 0, sizeof(this->dev_info_));
548 this->enable();
549 this->write_byte16(PACKET_TYPE_READ);
550 wait_for_hardware_ready(this->busy_pin_);
551 this->write_byte16(0x0000); // dummy
552 wait_for_hardware_ready(this->busy_pin_);
553 auto *words = reinterpret_cast<uint16_t *>(&this->dev_info_);
554 constexpr uint32_t word_count = sizeof(this->dev_info_) / sizeof(uint16_t);
555 for (uint32_t i = 0; i < word_count; i++) {
556 const uint8_t hi = this->transfer_byte(0);
557 const uint8_t lo = this->transfer_byte(0);
558 words[i] = encode_uint16(hi, lo);
559 }
560 this->disable();
561}
562
563// --- Compound Ops ------------------------------------------------------------
564
566 // Set image-buffer target address. Two register writes = 4 CS transactions.
567 // Push to FRONT in reverse order so they execute before the rest of the queue.
568 this->prepend_(OpType::WRITE_REG, LISAR, this->img_buf_addr_l_);
569 this->prepend_(OpType::CMD, TCON_REG_WR, 0);
570 this->prepend_(OpType::WRITE_REG, static_cast<uint16_t>(LISAR + 2), this->img_buf_addr_h_);
571 this->prepend_(OpType::CMD, TCON_REG_WR, 0);
572}
573
575 // Single CS transaction: WRITE preamble + 5 area-parameter words describing
576 // the full update region. Sent once when the load is opened (transfer_row_ is
577 // 0); XFER_ROWS then streams every row into this one area.
578 uint16_t args[5];
579 if (this->grayscale_) {
580 args[0] = static_cast<uint16_t>((LDIMG_B_ENDIAN << 8) | (PIXEL_4BPP << 4));
581 args[1] = this->area_x_;
582 args[2] = this->area_y_;
583 args[3] = this->area_w_;
584 args[4] = this->area_h_;
585 } else {
586 // Monochrome is loaded via the 8bpp-packed trick: x and width are expressed
587 // in bytes (8 pixels each) and the controller unpacks one bit per pixel.
588 args[0] = static_cast<uint16_t>((LDIMG_L_ENDIAN << 8) | (PIXEL_8BPP << 4));
589 args[1] = static_cast<uint16_t>(this->area_x_ / 8);
590 args[2] = this->area_y_;
591 args[3] = static_cast<uint16_t>(this->area_w_ / 8);
592 args[4] = this->area_h_;
593 }
594 this->spi_write_args_(args, 5);
595}
596
597void IT8951Display::op_xfer_area_end_() { this->spi_cmd_(TCON_LD_IMG_END); }
598
600 const uint32_t start_time = millis();
601 const uint16_t area_y = this->area_y_;
602 const uint16_t area_h = this->area_h_;
603
604 // Bytes per source row, and the byte offset of area_x within a row, in the
605 // framebuffer's native packing. These match the per-row byte count the
606 // controller expects from op_xfer_area_args_: area_w/2 for 4bpp grayscale,
607 // area_w/8 for the 1bpp-packed monochrome trick. area_x / area_w are
608 // 16-pixel aligned (see prepare_update_region_), so both divisions are exact.
609 const uint16_t bytes_per_row =
610 this->grayscale_ ? static_cast<uint16_t>(this->area_w_ >> 1) : static_cast<uint16_t>(this->area_w_ >> 3);
611 const uint16_t row_x_bytes =
612 this->grayscale_ ? static_cast<uint16_t>(this->area_x_ >> 1) : static_cast<uint16_t>(this->area_x_ >> 3);
613
614 // Single CS write transaction — HW_RDY was confirmed high by the loop gate.
615 this->enable();
616 this->write_byte16(PACKET_TYPE_WRITE);
617 wait_for_hardware_ready(this->busy_pin_);
618
619 // Each source row is a contiguous slice of the framebuffer in both formats —
620 // the buffer already holds the wire bytes — so stream it straight to SPI with
621 // no per-pixel packing or temporary buffer.
622 while (this->transfer_row_ < area_h) {
623 const uint32_t offset = (static_cast<uint32_t>(area_y) + this->transfer_row_) * this->row_width_ + row_x_bytes;
624 this->write_array(&this->buffer_[offset], bytes_per_row);
625 this->transfer_row_++;
626 if (millis() - start_time >= MAX_TRANSFER_TIME_MS)
627 break;
628 }
629
630 this->disable();
631 return this->transfer_row_ >= area_h;
632}
633
635 // I80_CMD_DPY_BUF_AREA (0x0037) takes 7 args (with explicit buffer addr).
636 // I80_CMD_DPY_AREA (0x0034) takes 5 args; the buffer address is taken
637 // from LISAR which we program during the transfer phase, so this is safe.
638 if (this->use_legacy_dpy_area_) {
639 const uint16_t args[5] = {
640 this->area_x_, this->area_y_, this->area_w_, this->area_h_, static_cast<uint16_t>(this->active_mode_),
641 };
642 this->spi_write_args_(args, 5);
643 return;
644 }
645 const uint16_t args[7] = {
646 this->area_x_,
647 this->area_y_,
648 this->area_w_,
649 this->area_h_,
650 static_cast<uint16_t>(this->active_mode_),
651 this->img_buf_addr_l_,
652 this->img_buf_addr_h_,
653 };
654 this->spi_write_args_(args, 7);
655}
656
658 ESP_LOGV(TAG, "Checking LUT idle, read_result_=0x%04X", this->read_result_);
659 // read_result_ holds LUTAFSR value from the preceding READ_WORD op.
660 if (this->read_result_ != 0) {
661 // LUT still busy — re-enqueue the full read sequence after a short delay.
662 this->prepend_(OpType::CHECK_LUT_IDLE, 0, 0);
663 this->prepend_(OpType::READ_WORD, 0, 0);
664 this->prepend_(OpType::WRITE_W, LUTAFSR, 0);
665 this->prepend_(OpType::CMD, TCON_REG_RD, 0);
666 this->prepend_(OpType::DELAY_MS, 5, 0);
667 }
668}
669
671 // read_result_ holds UP1SR+2 value. Set bit 2 and write back, then set BGVR.
672 // Push to FRONT in reverse order so they execute before DPY_BUF_CMD/ARGS
673 // that are already in the queue.
674 const uint16_t modified = static_cast<uint16_t>(this->read_result_ | (1U << 2));
675 this->prepend_(OpType::WRITE_REG, BGVR, 0xFF00);
676 this->prepend_(OpType::CMD, TCON_REG_WR, 0);
677 this->prepend_(OpType::WRITE_REG, UP1SR + 2, modified);
678 this->prepend_(OpType::CMD, TCON_REG_WR, 0);
679}
680
681// --- Update prep / public API ------------------------------------------------
682
684 this->partial_update_count_++;
685 const bool full_update = this->partial_update_count_ >= this->full_update_every_;
686 if (full_update) {
687 this->partial_update_count_ = 0;
688 mode = UPDATE_MODE_GC16;
689 this->x_low_ = 0;
690 this->y_low_ = 0;
691 this->x_high_ = this->width_;
692 this->y_high_ = this->height_;
693 } else {
694 // Align the partial region's X extent to 32 pixels. The IT8951's partial
695 // display refresh snaps the X start/width to a 32-pixel boundary (the panel
696 // source driver fetches 32-pixel chunks); refreshing a region whose X is
697 // only 16-aligned makes the panel snap it down to the previous boundary,
698 // shifting that update ~16px to the left. 32-alignment also satisfies the
699 // load constraints (4bpp X must be a multiple of 4; the 8bpp-packed mono
700 // load needs x/8 even, i.e. X a multiple of 16).
701 this->x_low_ &= 0xFFE0;
702 uint16_t temp_max = this->x_high_ > 0 ? static_cast<uint16_t>(this->x_high_ - 1) : 0;
703 temp_max = static_cast<uint16_t>(temp_max | 0x001F);
704 if (temp_max >= this->width_)
705 temp_max = static_cast<uint16_t>(this->width_ - 1);
706 this->x_high_ = static_cast<uint16_t>(temp_max + 1);
707 }
708
710 this->reset_dirty_region_();
711 return false;
712 }
713
714 const uint16_t x = this->x_low_;
715 const uint16_t y = this->y_low_;
716 const uint16_t width = static_cast<uint16_t>(this->x_high_ - this->x_low_);
717 const uint16_t height = static_cast<uint16_t>(this->y_high_ - this->y_low_);
718
719 if (x >= this->width_ || y >= this->height_ || (x + width) > this->width_ || (y + height) > this->height_) {
720 ESP_LOGE(TAG, "Dirty region (%u,%u %ux%u) out of bounds", x, y, width, height);
721 this->reset_dirty_region_();
722 return false;
723 }
724
725 this->area_x_ = x;
726 this->area_y_ = y;
727 this->area_w_ = width;
728 this->area_h_ = height;
729 this->transfer_row_ = 0;
730
731 // On non-full updates, downgrade monochrome frames from the full, flashy GC16
732 // clear to DU — a fast, low-flash absolute waveform — so full_update_every
733 // buys cheaper refreshes between the periodic GC16 cleans that clear
734 // accumulated ghosting.
735 //
736 // Grayscale frames are deliberately left on GC16: every reduced grayscale
737 // waveform this controller exposes (the non-flashing GL family GL16/GLR16/
738 // GLD16, and the 4-tone DU4) renders incorrectly on the supported panels —
739 // a white background is driven to grey rather than staying white. GC16 is the
740 // only waveform that reproduces grayscale faithfully, so we keep it.
741 //
742 // An explicitly configured non-GC16 update_mode is honoured as-is.
743 if (!full_update && mode == UPDATE_MODE_GC16 && !this->grayscale_)
744 mode = UPDATE_MODE_DU;
745
746 this->reset_dirty_region_();
747
748 ESP_LOGV(TAG, "Update: %ux%u@%u,%u mode=%u (%s)", width, height, x, y, static_cast<unsigned>(mode),
749 this->grayscale_ ? LOG_STR_LITERAL("grayscale") : LOG_STR_LITERAL("mono"));
750 return true;
751}
752
754 this->x_low_ = this->width_;
755 this->x_high_ = 0;
756 this->y_low_ = this->height_;
757 this->y_high_ = 0;
758}
759
761 if (this->phase_ == Phase::IDLE && this->initialised_) {
762 this->update_started_at_ = millis();
763 this->active_mode_ = mode;
765 this->enable_loop();
766 this->advance_phase_();
767 } else {
768 // Coalesce: latest pending mode wins.
769 this->update_pending_ = true;
771 this->enable_loop();
772 }
773}
774
776 if (!this->is_ready())
777 return;
780 return;
781 }
783}
784
786 if (mode == UPDATE_MODE_NONE) {
787 this->update();
788 return;
789 }
790 if (!this->is_ready())
791 return;
792 this->start_update_(mode);
793}
794
795// --- Recovery ----------------------------------------------------------------
796
798 if (++this->recovery_attempts_ > 3) {
799 ESP_LOGE(TAG, "Recovery failed after %u attempts; giving up. Check BUSY pin wiring and power.",
800 this->recovery_attempts_);
801 this->mark_failed(LOG_STR("IT8951 recovery exhausted"));
802 this->queue_.clear();
803 this->set_phase_(Phase::IDLE);
804 this->disable_loop();
805 return;
806 }
807 ESP_LOGW(TAG, "Recovering (attempt %u): hardware-resetting controller (was in phase %u)", this->recovery_attempts_,
808 static_cast<unsigned>(this->phase_));
809 this->queue_.clear();
810 this->update_pending_ = false;
811 this->transfer_row_ = 0;
812 this->initialised_ = false;
813 this->dev_info_attempts_ = 0;
814
815 // Drop SPI clock back to the safe probe rate for the re-init handshake.
816 if (this->configured_data_rate_ != 0 && this->data_rate_ != SPI_PROBE_FREQUENCY) {
817 this->spi_teardown();
818 this->set_data_rate(SPI_PROBE_FREQUENCY);
819 this->spi_setup();
820 }
821
822 // Force a full redraw on next opportunity.
823 this->x_low_ = 0;
824 this->y_low_ = 0;
825 this->x_high_ = this->width_;
826 this->y_high_ = this->height_;
827
829 this->enqueue_init_reset_();
830 this->update_pending_ = true;
832 this->enable_loop();
833}
834
835// --- Coordinate transform ----------------------------------------------------
836
838 switch (this->rotation_) {
840 this->effective_transform_ = this->transform_ ^ (TRANSFORM_SWAP_XY | TRANSFORM_MIRROR_X);
841 break;
843 this->effective_transform_ = this->transform_ ^ (TRANSFORM_MIRROR_Y | TRANSFORM_MIRROR_X);
844 break;
846 this->effective_transform_ = this->transform_ ^ (TRANSFORM_SWAP_XY | TRANSFORM_MIRROR_Y);
847 break;
848 default:
849 this->effective_transform_ = this->transform_;
850 break;
851 }
852}
853
854void IT8951Display::apply_transform_(int &x, int &y) const {
855 if (this->effective_transform_ & TRANSFORM_SWAP_XY)
856 std::swap(x, y);
857 if (this->effective_transform_ & TRANSFORM_MIRROR_X)
858 x = this->width_ - x - 1;
859 if (this->effective_transform_ & TRANSFORM_MIRROR_Y)
860 y = this->height_ - y - 1;
861}
862
864 if (this->is_point_clipped(x, y))
865 return false;
866 this->apply_transform_(x, y);
867 if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
868 return false;
869 this->x_low_ = clamp_at_most(this->x_low_, x);
870 this->x_high_ = clamp_at_least(this->x_high_, x + 1);
871 this->y_low_ = clamp_at_most(this->y_low_, y);
872 this->y_high_ = clamp_at_least(this->y_high_, y + 1);
873 return true;
874}
875
876// --- Color / drawing ---------------------------------------------------------
877
878static uint8_t quantize_8bit_to_nibble(uint8_t value) {
879 uint8_t nibble = static_cast<uint8_t>((static_cast<uint16_t>(value) + 8) >> 4);
880 return nibble > 0x0F ? 0x0F : nibble;
881}
882
883static uint8_t color_to_nibble(const Color &color) {
884 // Grayscale images are emitted as Color(gray, gray, gray, 0xFF).
885 // Handle this shape first so endpoint values don't alias COLOR_ON/OFF.
886 if (color.w == 0xFF && color.r == color.g && color.g == color.b)
887 return quantize_8bit_to_nibble(color.r);
888
889 if (color.raw_32 == 0)
890 return 0x00; // black
891 if (color.raw_32 == 0xFFFFFFFF)
892 return 0x0F; // white
893
894 // Rec.601 luma is the standard for converting SDR images to grayscale and spreads saturated colours
895 // across the mid-range; Rec.709 instead crams them against white/black where the 16 panel levels are
896 // hard to tell apart.
897 return quantize_8bit_to_nibble(display::rec601_luma(color));
898}
899
900// 4x4 ordered (Bayer) dither threshold over the weighted-luma range (0..65535).
901// A pixel whose luma is below the threshold renders black, so lighter pixels
902// produce progressively sparser black dots instead of vanishing to white. The
903// matrix averages to 32768, matching the conventional monochrome cut, while the
904// per-pixel variation reproduces intermediate gray levels.
905static uint16_t dither_threshold(uint16_t x, uint16_t y) {
906 static const uint8_t BAYER4[16] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5};
907 return static_cast<uint16_t>(BAYER4[((y & 3) << 2) | (x & 3)] * 4096u + 2048u);
908}
909
911 if (this->buffer_ == nullptr)
912 return;
913 if (this->get_clipping().is_set()) {
914 Display::fill(color);
915 return;
916 }
917 uint8_t packed = color_to_nibble(color);
918 if (this->invert_colors_)
919 packed = 0x0F - packed;
920 uint8_t fill_byte;
921 if (this->grayscale_) {
922 fill_byte = static_cast<uint8_t>((packed << 4) | packed);
923 } else {
924 fill_byte = (packed <= 0x07) ? 0xFF : 0x00;
925 }
926 memset(this->buffer_, fill_byte, this->buffer_length_);
927 this->x_low_ = 0;
928 this->y_low_ = 0;
929 this->x_high_ = this->width_;
930 this->y_high_ = this->height_;
931}
932
933void HOT IT8951Display::draw_pixel_at(int x, int y, Color color) {
934 if (this->buffer_ == nullptr)
935 return;
936 this->feed_wdt_per_pixel_();
937 if (!this->rotate_coordinates_(x, y))
938 return;
939 this->write_pixel_native_(static_cast<uint16_t>(x), static_cast<uint16_t>(y), color);
940}
941
942void HOT IT8951Display::write_pixel_native_(uint16_t x, uint16_t y, const Color &color) const {
943 if (this->grayscale_) {
944 uint8_t nibble = color_to_nibble(color);
945 if (this->invert_colors_)
946 nibble = static_cast<uint8_t>(0x0F - nibble);
947 this->set_gray_pixel_(x, y, nibble);
948 } else {
949 // Rec.601 luma (see color_to_nibble). Weights sum to 257 so white maps to
950 // exactly 65535, using the full 16-bit range without overflow.
951 auto lum = static_cast<uint16_t>(77u * color.r + 151u * color.g + 29u * color.b);
952 if (this->invert_colors_)
953 lum = static_cast<uint16_t>(65535u - lum);
954 // Set the bit (foreground/black) when this pixel is darker than its
955 // threshold. With dithering the threshold varies per pixel so pale colours
956 // render as visible texture; otherwise it's the fixed ~50% cut (r+g+b<32768).
957 const uint16_t threshold = this->dithering_ ? dither_threshold(x, y) : 32768;
958 this->set_mono_pixel_(x, y, lum < threshold);
959 }
960}
961
962void HOT IT8951Display::draw_pixels_internal(int x_start, int y_start, int w, int h, const uint8_t *ptr,
964 int x_pad) {
965 // A writer (e.g. LVGL) may push pixels before the framebuffer is ready or
966 // after an allocation failure; ignore those rather than dereferencing null.
967 if (this->buffer_ == nullptr)
968 return;
969 // A clipping rectangle would need a per-pixel test; that's rare for the bulk
970 // blit callers (LVGL, images), so fall back to the base per-pixel path then.
971 if (this->get_clipping().is_set()) {
973 return;
974 }
975
976 const size_t line_stride = static_cast<size_t>(x_offset) + w + x_pad; // source line length in pixels
977 for (int y = 0; y < h; y++) {
978 App.feed_wdt();
979 size_t source_idx = (static_cast<size_t>(y_offset) + y) * line_stride + x_offset;
980 for (int x = 0; x < w; x++, source_idx++) {
981 uint32_t color_value;
982 switch (bitness) {
983 case COLOR_BITNESS_565: {
984 const size_t i = source_idx * 2;
985 color_value = big_endian ? (static_cast<uint32_t>(ptr[i]) << 8) | ptr[i + 1]
986 : ptr[i] | (static_cast<uint32_t>(ptr[i + 1]) << 8);
987 break;
988 }
989 case COLOR_BITNESS_888: {
990 const size_t i = source_idx * 3;
991 color_value = ptr[i] | (static_cast<uint32_t>(ptr[i + 1]) << 8) | (static_cast<uint32_t>(ptr[i + 2]) << 16);
992 break;
993 }
994 default:
995 color_value = ptr[source_idx];
996 break;
997 }
998 int nx = x_start + x;
999 int ny = y_start + y;
1000 this->apply_transform_(nx, ny);
1001 if (nx < 0 || ny < 0 || nx >= this->width_ || ny >= this->height_)
1002 continue;
1003 this->write_pixel_native_(static_cast<uint16_t>(nx), static_cast<uint16_t>(ny),
1004 ColorUtil::to_color(color_value, bitness));
1005 }
1006 }
1007
1008 // Expand the dirty bounding box once from the transformed block corners: the
1009 // image of an axis-aligned rectangle under swap/mirror is still axis-aligned,
1010 // so its two opposite corners bound it.
1011 int x0 = x_start, y0 = y_start;
1012 int x1 = x_start + w - 1, y1 = y_start + h - 1;
1013 this->apply_transform_(x0, y0);
1014 this->apply_transform_(x1, y1);
1015 const int nx_lo = std::max(0, std::min(x0, x1));
1016 const int ny_lo = std::max(0, std::min(y0, y1));
1017 const int nx_hi = std::min(this->width_ - 1, std::max(x0, x1));
1018 const int ny_hi = std::min(this->height_ - 1, std::max(y0, y1));
1019 if (nx_hi >= nx_lo && ny_hi >= ny_lo) {
1020 this->x_low_ = clamp_at_most(this->x_low_, nx_lo);
1021 this->x_high_ = clamp_at_least(this->x_high_, nx_hi + 1);
1022 this->y_low_ = clamp_at_most(this->y_low_, ny_lo);
1023 this->y_high_ = clamp_at_least(this->y_high_, ny_hi + 1);
1024 }
1025}
1026
1027void IT8951Display::set_mono_pixel_(uint16_t x, uint16_t y, bool value) const {
1028 // The monochrome framebuffer holds the exact bytes streamed to the
1029 // controller for the 8bpp-load / 1bpp-display trick (L_ENDIAN). Pixels are
1030 // grouped in 16s; on the wire the high byte (pixels 8..15) precedes the low
1031 // byte (pixels 0..7), and the bit index within a byte is the pixel's offset
1032 // (LSB = lowest x). Storing in that order lets op_xfer_rows_ copy rows
1033 // verbatim with no packing or byte-swapping.
1034 const uint16_t group = static_cast<uint16_t>(x >> 4);
1035 const uint8_t sub = static_cast<uint8_t>(x & 0x0F);
1036 const uint16_t byte_index = static_cast<uint16_t>(group * 2u + (sub < 8u ? 1u : 0u));
1037 const uint8_t mask = static_cast<uint8_t>(1u << (sub & 0x07));
1038 const uint32_t index = static_cast<uint32_t>(y) * this->row_width_ + byte_index;
1039 if (value) {
1040 this->buffer_[index] |= mask;
1041 } else {
1042 this->buffer_[index] &= static_cast<uint8_t>(~mask);
1043 }
1044}
1045
1046void IT8951Display::set_gray_pixel_(uint16_t x, uint16_t y, uint8_t nibble) const {
1047 const uint32_t index = static_cast<uint32_t>(y) * this->row_width_ + (static_cast<uint32_t>(x) >> 1);
1048 uint8_t buf = this->buffer_[index];
1049 if (x & 0x1) {
1050 buf = (buf & 0xF0) | nibble;
1051 } else {
1052 buf = (buf & 0x0F) | static_cast<uint8_t>(nibble << 4);
1053 }
1054 this->buffer_[index] = buf;
1055}
1056
1057// --- Diagnostics -------------------------------------------------------------
1058
1060 LOG_DISPLAY("", "IT8951 E-Paper", this);
1061 char force_temperature[24];
1062 if (this->force_temperature_set_) {
1063 snprintf(force_temperature, sizeof(force_temperature), "%d °C", this->force_temperature_);
1064 } else {
1065 strncpy(force_temperature, "(controller default)", sizeof(force_temperature));
1066 force_temperature[sizeof(force_temperature) - 1] = '\0';
1067 }
1068 ESP_LOGCONFIG(
1069 TAG,
1070 " Model preset: %s"
1071 "\n Dimensions: %dx%d"
1072 "\n Buffer: %u bytes"
1073 "\n Image buffer addr: 0x%04X%04X"
1074 "\n VCOM: %.02fV (set selector 0x%04X)"
1075 "\n Force temperature: %s"
1076 "\n Display command: %s"
1077 "\n Sleep when done: %s"
1078 "\n Full update every: %u"
1079 "\n Inverted colors: %s"
1080 "\n Pixel format: %s"
1081 "\n Reset duration: %" PRIu32 "ms",
1082 this->name_ != nullptr ? this->name_ : LOG_STR_LITERAL("(unknown)"), this->get_width_internal(),
1083 this->get_height_internal(), static_cast<unsigned>(this->buffer_length_), this->img_buf_addr_h_,
1084 this->img_buf_addr_l_, static_cast<float>(this->vcom_) / 1000.0f, this->vcom_register_, force_temperature,
1085 this->use_legacy_dpy_area_ ? LOG_STR_LITERAL("DPY_AREA (0x0034, legacy)")
1086 : LOG_STR_LITERAL("DPY_BUF_AREA (0x0037)"),
1087 YESNO(this->sleep_when_done_), this->full_update_every_, YESNO(this->invert_colors_),
1088 this->grayscale_ ? LOG_STR_LITERAL("4bpp grayscale") : LOG_STR_LITERAL("1bpp monochrome"), this->reset_duration_);
1089 LOG_PIN(" Reset Pin: ", this->reset_pin_);
1090 LOG_PIN(" Busy Pin: ", this->busy_pin_);
1091 LOG_PIN(" CS Pin: ", this->cs_);
1092 LOG_UPDATE_INTERVAL(this);
1093}
1094
1095} // namespace esphome::it8951
BedjetMode mode
BedJet operating mode.
void feed_wdt()
Feed the task watchdog.
void mark_failed()
Mark this component as failed.
bool is_ready() const
void enable_loop()
Enable this component's loop.
Definition component.h:248
void disable_loop()
Disable this component's loop.
virtual void setup()=0
virtual void digital_write(bool value)=0
virtual bool digital_read()=0
void stop()
Stop running the loop continuously.
Definition helpers.cpp:841
void start()
Start running the loop continuously.
Definition helpers.cpp:835
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2222
T * allocate(size_t n)
Definition helpers.h:2249
static Color to_color(uint32_t colorcode, ColorBitness color_bitness=ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned=true)
Unpacks a color of the given bitness, red in the most significant bits.
int int int const uint8_t ColorOrder ColorBitness bitness
Definition display.h:389
void ESPHOME_ALWAYS_INLINE feed_wdt_per_pixel_()
Watchdog feed for per pixel loops.
Definition display.h:842
int int int const uint8_t ColorOrder ColorBitness bool int int y_offset
Definition display.h:389
virtual void fill(Color color)
Fill the entire screen with the given color.
Definition display.cpp:16
int int int const uint8_t ColorOrder ColorBitness bool int int int x_pad
Definition display.h:389
int int int const uint8_t * ptr
Definition display.h:388
bool ESPHOME_ALWAYS_INLINE is_point_clipped(int x, int y) const
Whether (x, y) falls outside the active clipping rectangle.
Definition display.h:807
int int int const uint8_t ColorOrder ColorBitness bool big_endian
Definition display.h:389
Rect get_clipping() const
Get the current the clipping rectangle.
Definition display.cpp:826
int int int const uint8_t ColorOrder ColorBitness bool int x_offset
Definition display.h:389
DisplayRotation rotation_
Definition display.h:860
virtual void draw_pixels_internal(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad)
Draws a block for draw_pixels_at(); the parameters are the same.
Definition display.cpp:119
void on_powerdown() override
Definition it8951.cpp:354
void update_mode(UpdateMode mode)
Definition it8951.cpp:785
void start_update_(UpdateMode mode)
Definition it8951.cpp:760
void spi_write_args_(const uint16_t *args, uint16_t count)
Definition it8951.cpp:518
HighFrequencyLoopRequester high_freq_
Definition it8951.h:287
void set_phase_(Phase next)
Definition it8951.cpp:154
void spi_cmd_(uint16_t cmd)
Definition it8951.cpp:491
bool rotate_coordinates_(int &x, int &y)
Definition it8951.cpp:863
void enqueue_(OpType type, uint16_t a=0, uint16_t b=0)
Definition it8951.cpp:21
uint16_t compute_row_width_() const
Definition it8951.h:227
void spi_write_reg_(uint16_t addr, uint16_t value)
Definition it8951.cpp:507
void prepend_(OpType type, uint16_t a=0, uint16_t b=0)
Definition it8951.cpp:28
std::vector< GPIOPin * > enable_pins_
Definition it8951.h:337
int get_height_internal() override
Definition it8951.h:211
StaticOpQueue< Op, OP_QUEUE_SIZE > queue_
Definition it8951.h:281
void draw_pixels_internal(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override
Definition it8951.cpp:962
void set_mono_pixel_(uint16_t x, uint16_t y, bool value) const
Definition it8951.cpp:1027
void write_pixel_native_(uint16_t x, uint16_t y, const Color &color) const
Definition it8951.cpp:942
void apply_transform_(int &x, int &y) const
Definition it8951.cpp:854
void process_op_(const Op &op)
Definition it8951.cpp:81
void fill(Color color) override
Definition it8951.cpp:910
void spi_write_word_(uint16_t value)
Definition it8951.cpp:499
void on_safe_shutdown() override
Definition it8951.cpp:349
static constexpr uint32_t BUSY_TIMEOUT_MS
Definition it8951.h:279
bool prepare_update_region_(UpdateMode &mode)
Definition it8951.cpp:683
void set_gray_pixel_(uint16_t x, uint16_t y, uint8_t nibble) const
Definition it8951.cpp:1046
int get_width_internal() override
Definition it8951.h:212
void draw_pixel_at(int x, int y, Color color) override
Definition it8951.cpp:933
uint32_t data_rate_
Definition spi.h:430
uint8_t type
unsigned count
Definition gif.cpp:51
@ DISPLAY_ROTATION_270_DEGREES
Definition display.h:139
@ DISPLAY_ROTATION_180_DEGREES
Definition display.h:138
@ DISPLAY_ROTATION_90_DEGREES
Definition display.h:137
constexpr uint8_t rec601_luma(Color color)
Rec.601 luma of an RGB color, for optimum perceptual brightness.
static void int int int int const uint8_t ColorOrder ColorBitness bool big_endian
Definition display.cpp:101
static void int int int int const uint8_t ColorOrder ColorBitness bitness
Definition display.cpp:101
static void int x_start
Definition display.cpp:62
static void int int int int const uint8_t int x_offset
Definition display.cpp:63
static void int int y_start
Definition display.cpp:62
static void int int int int const uint8_t int int y_offset
Definition display.cpp:63
static void int int int w
Definition display.cpp:62
ESPHOME_LOG_TAG(TAG, "display")
static void int int int int h
Definition display.cpp:62
static void int int int int const uint8_t * ptr
Definition display.cpp:63
static void int int int int const uint8_t int int int x_pad
Definition display.cpp:63
T clamp_at_most(T value, U max)
Definition helpers.h:2402
void IRAM_ATTR HOT delayMicroseconds(uint32_t us)
Definition hal.cpp:48
T clamp_at_least(T value, U min)
Definition helpers.h:2397
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:980
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
uint8_t w
Definition color.h:42
uint8_t g
Definition color.h:34
uint8_t b
Definition color.h:38
uint32_t raw_32
Definition color.h:47
static const Color WHITE
Definition color.h:191
uint8_t r
Definition color.h:30
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6