Skip to content

Instantly share code, notes, and snippets.

@CrazyCoder
Last active February 12, 2026 17:36
Show Gist options
  • Select an option

  • Save CrazyCoder/82fec0bbd0e515dcc237d3db7451ec6f to your computer and use it in GitHub Desktop.

Select an option

Save CrazyCoder/82fec0bbd0e515dcc237d3db7451ec6f to your computer and use it in GitHub Desktop.
Xteink X3 EPD display analysis

Xteink X3 EPD Display Driver Analysis

Display Hardware

Property Value Evidence
Controller SSD1677 Command set, LUT format (5x42 bytes), register layout
Diagonal 3.68" Alibaba listing + mechanical drawing; sqrt(51.84² + 77.76²) = 93.5mm = 3.68"
Buffer resolution 792 x 528 Constructor param, buffer size = 52,272 bytes (99 bytes/row x 528 rows)
Hardware resolution 792 x 600 CMD 0x61 data: [0x03, 0x18, 0x02, 0x58] → (0x0318=792) x (0x0258=600)
Pixel density ~259 PPI 25.4mm / 0.0982mm pixel pitch; marketing spec says "250 PPI"
Pixel pitch 98.2 µm × 98.2 µm 51.84mm / 528 = 77.76mm / 792 = 0.0982mm (square pixels)
Active area 51.84mm (H) × 77.76mm (V) Mechanical drawing (AA dimension)
Outline 56.24mm (H) × 86.54mm (V) × 0.87mm (D) Mechanical drawing (OD dimension)
Color Black/White (B/W variant) Two RAM planes (old/new data), no red channel commands
Panel color options BW / BWYR Alibaba listing; panel also available in black/white/yellow/red
SPI frequency 10 MHz Constructor parameter in epd_gxepd2_constructor
FPC connector 24-pin, 0.5mm pitch Mechanical drawing
Viewing angle ~180° Alibaba listing
Operating temperature 0–50°C Alibaba listing
Panel identifier "XT-EPD" String at 0x3C161F70; no GDEQ/GDEW/GDEY model found
Panel type Generic e-paper module 3.68" 528×792 SSD1677 panel sold on Alibaba as commodity part

Mechanical Dimensions (from drawing)

Front view (正面):                          Side view (侧面):

  56.24 ± 0.2  OD (outline)                 ┌─────┐
  54.94 ± 0.2  PS (protective sheet)        │ PS  │ 0.20mm
  53.14 ± 0.2  EPL (e-paper layer)          │ EPL │ 0.17mm
  51.84        AA (active area)             │ TFT │ 0.50mm
                                            └─────┘
  Heights (V):                               Total: 0.87 ± 0.1mm
  86.54 ± 0.2  OD
  84.11 ± 0.2  PS                           FPC tail: 0.10 ± 0.03mm
  82.01 ± 0.2  EPL                          FPC + stiffener: 0.30 ± 0.05mm
  77.76        AA                           Glue area: 1.00mm MAX

  Border (OD to AA):
    Left/Right: (56.24 - 51.84) / 2 = 2.20mm
    Top: ~2.20mm
    Bottom: ~6.58mm (includes FPC exit)

  FPC connector: 12.50 ± 0.1mm wide, offset 21.87 ± 0.3mm from left
  Sealing edge: 0.92 ± 0.1mm

FPC Pinout (24-pin, 0.5mm pitch)

Pin Signal Description
1 NC Not connected
2 GDR Gate driver output
3 RESE External sense resistor (current sensing)
4 NC Not connected
5 VDHR Red driving voltage (unused in B/W mode)
6 TSCL Temperature sensor I2C clock (SSD1677 built-in)
7 TSDA Temperature sensor I2C data (SSD1677 built-in)
8 BS Bus select: LOW = SPI 4-wire
9 BUSY_N Busy status, active LOW
10 RST_N Reset, active LOW
11 DC Data/Command select: LOW = cmd, HIGH = data
12 CSB Chip select, active LOW
13 SCLK SPI clock
14 SDA SPI data in (MOSI)
15 VDDIO I/O logic supply voltage
16 VDD Core logic supply voltage
17 GND Ground
18 VDDD Digital supply voltage
19 VPP Programming voltage (OTP)
20 VSH Source high voltage
21 VGH Gate high voltage
22 VSL Source low voltage
23 VGL Gate low voltage
24 VCOM Common electrode voltage

ESP32-C3 GPIO Wiring

Signal GPIO FPC Pin Notes
CS 21 12 (CSB) Directly toggled in SPI send functions
DC 4 11 (DC) LOW = command, HIGH = data
RST 5 10 (RST_N) Active LOW, 15ms reset pulse + 10ms settle
BUSY 6 9 (BUSY_N) Poll until HIGH = idle, 5s timeout
SCLK 8 13 (SCLK) Shared SPI bus (with SD card)
MOSI 10 14 (SDA) Shared SPI bus

Library: GxEPD2

The firmware uses GxEPD2 by Jean-Marc Zingg. The display driver is a custom class for this 3.68" panel (no standard GxEPD2 driver exists for 528×792).

Evidence

  1. VTable at 0x3C5A64DC matches GxEPD2's GxEPD2_EPD class hierarchy exactly
  2. Constructor signature: epd_gxepd2_constructor(obj, cs, dc, rst, busy) with resolution and SPI freq
  3. Two-plane buffer approach (old data + new data) — standard GxEPD2 pattern
  4. Partial window protocol via CMD 0x90/0x91/0x92 — matches GxEPD2's partial update implementation

VTable Layout (0x3C5A64DC)

Index Address Method Purpose
0 0x42065912 init() Calls _InitDisplay with default params
1 0x42065AC0 _InitDisplay() Pin setup, HW reset, SPI init
2 0x42065920 _endSPI() End SPI, set pins to input
3 0x42065FC8 _Update_Full() Full update: clear + fill + refresh
4 0x42065FF8 _writeScreenBuffer() Fill both RAMs (52,272 bytes each)
5 0x42066B2A writeImage() Write image to RAM with init check
6 0x420669C0 writeImagePart() Write partial image region to RAM
7 0x420665BE writeImageAgain() Write image for partial update
8 0x420658C8 (unknown)
9 0x42066450 _writeImage() Low-level image write to RAM
10 0x42065FC6 (unknown)
11 0x42065908 (stub)
12 0x4206590A (stub)
13 0x42066EAA refresh(full) Full or partial refresh dispatch
14 0x42066EC8 refresh(partial) Partial refresh with LUT check
15 0x420663A4 powerOff() Power off / end display update
16 0x42066060 hibernate() CMD 0x07 + 0xA5 deep sleep
17 0x4206590C (stub)
18 0x4206590E (stub)
19 0x42065910 (stub)

Initialization Sequence

Function: sub_42066816 (_InitDisplay)

 1. HW RESET
    - RST pin LOW → wait 15ms → HIGH → wait ≥10ms
    - (sub_42065A14: active-LOW reset with configurable delay from struct offset 0x36)

 2. WAIT BUSY (poll GPIO 6 until HIGH)

 3. CMD 0x00 (PANEL_SETTING):          [0x3F, 0x0A]
    - 0x3F = 0b00111111
      - Bit 5: LUT from register (not OTP)
      - Bit 4: RES selection = 1 (non-default resolution)
      - Bits 3:0: panel-specific config
    - 0x0A = scan settings

 4. CMD 0x61 (RESOLUTION_SETTING):     [0x03, 0x18, 0x02, 0x58]
    - Width:  (0x03 << 8) | 0x18 = 792
    - Height: (0x02 << 8) | 0x58 = 600 (physical; firmware uses 528)
    WAIT BUSY

 5. CMD 0x65 (unknown/flash control):  [0x00, 0x00, 0x00, 0x00]

 6. CMD 0x03 (GATE_DRIVING_VOLTAGE):   [0x20]
    - Gate voltage = 20V

 7. CMD 0x01 (POWER_SETTING):          [0x07, 0x17, 0x3F, 0x3F, 0x17]
    - VGH/VGL and source driving voltage configuration
    - 5 bytes: internal power generation settings

 8. CMD 0x82 (VCM_DC_SETTING):         [0x24]
    - VCOM DC level = 0x24 (-1.8V typical)

 9. CMD 0x06 (BOOSTER_SOFT_START):     [0x25, 0x25, 0x3C, 0x37]
    - Phase A: 0x25 (strength, min off time)
    - Phase B: 0x25
    - Phase C: 0x3C
    - Duration: 0x37

10. CMD 0x30 (PLL_CONTROL):            [0x09]
    - Frame rate control

11. CMD 0xE1 (undocumented):           [0x02]
    - Possibly temperature sensor or power sequence related

Display RAM Operations

Buffer Geometry

Width in bytes:  792 / 8 = 99 bytes per row
Height:          528 rows
Buffer size:     99 * 528 = 52,272 bytes per plane
Two planes:      "old" (CMD 0x10) and "new" (CMD 0x13)

Write Screen Buffer (sub_42065FF8)

Fills both RAM planes:

CMD 0x13 (Data Start Transmission 2 — "new" data):
    → 52,272 bytes of fill value (parameter: 0x00=black or 0xFF=white)

CMD 0x10 (Data Start Transmission 1 — "old" data):
    → 52,272 bytes of 0xFF (always white)

Write Image (sub_42007AB4)

Used for rendering content to the display:

1. Open file / prepare image buffer
2. Write LUT from 0x3C5856D8 (image-write LUT, 210 bytes)
   - Sends 5 x 42 bytes to CMD 0x20-0x24 via sub_4200531A
3. CMD 0x13 (write "new" RAM) → image data (with optional flip/center)
4. CMD 0x10 (write "old" RAM) → image data
5. Trigger display refresh (CMD 0x04 + CMD 0x12)

Partial Window (sub_420663A8 = _setPartialRamArea)

CMD 0x90 (Partial Window): 9 data bytes
    [x_start_hi, x_start_lo,     ← aligned to 8-pixel boundary (& 0xFFF8)
     x_end_hi,   x_end_lo,       ← OR'd with 0x07 (round up to byte)
     y_start_hi, y_start_lo,
     y_end_hi,   y_end_lo,
     0x01]                        ← partial scan mode enable

Partial Update Sequence

CMD 0x91 (Partial In)     ← enter partial mode
CMD 0x90 (Partial Window) ← set window coordinates
CMD 0x13 (write new data) ← write pixels for partial region
CMD 0x92 (Partial Out)    ← exit partial mode

Display Refresh Modes

Full Refresh (sub_42066D56sub_42066E44)

1. CMD 0x50 (VCOM_AND_DATA_INTERVAL): [0xA9, 0x07]
   - 0xA9: CDI=9, DDX=2, border waveform for full refresh
   - 0x07: data interval settings

2. Send full refresh LUT set (5 blocks, cmd+42 bytes each):
   - 0x3C5A652C → CMD 0x20 (LUT_VCOM)
   - 0x3C5A6584 → CMD 0x21 (LUT_WW)
   - 0x3C5A65DC → CMD 0x22 (LUT_BW)
   - 0x3C5A6634 → CMD 0x23 (LUT_WB)
   - 0x3C5A668C → CMD 0x24 (LUT_BB)

3. CMD 0x04 (Power On) + WAIT BUSY

4. CMD 0x12 (Display Update / Refresh) + WAIT BUSY

Partial Refresh (sub_42066CCEsub_42066DDE)

1. CMD 0x50 (VCOM_AND_DATA_INTERVAL): [0x29, 0x07]
   - 0x29: CDI settings for partial — less aggressive border waveform

2. Send partial refresh LUT set (5 blocks, cmd+42 bytes each):
   - 0x3C5A6558 → CMD 0x20 (LUT_VCOM)
   - 0x3C5A65B0 → CMD 0x21 (LUT_WW)
   - 0x3C5A6608 → CMD 0x22 (LUT_BW)
   - 0x3C5A6660 → CMD 0x23 (LUT_WB)
   - 0x3C5A66B8 → CMD 0x24 (LUT_BB)

3. CMD 0x04 (Power On) + WAIT BUSY

4. CMD 0x12 (Display Update / Refresh) + WAIT BUSY

Hibernate (sub_42066060)

CMD 0x07 (Deep Sleep): data = 0xA5
    - Check code 0xA5 required to enter deep sleep
    - All clocks and oscillators stopped
    - Only HW reset can wake the controller

LUT Data (Look-Up Tables)

The SSD1677 uses 5 LUT registers, each 42 bytes:

Register CMD Purpose
LUT_VCOM 0x20 VCOM voltage waveform
LUT_WW 0x21 White-to-White transition
LUT_BW 0x22 Black-to-White transition
LUT_WB 0x23 White-to-Black transition
LUT_BB 0x24 Black-to-Black transition

Each 42-byte LUT encodes voltage levels (VS) and timing phases (TP/RP). The SSD1677 LUT format uses groups of 6 bytes per phase: [VS_level, TP0, TP1, TP2, TP3, RP], with up to 7 phases per LUT.

LUT Set 1: Image Write (0x3C5856D8, 210 bytes)

Used by sub_42007AB4 (write_image). Sent via sub_4200531A as 5 x 42 bytes to CMD 0x20-0x24.

// LUT_VCOM (CMD 0x20) — 42 bytes at 0x3C5856D8
const uint8_t lut_vcom_image[] = {
    0x00, 0x08, 0x0B, 0x02, 0x03, 0x01,  // Phase 0: VS=0x00, TP=8/11/2/3, RP=1
    0x00, 0x0C, 0x02, 0x07, 0x02, 0x01,  // Phase 1: VS=0x00, TP=12/2/7/2, RP=1
    0x00, 0x01, 0x00, 0x02, 0x00, 0x01,  // Phase 2: VS=0x00, TP=1/0/2/0, RP=1
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Phase 3: unused
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Phase 4: unused
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Phase 5: unused
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Phase 6: unused
};

// LUT_WW (CMD 0x21) — 42 bytes at 0x3C585702
const uint8_t lut_ww_image[] = {
    0xA8, 0x08, 0x0B, 0x02, 0x03, 0x01,  // Phase 0: VS=0xA8, TP=8/11/2/3, RP=1
    0x44, 0x0C, 0x02, 0x07, 0x02, 0x01,  // Phase 1: VS=0x44, TP=12/2/7/2, RP=1
    0x04, 0x01, 0x00, 0x02, 0x00, 0x01,  // Phase 2: VS=0x04, TP=1/0/2/0, RP=1
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Phase 3-6: unused
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

// LUT_BW (CMD 0x22) — 42 bytes at 0x3C58572C
const uint8_t lut_bw_image[] = {
    0x80, 0x08, 0x0B, 0x02, 0x03, 0x01,  // Phase 0: VS=0x80, TP=8/11/2/3, RP=1
    0x62, 0x0C, 0x02, 0x07, 0x02, 0x01,  // Phase 1: VS=0x62, TP=12/2/7/2, RP=1
    0x00, 0x01, 0x00, 0x02, 0x00, 0x01,  // Phase 2: VS=0x00, TP=1/0/2/0, RP=1
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

// LUT_WB (CMD 0x23) — 42 bytes at 0x3C585756
const uint8_t lut_wb_image[] = {
    0x88, 0x08, 0x0B, 0x02, 0x03, 0x01,  // Phase 0: VS=0x88, TP=8/11/2/3, RP=1
    0x60, 0x0C, 0x02, 0x07, 0x02, 0x01,  // Phase 1: VS=0x60, TP=12/2/7/2, RP=1
    0x00, 0x01, 0x00, 0x02, 0x00, 0x01,  // Phase 2: VS=0x00, TP=1/0/2/0, RP=1
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

// LUT_BB (CMD 0x24) — 42 bytes at 0x3C585780
const uint8_t lut_bb_image[] = {
    0x00, 0x08, 0x0B, 0x02, 0x03, 0x01,  // Phase 0: VS=0x00, TP=8/11/2/3, RP=1
    0x4A, 0x0C, 0x02, 0x07, 0x02, 0x01,  // Phase 1: VS=0x4A, TP=12/2/7/2, RP=1
    0x88, 0x01, 0x00, 0x02, 0x00, 0x01,  // Phase 2: VS=0x88, TP=1/0/2/0, RP=1
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

LUT Set 2: Full Refresh (0x3C5A652C, 5 x 43 bytes)

Used by sub_42066D56 (_Update_Full). Each block = [cmd_byte, 42 LUT bytes]. Sent via sub_42065F5E which sends byte[0] as command (DC=LOW) and bytes[1-42] as data (DC=HIGH).

// LUT_VCOM (CMD 0x20) — 43 bytes at 0x3C5A652C (byte 0 = cmd 0x20)
const uint8_t lut_vcom_full[] = {
    0x20,                                 // Command byte (sent with DC=LOW)
    0x00, 0x06, 0x01, 0x06, 0x06, 0x01,  // Phase 0: VS=0x00, TP=6/1/6/6, RP=1
    0x00, 0x04, 0x01, 0x01, 0x00, 0x01,  // Phase 1: VS=0x00, TP=4/1/1/0, RP=1
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Phase 2-6: unused
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

// LUT_WW (CMD 0x21) — 43 bytes at 0x3C5A6584
const uint8_t lut_ww_full[] = {
    0x21,                                 // Command byte
    0x20, 0x06, 0x01, 0x06, 0x06, 0x01,  // Phase 0: VS=0x20, TP=6/1/6/6, RP=1
    0x00, 0x04, 0x01, 0x01, 0x00, 0x01,  // Phase 1: VS=0x00, TP=4/1/1/0, RP=1
    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,
};

// LUT_BW (CMD 0x22) — 43 bytes at 0x3C5A65DC
const uint8_t lut_bw_full[] = {
    0x22,                                 // Command byte
    0xAA, 0x06, 0x01, 0x06, 0x06, 0x01,  // Phase 0: VS=0xAA, TP=6/1/6/6, RP=1
    0xA0, 0x04, 0x01, 0x01, 0x00, 0x01,  // Phase 1: VS=0xA0, TP=4/1/1/0, RP=1
    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,
};

// LUT_WB (CMD 0x23) — 43 bytes at 0x3C5A6634
const uint8_t lut_wb_full[] = {
    0x23,                                 // Command byte
    0x55, 0x06, 0x01, 0x06, 0x06, 0x01,  // Phase 0: VS=0x55, TP=6/1/6/6, RP=1
    0x50, 0x04, 0x01, 0x01, 0x00, 0x01,  // Phase 1: VS=0x50, TP=4/1/1/0, RP=1
    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,
};

// LUT_BB (CMD 0x24) — 43 bytes at 0x3C5A668C
const uint8_t lut_bb_full[] = {
    0x24,                                 // Command byte
    0x00, 0x06, 0x01, 0x06, 0x06, 0x01,  // Phase 0: VS=0x00, TP=6/1/6/6, RP=1
    0x04, 0x04, 0x01, 0x01, 0x00, 0x01,  // Phase 1: VS=0x04, TP=4/1/1/0, RP=1
    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,
};

VS byte interpretation (2-bit pairs for 4 pixel-state transitions, MSB first):

VS Byte Bits 7:6 Bits 5:4 Bits 3:2 Bits 1:0 Meaning
0x00 00 00 00 00 No drive (all states)
0x20 00 10 00 00 Weak positive on one transition
0xAA 10 10 10 10 Negative drive on all transitions
0x55 01 01 01 01 Positive drive on all transitions
0xA0 10 10 00 00 Negative drive (partial)
0x50 01 01 00 00 Positive drive (partial)
0x04 00 00 01 00 Weak positive on one transition

Phase timing: Phase 0 drives 6+1+6+6=19 frame groups (flash/clean), Phase 1 drives 4+1+1+0=6 frame groups (settle). Total ~25 frame groups per full refresh cycle.

LUT Set 3: Partial Refresh (0x3C5A6558, 5 x 43 bytes)

Used by sub_42066CCE (_Update_Part). Faster transitions, fewer phases.

// LUT_VCOM (CMD 0x20) — 43 bytes at 0x3C5A6558
const uint8_t lut_vcom_partial[] = {
    0x20,                                 // Command byte
    0x00, 0x18, 0x04, 0x0E, 0x0A, 0x01,  // Phase 0: VS=0x00, TP=24/4/14/10, RP=1
    0x00, 0x0A, 0x00, 0x00, 0x00, 0x01,  // Phase 1: VS=0x00, TP=10/0/0/0, RP=1
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Phase 2-6: unused
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

// LUT_WW (CMD 0x21) — 43 bytes at 0x3C5A65B0
const uint8_t lut_ww_partial[] = {
    0x21,                                 // Command byte
    0x4A, 0x18, 0x04, 0x0E, 0x0A, 0x01,  // Phase 0: VS=0x4A, TP=24/4/14/10, RP=1
    0x00, 0x0A, 0x00, 0x00, 0x00, 0x01,  // Phase 1: VS=0x00, TP=10/0/0/0, RP=1
    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,
};

// LUT_BW (CMD 0x22) — 43 bytes at 0x3C5A6608
const uint8_t lut_bw_partial[] = {
    0x22,                                 // Command byte
    0x0A, 0x18, 0x04, 0x0E, 0x0A, 0x01,  // Phase 0: VS=0x0A, TP=24/4/14/10, RP=1
    0x00, 0x0A, 0x00, 0x00, 0x00, 0x01,  // Phase 1: VS=0x00, TP=10/0/0/0, RP=1
    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,
};

// LUT_WB (CMD 0x23) — 43 bytes at 0x3C5A6660
const uint8_t lut_wb_partial[] = {
    0x23,                                 // Command byte
    0x04, 0x18, 0x04, 0x0E, 0x0A, 0x01,  // Phase 0: VS=0x04, TP=24/4/14/10, RP=1
    0x40, 0x0A, 0x00, 0x00, 0x00, 0x01,  // Phase 1: VS=0x40, TP=10/0/0/0, RP=1
    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,
};

// LUT_BB (CMD 0x24) — 43 bytes at 0x3C5A66B8
const uint8_t lut_bb_partial[] = {
    0x24,                                 // Command byte
    0x84, 0x18, 0x04, 0x0E, 0x0A, 0x01,  // Phase 0: VS=0x84, TP=24/4/14/10, RP=1
    0x40, 0x0A, 0x00, 0x00, 0x00, 0x01,  // Phase 1: VS=0x40, TP=10/0/0/0, RP=1
    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,
};

Partial LUT VS byte interpretation:

VS Byte Bits 7:6 Bits 5:4 Bits 3:2 Bits 1:0 Effect
0x00 00 00 00 00 VCOM: no drive
0x4A 01 00 10 10 WW: mild positive + negative mix
0x0A 00 00 10 10 BW: gentle negative on lower bits
0x04 00 00 01 00 WB: gentle positive on bit pair 2
0x84 10 00 01 00 BB: negative high + positive low
0x40 01 00 00 00 Settle phase: mild positive

Phase timing: Phase 0 drives 24+4+14+10=52 frame groups (single long drive), Phase 1 drives 10+0+0+0=10 frame groups (short settle). Total ~62 frame groups — longer than full refresh per-phase but only 2 phases vs. the full refresh pattern. Results in faster overall update with potential ghosting.

LUT Format Reference

Each LUT register is 42 bytes = 7 phases x 6 bytes per phase:

Byte layout per phase:
  [VS, TP0, TP1, TP2, TP3, RP]

VS (Voltage Select): 4 pairs of 2-bit fields
  Bits 7:6 = VS[LUT0]  (transition pair 0)
  Bits 5:4 = VS[LUT1]  (transition pair 1)
  Bits 3:2 = VS[LUT2]  (transition pair 2)
  Bits 1:0 = VS[LUT3]  (transition pair 3)

  2-bit values:
    00 = GND (no drive)
    01 = VDH (positive / white drive)
    10 = VDL (negative / black drive)
    11 = VDHR (red drive, unused in B/W mode)

TP0-TP3: Timing for each sub-phase within the phase (in frame groups)
RP: Repeat count for this phase (0 = skip phase)

Display Object Layout

Outer GxEPD2_BW Struct at 0x3FC966E0 (BSS)

Offset Type Field
0x08 int16 Width (792)
0x0A int16 Height (528)
0x0C int16 Computed width (rotation-dependent)
0x0E int16 Computed height (rotation-dependent)
0x1A byte Rotation mode (0-3)

Inner GxEPD2_EPD Struct at 0x3FC96704 (offset 0x24 from outer)

Offset Type Field Value
0x00 dword VTable pointer 0x3C5A64DC
0x04 int16 WIDTH 792
0x06 int16 HEIGHT 528
0x08 dword _page_height / config 95 (0x5F)
0x0C byte _mirror 0
0x0D byte _using_partial_mode 0/1
0x0E byte _hibernating 0/1
0x10 int16 CS pin 21
0x12 int16 DC pin 4
0x14 int16 RST pin 5
0x16 int16 BUSY pin 6
0x18 int16 BUSY level 1 (active LOW → wait until HIGH)
0x20 byte _rst_is_active_low 0 (yes, active LOW)
0x21 byte _power_is_on flag 0/1
0x24 dword SPI class pointer 0x3FCA3F04
0x28 dword SPI frequency 10,000,000 (10 MHz)
0x2C dword SPI mode/settings 15,000,000 (alternate freq?)
0x30 byte _initial_write 0/1
0x32 byte _using_partial_mode_2nd 0/1
0x34 dword page buffer size 983,040 (0xF0000)
0x36 word Reset delay (ms) 15
0x40 byte _initial_refresh_done 0/1

SPI Communication Protocol

Send Command (sub_42065C56)

SPI.beginTransaction(freq, mode)
DC = LOW                          ← command mode
CS = LOW
SPI.transfer(command_byte)
CS = HIGH
DC = HIGH                         ← return to data mode

Send Data Byte (sub_42065CCA)

SPI.beginTransaction(freq, mode)
CS = LOW                          ← DC stays HIGH from last command
SPI.transfer(data_byte)
CS = HIGH

Send Data Block (sub_42065EC0 via sub_42065F5E)

Used for LUT blocks (43 bytes: cmd + 42 data bytes):

SPI.beginTransaction(freq, mode)
DC = LOW                          ← command mode
CS = LOW
SPI.transfer(block[0])            ← first byte as COMMAND
DC = HIGH                         ← switch to data mode
for i in 1..41:
    SPI.transfer(block[i])        ← remaining bytes as DATA
CS = HIGH

Send Data Buffer (sub_42065F9A / sub_4205A858)

Used for bulk pixel data:

CS = LOW  (via sub_42065F62)
SPI.transfer(buffer, length)      ← multi-byte transfer
CS = HIGH (via sub_42065FA0)

Key Functions Map

Function Address GxEPD2 Method Purpose
epd_gxepd2_constructor 0x42066090 Constructor Create display object (cs=21, dc=4, rst=5, busy=6, 792x528)
epd_spi_device_constructor 0x42065972 GxEPD2_EPD() Initialize SPI device struct
epd_display_init 0x4200DAB2 Setup wrapper Init display, set rotation, configure SPI
_InitDisplay 0x42066816 _InitDisplay() SSD1677 register initialization sequence
_hw_reset 0x42065A14 _reset() Hardware reset via RST pin
_send_command 0x42065C56 _writeCommand() DC=LOW, SPI transfer
_send_data_byte 0x42065CCA _writeData() DC=HIGH, SPI transfer
_send_data_block 0x42065EC0 _writeDataPGM() Send cmd+data block (for LUTs)
_cs_low 0x42065F62 _startTransfer() Begin SPI transfer, CS LOW
_spi_write_byte 0x42065F94 _transfer() SPI transfer single byte
_spi_write_buf 0x42065F9A _transfer() SPI transfer buffer
_cs_high 0x42065FA0 _endTransfer() End SPI transfer, CS HIGH
_writeScreenBuffer 0x42065FF8 _writeScreenBuffer() Fill both RAMs (52,272 bytes each)
_writeImage 0x42066450 _writeImage() Write pixel data to partial area
writeImage 0x42066B2A writeImage() Write image with init check
writeImagePart 0x420669C0 writeImagePart() Write partial image region
writeImageAgain 0x420665BE writeImageAgain() Re-write for partial update mode
_setPartialRamArea 0x420663A8 _setPartialRamArea() CMD 0x90: set partial window coords
_Update_Full 0x42066D56 _Update_Full() Full refresh: LUT + power on
_Update_Part 0x42066CCE _Update_Part() Partial refresh: LUT + power on
_refresh_full 0x42066E44 refresh(true) CMD 0x12 display update (full)
_refresh_part 0x42066DDE refresh(false) CMD 0x12 display update (partial)
refresh_dispatch 0x42066EAA refresh() Dispatch full or partial refresh
powerOff 0x420663A4 powerOff() Thunk → sub_42066338
_powerOff_impl 0x42066338 Internal Wait busy, clear power state
_waitWhileBusy 0x420660D4 _waitWhileBusy() Poll BUSY pin (GPIO 6) until HIGH
hibernate 0x42066060 hibernate() CMD 0x07 + 0xA5 → deep sleep
_endSPI 0x42065920 end() SPI end, set pins to input
_writeLUT 0x4200531A Custom Send 5x42 bytes to CMD 0x20-0x24
_trigger_refresh 0x4200538A Custom CMD 0x04 + CMD 0x12 + wait
write_image 0x42007AB4 App-level Write LUT + image data to both RAMs
_flip_buffer 0x42005158 Custom Vertical flip of image buffer
_scale_and_send 0x42005204 Custom Scale/center image and send to display

Using This Display in Custom Firmware

With GxEPD2 (Recommended)

The closest existing class is GxEPD2_583_GDEQ0583T31. Create a custom driver by modifying the init sequence:

#include <GxEPD2_BW.h>
#include <SPI.h>

// Custom class based on GxEPD2 SSD1677 driver
// Modify GxEPD2_583_GDEQ0583T31.h/.cpp:
//   - WIDTH = 792, HEIGHT = 528
//   - Resolution register: 0x0318, 0x0258
//   - Panel Setting: 0x3F, 0x0A
//   - Power Setting: 0x07, 0x17, 0x3F, 0x3F, 0x17
//   - VCM DC: 0x24
//   - Booster: 0x25, 0x25, 0x3C, 0x37
//   - PLL: 0x09
//   - Replace LUT arrays with the extracted data above

SPIClass spi(FSPI);
spi.begin(8, 7, 10, -1);  // SCLK=8, MISO=7, MOSI=10

GxEPD2_BW<GxEPD2_Custom, GxEPD2_Custom::HEIGHT> display(
    GxEPD2_Custom(/*CS=*/21, /*DC=*/4, /*RST=*/5, /*BUSY=*/6));

Raw SPI (Minimal)

// Pseudocode for minimal display operation

void epd_init() {
    // HW Reset
    digitalWrite(RST, LOW); delay(15);
    digitalWrite(RST, HIGH); delay(10);
    waitBusy();

    sendCommand(0x00); sendData(0x3F); sendData(0x0A);           // Panel Setting
    sendCommand(0x61); sendData(0x03); sendData(0x18);           // Resolution
                       sendData(0x02); sendData(0x58);
    waitBusy();
    sendCommand(0x65); sendData(0x00); sendData(0x00);           // Flash control
                       sendData(0x00); sendData(0x00);
    sendCommand(0x03); sendData(0x20);                           // Gate voltage
    sendCommand(0x01); sendData(0x07); sendData(0x17);           // Power setting
                       sendData(0x3F); sendData(0x3F);
                       sendData(0x17);
    sendCommand(0x82); sendData(0x24);                           // VCM DC
    sendCommand(0x06); sendData(0x25); sendData(0x25);           // Booster
                       sendData(0x3C); sendData(0x37);
    sendCommand(0x30); sendData(0x09);                           // PLL
    sendCommand(0xE1); sendData(0x02);                           // Undocumented
}

void epd_writeFullImage(uint8_t* data) {
    writeLUT(full_refresh_lut);          // Send 5x42 LUT bytes to 0x20-0x24
    sendCommand(0x50); sendData(0xA9); sendData(0x07);  // VCOM interval

    sendCommand(0x13);                   // New data RAM
    for (int i = 0; i < 52272; i++)
        sendData(data[i]);

    sendCommand(0x10);                   // Old data RAM
    for (int i = 0; i < 52272; i++)
        sendData(0xFF);

    sendCommand(0x04); waitBusy();       // Power on
    sendCommand(0x12); waitBusy();       // Display refresh
}

void epd_hibernate() {
    sendCommand(0x07); sendData(0xA5);   // Deep sleep
}

Alternative Libraries

Library SSD1677 Support Notes
GxEPD2 Yes (native) Best fit; already used by OEM firmware
Adafruit EPD Partial Supports some SSD1677 panels, may need customization
LovyanGFX Experimental E-paper support less mature
EPDiy No Targets parallel interface panels

SSD1677 Command Reference (Used in Firmware)

CMD Name Data Bytes Usage
0x00 Panel Setting 2 [0x3F, 0x0A] — LUT from register, resolution mode
0x01 Power Setting 5 [0x07, 0x17, 0x3F, 0x3F, 0x17] — VGH/VGL/VDHR
0x03 Gate Driving Voltage 1 [0x20] — 20V
0x04 Power On 0 Turn on charge pump; wait BUSY
0x06 Booster Soft Start 4 [0x25, 0x25, 0x3C, 0x37]
0x07 Deep Sleep 1 [0xA5] — hibernate check code
0x10 Data Start Transmission 1 N "Old" pixel data (52,272 bytes)
0x12 Display Refresh 0 Trigger display update; wait BUSY
0x13 Data Start Transmission 2 N "New" pixel data (52,272 bytes)
0x20 LUT VCOM 42 VCOM voltage waveform table
0x21 LUT WW 42 White-to-White waveform
0x22 LUT BW 42 Black-to-White waveform
0x23 LUT WB 42 White-to-Black waveform
0x24 LUT BB 42 Black-to-Black waveform
0x30 PLL Control 1 [0x09] — frame rate
0x50 VCOM and Data Interval 2 [0xA9/0x29, 0x07] — full/partial mode
0x61 Resolution Setting 4 [0x03, 0x18, 0x02, 0x58] — 792x600
0x65 Flash Mode 4 [0x00, 0x00, 0x00, 0x00]
0x82 VCM DC Setting 1 [0x24] — VCOM DC voltage
0x90 Partial Window 9 X/Y start/end + mode byte
0x91 Partial In 0 Enter partial update mode
0x92 Partial Out 0 Exit partial update mode
0xE1 (Undocumented) 1 [0x02]

XTH/XTCH Format Support (2-bit / 4-Level Grayscale)

Format Overview

The Xteink format family includes two grayscale-capable formats:

Format Magic Description
XTG 0x00475458 ("XTG\0") 1-bit monochrome image
XTH 0x00485458 ("XTH\0") 2-bit per pixel, 4-level grayscale image
XTC 0x00435458 ("XTC\0") Comic container (holds XTG/XTH pages)
XTCH 0x48435458 ("XTCH") Comic container variant

XTH files store two bit planes in vertical scan order (column-major, right-to-left). On the X4 model (SSD1680), these are sent to CMD 0x24 and CMD 0x26. On the X3 (SSD1677), the equivalent would be CMD 0x10 (old data) and CMD 0x13 (new data).

File Type Detection (sub_420033AC)

The firmware extracts the file extension and maps it:

Extension Type Code Handler
txt 1 Text viewer
bmp 2 Bitmap renderer
jpg/jpeg 3 JPEG decoder
xtg 4 XTG page renderer
xtc 5 XTC container reader
xth 6 XTH page renderer (degraded)
xtch 7 XTCH container reader
png 8 PNG decoder
epub EPUB reader

X3 Firmware: XTH Rendered as 1-bit (No Grayscale)

The X3 firmware does NOT implement 4-level grayscale display. XTH pages are degraded to 1-bit black/white by reading only the first bit plane.

Magic Number Check (sub_42047DE2 at 0x4204802A)

Both XTG and XTH are accepted by a single bitmask comparison:

if (((magic - 0x475458) & 0xFFFEFFFF) != 0)
    error("unsupported format");
// XTG = 0x00475458, XTH = 0x00485458
// Difference is bit 16 (0x10000), which is masked out by 0xFFFEFFFF

XTG Path (1-bit, full GxEPD2 paged drawing)

if (magic == 0x475458) {          // Specifically XTG
    if (context[415]) flag = 1;   // Was previously XTH? Note transition
    context[415] = 0;             // Clear grayscale flag
    buf = malloc(data_size);
    read(file, buf, data_size);
    // Use GxEPD2 pixel-by-pixel drawing via sub_4200DB64
    // Supports partial refresh, page-by-page rendering
}

XTH Path (degraded to 1-bit bulk write)

else {                            // XTH (magic == 0x485458)
    context[415] = 1;             // Set grayscale flag
    if (partial_mode_active())
        clear_and_full_refresh(); // sub_4200E410: wipe display before XTH
    plane_size = (width * height) / 8;   // ONE bit plane only
    buf = malloc(plane_size);
    write_image(file, buf, plane_size, width, height);
    // sub_42007AB4: reads plane_size bytes from file stream
    // This reads ONLY bit plane 1 (the first plane after the 22-byte header)
    // Bit plane 2 (grayscale data) is NEVER read — skipped entirely
}

write_image (sub_42007AB4) — Same Path for XTG and XTH

1. Send image-write LUT from 0x3C5856D8 → CMD 0x20-0x24
2. Read data from file into buffer
3. CMD 0x13 (new data RAM): send buffer (with flip/scale)
4. Re-read same data from file
5. CMD 0x10 (old data RAM): send SAME buffer
6. CMD 0x04 (Power On) + CMD 0x12 (Refresh)

Both RAMs receive identical data → no grayscale information, just 1-bit black/white.

Grayscale Mode Flag (Reader Context Offset 415)

Transition Flag Action Display Effect
Enter XTH page context[415] = 1 Triggers full clear if partial mode was active
Enter XTG page (after XTH) Read flag, then context[415] = 0 Notes transition for bookmark/settings save
Enter XTG page (after XTG) context[415] already 0 Normal operation

XTCH Cover Support

The firmware contains:

"XTCH cover not supported yet" (0x3C166214)
"暂不支持XTCH的封面" (0x3C166234)

XTCH covers are explicitly unsupported. The cover generation function (sub_42020E00) only writes .xtg format covers to /XTCache/xtc/.

Theoretical 4-Level Grayscale on SSD1677

The SSD1677 controller IS capable of 4-level grayscale through its dual-RAM + LUT architecture. To implement it:

  1. Write different data to each RAM:

    • CMD 0x10: bit plane 1 (high bit)
    • CMD 0x13: bit plane 2 (low bit)
  2. Use a grayscale LUT that maps the 4 (old, new) combinations:

    Old (0x10) New (0x13) Pixel Value Display
    0 0 0 (00) White
    0 1 1 (01) Dark Grey
    1 0 2 (10) Light Grey
    1 1 3 (11) Black
  3. Design LUT VS bytes for each register:

    • LUT_WW (0x21): drives white→white transitions (no change for value 0)
    • LUT_BW (0x22): drives transitions from black to white
    • LUT_WB (0x23): drives transitions from white to black
    • LUT_BB (0x24): drives black→black transitions (no change for value 3)
    • Each VS byte's 2-bit pairs control the voltage applied during the waveform

    The VS byte encodes 4 transition pairs [7:6][5:4][3:2][1:0] where each pair maps to the (old,new) pixel combination. Values: 00=GND, 01=VDH (positive/white), 10=VDL (negative/black), 11=VDHR (unused in B/W).

  4. No firmware support exists — the X3 binary contains no grayscale LUT data and no function that writes different data to the two RAMs.

Data Addresses (DROM)

Address Size Content
0x3C5856D8 210 Image-write LUT (5 x 42 bytes)
0x3C5A652C 43 Full refresh LUT_VCOM (cmd 0x20 + 42 data)
0x3C5A6558 43 Partial refresh LUT_VCOM (cmd 0x20 + 42 data)
0x3C5A6584 43 Full refresh LUT_WW (cmd 0x21 + 42 data)
0x3C5A65B0 43 Partial refresh LUT_WW (cmd 0x21 + 42 data)
0x3C5A65DC 43 Full refresh LUT_BW (cmd 0x22 + 42 data)
0x3C5A6608 43 Partial refresh LUT_BW (cmd 0x22 + 42 data)
0x3C5A6634 43 Full refresh LUT_WB (cmd 0x23 + 42 data)
0x3C5A6660 43 Partial refresh LUT_WB (cmd 0x23 + 42 data)
0x3C5A668C 43 Full refresh LUT_BB (cmd 0x24 + 42 data)
0x3C5A66B8 43 Partial refresh LUT_BB (cmd 0x24 + 42 data)
0x3C5A64DC 80 VTable for GxEPD2 display driver class
@CrazyCoder
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment