Driving E-Paper Displays with ESP32 and Arduino: SPI Wiring, GxEPD2, and Partial Refresh
E-paper (electrophoretic) displays show up in a lot of maker projects that OLEDs and TFTs are a poor fit for — weather stations, price tags, name badges, e-ink photo frames, and any battery-powered display that needs to stay visible for weeks without power. The catch is that e-paper doesn't behave like any other display technology makers are used to: it's bistable (the image stays without power), it refreshes slowly, and getting a clean picture out of one means understanding partial refresh, ghosting, and look-up tables (LUTs) that don't apply to LCD or OLED panels at all. This guide covers wiring a raw SPI e-paper panel to an ESP32 or Arduino, driving it with the GxEPD2 library, and the tricks needed to get fast, clean partial updates without wrecking the panel.
How E-Paper Actually Works
Unlike an LCD or OLED, an e-paper panel is made of millions of microcapsules containing charged black and white (or colored) particles suspended in fluid. Applying a voltage moves the particles to the top or bottom of each capsule, and once moved, they stay there with zero power draw — that's the "bistable" property that makes e-paper ideal for battery projects. The tradeoff is speed: a full refresh takes anywhere from 1 to 5 seconds depending on panel size and resolution, and every full refresh flashes the screen through several black/white inversions to fully clear old image data from the capsules before drawing the new one.
Choosing a Panel
Almost all hobbyist e-paper panels come from a small number of panel manufacturers (mostly Good Display and E Ink Corp itself) and get re-sold under various brands — Waveshare, DFRobot, Adafruit, and generic AliExpress listings. What actually matters for wiring and code is the driver IC on the panel's flex cable, not the reseller brand.
Driver ICCommon Panel SizesNotes SSD1680 / SSD16811.54" – 2.13"Very common on small Waveshare modules, supports fast partial refresh UC8151 / UC8151D2.13" – 2.9"Found on many budget panels, good partial refresh support IL3820 / IL03731.54" – 4.2"Older but well-supported driver, three-color (black/white/red) variants common UC8179 / GDEW-series4.2" – 7.5"Larger panels, slower full refresh (3–5s), some support fast partial mode ACeP (color e-paper)5.65" – 7.3"6-color panels, no partial refresh support, full refresh only, refresh takes 15–30sFor a first project, stick to a monochrome or black/white/red panel in the 2.13"–4.2" range with SSD1680 or UC8151 — they're cheap, well-documented, and support fast partial refresh, which matters a lot for anything you'll update more than once a day.
Wiring an SPI E-Paper Panel
Almost every raw e-paper breakout uses the same seven-wire SPI interface. This is the standard pinout you'll see on Waveshare and clone boards:
Panel PinFunctionESP32 Pin (example)Arduino Uno Pin VCC3.3V power3V33.3V GNDGroundGNDGND DINSPI MOSIGPIO23D11 CLKSPI SCKGPIO18D13 CSChip selectGPIO5D10 DCData/command selectGPIO17D9 RSTReset (active low)GPIO16D8 BUSYPanel busy flag (input)GPIO4D7The BUSY line is the one people forget: it's an input from the panel telling your microcontroller when a refresh operation is still in progress. GxEPD2 polls this pin automatically, but if you're writing your own driver code, ignoring BUSY and sending commands too fast is the most common cause of a garbled or half-updated screen.
Setting Up GxEPD2
GxEPD2 (by ZinggJM) is the de facto standard Arduino/ESP32 library for driving SPI e-paper panels and supports nearly every driver IC on the market. Install it through the Arduino IDE Library Manager, along with its dependency, Adafruit GFX.
Panel selection happens at compile time through a template class matching your exact panel — for example, a 2.13" SSD1680 panel is instantiated as:
GxEPD2_BW<GxEPD2_213_B74, GxEPD2_213_B74::HEIGHT> display(GxEPD2_213_B74(/*CS=*/5, /*DC=*/17, /*RST=*/16, /*BUSY=*/4));Getting this class name wrong (there are dozens of near-identical 2.13" panel variants with different LUTs) is the single most common source of "screen shows garbage" bug reports for this library — check the exact model number printed on your panel's flex cable against the GxEPD2 wiki panel list before assuming your wiring is the problem.
Partial Refresh and Ghosting
Ghosting — faint traces of the previous image bleeding through — is the defining annoyance of e-paper development, and it's caused by how partial refresh works. A full refresh fully cycles every pixel's particles through black and white several times, guaranteeing a clean slate. A partial refresh skips that cycling and only moves the particles that need to change, which is much faster (200–500ms vs. 1–3s) but leaves residual particles that weren't fully repositioned, building up visible ghosting over repeated partial updates.
The standard mitigation: call display.setFullWindow() and do a full refresh every 10–20 partial updates (GxEPD2's display.displayWindow() handles partial regions). For a clock or dashboard that updates every minute, that means a full clearing refresh roughly every 10–20 minutes. Many panels also expose a manufacturer LUT specifically tuned for fast partial refresh with less ghosting — GxEPD2 selects this automatically for supported panels when you call display.setPartialWindow().
Power and Deep Sleep
The bistable nature of e-paper is what makes it a great match for ESP32 deep sleep projects: the display holds its image with the ESP32 fully powered down, drawing effectively zero current, and only wakes to update. Always call display.hibernate() after a refresh completes — this puts the panel's own driver IC into its lowest power state (a few microamps) rather than leaving it in an active-but-idle state that can draw several hundred microamps continuously. Combined with ESP32 deep sleep between updates, a battery-powered e-paper weather display or price tag can realistically run for 6–12 months on a single 18650 cell, refreshing once or twice a day.
Common Problems
SymptomLikely CauseFix Screen stays blank/whiteWrong GxEPD2 panel class, or BUSY/RST swappedVerify exact panel model against GxEPD2 wiki; double-check BUSY is wired as input, not left floating Heavy ghosting after a few updatesToo many consecutive partial refreshesForce a full refresh (setFullWindow) periodically Image inverted or mirroredWrong rotation settingCall display.setRotation() with 0–3 to match orientation Refresh takes 5+ seconds even for small updatesUsing setFullWindow() everywhereSwitch to setPartialWindow() for the region that actually changed Panel draws current even after code finishesMissing hibernate() callAlways call display.hibernate() before entering deep sleepA Note on Handling
The glass and flex cable on small e-paper panels are genuinely fragile — the ribbon connector where the flex meets the PCB is the most common failure point, and it doesn't tolerate repeated flexing or a tight enclosure fit. Mount the panel with the flex cable in a gentle, unstressed curve, and avoid designing an enclosure that requires bending it sharply to close.
Once the wiring and refresh logic are sorted, e-paper opens up a category of projects that just don't work well with any other display: shelf-edge labels, always-on dashboards, e-ink photo frames, and outdoor signage that needs to survive on a coin cell or small solar panel for months at a time. The GxEPD2 examples folder is worth working through panel-by-panel before starting a custom layout — it covers fonts, bitmaps, and both refresh modes for essentially every panel on the market.
Related Guides
- I2C vs SPI vs UART: How to Choose and Use Serial Communication Protocols
- Shift Registers and I/O Expanders for Arduino and ESP32: 74HC595, MCP23017, and PCF8574 Explained
- How to Program Addressable LED Strips: WS2812B Patterns, Effects, and Power Design
- How to Use Sensors with Arduino and ESP32: Temperature, Distance, Load, Current, and Hall Effect
- How to Control Motors with Arduino and ESP32: Stepper, DC, and Servo Drivers
- ESP32: Setting Up for Arduino IDE