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.
This commit is contained in:
Justinian
2026-04-07 17:12:58 -07:00
committed by GitHub
parent 157d724d7a
commit a64a3c29be
+40 -7
View File
@@ -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);