epdsi: E-Paper Display Serial Interface Framework
A no_std, embedded-hal 1.0 compatible Rust driver framework for Electronic Paper Displays (EPD).

Every panel above driven by epdsi on an RP2350 Pico 2 — left to right: GDEQ0426T82 (SSD1677),
GDEY037T03 (UC8253), ZJY122250 (JD79661), GDEM0213B74 (SSD1680), GDEM0154Z90 (SSD1681),
E2417QS0A3 and E2154QS0F1 (Pervasive Spectra-4 BWRY, Drivers A and F).
Features
- Modular Architecture: Decouples driver IC logic (
EpdController) from physical panel specifications (EpdPanel). embedded-hal1.0 Compatible: Built around standardSpiDevice,OutputPin,InputPin, andDelayNstraits.- Low-RAM Paged Rendering: Built-in support for GxEPD2-style closure-based paged graphics rendering using tiny stack buffers.
embedded-graphicsIntegration: ImplementsDrawTargetandDimensionsvia optionalgraphicsfeature (enabled by default).- Multi-Color Support: Unified handling for Monochrome, Tri-Color (Black/White/Red), Quad-Color (Black/White/Yellow/Red 2bpp), and 4bpp ACeP / E Ink Spectra palette displays.
- Automatic RAM Alignment: Driver IC controllers automatically align panel widths to hardware byte boundaries (
div_ceil(8) * 8).
Supported Controllers & Panels
| Controller IC | Supported Panels | Resolution | Color Mode | Notes |
|---|---|---|---|---|
SSD1681 (Ssd1681Controller / Ssd168xController) |
GDEM0154Z90 |
200 × 200 | Tri-Color | 1.54" Tri-Color SPI panel, Full refresh only (~14 s). Ssd168xRefreshMode::Partial is not usable — see note below. Partial window updates work via set_window at full-refresh speed |
SSD1680(Z) (Ssd1680Controller / Ssd168xController) |
GDEM0213B74 |
122 × 250 | Monochrome | 2.13" Monochrome (Adafruit 6383), Full/Partial refresh |
JD79661 (Jd79661Controller) |
ZJY122250_0213AJH_E5 / GDEY0213F51 |
122 × 250 | Quad-Color | 2.13" Quad-Color (Good Display GDEY0213F51, Seeed Studio 5779, Adafruit 6373, Active-Low BUSY) |
UC8253 (Uc8253Controller) |
GDEY037T03 (GxEPD2_370_GDEY037T03) |
240 × 416 | Monochrome | 3.7" Monochrome (Adafruit 6395), Active-Low BUSY, Full/FastFull/Partial/FastPartial refresh |
SSD1677 (Ssd1677Controller) |
GDEQ0426T82 |
800 × 480 | Monochrome | 4.26" Monochrome (Seeed Studio 6398, SE8350/SSD1677), Full/FastFull/Partial refresh |
ED2208 (Ed2208Controller) |
GDEP073E01 (GxEPD2_730c_GDEP073E01) |
800 × 480 | Spectra 6 (4bpp) | 7.3" six-colour E Ink Spectra 6 / GDEP073E01(E6) — black, white, red, yellow, blue, green. SevenColor::Orange is ACeP-7 only and not renderable here (Seeed reTerminal E1002) |
Pervasive Displays (PervasiveBwController) |
E2266KS0C1 (EPD_266_KS_0C), E2290KS0F1 (EPD_290_KS_0F) |
152 × 296, 168 × 384 | Monochrome | Pervasive Displays 2.66" (Driver C) & 2.90" (Driver F) Panels |
Pervasive Displays BWRY (PervasiveBwryController) |
E2154QS0F1 (EPD_154_QS_0F), E2417QS0A3 (EPD_417_QS_0A) |
152 × 152, 400 × 300 | Quad-Color (Spectra-4) | Pervasive Displays 1.54" (Driver F) & 4.2" (Driver A), OTP-sourced registers read via a bit-banged 3-wire handshake (epdsi::bus3::Spi3Bus), Active-Low BUSY |
Hardware Note for EXT3-1 Extension Boards: Ensure the J3 jumper is OPEN ($10,\mu\text{H}$ inductor path) for panels $\le 3.7"$ (e.g. 2.66" and 2.9" panels). If J3 is closed ($47,\mu\text{H}$ path), the DC-DC booster chokes during current bursts, causing voltage sags and BUSY pin hangs.
Tri-Color panels and partial refresh
Colour panels have no fast/differential waveform. The red (or yellow) pigment is a
heavier particle that needs the full OTP waveform to migrate, so every update on a
Tri-Color or Quad-Color panel takes seconds — roughly 14 s on the GDEM0154Z90.
Ssd168xRefreshMode::Partial drives UPDATE_DISPLAY_CTRL2 = 0xFC, selecting the
controller's built-in fast LUT. That LUT only exists for monochrome panels. On a colour
panel it is not a speed-up and actively breaks the image: the update runs at full-refresh
speed anyway, and because the fast path only rewrites the Black/White RAM, all red content
is dropped. Keep colour panels on Ssd168xRefreshMode::Full.
Region-limited updates still work on colour panels — narrow the RAM window with
set_window/set_cursor, write both colour channels for that region, then refresh on
the Full waveform. Only the windowed area is redrawn, but it costs a full refresh. This
mirrors GxEPD2's GxEPD2_154_Z90c, where partial_refresh_time == full_refresh_time and
hasFastPartialUpdate == false.
For genuine sub-second differential updates, use a monochrome panel: GDEM0213B74
(Ssd1680RefreshMode::Partial), GDEY037T03 (Uc8253RefreshMode::FastPartial),
GDEQ0426T82 (Ssd1677RefreshMode::Partial), or the Pervasive Displays panels via
PervasiveRefreshMode::Fast and write_fast_frame.
Quick Start
Add epdsi to your Cargo.toml:
[]
= "0.1.0"
= "0.8"
Cargo features
| Feature | Default | Description |
|---|---|---|
graphics |
yes | Implements embedded-graphics-core's DrawTarget and Dimensions for PageBuffer. Disable it to drop the embedded-graphics-core dependency; PageBuffer and render_paged still work, you just draw into the buffer yourself. |
defmt |
no | Derives defmt::Format on the public error and mode enums (EpdBusError, Spi3BusError, PervasiveBwryOtpError, ColorMode, ColorChannel, SevenColor, and the per-controller refresh/variant enums) for logging on embedded targets. |
The minimum supported Rust version is 1.75.
The snippets below are abridged; for complete flashable programs see Examples on real hardware.
1. Usage Example (SSD1681 Controller + GDEM0154Z90 Panel)
use *;
use ;
// Initialize SPI bus wrapper and controller
let epd_bus = new;
let controller = new;
// Build driver orchestrator
let mut epd = new.build;
// Initialize display
epd.init.unwrap;
// Clear RAM channels
epd.clear_frame.unwrap;
epd.clear_frame.unwrap;
// Render graphics using PageBuffer
let mut bw_buf = ;
let mut display = new;
new
.into_styled
.draw
.unwrap;
// Send frame and refresh display
epd.write_frame.unwrap;
epd.refresh.unwrap;
2. Usage Example (JD79661 Controller + 2.13" Quad-Color Panel)
use *;
// Initialize SPI bus wrapper and JD79661 controller
let epd_bus = new;
let controller = new;
// Build driver for Adafruit 6373 Quad-Color 122x250 display
let mut epd = new.build;
epd.init.unwrap;
// Send 2bpp packed QuadColor frame buffer (8,000 bytes for 128x250 hardware RAM)
epd.write_frame.unwrap;
epd.refresh.unwrap;
3. Usage Example (PervasiveBwController + E2266KS0C1 Panel)
use *;
// Initialize SPI bus wrapper and Pervasive Displays controller
let epd_bus = new;
let controller = new;
// Build driver for Pervasive Displays 2.66" 152x296 Monochrome panel
let mut epd = new.build;
epd.init.unwrap;
// Clear RAM channel and refresh
epd.clear_frame.unwrap;
epd.refresh.unwrap;
epd.sleep.unwrap;
4. Usage Example (ED2208 Controller + GDEP073E01 Spectra 6 Panel)
use *;
// Initialize SPI bus wrapper and ED2208 controller
let epd_bus = new;
let controller = new;
// Build driver for 7.3" 800x480 Spectra 6 EPD display (e.g. Seeed reTerminal E1002)
let mut epd = new.build;
epd.init.unwrap;
// Clear display frame buffer (fill with White, 0x11)
epd.clear_frame.unwrap;
// Send 4bpp packed 7-color frame buffer (192,000 bytes for 800x480)
epd.write_frame.unwrap;
epd.refresh.unwrap;
5. Usage Example (Ssd1680Controller + GDEM0213B74 Panel)
use *;
// Initialize SPI bus wrapper and SSD1680(Z) controller
let epd_bus = new;
let controller = new
.with_refresh_mode;
// Build driver for Adafruit 6383 2.13" Monochrome display
let mut epd = new.build;
epd.init.unwrap;
epd.clear_frame.unwrap;
epd.refresh.unwrap;
epd.sleep.unwrap;
6. Usage Example (Uc8253Controller + GDEY037T03 Panel)
use *;
// Initialize SPI bus wrapper and UC8253 controller (note: this panel's BUSY pin is active-low)
let epd_bus = new;
let controller = new
.with_refresh_mode;
// Build driver for Adafruit 6395 3.7" Monochrome display
let mut epd = new.build;
epd.init.unwrap;
epd.clear_frame.unwrap;
epd.refresh.unwrap;
epd.sleep.unwrap;
7. Usage Example (Ssd1677Controller + GDEQ0426T82 Panel)
use *;
// Initialize SPI bus wrapper and SSD1677 controller
let epd_bus = new;
let controller = new;
// Build driver for Seeed Studio 6398 4.26" Monochrome display
let mut epd = new.build;
epd.init.unwrap;
epd.clear_frame.unwrap;
epd.refresh.unwrap;
epd.sleep.unwrap;
8. Usage Example (PervasiveBwryController + E2154QS0F1 Panel)
use *;
// The BWRY OTP register read is a bit-banged 3-wire handshake (SCK + a single bidirectional
// DATA line), NOT the hardware SPI peripheral — the panel drives its response back on MOSI, and
// MISO is never used. `sck`/`mosi` must start as plain GPIO here (not SPI-function-bound) so
// `read_otp` can flip `mosi`'s direction; `mosi` must implement `epdsi::bus3::DynamicPin`.
let mut controller = new
.with_variant
.with_temperature;
let mut bus3 = new;
controller.read_otp.unwrap;
let = bus3.release;
// Reconfigure sck_pin/mosi_pin into the hardware SPI peripheral's function, build the SPI
// device, then wrap it with the normal 4-wire SpiBusWrapper for everything else.
let epd_bus = new;
let mut epd = new.build;
epd.init.unwrap;
// Send 2bpp packed BWRY frame buffer (5,776 bytes for the 152x152 panel)
epd.write_frame.unwrap;
epd.refresh.unwrap;
epd.sleep.unwrap;
Examples on real hardware
The snippets above are rust,ignore because they need real SPI and GPIO. For complete,
flashable programs covering every supported controller, see:
rust-rpico2-discovery— RP2350 Pico 2,rp-halrust-reterminal-e1002-examples— Seeed reTerminal E1002 (XIAO ESP32-S3), Embassy +esp-hal
| Example | Controller | Panel | Board |
|---|---|---|---|
ssd1681_gdem0154z90_epd.rs |
Ssd1681Controller |
GDEM0154Z90 — 1.54" Tri-Color |
RP2350 |
ssd1680_gdem0213b74_epd.rs |
Ssd1680Controller |
GDEM0213B74 — 2.13" Mono |
RP2350 |
jd79661_zjy122250_epd.rs |
Jd79661Controller |
ZJY122250_0213AJH_E5 — 2.13" Quad-Color |
RP2350 |
uc8253_gdey037t03_epd.rs |
Uc8253Controller |
GDEY037T03 — 3.7" Mono |
RP2350 |
ssd1677_gdeq0426t82_epd.rs |
Ssd1677Controller |
GDEQ0426T82 — 4.26" Mono |
RP2350 |
pdi_e2266ks0c1.rs |
PervasiveBwController (Driver C) |
E2266KS0C1 — 2.66" Mono |
RP2350 |
pdi_e2290ks0f1.rs |
PervasiveBwController (Driver F) |
E2290KS0F1 — 2.90" Mono |
RP2350 |
pdi_e2154qs0f1.rs |
PervasiveBwryController (Driver F) |
E2154QS0F1 — 1.54" Spectra-4 |
RP2350 |
pdi_e2417qs0a3.rs |
PervasiveBwryController (Driver A) |
E2417QS0A3 — 4.2" Spectra-4 |
RP2350 |
epd_ed2208_demo.rs |
Ed2208Controller |
GDEP073E01 — 7.3" ACeP |
ESP32-S3 |
epd_ed2208_bmp.rs |
Ed2208Controller |
GDEP073E01 — 7.3" ACeP, BMP rendering |
ESP32-S3 |
Between the two repos every supported controller has a working example, across both Cortex-M (RP2350) and Xtensa (ESP32-S3) hosts.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.