Skip to main content

Arduino Cookbook: ePaper Display (reTerminal E Series)

Looking for the hardware peripherals?

This page focuses on driving the ePaper screen from Arduino. If you want to use the onboard LED, buzzer, buttons, SHT4x sensor, battery monitor, or microSD card slot, head over to Arduino Cookbook: Onboard Peripherals. For RTC, low-power modes, and the onboard microphone, see Arduino Cookbook: RTC, Low Power & Audio.

The shared boilerplate — Arduino IDE setup, ESP32 board package, installing Seeed_GFX, generating driver.h — also lives in Work with Arduino. Skim that first if you are new to Arduino on Seeed ePaper.

Introduction

The reTerminal E Series is Seeed Studio's industrial HMI line, built on the XIAO ESP32-S3 and featuring integrated ePaper displays. This cookbook walks through everything you need to render text, graphics, and images on the screen:

  • Hardware overview & buying links for E1001 / E1002 / E1003 / E1004.
  • Arduino IDE environment setup for all four models (XIAO_ESP32S3 board, OPI PSRAM).
  • A first Hello World on each model using the Seeed_GFX library (with the matching BOARD_SCREEN_COMBO).
  • Panel-specific advanced examples with Seeed_GFX — 4-level grayscale on E1001 and 16-level grayscale on E1003.
  • An alternative Hello World using the popular GxEPD2 library.
  • Troubleshooting tips for ePaper refresh issues and upload failures.

Materials Required

To complete this tutorial, please prepare one of the following reTerminal E Series devices:

reTerminal E1001reTerminal E1002reTerminal E1003reTerminal E1004

Environmental Preparation

To program reTerminal E Series ePaper Display with Arduino, you'll need to set up the Arduino IDE with ESP32 support.

tip

If this is your first time using Arduino, we highly recommend you to refer to Getting Started with Arduino.

Arduino IDE Setup

Step 1. Download and install the Arduino IDE and launch the Arduino application.


Step 2. Add ESP32 board support to Arduino IDE.

In Arduino IDE, go to File > Preferences and add the following URL to the "Additional Boards Manager URLs" field:

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

Step 3. Install ESP32 board package.

Navigate to Tools > Board > Boards Manager, search for "esp32" and install the ESP32 package by Espressif Systems.

Step 4. Select the correct board.

Go to Tools > Board > ESP32 Arduino and select XIAO_ESP32S3.

Step 5. Connect your reTerminal E Series ePaper Display to your computer using a USB-C cable.

Step 6. Select the correct port from Tools > Port.

ePaper Display Programming

The reTerminal E1001 features a 7.5-inch black and white ePaper display, while the reTerminal E1002 is equipped with a 7.3-inch full color ePaper display. Both displays provide excellent visibility in various lighting conditions with ultra-low power consumption, making them ideal for industrial applications that require always-on displays with minimal power usage.

Using the Seeed_GFX Library

To control the ePaper display, we'll use the Seeed_GFX library, which provides comprehensive support for various Seeed Studio display devices.

Step 1. Download the Seeed_GFX library from GitHub:


Step 2. Install the library by adding the ZIP file in Arduino IDE. Go to Sketch > Include Library > Add .ZIP Library and select the downloaded ZIP file.

note

If you have previously installed the TFT_eSPI library, you may need to temporarily remove or rename it from your Arduino libraries folder to avoid conflicts, as Seeed_GFX is a fork of TFT_eSPI with additional features for Seeed Studio displays.

Programming reTerminal E1001 (7.5-inch Black & White ePaper)

Let's explore a simple example that demonstrates basic drawing operations on the black and white ePaper display.

Step 1. Open the example sketch from the Seeed_GFX library: File > Examples > Seeed_GFX > ePaper > Basic > HelloWorld

Step 2. Enable OPI PSRAM in the Arduino IDE: Tools > PSRAM > OPI PSRAM

Step 3. Create a new file named driver.h in the same folder as your sketch. You can do this by clicking the arrow button in the Arduino IDE and selecting "New Tab", then naming it driver.h.

Step 4. Copy the generated configuration code and paste it into the driver.h file. The code should look like this:

#define BOARD_SCREEN_COMBO 520 // reTerminal E1001 (UC8179)

Step 5. Upload the sketch to your reTerminal E1001. You should see the display showing various graphics including lines, text, and shapes demonstrating the basic drawing capabilities.

Multi-Level Grayscale with Seeed_GFX

The Hello World sketches above are intentionally minimal so they fit on every model. The monochrome panels on E1001 and E1003 actually support multi-level grayscale on top of plain black-and-white — 4 levels on E1001 and 16 levels on E1003 — and Seeed_GFX exposes both modes through epaper.initGrayMode(...) plus a set of TFT_GRAY_* palette constants. The two examples below walk through each.

4-Level Grayscale on reTerminal E1001

The reTerminal E1001's 7.5" monochrome panel can render 4 levels of grayscale instead of pure black and white. Seeed_GFX exposes this through epaper.initGrayMode(GRAY_LEVEL4) and four palette constants:

ConstantRendered Shade
TFT_GRAY_0Black
TFT_GRAY_1Dark gray
TFT_GRAY_2Light gray
TFT_GRAY_3White

The example below first paints four horizontal stripes — one per gray level — so you can visually verify the palette, then blits an 800×480 grayscale bitmap onto the screen. The Seeed_GFX library already ships this as a ready-to-flash example, including the pre-converted image.h, so you don't need to generate any bitmap data yourself.

Step 1. Open the example sketch from the Seeed_GFX library: File > Examples > Seeed_GFX > ePaper > Gray > GrayLevel4. The sketch and its accompanying image.h will open in the editor.

Step 2. Enable OPI PSRAM in the Arduino IDE: Tools > PSRAM > OPI PSRAM.

Step 3. Add a driver.h file alongside the example (same workflow as the Hello World) and select the E1001 board–screen combo:

#define BOARD_SCREEN_COMBO 520 // reTerminal E1001 (UC8179)

Step 4. Upload the sketch. The display first shows four grayscale stripes — black at the top, then dark gray, light gray, and white at the bottom — and then clears and renders the bitmap from image.h.

For reference, the example sketch looks like this:

/*
* 4-Level Grayscale demo for reTerminal E1001
* (Seeed_GFX/examples/ePaper/Gray/Shape/Shape.ino)
*
* The 7.5" monochrome panel supports 4 gray levels:
* TFT_GRAY_0 black
* TFT_GRAY_1 dark gray
* TFT_GRAY_2 light gray
* TFT_GRAY_3 white
*/
#include "TFT_eSPI.h"
#include "image.h"

#ifdef EPAPER_ENABLE // Defined when an ePaper combo is selected in driver.h
EPaper epaper;
#endif

void setup() {
#ifdef EPAPER_ENABLE
epaper.begin();
epaper.fillScreen(TFT_WHITE);
epaper.update();
epaper.initGrayMode(GRAY_LEVEL4);

// Draw four horizontal stripes, one per gray level
epaper.fillRect(0, 0, epaper.width(), epaper.height() / 4, TFT_GRAY_0);
epaper.fillRect(0, epaper.height() * 1 / 4, epaper.width(), epaper.height() / 4, TFT_GRAY_1);
epaper.fillRect(0, epaper.height() * 2 / 4, epaper.width(), epaper.height() / 4, TFT_GRAY_2);
epaper.fillRect(0, epaper.height() * 3 / 4, epaper.width(), epaper.height() / 4, TFT_GRAY_3);
epaper.update();

// Then clear and show a 800x480 grayscale bitmap from image.h
epaper.fillScreen(TFT_GRAY_3);
epaper.pushImage(0, 0, 800, 480, (uint16_t *)L4_GRAY);
epaper.update();
#endif
}

void loop() {
// Nothing to do — ePaper holds the last frame without power
}
Want to use your own image?

The L4_GRAY array in image.h is just an 800×480 grayscale bitmap pre-converted to a C array. To swap in your own picture, regenerate the array from a 800×480 grayscale source using any standard "image to C array" converter and replace L4_GRAY in image.h. The sketch itself does not need to change.

tip

4-level grayscale refresh is roughly 4× slower than a 1-bit black-and-white update because the controller drives every pixel through four target voltages instead of two. Use it for static content like photos, illustrations, or detail-heavy dashboards, and stick to standard 1-bit mode for fast UI updates.

Using the GxEPD2 Library

Besides Seeed_GFX, you can also use the GxEPD2 library to drive the reTerminal's ePaper display. Seeed has forked the popular GxEPD2 library and added dedicated support for the reTerminal E10xx series, making it the recommended choice for reTerminal users.

Installing the Seeed_GxEPD2 Library

To use this library with reTerminal products, you need to install Seeed_GxEPD2 — Seeed's custom fork specifically adapted for the reTerminal E10xx series.

Step 1. Go to the Seeed_GxEPD2 GitHub repository. Click the "Code" button and then select "Download ZIP" to save the library to your computer.


Step 2. In the Arduino IDE, install the library from the downloaded file. Navigate to Sketch > Include Library > Add .ZIP Library... and select the ZIP file you just downloaded.

Step 3. The Seeed_GxEPD2 library requires the Adafruit GFX Library to function, which you must also install. The easiest way to do this is through the Library Manager: go to Tools > Manage Libraries..., search for "Adafruit GFX Library", and click "Install".

note

Seeed_GxEPD2 is Seeed's custom fork of the original GxEPD2 library, with dedicated drivers and optimizations for the reTerminal E10xx series. We strongly recommend using this fork instead of the upstream library to ensure full compatibility with your reTerminal device.

Programming reTerminal E1001 (7.5" Black & White Screen)

The reTerminal E1001 features a 7.5" black and white ePaper display (800×480, GDEY075T7 panel, UC8179 controller). The example below demonstrates multiple screens including a splash, system info, typography, geometry, patterns, and a dashboard layout.

After installing the Seeed_GxEPD2 library, you can find this example in the Arduino IDE via File > Examples > Seeed_GxEPD2 > GxEPD2_reTerminal_E1001, or locate it manually at Seeed_GxEPD2/examples/GxEPD2_reTerminal_E1001/GxEPD2_reTerminal_E1001.ino.

Click here to view the full code
#include <SPI.h>
#include <GxEPD2_BW.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>
#include <Fonts/FreeSansBold18pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeMono9pt7b.h>

// ===== Pin mapping =====
#define EPD_SCK_PIN 7
#define EPD_MOSI_PIN 9
#define EPD_CS_PIN 10
#define EPD_DC_PIN 11
#define EPD_RES_PIN 12
#define EPD_BUSY_PIN 13

SPIClass hspi(HSPI);

// ===== Display: 7.5" B&W 800x480 =====
#define MAX_DISPLAY_BUFFER_SIZE 16000u
#define MAX_HEIGHT(EPD) \
(EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) \
? EPD::HEIGHT \
: MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))

GxEPD2_BW<GxEPD2_750_GDEY075T7, MAX_HEIGHT(GxEPD2_750_GDEY075T7)>
display(GxEPD2_750_GDEY075T7(EPD_CS_PIN, EPD_DC_PIN, EPD_RES_PIN, EPD_BUSY_PIN));

void setup()
{
Serial.begin(115200);
delay(200);
Serial.println(F("[E1001] GxEPD2 reTerminal E1001 Demo (7.5\" B&W)"));

pinMode(EPD_RES_PIN, OUTPUT);
pinMode(EPD_DC_PIN, OUTPUT);
pinMode(EPD_CS_PIN, OUTPUT);

hspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);
display.epd2.selectSPI(hspi, SPISettings(2000000, MSBFIRST, SPI_MODE0));
display.init(0);

Serial.println(F("[E1001] Screen 1: Splash"));
showSplashScreen();
delay(3000);

Serial.println(F("[E1001] Screen 2: System Info"));
showSystemInfo();
delay(3000);

Serial.println(F("[E1001] Screen 3: Typography"));
showTypographyDemo();
delay(3000);

Serial.println(F("[E1001] Screen 4: Geometry"));
showGeometryDemo();
delay(3000);

Serial.println(F("[E1001] Screen 5: Patterns"));
showPatternDemo();
delay(3000);

Serial.println(F("[E1001] Screen 6: Dashboard"));
showDashboardDemo();

Serial.println(F("[E1001] Demo complete. Hibernating."));
delay(2000);
display.hibernate();
}

void loop() {}

// =====================================================================
void drawCenteredText(const char* text, int16_t y, const GFXfont* font)
{
display.setFont(font);
int16_t tbx, tby; uint16_t tbw, tbh;
display.getTextBounds(text, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor((display.width() - tbw) / 2 - tbx, y);
display.print(text);
}

// =====================================================================
// Screen 1: Splash
// =====================================================================
void showSplashScreen()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.drawRect(10, 10, W - 20, H - 20, GxEPD_BLACK);
display.drawRect(14, 14, W - 28, H - 28, GxEPD_BLACK);

display.setTextColor(GxEPD_BLACK);
drawCenteredText("reTerminal E1001", H / 2 - 60, &FreeSansBold24pt7b);
drawCenteredText("7.5\" e-Paper Display", H / 2 - 10, &FreeSansBold12pt7b);
display.drawFastHLine(W / 4, H / 2 + 10, W / 2, GxEPD_BLACK);
drawCenteredText("GxEPD2 + UC8179 Driver Demo", H / 2 + 45, &FreeSansBold12pt7b);
drawCenteredText("800 x 480 pixels | Black & White", H / 2 + 75, &FreeSans9pt7b);
drawCenteredText("Seeed Studio x GxEPD2", H - 40, &FreeSans9pt7b);
} while (display.nextPage());
}

// =====================================================================
// Screen 2: System Info
// =====================================================================
void showSystemInfo()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, W, 40, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
drawCenteredText("System Information", 30, &FreeSansBold12pt7b);

display.setTextColor(GxEPD_BLACK);
display.setFont(&FreeMonoBold9pt7b);

const char* labels[] = {"MCU", "Display", "Panel", "Controller", "Interface", "Color Depth"};
char chipBuf[48];
snprintf(chipBuf, sizeof(chipBuf), "ESP32-S3 @ %lu MHz", (unsigned long)ESP.getCpuFreqMHz());
const char* values[] = {chipBuf, "800 x 480", "GDEY075T7", "UC8179", "SPI (HSPI) @ 2MHz", "B&W (1-bit)"};

int y = 95;
for (int i = 0; i < 6; i++) {
display.setCursor(50, y);
display.print(labels[i]);
display.setCursor(250, y);
display.print(": ");
display.print(values[i]);
y += 48;
if (i < 5) display.drawFastHLine(50, y - 18, W - 100, GxEPD_BLACK);
}

display.drawFastHLine(30, H - 40, W - 60, GxEPD_BLACK);
drawCenteredText("reTerminal E1001 | GxEPD2 Demo", H - 20, &FreeSans9pt7b);
} while (display.nextPage());
}

// =====================================================================
// Screen 3: Typography
// =====================================================================
void showTypographyDemo()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, W, 40, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
drawCenteredText("Typography Demo", 30, &FreeSansBold12pt7b);

display.setTextColor(GxEPD_BLACK);
int y = 85, x = 40;

display.setFont(&FreeSansBold24pt7b);
display.setCursor(x, y); display.print("Sans Bold 24pt");
y += 65;

display.setFont(&FreeSansBold18pt7b);
display.setCursor(x, y); display.print("Sans Bold 18pt");
y += 50;

display.setFont(&FreeSansBold12pt7b);
display.setCursor(x, y); display.print("Sans Bold 12pt - Clean and Modern");
y += 42;

display.setFont(&FreeSans9pt7b);
display.setCursor(x, y); display.print("Sans 9pt - Body text for dense info display.");
y += 40;

display.drawFastHLine(x, y, W - 80, GxEPD_BLACK);
y += 25;

display.setFont(&FreeMonoBold12pt7b);
display.setCursor(x, y); display.print("Mono Bold 12pt");
y += 38;

display.setFont(&FreeMono9pt7b);
display.setCursor(x, y); display.print("Mono 9pt: 0123456789 ABCDEF");
y += 38;

display.setFont(&FreeSansBold18pt7b);
display.setCursor(x, y); display.print("0 1 2 3 4 5 6 7 8 9");

drawCenteredText("Multiple fonts supported", H - 20, &FreeSans9pt7b);
} while (display.nextPage());
}

// =====================================================================
// Screen 4: Geometry
// =====================================================================
void showGeometryDemo()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, W, 40, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
drawCenteredText("Geometry Demo", 30, &FreeSansBold12pt7b);
display.setTextColor(GxEPD_BLACK);

// Rectangles
for (int i = 0; i < 4; i++)
display.drawRect(40 + i * 80, 60, 60, 50, GxEPD_BLACK);
display.fillRect(40 + 4 * 80, 60, 60, 50, GxEPD_BLACK);

// Circles
for (int i = 0; i < 4; i++)
display.drawCircle(70 + i * 90, 170, 25 + i * 3, GxEPD_BLACK);
for (int i = 0; i < 3; i++)
display.fillCircle(70 + (i + 4) * 90, 170, 25 + i * 3, GxEPD_BLACK);

// Triangles
for (int i = 0; i < 5; i++) {
int tx = 50 + i * 120, sz = 40 + i * 5;
display.drawTriangle(tx, 270 + sz, tx + sz / 2, 270, tx + sz, 270 + sz, GxEPD_BLACK);
}

// Fan of lines
int fcx = 150, fcy = 410;
for (int a = 0; a < 180; a += 12) {
float rad = a * 3.14159f / 180.0f;
display.drawLine(fcx, fcy, fcx + (int)(60 * cosf(rad)), fcy - (int)(60 * sinf(rad)), GxEPD_BLACK);
}

// Concentric circles
for (int r = 8; r <= 56; r += 8)
display.drawCircle(400, 400, r, GxEPD_BLACK);

// Rounded rects
for (int i = 0; i < 3; i++)
display.drawRoundRect(550 + i * 8, 340 + i * 8, 120 - i * 16, 100 - i * 16, 8 + i * 4, GxEPD_BLACK);

drawCenteredText("Adafruit GFX primitives on 7.5\" e-Paper", H - 15, &FreeSans9pt7b);
} while (display.nextPage());
}

// =====================================================================
// Screen 5: Patterns
// =====================================================================
void showPatternDemo()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, W, 40, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
drawCenteredText("Pattern Demo", 30, &FreeSansBold12pt7b);
display.setTextColor(GxEPD_BLACK);
display.setFont(&FreeSans9pt7b);

int bx = 30, by = 55, bw = 150, bh = 150, gap = 30;

// Checkerboard
display.setCursor(bx + 25, by + 15); display.print("Checker");
by += 20;
for (int py = 0; py < bh / 12; py++)
for (int px = 0; px < bw / 12; px++)
if ((px + py) & 1)
display.fillRect(bx + px * 12, by + py * 12, 12, 12, GxEPD_BLACK);

// H-stripes
int bx2 = bx + bw + gap; int by2 = by - 20;
display.setCursor(bx2 + 25, by2 + 15); display.print("H-Stripes");
by2 += 20;
for (int py = 0; py < bh; py += 8)
if ((py / 8) & 1)
display.fillRect(bx2, by2 + py, bw, 8, GxEPD_BLACK);

// V-stripes
int bx3 = bx2 + bw + gap; int by3 = by2 - 20;
display.setCursor(bx3 + 25, by3 + 15); display.print("V-Stripes");
by3 += 20;
for (int px = 0; px < bw; px += 8)
if ((px / 8) & 1)
display.fillRect(bx3 + px, by3, 8, bh, GxEPD_BLACK);

// Dot grid
int bx4 = bx3 + bw + gap; int by4 = by3 - 20;
display.setCursor(bx4 + 30, by4 + 15); display.print("Dot Grid");
by4 += 20;
for (int py = 0; py < bh; py += 12)
for (int px = 0; px < bw; px += 12)
display.fillCircle(bx4 + px + 6, by4 + py + 6, 3, GxEPD_BLACK);

// Dither gradient (bottom)
int gx = 30, gy = 310, gw = W - 60, gh = 120;
display.setFont(&FreeSans9pt7b);
display.setCursor(gx, gy - 5); display.print("Dither Gradient:");
display.drawRect(gx, gy, gw, gh, GxEPD_BLACK);
for (int py = 0; py < gh; py++)
for (int px = 0; px < gw; px++) {
int density = (px * 255) / gw;
if ((((px * 7 + py * 13) ^ (px * py)) & 0xFF) < density)
display.drawPixel(gx + px, gy + py, GxEPD_BLACK);
}

drawCenteredText("Fill patterns and dithering", H - 15, &FreeSans9pt7b);
} while (display.nextPage());
}

// =====================================================================
// Screen 6: Dashboard
// =====================================================================
void drawCard(int x, int y, int w, int h, const char* title, const char* value, const char* unit)
{
display.drawRoundRect(x, y, w, h, 6, GxEPD_BLACK);
display.fillRoundRect(x + 2, y + 2, w - 4, 26, 4, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
display.setFont(&FreeSansBold12pt7b);
int16_t tbx, tby; uint16_t tbw, tbh;
display.getTextBounds(title, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor(x + (w - tbw) / 2 - tbx, y + 22);
display.print(title);

display.setTextColor(GxEPD_BLACK);
display.setFont(&FreeSansBold18pt7b);
display.getTextBounds(value, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor(x + (w - tbw) / 2 - tbx, y + h / 2 + 12);
display.print(value);

display.setFont(&FreeSans9pt7b);
display.getTextBounds(unit, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor(x + (w - tbw) / 2 - tbx, y + h - 10);
display.print(unit);
}

void showDashboardDemo()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, W, 40, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
drawCenteredText("Dashboard Demo", 30, &FreeSansBold12pt7b);
display.setTextColor(GxEPD_BLACK);

int cw = 170, ch = 130, gap = 20;
int sx = (W - 4 * cw - 3 * gap) / 2;
int row1Y = 60;

char uptBuf[16]; snprintf(uptBuf, sizeof(uptBuf), "%lu", millis() / 1000);
char heapBuf[16]; snprintf(heapBuf, sizeof(heapBuf), "%lu", (unsigned long)(ESP.getFreeHeap() / 1024));

drawCard(sx, row1Y, cw, ch, "Temp", "23.5", "Celsius");
drawCard(sx + cw + gap, row1Y, cw, ch, "Humidity", "65", "% RH");
drawCard(sx + 2 * (cw + gap), row1Y, cw, ch, "Heap", heapBuf, "kB free");
drawCard(sx + 3 * (cw + gap), row1Y, cw, ch, "Uptime", uptBuf, "seconds");

// Log area
int logY = row1Y + ch + 20;
display.drawRoundRect(sx, logY, W - 2 * sx, 200, 6, GxEPD_BLACK);
display.fillRoundRect(sx + 2, logY + 2, W - 2 * sx - 4, 26, 4, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
display.setFont(&FreeSansBold12pt7b);
display.setCursor(sx + 15, logY + 22);
display.print("Activity Log");
display.setTextColor(GxEPD_BLACK);
display.setFont(&FreeMono9pt7b);

const char* logs[] = {
"[00:01] System boot - ESP32-S3",
"[00:02] Panel: GDEY075T7 800x480",
"[00:03] UC8179 controller ready",
"[00:04] SPI @ 2MHz (HSPI)",
"[00:05] Demo sequence started",
};
int ly = logY + 50;
for (int i = 0; i < 5; i++) {
display.setCursor(sx + 15, ly);
display.print(logs[i]);
ly += 28;
}

// Progress bar
int barY = logY + 210;
display.setFont(&FreeSansBold12pt7b);
display.setCursor(sx, barY + 15);
display.print("Progress:");
int barX = sx + 170, barW = W - 2 * sx - 180, barH = 20;
display.drawRect(barX, barY, barW, barH, GxEPD_BLACK);
display.fillRect(barX + 2, barY + 2, barW - 4, barH - 4, GxEPD_BLACK);
display.setFont(&FreeSans9pt7b);
display.setCursor(barX + barW + 8, barY + 14);
display.print("100%");

drawCenteredText("E-paper: zero power to maintain image", H - 15, &FreeSans9pt7b);
} while (display.nextPage());
}

The following figure shows the actual display effect of the E1001 example:

Using the GxEPD2 Library for Multi-Level Grayscale Display

In addition to the standard black-and-white and 6-color modes, some reTerminal displays support multi-level grayscale rendering. The Seeed_GxEPD2 library includes dedicated grayscale examples that bypass the normal GxEPD2_BW / GxEPD2_7C drivers and instead drive the display controller directly using custom LUT waveforms, while still leveraging Adafruit_GFX for drawing.

  • reTerminal E1001 (7.5" B&W): The UC8179 controller supports a 4-level grayscale mode via specialized VCOM/WW/KW/WK/KK LUT tables. A 2bpp (96 KB) framebuffer is used, and each pixel can be Black, Dark Gray, Light Gray, or White.
  • reTerminal E1003 (10.3" Monochrome): The IT8951 controller natively supports 16-level grayscale via its GC16 waveform mode. A 4bpp (~1.25 MB) framebuffer is allocated in PSRAM, and each pixel can render one of 16 gray levels.

Programming reTerminal E1001 — 4-Level Grayscale

The E1001's UC8179 controller can be switched from its normal 1-bit mode into a 4-level grayscale mode by uploading custom LUT tables (VCOM, LUTWW, LUTKW, LUTWK, LUTKK). This example creates a Gray4Canvas (2bpp, 96 KB) and uses Adafruit_GFX for drawing, then uploads two bit-planes to the controller for grayscale rendering.

After installing the Seeed_GxEPD2 library, you can find this example in the Arduino IDE via File > Examples > Seeed_GxEPD2 > GxEPD2_reTerminal_E1001_Gray4, or locate it manually at Seeed_GxEPD2/examples/GxEPD2_reTerminal_E1001_Gray4/GxEPD2_reTerminal_E1001_Gray4.ino.

Click here to view the full code
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#include <Fonts/FreeSansBold18pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSans9pt7b.h>

// ===== Pin mapping (E1001) =====
#define EPD_SCK_PIN 7
#define EPD_MOSI_PIN 9
#define EPD_CS_PIN 10
#define EPD_DC_PIN 11
#define EPD_RES_PIN 12
#define EPD_BUSY_PIN 13

#define EPD_W 800
#define EPD_H 480

SPIClass hspi(HSPI);
SPISettings spiSet(2000000, MSBFIRST, SPI_MODE0);

// Gray levels
#define G_BLACK 0
#define G_DARK_GRAY 1
#define G_LIGHT_GRAY 2
#define G_WHITE 3

// UC8179 gray LUT tables (verbatim from Seeed_GFX UC8179_Defines.h)
// Each LUT is 7 phases x 6 bytes = 42 bytes.
static const uint8_t LUT_VCOM_GRAY[] = {
0x00,0x00,0x06,0x08,0x07,0x01,
0x00,0x06,0x0A,0x0B,0x0A,0x01,
0x00,0x03,0x03,0x00,0x00,0x03,
0x00,0x05,0x09,0x06,0x06,0x01,
0x00,0x02,0x02,0x0A,0x0A,0x01,
0x00,0x0A,0x11,0x06,0x07,0x01,
0x00,0x02,0x01,0x02,0x01,0x01,
};

static const uint8_t LUT_WW_GRAY[] = {
0x15,0x00,0x06,0x08,0x07,0x01,
0x54,0x06,0x0A,0x0B,0x0A,0x01,
0x90,0x03,0x03,0x00,0x00,0x03,
0x2A,0x05,0x09,0x06,0x06,0x01,
0xAA,0x02,0x02,0x0A,0x0A,0x01,
0x00,0x0A,0x11,0x06,0x07,0x01,
0x28,0x02,0x01,0x02,0x01,0x01,
};

static const uint8_t LUT_KW_GRAY[] = {
0x2A,0x00,0x06,0x08,0x07,0x01,
0x59,0x06,0x0A,0x0B,0x0A,0x01,
0x90,0x03,0x03,0x00,0x00,0x03,
0x5A,0x05,0x09,0x06,0x06,0x01,
0xA8,0x02,0x02,0x0A,0x0A,0x01,
0x45,0x0A,0x11,0x06,0x07,0x01,
0xA8,0x02,0x01,0x02,0x01,0x01,
};

static const uint8_t LUT_WK_GRAY[] = {
0x16,0x00,0x06,0x08,0x07,0x01,
0xA0,0x06,0x0A,0x0B,0x0A,0x01,
0x90,0x03,0x03,0x00,0x00,0x03,
0x99,0x05,0x09,0x06,0x06,0x01,
0xA0,0x02,0x02,0x0A,0x0A,0x01,
0x40,0x0A,0x11,0x06,0x07,0x01,
0x20,0x02,0x01,0x02,0x01,0x01,
};

static const uint8_t LUT_KK_GRAY[] = {
0x26,0x00,0x06,0x08,0x07,0x01,
0x6A,0x06,0x0A,0x0B,0x0A,0x01,
0x90,0x03,0x03,0x00,0x00,0x03,
0x65,0x05,0x09,0x06,0x06,0x01,
0x50,0x02,0x02,0x0A,0x0A,0x01,
0x10,0x0A,0x11,0x06,0x07,0x01,
0x10,0x02,0x01,0x02,0x01,0x01,
};

static const uint8_t CMD_USER_GRAY[] = {
0x17, 0x3F, 0x3F, 0x07, 0x06, 0x12,
};

// ============================================================
// 4-level grayscale canvas (2bpp)
// ============================================================
class Gray4Canvas : public Adafruit_GFX
{
public:
Gray4Canvas(uint16_t w, uint16_t h) : Adafruit_GFX(w, h), _buf(nullptr) {}

bool begin()
{
uint32_t sz = uint32_t(_width) * _height / 4;
_buf = (uint8_t*)malloc(sz);
if (_buf) memset(_buf, 0xFF, sz); // fill white (0b11 = 3 = white)
return _buf != nullptr;
}

void drawPixel(int16_t x, int16_t y, uint16_t color) override
{
if (!_buf) return;
if (x < 0 || x >= width() || y < 0 || y >= height()) return;

int16_t rx = x, ry = y;
switch (getRotation()) {
case 1: rx = _width - 1 - y; ry = x; break;
case 2: rx = _width - 1 - x; ry = _height - 1 - y; break;
case 3: rx = y; ry = _height - 1 - x; break;
}

uint8_t g = color & 0x03;
uint32_t idx = uint32_t(ry) * (_width / 4) + rx / 4;
uint8_t shift = (3 - (rx & 3)) * 2;
_buf[idx] = (_buf[idx] & ~(0x03 << shift)) | (g << shift);
}

void fillScreen(uint16_t color) override
{
if (!_buf) return;
uint8_t g = color & 0x03;
uint8_t fill = (g << 6) | (g << 4) | (g << 2) | g;
memset(_buf, fill, uint32_t(_width) * _height / 4);
}

uint8_t getPixel(int16_t x, int16_t y) const
{
if (!_buf || x < 0 || x >= _width || y < 0 || y >= _height) return 0;
uint32_t idx = uint32_t(y) * (_width / 4) + x / 4;
uint8_t shift = (3 - (x & 3)) * 2;
return (_buf[idx] >> shift) & 0x03;
}

uint8_t* buffer() { return _buf; }

private:
uint8_t* _buf;
};

Gray4Canvas canvas(EPD_W, EPD_H);

// ============================================================
// UC8179 SPI helpers
// ============================================================
void checkBusy() {
delay(10);
while (!digitalRead(EPD_BUSY_PIN)) delay(10);
}

void writeCommand(uint8_t cmd) {
hspi.beginTransaction(spiSet);
digitalWrite(EPD_DC_PIN, LOW);
digitalWrite(EPD_CS_PIN, LOW);
hspi.transfer(cmd);
digitalWrite(EPD_CS_PIN, HIGH);
digitalWrite(EPD_DC_PIN, HIGH);
hspi.endTransaction();
}

void writeData(uint8_t data) {
hspi.beginTransaction(spiSet);
digitalWrite(EPD_CS_PIN, LOW);
hspi.transfer(data);
digitalWrite(EPD_CS_PIN, HIGH);
hspi.endTransaction();
}

void writeLUT(uint8_t cmd, const uint8_t* lut, uint16_t len) {
writeCommand(cmd);
for (uint16_t i = 0; i < len; i++) writeData(lut[i]);
}

// ============================================================
// UC8179 gray mode initialization
// ============================================================
void initGrayMode()
{
// Hardware reset
digitalWrite(EPD_RES_PIN, LOW); delay(10);
digitalWrite(EPD_RES_PIN, HIGH); delay(10);
checkBusy();

// Power setting (0x01)
writeCommand(0x01);
writeData(0x07);
writeData(CMD_USER_GRAY[0]);
writeData(CMD_USER_GRAY[1]);
writeData(CMD_USER_GRAY[2]);
writeData(CMD_USER_GRAY[3]);

// PLL (0x30)
writeCommand(0x30);
writeData(CMD_USER_GRAY[4]);

// VCOM DC (0x82)
writeCommand(0x82);
writeData(CMD_USER_GRAY[5]);

// Booster (0x06)
writeCommand(0x06);
writeData(0x27);
writeData(0x27);
writeData(0x28);
writeData(0x17);

// Power ON (0x04)
writeCommand(0x04);
delay(100);
checkBusy();

// Panel Setting (0x00)
writeCommand(0x00);
writeData(0x3F);

// Power saving (0xE3)
writeCommand(0xE3);
writeData(0x88);

// VCOM and Data interval (0x50)
writeCommand(0x50);
writeData(0x10);
writeData(0x07);

// PLL setting (0x52)
writeCommand(0x52);
writeData(0x00);

// Resolution (0x61)
writeCommand(0x61);
writeData(EPD_W >> 8);
writeData(EPD_W & 0xFF);
writeData(EPD_H >> 8);
writeData(EPD_H & 0xFF);

// Write LUTs for gray mode. CRITICAL ordering — UC8179:
// 0x20 = LUTC (VCOM) ← must be present
// 0x21 = LUTWW (W -> W)
// 0x22 = LUTKW (K -> W)
// 0x23 = LUTWK (W -> K)
// 0x24 = LUTKK (K -> K)
writeLUT(0x20, LUT_VCOM_GRAY, sizeof(LUT_VCOM_GRAY));
checkBusy();
writeLUT(0x21, LUT_WW_GRAY, sizeof(LUT_WW_GRAY));
checkBusy();
writeLUT(0x22, LUT_KW_GRAY, sizeof(LUT_KW_GRAY));
checkBusy();
writeLUT(0x23, LUT_WK_GRAY, sizeof(LUT_WK_GRAY));
writeLUT(0x24, LUT_KK_GRAY, sizeof(LUT_KK_GRAY));

Serial.println(F("[Gray4] UC8179 gray mode init done"));
}

// ============================================================
// Upload 2bpp canvas to UC8179 as two bit-planes.
// ============================================================
void uploadGray4Frame()
{
const uint32_t bytesPerRow = EPD_W / 4; // 200 bytes (2bpp, 4 pixels/byte)

// ---- Bit plane → DTM1 (command 0x10) ----
writeCommand(0x10);
hspi.beginTransaction(spiSet);
digitalWrite(EPD_CS_PIN, LOW);
for (uint16_t row = 0; row < EPD_H; row++) {
const uint8_t* rp = canvas.buffer() + uint32_t(row) * bytesPerRow;
for (uint16_t col8 = 0; col8 < EPD_W / 8; col8++) {
uint8_t out = 0;
for (uint8_t bit = 0; bit < 8; bit++) {
uint16_t px = col8 * 8 + bit;
uint32_t idx = px / 4;
uint8_t shift = (3 - (px & 3)) * 2;
uint8_t gray = 3 - ((rp[idx] >> shift) & 0x03); // INVERT
if (gray & 0x01) out |= (0x80 >> bit);
}
hspi.transfer(out);
}
}
digitalWrite(EPD_CS_PIN, HIGH);
hspi.endTransaction();

// ---- Bit plane → DTM2 (command 0x13) ----
writeCommand(0x13);
hspi.beginTransaction(spiSet);
digitalWrite(EPD_CS_PIN, LOW);
for (uint16_t row = 0; row < EPD_H; row++) {
const uint8_t* rp = canvas.buffer() + uint32_t(row) * bytesPerRow;
for (uint16_t col8 = 0; col8 < EPD_W / 8; col8++) {
uint8_t out = 0;
for (uint8_t bit = 0; bit < 8; bit++) {
uint16_t px = col8 * 8 + bit;
uint32_t idx = px / 4;
uint8_t shift = (3 - (px & 3)) * 2;
uint8_t gray = 3 - ((rp[idx] >> shift) & 0x03); // INVERT
if (gray & 0x02) out |= (0x80 >> bit);
}
hspi.transfer(out);
}
}
digitalWrite(EPD_CS_PIN, HIGH);
hspi.endTransaction();

Serial.println(F("[Gray4] Frame uploaded (2 bit planes)"));
}

void refreshDisplay()
{
unsigned long t0 = millis();
writeCommand(0x12); // Display Refresh
delay(100);
checkBusy();
Serial.printf("[Gray4] Refresh %lu ms\n", millis() - t0);
}

void sleepDisplay()
{
writeCommand(0x02); // Power OFF
checkBusy();
writeCommand(0x07); // Deep sleep
writeData(0xA5);
}

// ============================================================
// Demo drawing
// ============================================================
void drawCenteredText(const char* text, int16_t y, const GFXfont* font, uint8_t gray)
{
canvas.setFont(font);
canvas.setTextColor(gray);
int16_t tbx, tby; uint16_t tbw, tbh;
canvas.getTextBounds(text, 0, 0, &tbx, &tby, &tbw, &tbh);
canvas.setCursor((EPD_W - tbw) / 2 - tbx, y);
canvas.print(text);
}

void showGrayscaleDemo()
{
Serial.println(F("[Gray4] Drawing demo..."));
canvas.fillScreen(G_WHITE);

// Title bar
canvas.fillRect(0, 0, EPD_W, 40, G_BLACK);
drawCenteredText("4-Level Grayscale Demo - E1001", 30, &FreeMonoBold12pt7b, G_WHITE);

// 4 large gray bands
int bandH = 70, startY = 55;
const char* labels[] = {"Gray 0: Black", "Gray 1: Dark Gray", "Gray 2: Light Gray", "Gray 3: White"};
for (int i = 0; i < 4; i++) {
int y = startY + i * (bandH + 8);
canvas.fillRect(30, y, EPD_W - 60, bandH, i);
uint8_t textGray = (i < 2) ? G_WHITE : G_BLACK;
canvas.setFont(&FreeSansBold12pt7b);
canvas.setTextColor(textGray);
canvas.setCursor(60, y + bandH / 2 + 8);
canvas.print(labels[i]);
}

// Concentric gray circles
int areaTop = startY + 4 * (bandH + 8);
int areaBot = EPD_H - 30;
int cy = (areaTop + areaBot) / 2;
int cx = EPD_W - 80;
canvas.setFont(&FreeMonoBold9pt7b);
canvas.setTextColor(G_BLACK);
canvas.setCursor(30, cy - 6);
canvas.print("Concentric gray circles");
canvas.setCursor(30, cy + 14);
canvas.print("(black / dark / light / white)");
canvas.fillCircle(cx, cy, 38, G_BLACK);
canvas.fillCircle(cx, cy, 28, G_DARK_GRAY);
canvas.fillCircle(cx, cy, 18, G_LIGHT_GRAY);
canvas.fillCircle(cx, cy, 9, G_WHITE);

// Footer
canvas.fillRect(0, EPD_H - 30, EPD_W, 30, G_BLACK);
drawCenteredText("UC8179 4-gray LUT mode | E1001", EPD_H - 8, &FreeMonoBold9pt7b, G_WHITE);

Serial.println(F("[Gray4] Uploading..."));
uploadGray4Frame();
refreshDisplay();
Serial.println(F("[Gray4] Done."));
}

// ============================================================
void setup()
{
Serial.begin(115200);
delay(200);
Serial.println(F("[E1001 Gray4] Starting..."));

pinMode(EPD_CS_PIN, OUTPUT); digitalWrite(EPD_CS_PIN, HIGH);
pinMode(EPD_DC_PIN, OUTPUT); digitalWrite(EPD_DC_PIN, HIGH);
pinMode(EPD_RES_PIN, OUTPUT); digitalWrite(EPD_RES_PIN, HIGH);
pinMode(EPD_BUSY_PIN, INPUT);

hspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);

if (!canvas.begin()) {
Serial.println(F("[Gray4] FATAL: alloc failed (96 KB)"));
while (true) delay(1000);
}
Serial.printf("[Gray4] Canvas OK (%lu bytes)\n", (unsigned long)(EPD_W * EPD_H / 4));

initGrayMode();
showGrayscaleDemo();
sleepDisplay();
}

void loop() {}

The following figure shows the actual display effect of the E1001 4-level grayscale example:

note

ePaper displays have a relatively slow refresh rate. Black & white displays (E1001, E1003) typically refresh in 1-3 seconds, while 6-color displays (E1002, E1004) can take 25-40 seconds for a full refresh. This is normal behavior and is a trade-off for the ultra-low power consumption and excellent visibility without backlighting.

Troubleshooting

Q1: Why does the reTerminal's ePaper display not show anything or refresh when running the code above?

This issue may occur if you have inserted a MicroSD card into the reTerminal. The reason is that the MicroSD card and the ePaper display share the same SPI bus on the reTerminal. If a MicroSD card is inserted but its enable (chip select) pin is not properly managed, it can cause a conflict on the SPI bus. Specifically, the MicroSD card may hold the BUSY line high, which prevents the ePaper display from functioning correctly—resulting in no display updates or refreshes.

// Initialize SD Card
pinMode(SD_EN_PIN, OUTPUT);
digitalWrite(SD_EN_PIN, HIGH);
pinMode(SD_DET_PIN, INPUT_PULLUP);

To resolve this, you must ensure that the MicroSD card is properly enabled using the code provided above. The code initializes and enables the MicroSD card by setting the correct pin states, which prevents SPI bus conflicts and allows both the SD card and the ePaper display to work together. Always use the recommended initialization code when using a MicroSD card with the reTerminal to avoid such issues.

If the MicroSD card is not used inside your project, we recommend powering down the device and removing the card before running the display program. If the card has been inserted into the reTerminal, you will need to add the above code to ensure that you can get the screen to display properly, regardless of whether you are using a MicroSD card or not.

Q2: Why can't I upload programs to reTerminal?

If you encounter the following error when uploading a program to reTerminal.

Then, it's likely that your Arduino IDE is set to an excessively high upload speed. Please change it to 115200 baud to resolve this issue.

Tech Support & Product Discussion

Thank you for choosing our products! We are here to provide you with different support to ensure that your experience with our products is as smooth as possible. We offer several communication channels to cater to different preferences and needs.

Loading Comments...