ESPHome 2026.8.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
13static const char *const 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
354// --- Init op enqueuers -------------------------------------------------------
355
357 // A reset (including recovery) re-runs SYS_RUN below, so the controller is
358 // awake once this sequence completes.
359 this->asleep_ = false;
360 // Reset pulse: high -> low (reset_duration) -> high -> wait for ROM boot.
363 this->enqueue_(OpType::DELAY_MS, static_cast<uint16_t>(this->reset_duration_));
365 // SPI ROM boot. HW_RDY gating in loop() handles the actual wait, but a small
366 // floor avoids hammering SPI before HW_RDY has settled high. 300ms matches
367 // what most IT8951 reference drivers use for safety.
368 this->enqueue_(OpType::DELAY_MS, 300);
369 this->enqueue_(OpType::CMD, TCON_SYS_RUN);
370 this->enqueue_(OpType::DELAY_MS, 10); // clocks settle after SYS_RUN
371 this->enqueue_(OpType::CMD, TCON_REG_WR); // packed write mode
372 this->enqueue_(OpType::WRITE_REG, I80CPCR, 0x0001);
373}
374
376 // CMD triggers the controller to prepare DevInfo. HW_RDY drops while it works.
377 // The loop-level HW_RDY gate non-blockingly waits before dispatching READ_DEV_INFO.
378 this->enqueue_(OpType::CMD, I80_CMD_GET_DEV_INFO);
380}
381
383 // Always write configured VCOM. The IT8951 stores it in OTP-backed RAM;
384 // rewriting the same value is harmless. The VCOM SET selector is
385 // panel-specific (see I80_CMD_VCOM_WRITE / I80_CMD_VCOM_WRITE_ALT in
386 // it8951_defs.h) and is supplied via the model preset.
387 this->enqueue_(OpType::CMD, I80_CMD_VCOM);
389 this->enqueue_(OpType::WRITE_W, this->vcom_);
390}
391
393 // Force panel temperature (in degrees C) so the controller selects the
394 // correct waveform LUT. Some panels (e.g. Seeed reTerminal E1003) ship
395 // with auto-temperature disabled and rely on the host to declare the
396 // operating temperature; without this, grayscale waveforms run against
397 // a mismatched LUT and pixels do not visibly change even though the LUT
398 // engine completes a full cycle.
399 this->enqueue_(OpType::CMD, I80_CMD_FORCE_TEMP);
400 this->enqueue_(OpType::WRITE_W, I80_CMD_FORCE_TEMP_WRITE);
401 this->enqueue_(OpType::WRITE_W, static_cast<uint16_t>(this->force_temperature_));
402}
403
404// --- Update op enqueuers -----------------------------------------------------
405
407 // If the controller was put to sleep after the previous update, wake it
408 // before touching the display engine. TCON_SLEEP gates off all clocks; a
409 // register read (e.g. the LUTAFSR poll in UPDATE_REFRESH) returns a frozen
410 // value while asleep, so without this the next update stalls forever in
411 // op_check_lut_idle_(). SRAM/registers (packed-write mode, VCOM, LUT) are
412 // retained across sleep, so SYS_RUN + a short settle is all that's needed.
413 if (this->asleep_) {
414 this->enqueue_(OpType::CMD, TCON_SYS_RUN);
415 this->enqueue_(OpType::DELAY_MS, 10); // clocks settle after SYS_RUN
416 this->asleep_ = false;
417 }
418 this->transfer_row_ = 0;
419 // Open a single LD_IMG_AREA load for the whole region. XFER_ROWS streams into
420 // it across as many time-sliced passes as needed and emits the one matching
421 // LD_IMG_END when the last row is sent (see the XFER_ROWS handler).
426}
427
429 ESP_LOGV(TAG, "Enqueueing refresh ops: grayscale=%u", this->grayscale_);
430 // Poll LUT idle: CMD(REG_RD) → WRITE_W(LUTAFSR) → READ_WORD → CHECK_LUT_IDLE
431 this->enqueue_(OpType::CMD, TCON_REG_RD);
432 this->enqueue_(OpType::WRITE_W, LUTAFSR);
435 if (!this->grayscale_) {
436 // Read UP1SR+2: CMD(REG_RD) → WRITE_W(UP1SR+2) → READ_WORD → SET_1BPP
437 this->enqueue_(OpType::CMD, TCON_REG_RD);
438 this->enqueue_(OpType::WRITE_W, static_cast<uint16_t>(UP1SR + 2));
441 }
444}
445
447 if (this->sleep_when_done_) {
448 this->enqueue_(OpType::CMD, TCON_SLEEP);
449 // Remember that the controller is now asleep so the next update wakes it
450 // (see enqueue_update_transfer_) before polling any register.
451 this->asleep_ = true;
452 }
453}
454
455// --- SPI primitives ----------------------------------------------------------
456//
457// IT8951 SPI protocol: no DC pin. 16-bit preamble word identifies whether
458// the transaction is command (0x6000), write-data (0x0000), or read-data
459// (0x1000).
460//
461// All ops are fully non-blocking at the loop level. The loop-level HW_RDY gate
462// guarantees the controller is ready before any op is dispatched.
463//
464// Within a single CS-asserted transaction, the IT8951 requires HW_RDY to be
465// checked after the preamble word before sending the first data word. This
466// is a hardware protocol requirement — the controller needs a few clock
467// cycles to latch the preamble and configure its internal bus direction.
468// In practice this completes in <1µs for write ops; we use a short spin
469// (max ~50µs) that never triggers under normal operation.
470
471static constexpr uint32_t INTRA_CS_READY_TIMEOUT_US = 50;
472
473static inline void wait_for_hardware_ready(GPIOPin *busy_pin) {
474 if (busy_pin == nullptr)
475 return;
476 uint32_t waited = 0;
477 while (!busy_pin->digital_read()) {
478 if (waited >= INTRA_CS_READY_TIMEOUT_US)
479 return;
481 waited += 1;
482 }
483}
484
485void IT8951Display::spi_cmd_(uint16_t cmd) {
486 this->enable();
487 this->write_byte16(PACKET_TYPE_CMD);
488 wait_for_hardware_ready(this->busy_pin_);
489 this->write_byte16(cmd);
490 this->disable();
491}
492
493void IT8951Display::spi_write_word_(uint16_t value) {
494 this->enable();
495 this->write_byte16(PACKET_TYPE_WRITE);
496 wait_for_hardware_ready(this->busy_pin_);
497 this->write_byte16(value);
498 this->disable();
499}
500
501void IT8951Display::spi_write_reg_(uint16_t addr, uint16_t value) {
502 // Single CS transaction: WRITE preamble + addr + value.
503 // Caller must have already sent CMD(TCON_REG_WR) as a prior op.
504 this->enable();
505 this->write_byte16(PACKET_TYPE_WRITE);
506 wait_for_hardware_ready(this->busy_pin_);
507 this->write_byte16(addr);
508 this->write_byte16(value);
509 this->disable();
510}
511
512void IT8951Display::spi_write_args_(const uint16_t *args, uint16_t count) {
513 // Single CS transaction: WRITE preamble + N data words.
514 this->enable();
515 this->write_byte16(PACKET_TYPE_WRITE);
516 wait_for_hardware_ready(this->busy_pin_);
517 for (uint16_t i = 0; i < count; i++)
518 this->write_byte16(args[i]);
519 this->disable();
520}
521
523 // Single CS read transaction. HW_RDY was confirmed HIGH by the loop gate
524 // before this op was dispatched, so data is ready.
525 this->enable();
526 this->write_byte16(PACKET_TYPE_READ);
527 wait_for_hardware_ready(this->busy_pin_);
528 this->write_byte16(0x0000); // dummy — provides clock cycles for controller
529 wait_for_hardware_ready(this->busy_pin_);
530 // Read byte-by-byte: a 2-byte transfer_array can lose the low byte on
531 // ESP-IDF SPI DMA due to 4-byte alignment requirements.
532 const uint8_t hi = this->transfer_byte(0);
533 const uint8_t lo = this->transfer_byte(0);
534 this->disable();
535 return encode_uint16(hi, lo);
536}
537
539 // Read DevInfo struct. The CMD(GET_DEV_INFO) was already sent as a prior op,
540 // and the loop HW_RDY gate waited for the controller to prepare data.
541 std::memset(&this->dev_info_, 0, sizeof(this->dev_info_));
542 this->enable();
543 this->write_byte16(PACKET_TYPE_READ);
544 wait_for_hardware_ready(this->busy_pin_);
545 this->write_byte16(0x0000); // dummy
546 wait_for_hardware_ready(this->busy_pin_);
547 auto *words = reinterpret_cast<uint16_t *>(&this->dev_info_);
548 constexpr uint32_t word_count = sizeof(this->dev_info_) / sizeof(uint16_t);
549 for (uint32_t i = 0; i < word_count; i++) {
550 const uint8_t hi = this->transfer_byte(0);
551 const uint8_t lo = this->transfer_byte(0);
552 words[i] = encode_uint16(hi, lo);
553 }
554 this->disable();
555}
556
557// --- Compound Ops ------------------------------------------------------------
558
560 // Set image-buffer target address. Two register writes = 4 CS transactions.
561 // Push to FRONT in reverse order so they execute before the rest of the queue.
562 this->prepend_(OpType::WRITE_REG, LISAR, this->img_buf_addr_l_);
563 this->prepend_(OpType::CMD, TCON_REG_WR, 0);
564 this->prepend_(OpType::WRITE_REG, static_cast<uint16_t>(LISAR + 2), this->img_buf_addr_h_);
565 this->prepend_(OpType::CMD, TCON_REG_WR, 0);
566}
567
569 // Single CS transaction: WRITE preamble + 5 area-parameter words describing
570 // the full update region. Sent once when the load is opened (transfer_row_ is
571 // 0); XFER_ROWS then streams every row into this one area.
572 uint16_t args[5];
573 if (this->grayscale_) {
574 args[0] = static_cast<uint16_t>((LDIMG_B_ENDIAN << 8) | (PIXEL_4BPP << 4));
575 args[1] = this->area_x_;
576 args[2] = this->area_y_;
577 args[3] = this->area_w_;
578 args[4] = this->area_h_;
579 } else {
580 // Monochrome is loaded via the 8bpp-packed trick: x and width are expressed
581 // in bytes (8 pixels each) and the controller unpacks one bit per pixel.
582 args[0] = static_cast<uint16_t>((LDIMG_L_ENDIAN << 8) | (PIXEL_8BPP << 4));
583 args[1] = static_cast<uint16_t>(this->area_x_ / 8);
584 args[2] = this->area_y_;
585 args[3] = static_cast<uint16_t>(this->area_w_ / 8);
586 args[4] = this->area_h_;
587 }
588 this->spi_write_args_(args, 5);
589}
590
591void IT8951Display::op_xfer_area_end_() { this->spi_cmd_(TCON_LD_IMG_END); }
592
594 const uint32_t start_time = millis();
595 const uint16_t area_y = this->area_y_;
596 const uint16_t area_h = this->area_h_;
597
598 // Bytes per source row, and the byte offset of area_x within a row, in the
599 // framebuffer's native packing. These match the per-row byte count the
600 // controller expects from op_xfer_area_args_: area_w/2 for 4bpp grayscale,
601 // area_w/8 for the 1bpp-packed monochrome trick. area_x / area_w are
602 // 16-pixel aligned (see prepare_update_region_), so both divisions are exact.
603 const uint16_t bytes_per_row =
604 this->grayscale_ ? static_cast<uint16_t>(this->area_w_ >> 1) : static_cast<uint16_t>(this->area_w_ >> 3);
605 const uint16_t row_x_bytes =
606 this->grayscale_ ? static_cast<uint16_t>(this->area_x_ >> 1) : static_cast<uint16_t>(this->area_x_ >> 3);
607
608 // Single CS write transaction — HW_RDY was confirmed high by the loop gate.
609 this->enable();
610 this->write_byte16(PACKET_TYPE_WRITE);
611 wait_for_hardware_ready(this->busy_pin_);
612
613 // Each source row is a contiguous slice of the framebuffer in both formats —
614 // the buffer already holds the wire bytes — so stream it straight to SPI with
615 // no per-pixel packing or temporary buffer.
616 while (this->transfer_row_ < area_h) {
617 const uint32_t offset = (static_cast<uint32_t>(area_y) + this->transfer_row_) * this->row_width_ + row_x_bytes;
618 this->write_array(&this->buffer_[offset], bytes_per_row);
619 this->transfer_row_++;
620 if (millis() - start_time >= MAX_TRANSFER_TIME_MS)
621 break;
622 }
623
624 this->disable();
625 return this->transfer_row_ >= area_h;
626}
627
629 // I80_CMD_DPY_BUF_AREA (0x0037) takes 7 args (with explicit buffer addr).
630 // I80_CMD_DPY_AREA (0x0034) takes 5 args; the buffer address is taken
631 // from LISAR which we program during the transfer phase, so this is safe.
632 if (this->use_legacy_dpy_area_) {
633 const uint16_t args[5] = {
634 this->area_x_, this->area_y_, this->area_w_, this->area_h_, static_cast<uint16_t>(this->active_mode_),
635 };
636 this->spi_write_args_(args, 5);
637 return;
638 }
639 const uint16_t args[7] = {
640 this->area_x_,
641 this->area_y_,
642 this->area_w_,
643 this->area_h_,
644 static_cast<uint16_t>(this->active_mode_),
645 this->img_buf_addr_l_,
646 this->img_buf_addr_h_,
647 };
648 this->spi_write_args_(args, 7);
649}
650
652 ESP_LOGV(TAG, "Checking LUT idle, read_result_=0x%04X", this->read_result_);
653 // read_result_ holds LUTAFSR value from the preceding READ_WORD op.
654 if (this->read_result_ != 0) {
655 // LUT still busy — re-enqueue the full read sequence after a short delay.
656 this->prepend_(OpType::CHECK_LUT_IDLE, 0, 0);
657 this->prepend_(OpType::READ_WORD, 0, 0);
658 this->prepend_(OpType::WRITE_W, LUTAFSR, 0);
659 this->prepend_(OpType::CMD, TCON_REG_RD, 0);
660 this->prepend_(OpType::DELAY_MS, 5, 0);
661 }
662}
663
665 // read_result_ holds UP1SR+2 value. Set bit 2 and write back, then set BGVR.
666 // Push to FRONT in reverse order so they execute before DPY_BUF_CMD/ARGS
667 // that are already in the queue.
668 const uint16_t modified = static_cast<uint16_t>(this->read_result_ | (1U << 2));
669 this->prepend_(OpType::WRITE_REG, BGVR, 0xFF00);
670 this->prepend_(OpType::CMD, TCON_REG_WR, 0);
671 this->prepend_(OpType::WRITE_REG, UP1SR + 2, modified);
672 this->prepend_(OpType::CMD, TCON_REG_WR, 0);
673}
674
675// --- Update prep / public API ------------------------------------------------
676
678 this->partial_update_count_++;
679 const bool full_update = this->partial_update_count_ >= this->full_update_every_;
680 if (full_update) {
681 this->partial_update_count_ = 0;
682 mode = UPDATE_MODE_GC16;
683 this->x_low_ = 0;
684 this->y_low_ = 0;
685 this->x_high_ = this->width_;
686 this->y_high_ = this->height_;
687 } else {
688 // Align the partial region's X extent to 32 pixels. The IT8951's partial
689 // display refresh snaps the X start/width to a 32-pixel boundary (the panel
690 // source driver fetches 32-pixel chunks); refreshing a region whose X is
691 // only 16-aligned makes the panel snap it down to the previous boundary,
692 // shifting that update ~16px to the left. 32-alignment also satisfies the
693 // load constraints (4bpp X must be a multiple of 4; the 8bpp-packed mono
694 // load needs x/8 even, i.e. X a multiple of 16).
695 this->x_low_ &= 0xFFE0;
696 uint16_t temp_max = this->x_high_ > 0 ? static_cast<uint16_t>(this->x_high_ - 1) : 0;
697 temp_max = static_cast<uint16_t>(temp_max | 0x001F);
698 if (temp_max >= this->width_)
699 temp_max = static_cast<uint16_t>(this->width_ - 1);
700 this->x_high_ = static_cast<uint16_t>(temp_max + 1);
701 }
702
704 this->reset_dirty_region_();
705 return false;
706 }
707
708 const uint16_t x = this->x_low_;
709 const uint16_t y = this->y_low_;
710 const uint16_t width = static_cast<uint16_t>(this->x_high_ - this->x_low_);
711 const uint16_t height = static_cast<uint16_t>(this->y_high_ - this->y_low_);
712
713 if (x >= this->width_ || y >= this->height_ || (x + width) > this->width_ || (y + height) > this->height_) {
714 ESP_LOGE(TAG, "Dirty region (%u,%u %ux%u) out of bounds", x, y, width, height);
715 this->reset_dirty_region_();
716 return false;
717 }
718
719 this->area_x_ = x;
720 this->area_y_ = y;
721 this->area_w_ = width;
722 this->area_h_ = height;
723 this->transfer_row_ = 0;
724
725 // On non-full updates, downgrade monochrome frames from the full, flashy GC16
726 // clear to DU — a fast, low-flash absolute waveform — so full_update_every
727 // buys cheaper refreshes between the periodic GC16 cleans that clear
728 // accumulated ghosting.
729 //
730 // Grayscale frames are deliberately left on GC16: every reduced grayscale
731 // waveform this controller exposes (the non-flashing GL family GL16/GLR16/
732 // GLD16, and the 4-tone DU4) renders incorrectly on the supported panels —
733 // a white background is driven to grey rather than staying white. GC16 is the
734 // only waveform that reproduces grayscale faithfully, so we keep it.
735 //
736 // An explicitly configured non-GC16 update_mode is honoured as-is.
737 if (!full_update && mode == UPDATE_MODE_GC16 && !this->grayscale_)
738 mode = UPDATE_MODE_DU;
739
740 this->reset_dirty_region_();
741
742 ESP_LOGV(TAG, "Update: %ux%u@%u,%u mode=%u (%s)", width, height, x, y, static_cast<unsigned>(mode),
743 this->grayscale_ ? "grayscale" : "mono");
744 return true;
745}
746
748 this->x_low_ = this->width_;
749 this->x_high_ = 0;
750 this->y_low_ = this->height_;
751 this->y_high_ = 0;
752}
753
755 if (this->phase_ == Phase::IDLE && this->initialised_) {
756 this->update_started_at_ = millis();
757 this->active_mode_ = mode;
759 this->enable_loop();
760 this->advance_phase_();
761 } else {
762 // Coalesce: latest pending mode wins.
763 this->update_pending_ = true;
765 this->enable_loop();
766 }
767}
768
770 if (!this->is_ready())
771 return;
774 return;
775 }
777}
778
780 if (!this->is_ready())
781 return;
782 if (mode == UPDATE_MODE_NONE) {
783 ESP_LOGW(TAG, "Unknown update mode");
784 return;
785 }
786 this->start_update_(mode);
787}
788
789// --- Recovery ----------------------------------------------------------------
790
792 if (++this->recovery_attempts_ > 3) {
793 ESP_LOGE(TAG, "Recovery failed after %u attempts; giving up. Check BUSY pin wiring and power.",
794 this->recovery_attempts_);
795 this->mark_failed(LOG_STR("IT8951 recovery exhausted"));
796 this->queue_.clear();
797 this->set_phase_(Phase::IDLE);
798 this->disable_loop();
799 return;
800 }
801 ESP_LOGW(TAG, "Recovering (attempt %u): hardware-resetting controller (was in phase %u)", this->recovery_attempts_,
802 static_cast<unsigned>(this->phase_));
803 this->queue_.clear();
804 this->update_pending_ = false;
805 this->transfer_row_ = 0;
806 this->initialised_ = false;
807 this->dev_info_attempts_ = 0;
808
809 // Drop SPI clock back to the safe probe rate for the re-init handshake.
810 if (this->configured_data_rate_ != 0 && this->data_rate_ != SPI_PROBE_FREQUENCY) {
811 this->spi_teardown();
812 this->set_data_rate(SPI_PROBE_FREQUENCY);
813 this->spi_setup();
814 }
815
816 // Force a full redraw on next opportunity.
817 this->x_low_ = 0;
818 this->y_low_ = 0;
819 this->x_high_ = this->width_;
820 this->y_high_ = this->height_;
821
823 this->enqueue_init_reset_();
824 this->update_pending_ = true;
826 this->enable_loop();
827}
828
829// --- Coordinate transform ----------------------------------------------------
830
832 switch (this->rotation_) {
834 this->effective_transform_ = this->transform_ ^ (TRANSFORM_SWAP_XY | TRANSFORM_MIRROR_X);
835 break;
837 this->effective_transform_ = this->transform_ ^ (TRANSFORM_MIRROR_Y | TRANSFORM_MIRROR_X);
838 break;
840 this->effective_transform_ = this->transform_ ^ (TRANSFORM_SWAP_XY | TRANSFORM_MIRROR_Y);
841 break;
842 default:
843 this->effective_transform_ = this->transform_;
844 break;
845 }
846}
847
848void IT8951Display::apply_transform_(int &x, int &y) const {
849 if (this->effective_transform_ & TRANSFORM_SWAP_XY)
850 std::swap(x, y);
851 if (this->effective_transform_ & TRANSFORM_MIRROR_X)
852 x = this->width_ - x - 1;
853 if (this->effective_transform_ & TRANSFORM_MIRROR_Y)
854 y = this->height_ - y - 1;
855}
856
858 if (!this->get_clipping().inside(x, y))
859 return false;
860 this->apply_transform_(x, y);
861 if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
862 return false;
863 this->x_low_ = clamp_at_most(this->x_low_, x);
864 this->x_high_ = clamp_at_least(this->x_high_, x + 1);
865 this->y_low_ = clamp_at_most(this->y_low_, y);
866 this->y_high_ = clamp_at_least(this->y_high_, y + 1);
867 return true;
868}
869
870// --- Color / drawing ---------------------------------------------------------
871
872static uint8_t quantize_8bit_to_nibble(uint8_t value) {
873 uint8_t nibble = static_cast<uint8_t>((static_cast<uint16_t>(value) + 8) >> 4);
874 return nibble > 0x0F ? 0x0F : nibble;
875}
876
877static uint8_t color_to_nibble(const Color &color) {
878 // Grayscale images are emitted as Color(gray, gray, gray, 0xFF).
879 // Handle this shape first so endpoint values don't alias COLOR_ON/OFF.
880 if (color.w == 0xFF && color.r == color.g && color.g == color.b)
881 return quantize_8bit_to_nibble(color.r);
882
883 if (color.raw_32 == 0)
884 return 0x00; // black
885 if (color.raw_32 == 0xFFFFFFFF)
886 return 0x0F; // white
887
888 // Derive luma from RGB using Rec.601 weights (0.299/0.587/0.114, scaled by
889 // 256). Rec.601 is the standard for converting SDR images to grayscale and
890 // spreads saturated colours across the mid-range; Rec.709 instead crams them
891 // against white/black where the 16 panel levels are hard to tell apart.
892 auto luma = static_cast<uint8_t>((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8);
893 return quantize_8bit_to_nibble(luma);
894}
895
896// 4x4 ordered (Bayer) dither threshold over the weighted-luma range (0..65535).
897// A pixel whose luma is below the threshold renders black, so lighter pixels
898// produce progressively sparser black dots instead of vanishing to white. The
899// matrix averages to 32768, matching the conventional monochrome cut, while the
900// per-pixel variation reproduces intermediate gray levels.
901static uint16_t dither_threshold(uint16_t x, uint16_t y) {
902 static const uint8_t BAYER4[16] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5};
903 return static_cast<uint16_t>(BAYER4[((y & 3) << 2) | (x & 3)] * 4096u + 2048u);
904}
905
907 if (this->buffer_ == nullptr)
908 return;
909 if (this->get_clipping().is_set()) {
910 Display::fill(color);
911 return;
912 }
913 uint8_t packed = color_to_nibble(color);
914 if (this->invert_colors_)
915 packed = 0x0F - packed;
916 uint8_t fill_byte;
917 if (this->grayscale_) {
918 fill_byte = static_cast<uint8_t>((packed << 4) | packed);
919 } else {
920 fill_byte = (packed <= 0x07) ? 0xFF : 0x00;
921 }
922 memset(this->buffer_, fill_byte, this->buffer_length_);
923 this->x_low_ = 0;
924 this->y_low_ = 0;
925 this->x_high_ = this->width_;
926 this->y_high_ = this->height_;
927}
928
929void HOT IT8951Display::draw_pixel_at(int x, int y, Color color) {
930 if (this->buffer_ == nullptr)
931 return;
932 App.feed_wdt();
933 if (!this->rotate_coordinates_(x, y))
934 return;
935 this->write_pixel_native_(static_cast<uint16_t>(x), static_cast<uint16_t>(y), color);
936}
937
938void HOT IT8951Display::write_pixel_native_(uint16_t x, uint16_t y, const Color &color) const {
939 if (this->grayscale_) {
940 uint8_t nibble = color_to_nibble(color);
941 if (this->invert_colors_)
942 nibble = static_cast<uint8_t>(0x0F - nibble);
943 this->set_gray_pixel_(x, y, nibble);
944 } else {
945 // Rec.601 luma (see color_to_nibble). Weights sum to 257 so white maps to
946 // exactly 65535, using the full 16-bit range without overflow.
947 auto lum = static_cast<uint16_t>(77u * color.r + 151u * color.g + 29u * color.b);
948 if (this->invert_colors_)
949 lum = static_cast<uint16_t>(65535u - lum);
950 // Set the bit (foreground/black) when this pixel is darker than its
951 // threshold. With dithering the threshold varies per pixel so pale colours
952 // render as visible texture; otherwise it's the fixed ~50% cut (r+g+b<32768).
953 const uint16_t threshold = this->dithering_ ? dither_threshold(x, y) : 32768;
954 this->set_mono_pixel_(x, y, lum < threshold);
955 }
956}
957
958void HOT IT8951Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
959 ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
960 // A writer (e.g. LVGL) may push pixels before the framebuffer is ready or
961 // after an allocation failure; ignore those rather than dereferencing null.
962 if (this->buffer_ == nullptr)
963 return;
964 // A clipping rectangle would need a per-pixel test; that's rare for the bulk
965 // blit callers (LVGL, images), so fall back to the base per-pixel path then.
966 if (this->get_clipping().is_set()) {
967 Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
968 return;
969 }
970
971 const size_t line_stride = static_cast<size_t>(x_offset) + w + x_pad; // source line length in pixels
972 for (int y = 0; y < h; y++) {
973 App.feed_wdt();
974 size_t source_idx = (static_cast<size_t>(y_offset) + y) * line_stride + x_offset;
975 for (int x = 0; x < w; x++, source_idx++) {
976 uint32_t color_value;
977 switch (bitness) {
978 case COLOR_BITNESS_565: {
979 const size_t i = source_idx * 2;
980 color_value = big_endian ? (static_cast<uint32_t>(ptr[i]) << 8) | ptr[i + 1]
981 : ptr[i] | (static_cast<uint32_t>(ptr[i + 1]) << 8);
982 break;
983 }
984 case COLOR_BITNESS_888: {
985 const size_t i = source_idx * 3;
986 color_value =
987 big_endian
988 ? (static_cast<uint32_t>(ptr[i]) << 16) | (static_cast<uint32_t>(ptr[i + 1]) << 8) | ptr[i + 2]
989 : ptr[i] | (static_cast<uint32_t>(ptr[i + 1]) << 8) | (static_cast<uint32_t>(ptr[i + 2]) << 16);
990 break;
991 }
992 default:
993 color_value = ptr[source_idx];
994 break;
995 }
996 int nx = x_start + x;
997 int ny = y_start + y;
998 this->apply_transform_(nx, ny);
999 if (nx < 0 || ny < 0 || nx >= this->width_ || ny >= this->height_)
1000 continue;
1001 this->write_pixel_native_(static_cast<uint16_t>(nx), static_cast<uint16_t>(ny),
1002 ColorUtil::to_color(color_value, order, bitness));
1003 }
1004 }
1005
1006 // Expand the dirty bounding box once from the transformed block corners: the
1007 // image of an axis-aligned rectangle under swap/mirror is still axis-aligned,
1008 // so its two opposite corners bound it.
1009 int x0 = x_start, y0 = y_start;
1010 int x1 = x_start + w - 1, y1 = y_start + h - 1;
1011 this->apply_transform_(x0, y0);
1012 this->apply_transform_(x1, y1);
1013 const int nx_lo = std::max(0, std::min(x0, x1));
1014 const int ny_lo = std::max(0, std::min(y0, y1));
1015 const int nx_hi = std::min(this->width_ - 1, std::max(x0, x1));
1016 const int ny_hi = std::min(this->height_ - 1, std::max(y0, y1));
1017 if (nx_hi >= nx_lo && ny_hi >= ny_lo) {
1018 this->x_low_ = clamp_at_most(this->x_low_, nx_lo);
1019 this->x_high_ = clamp_at_least(this->x_high_, nx_hi + 1);
1020 this->y_low_ = clamp_at_most(this->y_low_, ny_lo);
1021 this->y_high_ = clamp_at_least(this->y_high_, ny_hi + 1);
1022 }
1023}
1024
1025void IT8951Display::set_mono_pixel_(uint16_t x, uint16_t y, bool value) const {
1026 // The monochrome framebuffer holds the exact bytes streamed to the
1027 // controller for the 8bpp-load / 1bpp-display trick (L_ENDIAN). Pixels are
1028 // grouped in 16s; on the wire the high byte (pixels 8..15) precedes the low
1029 // byte (pixels 0..7), and the bit index within a byte is the pixel's offset
1030 // (LSB = lowest x). Storing in that order lets op_xfer_rows_ copy rows
1031 // verbatim with no packing or byte-swapping.
1032 const uint16_t group = static_cast<uint16_t>(x >> 4);
1033 const uint8_t sub = static_cast<uint8_t>(x & 0x0F);
1034 const uint16_t byte_index = static_cast<uint16_t>(group * 2u + (sub < 8u ? 1u : 0u));
1035 const uint8_t mask = static_cast<uint8_t>(1u << (sub & 0x07));
1036 const uint32_t index = static_cast<uint32_t>(y) * this->row_width_ + byte_index;
1037 if (value) {
1038 this->buffer_[index] |= mask;
1039 } else {
1040 this->buffer_[index] &= static_cast<uint8_t>(~mask);
1041 }
1042}
1043
1044void IT8951Display::set_gray_pixel_(uint16_t x, uint16_t y, uint8_t nibble) const {
1045 const uint32_t index = static_cast<uint32_t>(y) * this->row_width_ + (static_cast<uint32_t>(x) >> 1);
1046 uint8_t buf = this->buffer_[index];
1047 if (x & 0x1) {
1048 buf = (buf & 0xF0) | nibble;
1049 } else {
1050 buf = (buf & 0x0F) | static_cast<uint8_t>(nibble << 4);
1051 }
1052 this->buffer_[index] = buf;
1053}
1054
1055// --- Diagnostics -------------------------------------------------------------
1056
1058 LOG_DISPLAY("", "IT8951 E-Paper", this);
1059 char force_temperature[24];
1060 if (this->force_temperature_set_) {
1061 snprintf(force_temperature, sizeof(force_temperature), "%d °C", this->force_temperature_);
1062 } else {
1063 strncpy(force_temperature, "(controller default)", sizeof(force_temperature));
1064 force_temperature[sizeof(force_temperature) - 1] = '\0';
1065 }
1066 ESP_LOGCONFIG(TAG,
1067 " Model preset: %s"
1068 "\n Dimensions: %dx%d"
1069 "\n Buffer: %u bytes"
1070 "\n Image buffer addr: 0x%04X%04X"
1071 "\n VCOM: %.02fV (set selector 0x%04X)"
1072 "\n Force temperature: %s"
1073 "\n Display command: %s"
1074 "\n Sleep when done: %s"
1075 "\n Full update every: %u"
1076 "\n Inverted colors: %s"
1077 "\n Pixel format: %s"
1078 "\n Reset duration: %" PRIu32 "ms",
1079 this->name_ != nullptr ? this->name_ : "(unknown)", this->get_width_internal(),
1080 this->get_height_internal(), static_cast<unsigned>(this->buffer_length_), this->img_buf_addr_h_,
1081 this->img_buf_addr_l_, static_cast<float>(this->vcom_) / 1000.0f, this->vcom_register_,
1082 force_temperature, this->use_legacy_dpy_area_ ? "DPY_AREA (0x0034, legacy)" : "DPY_BUF_AREA (0x0037)",
1083 YESNO(this->sleep_when_done_), this->full_update_every_, YESNO(this->invert_colors_),
1084 this->grayscale_ ? "4bpp grayscale" : "1bpp monochrome", this->reset_duration_);
1085 LOG_PIN(" Reset Pin: ", this->reset_pin_);
1086 LOG_PIN(" Busy Pin: ", this->busy_pin_);
1087 LOG_PIN(" CS Pin: ", this->cs_);
1088 LOG_UPDATE_INTERVAL(this);
1089}
1090
1091} // namespace esphome::it8951
BedjetMode mode
BedJet operating mode.
uint8_t h
Definition bl0906.h:2
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:246
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:735
void start()
Start running the loop continuously.
Definition helpers.cpp:729
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2067
T * allocate(size_t n)
Definition helpers.h:2094
static Color to_color(uint32_t colorcode, ColorOrder color_order, ColorBitness color_bitness=ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned=true)
virtual void fill(Color color)
Fill the entire screen with the given color.
Definition display.cpp:14
Rect get_clipping() const
Get the current the clipping rectangle.
Definition display.cpp:766
DisplayRotation rotation_
Definition display.h:789
virtual void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order, ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad)
Given an array of pixels encoded in the nominated format, draw these into the display's buffer.
Definition display.cpp:54
void update_mode(UpdateMode mode)
Definition it8951.cpp:779
void start_update_(UpdateMode mode)
Definition it8951.cpp:754
void spi_write_args_(const uint16_t *args, uint16_t count)
Definition it8951.cpp:512
HighFrequencyLoopRequester high_freq_
Definition it8951.h:285
void set_phase_(Phase next)
Definition it8951.cpp:154
void spi_cmd_(uint16_t cmd)
Definition it8951.cpp:485
bool rotate_coordinates_(int &x, int &y)
Definition it8951.cpp:857
void enqueue_(OpType type, uint16_t a=0, uint16_t b=0)
Definition it8951.cpp:21
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order, ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override
Definition it8951.cpp:958
uint16_t compute_row_width_() const
Definition it8951.h:225
void spi_write_reg_(uint16_t addr, uint16_t value)
Definition it8951.cpp:501
void prepend_(OpType type, uint16_t a=0, uint16_t b=0)
Definition it8951.cpp:28
std::vector< GPIOPin * > enable_pins_
Definition it8951.h:335
int get_height_internal() override
Definition it8951.h:209
StaticOpQueue< Op, OP_QUEUE_SIZE > queue_
Definition it8951.h:279
void set_mono_pixel_(uint16_t x, uint16_t y, bool value) const
Definition it8951.cpp:1025
void write_pixel_native_(uint16_t x, uint16_t y, const Color &color) const
Definition it8951.cpp:938
void apply_transform_(int &x, int &y) const
Definition it8951.cpp:848
void process_op_(const Op &op)
Definition it8951.cpp:81
void fill(Color color) override
Definition it8951.cpp:906
void spi_write_word_(uint16_t value)
Definition it8951.cpp:493
void on_safe_shutdown() override
Definition it8951.cpp:349
static constexpr uint32_t BUSY_TIMEOUT_MS
Definition it8951.h:277
bool prepare_update_region_(UpdateMode &mode)
Definition it8951.cpp:677
void set_gray_pixel_(uint16_t x, uint16_t y, uint8_t nibble) const
Definition it8951.cpp:1044
int get_width_internal() override
Definition it8951.h:210
void draw_pixel_at(int x, int y, Color color) override
Definition it8951.cpp:929
uint32_t data_rate_
Definition spi.h:412
uint16_t type
@ DISPLAY_ROTATION_270_DEGREES
Definition display.h:137
@ DISPLAY_ROTATION_180_DEGREES
Definition display.h:136
@ DISPLAY_ROTATION_90_DEGREES
Definition display.h:135
T clamp_at_most(T value, U max)
Definition helpers.h:2205
const char int const __FlashStringHelper va_list args
Definition log.h:74
void IRAM_ATTR HOT delayMicroseconds(uint32_t us)
Definition hal.cpp:48
T clamp_at_least(T value, U min)
Definition helpers.h:2200
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:873
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
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:185
uint8_t r
Definition color.h:30
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6