115 lines
4.9 KiB
Markdown
115 lines
4.9 KiB
Markdown
|
|
# 03 — ePaper SPI protocol details (what we have to decode)
|
|||
|
|
|
|||
|
|
## Wiring (the 8-wire interface every Velxio panel will share)
|
|||
|
|
|
|||
|
|
Even across families (SSD1681 / UC8159 / Spectra 6) the **physical
|
|||
|
|
interface is the same six signals** plus power:
|
|||
|
|
|
|||
|
|
| Pin name | Direction (host → panel) | Purpose |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **VCC** | power | 3.3 V |
|
|||
|
|
| **GND** | power | ground |
|
|||
|
|
| **CLK** (SCK) | host → panel | SPI clock, mode 0, 4–20 MHz |
|
|||
|
|
| **DIN** (MOSI) | host → panel | SPI data |
|
|||
|
|
| **CS** | host → panel | active low chip select |
|
|||
|
|
| **DC** (D/C, RS) | host → panel | **LOW = command byte, HIGH = data byte** |
|
|||
|
|
| **RST** | host → panel | active-low hardware reset (>200 µs) |
|
|||
|
|
| **BUSY** | panel → host | **HIGH while controller is working** (poll before next op) |
|
|||
|
|
|
|||
|
|
Some panels (the Solomon family with red ink) also expose a MISO line, but
|
|||
|
|
neither GxEPD2 nor Adafruit_EPD reads back from it in practice.
|
|||
|
|
|
|||
|
|
## Frame in vs. command in
|
|||
|
|
|
|||
|
|
The DC pin distinguishes:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌── CS asserted ──────────────────────────────────────────────────┐
|
|||
|
|
│ │
|
|||
|
|
│ DC = LOW 1 byte → command opcode │
|
|||
|
|
│ DC = HIGH N bytes → command parameters / pixel data │
|
|||
|
|
│ │
|
|||
|
|
│ CS may stay asserted across the cmd→data transition; many │
|
|||
|
|
│ drivers lower CS once per (cmd, data...) pair instead. │
|
|||
|
|
└──────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The state machine the emulator needs:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
state ∈ { IDLE, WAIT_CMD, COLLECT_DATA(cmd) }
|
|||
|
|
|
|||
|
|
on CS falling edge: state ← WAIT_CMD
|
|||
|
|
on DC sample at SCK: if DC=LOW → state ← WAIT_CMD
|
|||
|
|
if DC=HIGH → state ← COLLECT_DATA(cmd)
|
|||
|
|
on byte received:
|
|||
|
|
if state == WAIT_CMD: cmd ← byte; state ← COLLECT_DATA(cmd)
|
|||
|
|
if state == COLLECT_DATA(cmd): apply parameter or pixel data
|
|||
|
|
on CS rising edge: state ← IDLE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Note the GxEPD2 / Adafruit_EPD libraries actually re-toggle DC for every
|
|||
|
|
write — they don't keep CS asserted for long bursts — so a per-byte DC
|
|||
|
|
sample is sufficient.
|
|||
|
|
|
|||
|
|
## BUSY pin semantics
|
|||
|
|
|
|||
|
|
After almost every command that triggers physical movement of pigment,
|
|||
|
|
the controller pulls BUSY **high**. The libraries poll BUSY in a tight
|
|||
|
|
loop (with a 10 ms `delay()` between samples). The emulator must:
|
|||
|
|
|
|||
|
|
1. After accepting `0x20` ACTIVE_DISPLAY_UPDATE: drive BUSY high.
|
|||
|
|
2. After a configurable refresh duration (1 s mono, 12 s Spectra 6),
|
|||
|
|
drive BUSY low.
|
|||
|
|
3. During HW reset (RST low) the controller re-asserts BUSY high until it
|
|||
|
|
finishes its internal init (~10 ms).
|
|||
|
|
|
|||
|
|
For the emulator, "configurable refresh duration" doesn't have to be real
|
|||
|
|
seconds — we can shrink it to ~50 ms for snappy testing as long as it's
|
|||
|
|
**non-zero** so the firmware's busy-wait loop sees a transition.
|
|||
|
|
|
|||
|
|
## Two independent RAM planes (B/W/R panels)
|
|||
|
|
|
|||
|
|
Tri-colour SSD1675/1680 panels have **two** RAM planes:
|
|||
|
|
|
|||
|
|
- `0x24` WRITE_BLACK_VRAM — black/white plane (1 = white, 0 = black)
|
|||
|
|
- `0x26` WRITE_RED_VRAM — red plane (1 = red, 0 = transparent / underlying B/W)
|
|||
|
|
|
|||
|
|
Compositing rule used by every SSD168x driver: **red wins over black**.
|
|||
|
|
On flush, for each pixel:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
if red_plane[x,y] == 1: pixel = RED
|
|||
|
|
elif bw_plane[x,y] == 1: pixel = WHITE
|
|||
|
|
else: pixel = BLACK
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
UC8159c (5.65" 7-colour ACeP) is different: **single plane, 4 bits/pixel
|
|||
|
|
packed 2 px/byte**, mapped to a 7-colour palette. Spectra 6 uses 3
|
|||
|
|
bits/pixel packed 2 px/byte (with a 6-colour palette).
|
|||
|
|
|
|||
|
|
## RAM addressing model (Solomon family)
|
|||
|
|
|
|||
|
|
Pixels live in a rectangular window selected by:
|
|||
|
|
|
|||
|
|
- `0x44 SET_RAMX_RANGE` — start_col, end_col (bytes; 1 byte = 8 horizontal pixels)
|
|||
|
|
- `0x45 SET_RAMY_RANGE` — start_row, end_row (in scanlines; 16 bits each)
|
|||
|
|
- `0x4E SET_RAMX_COUNTER` — current X (bytes)
|
|||
|
|
- `0x4F SET_RAMY_COUNTER` — current Y (scanlines)
|
|||
|
|
- `0x11 DATA_ENTRY_MODE` — bit 0: Y direction (0 = decrement, 1 = increment)
|
|||
|
|
bit 1: X direction (0 = decrement, 1 = increment)
|
|||
|
|
bit 2: address counter update direction (0 = X first, 1 = Y first)
|
|||
|
|
|
|||
|
|
Most drivers use `0x03` (X+, Y+, X-first) which means the RAM auto-increments
|
|||
|
|
column-by-column, then jumps to the next row. So a 200×200 panel needs
|
|||
|
|
200/8 = 25 bytes/row × 200 rows = 5000 bytes per `0x24` WRITE.
|
|||
|
|
|
|||
|
|
## Sources
|
|||
|
|
|
|||
|
|
- SSD1681 datasheet (commands 0x00–0x4F): see `02_controllers.md`
|
|||
|
|
- ESPHome `waveshare_epaper.cpp` (canonical reference for many panels):
|
|||
|
|
<https://api-docs.esphome.io/waveshare__epaper_8cpp_source>
|
|||
|
|
- GxEPD2 source: <https://github.com/ZinggJM/GxEPD2/tree/master/src>
|
|||
|
|
- Solomon Systech SSD1681 product page:
|
|||
|
|
<https://www.solomon-systech.com/product/ssd1681/>
|