E-paper panels are a pain in embedded Rust. Every controller IC (SSD1680, SSD1681, JD79661, UC8253, whatever) has its own init sequence, its own RAM addressing quirks, its own idea of which bit means "black." Most Rust e-paper drivers hard-code one panel to one board and stop there. Swap the panel and you're rewriting the driver.
epdsi was built to get rid of that. This project is the proof it worked: the same driver crate and the same panel set running on two completely different microcontrollers, an Adafruit RP2040 Feather ThinkInk and a Seeed Studio XIAO ESP32-C3, sharing nothing but the crate and the SPI wiring.
The Feather ThinkInk (Adafruit product 5727) ships with a 24-pin FPC socket for e-paper panels, but the panel I had, a 2.13" quad-color JD79661, had no Rust driver anywhere. I wrote one from scratch, porting the init sequence and RAM layout out of Adafruit's CircuitPython and Arduino reference code (full writeup here).
That one-off driver kept growing. Every new panel I plugged into the same socket, a tri-color SSD1681, a mono SSD1680, a 4.26" SSD1677, a pair of UC8253 mono/tri panels, needed the same shape of code: an init sequence, a RAM window, a refresh call, a busy wait. So the JD79661 driver got refactored into a framework instead of a one-panel hack. It turned into epdsi, a no_std, embedded-hal 1.0 driver framework that separates the panel from the controller from the transport.
epdsi splits an e-paper display into four pieces:
EpdPanel: a zero-sized type that just describes the physical panel: width, height, color mode, and any panel-specific voltage/LUT quirks.EpdController: the driver IC's command logic: init sequence, addressing, frame writes, refresh, sleep. This is what differs between an SSD1681 and a UC8253.SpiBusWrapper: the physical transport: anembedded-halSpiDeviceplus the DC/RST/BUSY control pins.EpdDriver: the orchestrator that ties the above together viaEpdBuilderand exposes the public API.
Color goes through ColorMode and ColorChannel enums instead of one-off per-panel handling, so a tri-color panel's red plane and a quad-color panel's yellow plane use the same write_frame(ColorChannel, ...) call.
The payoff: adding a panel to an already-supported controller is a single new file, and adding a new controller doesn't touch any existing panel code. As of this project, epdsi covers 8 controller ICs driving 13 panels: SSD1680/1681, SSD1677, JD79660/79661, UC8253, ED2208 (Spectra 6), and the Pervasive BW/BWRY families, behind one API, with both blocking (embedded-hal) and async (embedded-hal-async) builds selected by feature flag. The async shape mirrors the blocking one exactly, so switching doesn't mean relearning the API.
For boards too tight on RAM to hold a full frame buffer, render_paged() sweeps the panel in horizontal bands using stack-allocated buffers instead of one static full-frame array. That's how a 48,000-byte frame fits on hardware that would otherwise choke on it.
Once the framework existed, the test was whether it actually decoupled hardware the way it claimed to. On the Feather ThinkInk's single 24-pin FPC socket, I ran seven panels across five different controller ICs: no rewiring, no soldering, just unplug one panel, plug in the next, reflash. All seven worked on the first attempt (full story here). That run also cleared up an old false lead: two UC8253 panels that an earlier board had logged as "not working" turned out to be a dead FPC module, not a controller problem. That's its own lesson in not trusting the first negative result.
Porting to a different CPU architectureSeven panels on one socket proves the panel abstraction works. It doesn't prove the platform abstraction works, because the Feather ThinkInk is always the same RP2040 underneath. So the next step was putting the identical panels behind a different microcontroller family entirely: the Seeed Studio XIAO ESP32-C3, paired with Seeed's ePaper Driver Board for XIAO, running esp-hal on RISC-V instead of embassy-rp on Cortex-M0+.
That's what this repository is: xiao-esp32c3-blinky.
The XIAO's ePaper Driver Board hardwires the same six signals every example uses:
- RST: XIAO D0, ESP32-C3 GPIO2
- CS: XIAO D1, ESP32-C3 GPIO3
- BUSY: XIAO D2, ESP32-C3 GPIO4
- DC: XIAO D3, ESP32-C3 GPIO5
- SCK: XIAO D8, ESP32-C3 GPIO8
- MOSI: XIAO D10, ESP32-C3 GPIO10
MISO is unused, since e-paper is write-only. With that wiring fixed by the carrier board, every example is just: build an EpdController for the right IC, wrap the SPI bus, hand it to epdsi, draw with embedded-graphics. The panel-facing code above main() is close to byte-identical to the RP2040 originals, same epdsi types, same buffers, same drawing calls, and only the board bring-up differs. That's the whole point of the abstraction: it means something when the only thing that changes between a Cortex-M0+ board and a RISC-V board is the HAL glue, not the panel logic.
Panels confirmed working on the XIAO ESP32-C3 through epdsi:
- Good Display GDEQ0426T82, 4.26" mono, 800×480, SSD1677
- GDEM0213B74, 2.13" mono, 122×250, SSD1680
- ZJY122250-0213AJH-E5, 2.13" quad-color, 122×250, JD79661
- GDEM0154Z90, 1.54" tri-color, 200×200, SSD1681
- SE0352N14, 3.52" tri-color, UC8253
- GDEY037T03, 3.7" mono, UC8253
Reference timings measured at 4 MHz SPI: the 4.26" mono panel does a full refresh in about 3.75 s and a partial refresh in about 1.0 s. Moving a 4,000-byte frame over SPI takes about 8 ms, so the controller's own refresh cycle, not data transfer, is always the bottleneck. Colour panels are the outlier: the red/yellow pigment is a heavier particle that needs the full waveform to migrate every time, so tri- and quad-colour panels take 14 to 17 seconds per update and have no meaningful partial mode. The fast LUT only exists for monochrome.
A board that doesn't work hereNot every XIAO variant works with this carrier board. The XIAO ESP32-C6 produces only noise on the ePaper Driver Board, confirmed with both epdsi and stock Arduino GxEPD2, same panel, same board, only the microcontroller swapped. The C3, MG24, and nRF52840 all drive the exact same assembly correctly. Seeed's own compatibility list for this driver board doesn't include the C6 either. If you're picking a XIAO for an e-paper project, that decision is made for you: use the C3.
epdsi currently covers 8 controllers and 13 panels across two very different MCU architectures with the same driver code. The natural next targets are the XIAO's other siblings that a Rust HAL actually supports: RP2350 and ESP32-S3, to see whether the same panel code drops in with only a HAL-glue diff, like the one seen going from RP2040 to ESP32-C3.
git clone https://github.com/melastmohican/xiao-esp32c3-blinky
cd xiao-esp32c3-blinky
cargo run --release --example jd79661_zjy122250_epd # 2.13" quad-colour
cargo run --release --example ssd1677_gdeq0426t82_epd # 4.26" monoBuild with --release. A debug image runs over 5 MB against roughly 750 KB, with no upside.
- Repo: xiao-esp32c3-blinky
- Driver crate: epdsi on crates.io / epdsi on GitHub
- Origin story: Rust on Adafruit RP2040 Feather ThinkInk (JD79661 E-Paper)
- Multi-panel proof: epdsi on the Feather ThinkInk: From One Panel to Seven, One Socket
- Seeed Studio XIAO ESP32-C3
- Seeed ePaper Driver Board for XIAO (V2, 24-pin FPC)
- Good Display GDEQ0426T82, GDEM0213B74, GDEM0154Z90, GDEY037T03
- ZJY122250-0213AJH-E5 (also sold as Good Display GDEY0213F51 / Seeed 5779 / Adafruit 6373)
- SE0352N14
- Rust,
no_std,riscv32imc-unknown-none-elftarget esp-hal1.0.0epdsi(this crate)embedded-graphics







Comments