From a64a3c29bebc59b2ccdfe15492cfc4b5e4c26360 Mon Sep 17 00:00:00 2001 From: Justinian Date: Wed, 8 Apr 2026 01:12:58 +0100 Subject: [PATCH] feat: X3 grayscale LUTs and fast diff BB reinforcement (#32) ## Summary Adds dedicated X3 grayscale LUTs and improves fast differential refresh for better antialiased text rendering on the Xteink X3 display. ## Changes - Dedicated grayscale LUTs (lut_x3_*_gray): Single-phase waveforms with tuned VDL drive for dark gray (2 units) and light gray (3 units), with active GND hold on non-gray transitions to prevent crosstalk - Tight scan timing: TP2/TP3 reduced from 6 to 1 to minimize idle gate-on time (17 to 7 units per row), reducing parasitic charge leakage that causes white lines through letter strokes - BB reinforcement: Added mild VDH pulse (VS=0x10) to lut_x3_bb_full so black pixels get actively reinforced during fast differential refreshes, clearing gray residue/ghosting - displayGrayBuffer() updated to use the dedicated gray LUT bank instead of full refresh LUTs Companion to crosspoint-reader/crosspoint-reader#1607 ## AI Disclosure Yes, AI was used to assist with the development of these changes. --- libs/display/EInkDisplay/src/EInkDisplay.cpp | 47 +++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/libs/display/EInkDisplay/src/EInkDisplay.cpp b/libs/display/EInkDisplay/src/EInkDisplay.cpp index 4661113..6e06a9a 100644 --- a/libs/display/EInkDisplay/src/EInkDisplay.cpp +++ b/libs/display/EInkDisplay/src/EInkDisplay.cpp @@ -125,7 +125,40 @@ const uint8_t lut_x3_wb_full[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; const uint8_t lut_x3_bb_full[] PROGMEM = { - 0x00, 0x06, 0x02, 0x06, 0x06, 0x01, 0x00, 0x05, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x10, 0x06, 0x02, 0x06, 0x06, 0x01, 0x00, 0x05, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +// X3 dedicated grayscale LUTs — tuned drive strengths for 4-level gray +// All entries share the same single-phase timing so the controller scans +// every row with consistent gate timing. Source voltages differ per transition: +// VCOM: GND (stable common electrode reference) +// BB: GND (active hold — prevents floating source crosstalk) +// WW: brief VDL pulse (dark gray) +// BW: moderate VDL pulse (light gray) +// WB: GND (active hold — unused transition) +const uint8_t lut_x3_vcom_gray[] PROGMEM = { + 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const uint8_t lut_x3_ww_gray[] PROGMEM = { + // Dark gray: VS=0x20 → GND,VDL(2),GND,GND — brief pulse (sub-phase B) + 0x20, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const uint8_t lut_x3_bw_gray[] PROGMEM = { + // Light gray: VS=0x80 → VDL(3),GND,GND,GND — subtle pulse (sub-phase A, TP0=3) + 0x80, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const uint8_t lut_x3_wb_gray[] PROGMEM = { + // Active GND hold: VS=0x00 → all GND, matching timing + 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const uint8_t lut_x3_bb_gray[] PROGMEM = { + // Active GND hold: VS=0x00 → all GND, matching timing + 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; @@ -1014,14 +1047,14 @@ void EInkDisplay::displayGrayBuffer(const bool turnOffScreen) { } }; - const uint8_t* vcom = lut_x3_vcom_full; - const uint8_t* ww = lut_x3_ww_full; - const uint8_t* bw = lut_x3_bw_full; - const uint8_t* wb = lut_x3_wb_full; - const uint8_t* bb = lut_x3_bb_full; + const uint8_t* vcom = lut_x3_vcom_gray; + const uint8_t* ww = lut_x3_ww_gray; + const uint8_t* bw = lut_x3_bw_gray; + const uint8_t* wb = lut_x3_wb_gray; + const uint8_t* bb = lut_x3_bb_gray; uint8_t dataInterval0 = 0x29; uint8_t dataInterval1 = 0x07; - if (Serial) Serial.printf("[%lu] X3_GRAY_MODE=full29\n", millis()); + if (Serial) Serial.printf("[%lu] X3_GRAY_MODE=gray_tuned\n", millis()); sendCommandDataX3(0x20, vcom, 42); sendCommandDataX3(0x21, ww, 42); sendCommandDataX3(0x22, bw, 42);