From 2bd1cb4effb2fd48a4ed83aebdde179b524770b1 Mon Sep 17 00:00:00 2001 From: pierrehenrymuller Date: Fri, 19 Jun 2026 16:16:08 +0200 Subject: [PATCH] Add per-zone (partial) refresh for the IT8951 display (tested on reTerminal E1003) --- .../it8951_reterminal_e1003.cpp | 158 ++++++++++++++++-- .../it8951_reterminal_e1003.h | 21 +++ 2 files changed, 165 insertions(+), 14 deletions(-) diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp index 3e44258..41902d7 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp @@ -433,12 +433,9 @@ void IT8951ReTerminalE1003Display::draw_driver_test_pattern_() { } } -void IT8951ReTerminalE1003Display::update() { - if (this->framebuffer_ == nullptr) { - ESP_LOGW(TAG, "Skipping update because the framebuffer is not available"); - return; - } +void IT8951ReTerminalE1003Display::update() { this->full_refresh(); } +void IT8951ReTerminalE1003Display::wake_panel_() { if (this->it8951_sleeping_) { ESP_LOGD(TAG, "Waking IT8951 from inter-refresh sleep"); digitalWrite(IT8951_PIN_EN, HIGH); @@ -446,6 +443,22 @@ void IT8951ReTerminalE1003Display::update() { this->lcd_sys_run_(); this->it8951_sleeping_ = false; } +} + +void IT8951ReTerminalE1003Display::sleep_panel_() { + this->lcd_write_cmd_code_(IT8951_TCON_SLEEP); + digitalWrite(IT8951_PIN_EN, LOW); + this->it8951_sleeping_ = true; + ESP_LOGD(TAG, "IT8951 sleeping, EPD_Drive power cut"); +} + +void IT8951ReTerminalE1003Display::full_refresh() { + if (this->framebuffer_ == nullptr) { + ESP_LOGW(TAG, "Skipping update because the framebuffer is not available"); + return; + } + + this->wake_panel_(); this->do_update_(); this->log_framebuffer_stats_(); @@ -454,7 +467,7 @@ void IT8951ReTerminalE1003Display::update() { this->log_framebuffer_stats_(); } - ESP_LOGD(TAG, "Transferring image to IT8951..."); + ESP_LOGD(TAG, "Transferring full image to IT8951..."); const uint16_t w = this->get_width_internal(); const uint16_t h = this->get_height_internal(); @@ -474,16 +487,13 @@ void IT8951ReTerminalE1003Display::update() { const uint16_t one_bpp_width_bytes = w / 8; ESP_LOGD(TAG, "Using sharp 1bpp upload path"); this->it8951_load_img_area_start_(IT8951_LDIMG_L_ENDIAN, IT8951_8BPP, 0, 0, 0, one_bpp_width_bytes, h); - ESP_LOGD(TAG, "Uploading %u rows x %u packed bytes", h, one_bpp_width_bytes); this->lcd_write_framebuffer_1bpp_(w, h); } else { ESP_LOGD(TAG, "Using 4bpp grayscale upload path"); this->it8951_load_img_area_start_(IT8951_LDIMG_L_ENDIAN, IT8951_4BPP, 0, 0, 0, w, h); - ESP_LOGD(TAG, "Uploading %u rows x %u words", h, width_in_words); this->lcd_write_framebuffer_4bpp_(reinterpret_cast(this->framebuffer_), width_in_words, h); } - ESP_LOGD(TAG, "Framebuffer upload complete"); this->lcd_write_cmd_code_(IT8951_TCON_LD_IMG_END); // INIT pass (mode 0) fully discharges residual pixel state from the previous image, @@ -501,12 +511,122 @@ void IT8951ReTerminalE1003Display::update() { } this->first_update_ = false; - this->lcd_write_cmd_code_(IT8951_TCON_SLEEP); - digitalWrite(IT8951_PIN_EN, LOW); - this->it8951_sleeping_ = true; - ESP_LOGD(TAG, "IT8951 sleeping, EPD_Drive power cut"); + this->sleep_panel_(); + this->partials_since_full_ = 0; +} + +// Upload only a sub-rectangle of the framebuffer in 4bpp. x and w MUST be +// multiples of 4 (4 pixels per 16-bit word). Byte order matches +// lcd_write_framebuffer_4bpp_ (MSB-first per word). +void IT8951ReTerminalE1003Display::lcd_write_framebuffer_4bpp_area_(uint16_t x, uint16_t y, uint16_t w, + uint16_t h) { + const uint16_t words = w / 4; + const uint32_t row_size_bytes = uint32_t(words) * 2; + uint8_t row_buffer[936]; + if (row_size_bytes > sizeof(row_buffer)) { + ESP_LOGE(TAG, "Area row buffer too small for %u-byte transfer", static_cast(row_size_bytes)); + return; + } + const uint32_t fb_row_bytes = uint32_t(this->get_width_internal()) / 2; + const uint32_t x_byte = uint32_t(x) / 2; + + digitalWrite(IT8951_PIN_CS, LOW); + SPI.beginTransaction(SPISettings(this->spi_frequency_, MSBFIRST, SPI_MODE0)); + this->lcd_wait_for_ready_(); + this->spi_send_word_(0x0000); + this->lcd_wait_for_ready_(); + + for (uint16_t yy = 0; yy < h; yy++) { + const uint8_t *src = this->framebuffer_ + (uint32_t(y) + yy) * fb_row_bytes + x_byte; + for (uint16_t i = 0; i < words; i++) { + const uint8_t b0 = src[uint32_t(i) * 2]; + const uint8_t b1 = src[uint32_t(i) * 2 + 1]; + row_buffer[uint32_t(i) * 2] = b1; + row_buffer[uint32_t(i) * 2 + 1] = b0; + } + SPI.writeBytes(row_buffer, row_size_bytes); + if ((yy & 0x07) == 0) { + App.feed_wdt(); + } + } - ESP_LOGV(TAG, "Display update triggered"); + SPI.endTransaction(); + digitalWrite(IT8951_PIN_CS, HIGH); +} + +void IT8951ReTerminalE1003Display::render_framebuffer() { + if (this->framebuffer_ == nullptr) { + return; + } + // Runs the display lambda once. Heavy on PSRAM (~seconds); do this ONCE then + // call flush_zone() for each zone rather than re-rendering per zone. + this->do_update_(); +} + +void IT8951ReTerminalE1003Display::refresh_zone(int lx, int ly, int lw, int lh, int mode) { + this->render_framebuffer(); + this->flush_zone(lx, ly, lw, lh, mode); +} + +void IT8951ReTerminalE1003Display::flush_zone(int lx, int ly, int lw, int lh, int mode) { + if (this->framebuffer_ == nullptr || lw <= 0 || lh <= 0) { + return; + } + const int panel_w = this->get_width_internal(); + const int panel_h = this->get_height_internal(); + + // The framebuffer is mirrored in X (see draw_absolute_pixel_internal): + // logical column lx maps to panel column panel_w - 1 - lx. Convert the + // logical rectangle to panel coordinates, then align x/w to 4 px (4bpp). + int px = panel_w - lx - lw; + int pend = px + lw; + if (px < 0) px = 0; + if (pend > panel_w) pend = panel_w; + px &= ~0x03; + pend = (pend + 3) & ~0x03; + const int pw = pend - px; + + int y = ly; + int h = lh; + if (y < 0) { + h += y; + y = 0; + } + if (y + h > panel_h) { + h = panel_h - y; + } + if (pw <= 0 || h <= 0) { + return; + } + + this->wake_panel_(); + this->wait_for_display_ready_(); + + this->lcd_write_cmd_code_(USDEF_I80_CMD_TEMP); + this->lcd_write_data_(0x0001); // Force Set from host + this->lcd_write_data_(static_cast(this->temperature_)); + + this->it8951_write_reg_(UP1SR + 2, this->it8951_read_reg_(UP1SR + 2) & ~(1 << 2)); + this->set_img_buf_base_addr_(this->img_buf_addr_); + this->it8951_load_img_area_start_(IT8951_LDIMG_L_ENDIAN, IT8951_4BPP, 0, static_cast(px), + static_cast(y), static_cast(pw), + static_cast(h)); + this->lcd_write_framebuffer_4bpp_area_(static_cast(px), static_cast(y), + static_cast(pw), static_cast(h)); + this->lcd_write_cmd_code_(IT8951_TCON_LD_IMG_END); + this->wait_for_display_ready_(); + + ESP_LOGD(TAG, "Zone refresh panel x=%d y=%d w=%d h=%d mode=%d", px, y, pw, h, mode); + this->it8951_display_area_(static_cast(px), static_cast(y), static_cast(pw), + static_cast(h), static_cast(mode)); + this->wait_for_display_ready_(); + + this->sleep_panel_(); + + if (++this->partials_since_full_ >= MAX_PARTIALS_BEFORE_FULL) { + ESP_LOGD(TAG, "Partial threshold reached, forcing full refresh to purge ghosting"); + this->full_refresh(); + } } void IT8951ReTerminalE1003Display::dump_config() { @@ -528,6 +648,16 @@ void IT8951ReTerminalE1003Display::dump_config() { LOG_UPDATE_INTERVAL(this); } +void IT8951ReTerminalE1003Display::fill(Color color) { + if (this->framebuffer_ == nullptr) { + return; + } + const uint8_t gray8 = static_cast((77u * color.r + 150u * color.g + 29u * color.b) >> 8); + const uint8_t nib = gray8 >> 4; + const uint8_t byte = static_cast((nib << 4) | nib); + memset(this->framebuffer_, byte, static_cast(this->get_width_internal()) * this->get_height_internal() / 2); +} + void IT8951ReTerminalE1003Display::draw_absolute_pixel_internal(int x, int y, Color color) { if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height()) { return; diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h index 84cb011..5aaf6dd 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h @@ -61,6 +61,22 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { void setup() override; void update() override; void dump_config() override; + // Fast uniform clear (memset) instead of the base per-pixel loop (~2.6M calls + // on this panel). Makes the per-frame auto-clear ~instant. + void fill(Color color) override; + + // Full-screen INIT + GC16 refresh (flash). Used at boot and for the nightly + // deghost. Resets the partial-refresh counter. + void full_refresh(); + // Re-render the whole framebuffer in RAM (runs the display lambda once). Cheap + // on CPU but NOT on PSRAM (~seconds); call ONCE then flush_zone() several times. + void render_framebuffer(); + // Push ONLY the given logical rectangle (lambda coordinates) to the panel with + // a fast waveform (default mode 1 = DU, no flash). Does NOT re-render; assumes + // the framebuffer is current (call render_framebuffer() first). + void flush_zone(int x, int y, int w, int h, int mode = 1); + // Convenience: render_framebuffer() + flush_zone() for a single isolated zone. + void refresh_zone(int x, int y, int w, int h, int mode = 1); float get_setup_priority() const override { return setup_priority::HARDWARE; } display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; } @@ -77,6 +93,9 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { void lcd_write_n_data_(uint16_t *buf, uint32_t word_count); void lcd_write_framebuffer_4bpp_(uint16_t *buf, uint16_t width_in_words, uint16_t height); void lcd_write_framebuffer_1bpp_(uint16_t width, uint16_t height); + void lcd_write_framebuffer_4bpp_area_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void wake_panel_(); + void sleep_panel_(); uint16_t lcd_read_data_(); void lcd_read_n_data_(uint16_t *buf, uint32_t word_count); void lcd_wait_for_ready_(); @@ -119,6 +138,8 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { bool it8951_sleeping_{false}; int8_t temperature_{23}; uint32_t spi_read_frequency_{1000000}; + uint32_t partials_since_full_{0}; + static const uint32_t MAX_PARTIALS_BEFORE_FULL = 180; }; } // namespace it8951_reterminal_e1003