| 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 |
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
| 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 |
| 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 |
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).
- VTable at
0x3C5A64DCmatches GxEPD2'sGxEPD2_EPDclass hierarchy exactly - Constructor signature:
epd_gxepd2_constructor(obj, cs, dc, rst, busy)with resolution and SPI freq - Two-plane buffer approach (old data + new data) — standard GxEPD2 pattern
- Partial window protocol via CMD 0x90/0x91/0x92 — matches GxEPD2's partial update implementation
| 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) |
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
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)
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)
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)
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
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
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
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
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
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.
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,
};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.
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.
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)
| 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) |
| 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.beginTransaction(freq, mode)
DC = LOW ← command mode
CS = LOW
SPI.transfer(command_byte)
CS = HIGH
DC = HIGH ← return to data mode
SPI.beginTransaction(freq, mode)
CS = LOW ← DC stays HIGH from last command
SPI.transfer(data_byte)
CS = HIGH
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
Used for bulk pixel data:
CS = LOW (via sub_42065F62)
SPI.transfer(buffer, length) ← multi-byte transfer
CS = HIGH (via sub_42065FA0)
| 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 |
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));// 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
}| 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 |
| 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] |
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).
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 |
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.
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 0xFFFEFFFFif (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
}
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
}
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.
| 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 |
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/.
The SSD1677 controller IS capable of 4-level grayscale through its dual-RAM + LUT architecture. To implement it:
-
Write different data to each RAM:
- CMD 0x10: bit plane 1 (high bit)
- CMD 0x13: bit plane 2 (low bit)
-
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 -
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). -
No firmware support exists — the X3 binary contains no grayscale LUT data and no function that writes different data to the two RAMs.
| 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 |
See https://gist.github.com/CrazyCoder/1c5f846adee18e21f91e264601a6ddce for GPIO analysis.