Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -433,19 +433,32 @@ 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);
delay(10); // allow TPS22916 power switch to close before SPI
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_();
Expand All @@ -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();
Expand All @@ -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<uint16_t *>(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,
Expand All @@ -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<unsigned>(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<uint16_t>(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<uint16_t>(px),
static_cast<uint16_t>(y), static_cast<uint16_t>(pw),
static_cast<uint16_t>(h));
this->lcd_write_framebuffer_4bpp_area_(static_cast<uint16_t>(px), static_cast<uint16_t>(y),
static_cast<uint16_t>(pw), static_cast<uint16_t>(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<uint16_t>(px), static_cast<uint16_t>(y), static_cast<uint16_t>(pw),
static_cast<uint16_t>(h), static_cast<uint16_t>(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() {
Expand All @@ -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<uint8_t>((77u * color.r + 150u * color.g + 29u * color.b) >> 8);
const uint8_t nib = gray8 >> 4;
const uint8_t byte = static_cast<uint8_t>((nib << 4) | nib);
memset(this->framebuffer_, byte, static_cast<size_t>(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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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_();
Expand Down Expand Up @@ -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
Expand Down