Community project

ESP32 E-Reader

ESP32
Photo of ESP32 E-Reader
Generated with AI

מנדי קניג

Published November 2, 2026

This project builds a functional e-reader interface on the ESP32 using a 5.76-inch e-paper display and five-direction navigation controls. The GooDisplay e-paper module provides crisp, low-power text and graphics rendering, while the DESPI-C02 adapter simplifies wiring to the microcontroller. A LiPo battery powers the entire system for portable reading sessions.

The guide provides a complete wiring diagram, parts list, and firmware with debounced button handling that runs independently of display refresh cycles. Assembly steps cover preparing the e-paper display, connecting the SPI interface and control lines, integrating the navigation button module, and safely connecting the battery. After following this guide, readers will have a working e-reader platform ready for custom book content and UI development.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

GooDisplay GDEH0576T81 e-paper display with DESPI-C02 adapter

The 5.76-inch black-and-white e-paper screen and adapter board that show your books.

1

Five-direction navigation button module

A small joystick-like button board with four directions, a centre press, and SET and RST buttons for controlling the reader.

1

LiPo 3.7V 1000mAh Battery

3.7 V, 1000 mAh

Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging.

Assemble it in 6 steps

1. Prepare the screen and adapter

With the board unplugged, slide the e-paper screen’s thin flex cable fully and straight into the DESPI-C02 adapter. Match the exposed metal contacts to the direction marked on the adapter, then close its latch gently if it has one.

  • If the cable needs force, remove it and check its direction.
  • Do not bend, crease, or pull the thin display flex cable; it can tear and permanently damage the screen.

2. Wire the e-paper adapter

Use short wires from the DESPI-C02 adapter to the labelled side headers on the LOLIN S3 Pro: adapter 3.3V → board 3V3 (power), adapter GND → board GND (ground), adapter SDI → GPIO11 (picture data), adapter SCK → GPIO12 (screen clock), adapter CS → GPIO10 (screen select), adapter D/C → GPIO15 (command or picture-data choice), adapter RES → GPIO16 (screen reset), and adapter BUSY → GPIO17 (screen-ready signal).

  • Use the printed labels beside the pins, not the order of pins.
  • Keep the wires short and do not connect the adapter 3.3V pin to 5V.
  • Make sure 3.3V and GND are not swapped — swapped power can damage the display adapter or screen.

3. Wire the navigation controls

Connect the button module COM → a board GND pin (ground). Then connect UP → GPIO1 (move up), DWN → GPIO2 (move down), LFT → GPIO18 (previous page), RHT → GPIO4 (next page), MID → GPIO5 (open selected book), SET → GPIO6 (change text size), and RST → GPIO7 (return to the book list).

  • Each button press connects its named pin to COM, so the COM-to-GND wire is required.
  • A quick tap counts exactly once.
  • Do not connect a module power pin to 5V; this project uses the button contacts and COM only.

4. No book card needed for this test

The button test does not read the microSD card. You can leave the card inserted or remove it while power is off.

5. Connect the battery safely

Check that the battery plug fits the LOLIN S3 Pro battery socket without force. Plug it in only with red on the positive contact and black on the negative contact; the board charges it when USB is connected.

  • If the plug shape or wire order does not match, use a correctly wired adapter lead rather than forcing it.
  • Reversed battery wiring can damage the board and make the battery dangerously hot. Never use a swollen, cut, or damaged battery.

6. Power and test the buttons

Check the wiring, plug the board into USB, and press Deploy. Wait for BUTTON TEST and READY on the screen. Press and release UP, DWN, LFT, RHT, MID, SET, and RST one at a time. The screen shows the last button name and press counts. Wait for each screen refresh before the next test. No books or microSD card are needed.

  • The e-paper screen flashes during refresh; this is normal.
  • Holding a button counts once. Release it before pressing again.
  • Use RST on the button module, not the board reset button.
  • Disconnect power before changing any wires.

Review all connections

1. Connections between "epaper_1" and "ESP32"

Functionepaper_1ESP32
power3.3V3V3
groundGNDGND
digitalBUSYGPIO 17
spiSDIGPIO 11
spiSCKGPIO 12
digitalD/CGPIO 15
digitalRESGPIO 16
digitalCSGPIO 10

2. Connections between "nav_button_1" and "ESP32"

Functionnav_button_1ESP32
groundCOMGND
digitalUPGPIO 1
digitalDWNGPIO 2
digitalRHTGPIO 4
digitalMIDGPIO 5
digitalSETGPIO 6
digitalRSTGPIO 7
digitalLFTGPIO 18

3. Connections between "battery_1" and "ESP32"

Functionbattery_1ESP32
power+V → WEMOS LOLIN S3 Pro battery socket positive contactEXT
groundGND → WEMOS LOLIN S3 Pro battery socket negative contactEXT

Deploy the firmware

#include <Arduino.h>
#include <SPI.h>
#include <GxEPD2_BW.h>
#include <gdeh/GxEPD2_576_GDEH0576T81.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>

// Existing DESPI-C02 wiring is unchanged.
constexpr int EPD_SDI = 11, EPD_SCK = 12, EPD_CS = 10;
constexpr int EPD_D_C = 15, EPD_RES = 16, EPD_BUSY = 17;
constexpr int SD_CS = 46;
const int buttonPins[] = {1, 2, 18, 4, 5, 6, 7};
const char* const buttonNames[] = {"UP", "DWN", "LFT", "RHT", "MID", "SET", "RST"};
GxEPD2_BW<GxEPD2_576_GDEH0576T81, GxEPD2_576_GDEH0576T81::HEIGHT>
display(GxEPD2_576_GDEH0576T81(EPD_CS, EPD_D_C, EPD_RES, EPD_BUSY));
QueueHandle_t presses;
uint32_t counts[7] = {};
uint32_t total = 0;

// Scan independently so a screen refresh does not hide button presses.
void scanButtons(void*)
{
  bool raw[7], stable[7];
  uint32_t changed[7];
  for (int i = 0; i < 7; ++i) { raw[i] = stable[i] = HIGH; changed[i] = millis(); }
  for (;;) {
    uint32_t now = millis();
    for (uint8_t i = 0; i < 7; ++i) {
      bool value = digitalRead(buttonPins[i]);
      if (value != raw[i]) { raw[i] = value; changed[i] = now; }
      if (value != stable[i] && now - changed[i] >= 30) {
        stable[i] = value;
        if (value == LOW) xQueueSend(presses, &i, 0);
      }
    }
    vTaskDelay(pdMS_TO_TICKS(5));
  }
}

void drawTest(int last)
{
  display.setFullWindow();
  display.firstPage();
  do {
    display.fillScreen(GxEPD_WHITE);
    display.setTextColor(GxEPD_BLACK);
    display.setTextSize(3);
    display.setCursor(24, 30);
    display.print("BUTTON TEST");
    display.setTextSize(2);
    display.setCursor(24, 80);
    display.print("Last button pressed:");
    display.setTextSize(7);
    display.setCursor(24, 120);
    display.print(last < 0 ? "READY" : buttonNames[last]);
    display.setTextSize(2);
    display.setCursor(24, 210);
    display.print("Total presses: "); display.print(total);
    for (int i = 0; i < 7; ++i) {
      display.setCursor(24, 260 + i * 36);
      display.print(buttonNames[i]);
      display.print("  GPIO"); display.print(buttonPins[i]);
      display.print("  count: "); display.print(counts[i]);
    }
    display.setCursor(24, 560);
    display.print("Press and release any module button.");
    display.setCursor(24, 590);
    display.print("Wait for refresh before the next test.");
    display.setCursor(24, 620);
    display.print("Module RST is a test button, not reset.");
  } while (display.nextPage());
}

void setup()
{
  for (int pin : buttonPins) pinMode(pin, INPUT_PULLUP);
  // Keep the unused onboard microSD deselected on the shared SPI bus.
  pinMode(SD_CS, OUTPUT); digitalWrite(SD_CS, HIGH);
  pinMode(EPD_CS, OUTPUT); digitalWrite(EPD_CS, HIGH);
  pinMode(EPD_RES, OUTPUT); digitalWrite(EPD_RES, HIGH);
  SPI.begin(EPD_SCK, 13, EPD_SDI, EPD_CS);
  display.init(115200);
  display.setRotation(1);
  presses = xQueueCreate(32, sizeof(uint8_t));
  if (!presses) { for (;;) delay(1000); }
  if (xTaskCreate(scanButtons, "buttons", 2048, nullptr, 1, nullptr) != pdPASS) {
    for (;;) delay(1000);
  }
  drawTest(-1);
}

void loop()
{
  uint8_t button;
  if (xQueueReceive(presses, &button, pdMS_TO_TICKS(50)) == pdTRUE) {
    ++counts[button]; ++total;
    drawTest(button);
  }
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik