Documentation

2.9" E-Paper E-Ink Display Module 296x128 Black/White SPI for Arduino, Raspberry Pi & ESP32 | ShillehTek Product Manual
Documentation / 2.9" E-Paper E-Ink Display Module 296x128 Black/White SPI for Arduino, Raspberry Pi & ESP32 | ShillehTek Product Manual

2.9" E-Paper E-Ink Display Module 296x128 Black/White SPI for Arduino, Raspberry Pi & ESP32 | ShillehTek Product Manual

manualshillehtek

Overview

E-paper is the display technology that keeps showing your content with the power off. This 2.9" module packs 296x128 black-and-white pixels of it: microcapsules of charged pigment that the controller physically rearranges, then leaves in place — zero current to hold an image, readable in direct sunlight, with the soft paper-like look that makes badges, labels, and dashboards feel finished rather than blinking.

The interface is standard 4-line SPI plus three control signals: CS, CLK (SCK), DIN (MOSI), DC (data/command), RST (reset), and BUSY (the display's "still refreshing, hold on" flag). The BS jumper on the back selects 3-line vs 4-line SPI — leave it at 0 (4-line), which is what every mainstream library expects. The board runs from 3.3V or 5V and level-shifts appropriately, so it wires directly to everything from an Uno to a Pico.

The mental model that makes e-paper pleasant: it's a poster, not a video screen. Full refreshes take ~2 seconds with the characteristic black/white flash; partial refresh (supported on this panel) updates a region in a fraction of that for clocks and counters. Perfect fits: weather stations and calendar dashboards updated every few minutes, shelf labels and name badges, battery devices that sleep for hours between updates — anywhere "always readable, almost never powered" wins.

At a Glance

Display
2.9" e-ink, 296x128
Colors
Black / white
Interface
SPI + DC/RST/BUSY
Supply Voltage
3.3V / 5V
Hold Power
0 — image persists off
Refresh
~2 s full, partial supported

Specifications

Parameter Value
Panel 2.9 inch e-ink, 296 x 128 pixels, black/white
Viewing Reflective — no backlight, sunlight readable, ~180° angle
Interface SPI up to 4 MHz + DC, RST, BUSY handshake
BS Jumper 0 = 4-line SPI (default, use this), 1 = 3-line SPI
Supply Voltage 3.3 - 5V (onboard regulation and level shifting)
Refresh Current ~8 mA during refresh; ~0 idle (deep sleep ~1 uA)
Full Refresh ~2 s with black/white flashing (normal and required periodically)
Partial Refresh Supported — fast region updates without full flash
Operating Conditions 0 - 50°C, 35 - 65% RH (indoor-class panel)
Pins BUSY, RST, DC, CS, CLK, DIN, GND, VCC
Libraries GxEPD2 (Arduino/ESP32), Waveshare epd Python (Pi), Waveshare Pico examples

Pinout Diagram

The rear view shows everything you touch: the 8-pin cable connector (BUSY, RST, DC, CS, CLK, DIN, GND, VCC), the BS interface-select jumper with its table (keep it on 0), and the FPC that runs to the glass. The module ships with a color-coded 8-wire cable matching these labels.

2.9 inch e-Paper module rear view pinout showing BUSY RST DC CS CLK DIN GND VCC pins and BS jumper interface table

Wiring Guide

Arduino Wiring

e-Paper Pin Arduino Pin
VCC / GND 5V / GND
DIN D11 (MOSI)
CLK D13 (SCK)
CS D10
DC D9
RST D8
BUSY D7

ESP32 Wiring

e-Paper Pin ESP32 Pin Details
VCC / GND 3V3 / GND
DIN GPIO 23 VSPI MOSI
CLK GPIO 18 VSPI SCK
CS GPIO 5
DC GPIO 17
RST GPIO 16
BUSY GPIO 4 Input

Raspberry Pi Wiring (standard Waveshare mapping)

e-Paper Pin Pi Pin Details
VCC / GND Pin 1 (3.3V) / Pin 6
DIN Pin 19 (GPIO 10, MOSI)
CLK Pin 23 (GPIO 11, SCLK)
CS Pin 24 (GPIO 8, CE0)
DC Pin 22 (GPIO 25)
RST Pin 11 (GPIO 17)
BUSY Pin 18 (GPIO 24)
Tip: Enable SPI in raspi-config. This is the exact mapping Waveshare's Python examples assume, so their demos run unmodified.

Raspberry Pi Pico Wiring

e-Paper Pin Pico Pin Details
VCC / GND 3V3(OUT) / GND
DIN GP11 (SPI1 TX)
CLK GP10 (SPI1 SCK)
CS GP9
DC GP8
RST GP12
BUSY GP13 Input

Code Examples

Arduino/ESP32 use GxEPD2 (install "GxEPD2" plus "Adafruit GFX"). The Pi uses Waveshare's epd library from their e-Paper repo. The Pico sketch shows the MicroPython pattern with Waveshare's Pico driver file.

Arduino

epaper29_arduino.ino
// 2.9" e-Paper 296x128 - Arduino Example (GxEPD2)
// CS->10, DC->9, RST->8, BUSY->7, DIN->11, CLK->13
// Libraries: GxEPD2 + Adafruit GFX

#include <GxEPD2_BW.h>
#include <Fonts/FreeSansBold12pt7b.h>

// V2 2.9" panel; if your screen stays blank try GxEPD2_290 instead
GxEPD2_BW<GxEPD2_290_T94_V2, GxEPD2_290_T94_V2::HEIGHT>
  display(GxEPD2_290_T94_V2(10, 9, 8, 7));   // CS, DC, RST, BUSY

void setup() {
  display.init(115200);
  display.setRotation(1);                    // landscape 296x128
  display.setFont(&FreeSansBold12pt7b);
  display.setTextColor(GxEPD_BLACK);

  display.setFullWindow();
  display.firstPage();
  do {
    display.fillScreen(GxEPD_WHITE);
    display.setCursor(10, 40);
    display.print("ShillehTek");
    display.setCursor(10, 75);
    display.print("2.9\" e-Paper");
    display.drawRect(10, 95, 276, 20, GxEPD_BLACK);
    display.fillRect(10, 95, 180, 20, GxEPD_BLACK);   // fake gauge
  } while (display.nextPage());

  display.hibernate();                       // ~0 power, image stays
}

void loop() {}

ESP32 (Arduino IDE, partial refresh clock)

epaper29_esp32.ino
// 2.9" e-Paper - ESP32 partial-refresh counter (GxEPD2)
// CS->5, DC->17, RST->16, BUSY->4, DIN->23, CLK->18

#include <GxEPD2_BW.h>
#include <Fonts/FreeMonoBold18pt7b.h>

GxEPD2_BW<GxEPD2_290_T94_V2, GxEPD2_290_T94_V2::HEIGHT>
  display(GxEPD2_290_T94_V2(5, 17, 16, 4));

int counter = 0;

void setup() {
  display.init(115200);
  display.setRotation(1);
  display.setFont(&FreeMonoBold18pt7b);
  display.setTextColor(GxEPD_BLACK);

  display.setFullWindow();          // one full refresh at boot
  display.firstPage();
  do {
    display.fillScreen(GxEPD_WHITE);
    display.setCursor(20, 50);
    display.print("Counter:");
  } while (display.nextPage());
}

void loop() {
  // fast partial update of just the number area
  display.setPartialWindow(20, 70, 200, 40);
  display.firstPage();
  do {
    display.fillScreen(GxEPD_WHITE);
    display.setCursor(20, 100);
    display.print(counter);
  } while (display.nextPage());

  counter++;
  delay(3000);
  if (counter % 20 == 0) display.refresh();   // periodic full clean
}

Raspberry Pi (Python, Waveshare library)

epaper29_rpi.py
#!/usr/bin/env python3
# 2.9" e-Paper - Raspberry Pi Example (Waveshare epd library)
# Setup once:
#   git clone https://github.com/waveshareteam/e-Paper
#   cd e-Paper/RaspberryPi_JetsonNano/python && sudo python3 setup.py install
#   pip3 install pillow

from waveshare_epd import epd2in9_V2
from PIL import Image, ImageDraw, ImageFont
import time

epd = epd2in9_V2.EPD()
epd.init()
epd.Clear(0xFF)

font_big = ImageFont.truetype(
    "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 28)
font_small = ImageFont.truetype(
    "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 16)

# landscape canvas: width=296, height=128
image = Image.new("1", (epd.height, epd.width), 255)
draw = ImageDraw.Draw(image)

draw.text((10, 8), "ShillehTek", font=font_big, fill=0)
draw.text((10, 48), "2.9 inch e-Paper on Raspberry Pi", font=font_small, fill=0)
draw.text((10, 72), time.strftime("%Y-%m-%d %H:%M"), font=font_small, fill=0)
draw.rectangle((10, 100, 286, 118), outline=0)
draw.rectangle((10, 100, 190, 118), fill=0)

epd.display(epd.getbuffer(image))
epd.sleep()          # deep sleep - image remains
print("Displayed and sleeping.")

Raspberry Pi Pico (MicroPython)

epaper29_pico.py
# 2.9" e-Paper - Pico MicroPython Example
# Driver: copy Pico_ePaper-2.9.py from Waveshare's Pico_ePaper_Code repo
#   to the Pico as epaper29.py (it contains class EPD_2in9)
# Wiring here matches the tab: CS=9, DC=8, RST=12, BUSY=13, SCK=10, MOSI=11

from epaper29 import EPD_2in9
import time

epd = EPD_2in9()          # driver sets up SPI1 + pins
epd.Clear(0xff)

# The driver exposes a framebuf: draw with standard framebuf calls
epd.fill(0xff)
epd.text("ShillehTek", 8, 8, 0x00)
epd.text("e-Paper on Pico", 8, 24, 0x00)
epd.text("No power needed", 8, 40, 0x00)
epd.text("to keep this text!", 8, 56, 0x00)
epd.rect(8, 76, 120, 12, 0x00)
epd.fill_rect(8, 76, 80, 12, 0x00)

epd.display(epd.buffer)
time.sleep(2)
epd.sleep()               # image persists with zero power
print("Displayed and sleeping.")

Frequently Asked Questions

The screen stays blank (or BUSY never releases). What do I check?
First the BS jumper — it must be on 0 for the 4-line SPI every library uses. Then the panel class/driver name: 2.9" panels exist in several controller revisions, and initializing a V2 panel with a V1 driver (or vice versa) gives exactly "blank + endless BUSY." In GxEPD2 try GxEPD2_290, _290_T94, or _290_T94_V2; in Python try epd2in9 vs epd2in9_V2. Finally verify DC/RST/BUSY wiring — SPI alone isn't enough.
Why does it flash black-white-black during updates?
That's the full-refresh waveform doing ink hygiene: driving all particles hard in both directions erases ghosting and keeps contrast crisp. It's normal, necessary, and worth keeping — use partial refresh for frequent small updates, but let the panel do a real full refresh every dozen-or-so partials (the ESP32 example does) or ghost images gradually accumulate.
How fast can I update it? Can it play animations?
Full refresh ~2 s; partial refresh a few hundred ms on a small window. That's clocks, counters, tickers — not video. If you need motion, this is the wrong technology; if you need a display that sips microamps and reads like print, nothing else comes close. Design for update-then-sleep and the panel will feel instant in practice.
Does the image really survive with power removed? For how long?
Yes — the pigment particles are physically parked and stay put for weeks to months; badge and shelf-label uses rely on it. Contrast fades very slowly and sunlight/heat accelerate that, so refresh daily-ish for critical text. Always call the driver's sleep()/hibernate() before cutting power — parking the panel cleanly prevents faint artifacts.
Can I leave it powered and refreshing every few seconds forever?
E-ink longevity is measured in refresh cycles (hundreds of thousands to millions), so a 5-second refresh loop burns through them faster than needed and stresses the panel. Waveshare's guidance: no faster than every ~3 minutes for routine full refreshes in always-on service, partial refreshes in moderation between. For a clock, partial-update the minutes and full-refresh hourly — the panel will outlive the project.
Ghost images / previous content shows through. Fix?
Run a full refresh (or two) — Clear() then redraw — and ghosts vanish. Prevent them by mixing full refreshes into partial-update workflows, avoiding leaving high-contrast static content for months, and not driving the panel outside 0-50°C. Persistent ghosting after clears usually means the wrong driver revision's waveform is being used.
How do I show images and nicer fonts?
On the Pi, Pillow gives you TTF fonts and image conversion — dither photos to 1-bit and push them (the example's pattern). On Arduino/ESP32, GxEPD2 rides Adafruit GFX: custom fonts via the font converter and bitmaps via drawBitmap() from PROGMEM arrays (image2cpp converts PNGs). 296x128 is a comfortable canvas for a two-column dashboard with icons.

Related Tutorials