DEV Community

Cover image for Writing your own e-paper panel or controller in Rust with epdsi
Mariusz Jurgielewicz
Mariusz Jurgielewicz

Posted on Edited on

Writing your own e-paper panel or controller in Rust with epdsi

I have a Good Display GDEY0266T90H, a 2.66" black-and-white panel with an SSD1685 controller. GxEPD2 has no class for it, so I wrote one.

GxEPD2 lets you keep a custom panel class directly inside your sketch, and GxEPD2_BW accepts it like any built-in driver. The working class and demo sketch live in xiao-mg24-arduino-examples. Here is how that C++ driver works, and how epdsi, my no_std Rust framework, handles the same hardware.

Adapting a GxEPD2 panel class

The class header explains the lineage:

// Derived from GxEPD2_290_GDEY029T71H (same SSD1685 command set) by Jean-Marc Zingg,
// https://github.com/ZinggJM/GxEPD2 - GxEPD2 has no GDEY0266T90H class.
// Init and update values follow Good Display's AU-GDEY0266T90H-2FP-20230915 sample.
Enter fullscreen mode Exit fullscreen mode

Find the GxEPD2 panel that shares the controller (the 2.9" GDEY029T71H), copy the class, update the dimensions, and pull init and refresh tables from Good Display's reference code. GxEPD2 makes this fast because every panel implements an identical class interface. Credit for that clean structure belongs to Jean-Marc Zingg.

I wired the panel to a XIAO MG24 through the Seeed ePaper Driver Board v2. Two hardware quirks surfaced immediately:

  • Good Display's sample uses data entry mode 0x01 with a reversed y window. GxEPD2 rendered images mirrored because vendor demos assume pre-flipped bitmap assets. Switching to 0x03 (matching the 2.9" class) fixed orientation.
  • The panel exposes 184 source outputs, but power-on defaults to 200. The extra 16 lines show up as a noise band until you set bits 7 and 6 of the second Display Update Control byte to 01.

Panel separation in epdsi

The GxEPD2 class spans 22 public methods and 9 private helpers. epdsi separates this work into two traits: a panel (a zero-sized type declaring width, height, and color format) and a controller (seven hardware operations). Both traits are public, meaning you can implement them in an external crate and pass them straight into EpdBuilder.

In practice, adding hardware fits three situations:

  • Supported IC: declare the panel struct with dimensions and color mode, then reuse the existing controller.
  • New IC: implement both EpdController and EpdPanel.
  • Shipped IC with a custom init sequence: wrap the controller and forward unchanged methods.

That third approach uses the wrapper pattern from Does Rust Support Inheritance? Yes, No, and Maybe, All in the Same File. Ssd1680Controller and Ssd1681Controller work this way, acting as thin wrappers over a shared Ssd168xController.

The blocking/async feature trap (E0053)

EpdController switches between sync and async implementations based on epdsi's blocking feature. Your crate must mirror that flag using a feature named exactly blocking:

[dependencies]
epdsi = { version = "0.7", default-features = false }
embedded-hal = "1.0"
embedded-hal-async = "1.0"
maybe-async-cfg = "0.2"

[features]
default = ["blocking"]
blocking = ["epdsi/blocking"]   # the name must be `blocking`
Enter fullscreen mode Exit fullscreen mode

Any other feature name triggers compiler error E0053: "method init_sequence has an incompatible type for trait ... expected Result<..>, found future". Because the rustc diagnostic does not mention feature flags, you end up inspecting type signatures instead of Cargo.toml.

Hardware checks before applying power

Most driver bring-up failures stem from physical bus behavior rather than incorrect register values. Check these items on a scope or logic analyzer first:

  • Reset and BUSY polarity. An inverted wait_busy either blocks the CPU permanently or exits instantly without drawing.
  • Reset pulse duration. Start with the datasheet minimum, then check vendor sample delays. A pulse right at the datasheet floor latched intermittently on one of my test boards.
  • Pixel bit packing and row stride alignment.
  • Charge pump power state. Some controllers cut pump power after a refresh, dropping any subsequent refresh commands until you reissue PON.
  • Deep sleep wake-up. Many controllers ignore software SPI wake commands once asleep and require an active-low toggle on the physical reset line.

Next, transcribe the vendor demo's byte sequence, cross-reference it against the register definitions in the datasheet, and mark undocumented bytes as vendor-specific. Write a targeted test that records every SPI transaction alongside the DC pin level to lock the sequence into source control.

GDEY0266T90H support in epdsi

The SSD1685 is on my roadmap. The GxEPD2 demo confirms it needs only a minor variation of the existing SSD168x controller, and I will update the guide once physical testing finishes.

I add panels only after testing physical hardware on the bench. Datasheets omit too many controller quirks to write dependable drivers blind. I buy panels when they become available and occasionally accept donated hardware if documentation and provenance are clear. The panel support policy covers the criteria.

The full guide, including a dual-mode controller implementation, lives in the epdsi README and will publish to docs.rs with the next release.

Top comments (0)