Skip to main content

Arduino 菜谱:ePaper 显示屏(reTerminal E 系列)

在找硬件外设相关内容?

本页重点介绍如何在 Arduino 中驱动 ePaper 屏幕。如果你想使用板载 LED、蜂鸣器、按键、SHT4x 传感器、电池监测或 microSD 卡槽,请前往 Arduino 菜谱:板载外设。关于 RTC、低功耗模式和板载麦克风,请参见 Arduino 菜谱:RTC、低功耗与音频

通用的样板配置——Arduino IDE 设置、ESP32 开发板包、安装 Seeed_GFX、生成 driver.h——也都在 使用 Arduino 进行开发 中。如果你是第一次在 Seeed ePaper 上使用 Arduino,建议先快速浏览那一页。

介绍

reTerminal E 系列是 Seeed Studio 的工业 HMI 产品线,基于 XIAO ESP32-S3,集成了 ePaper 显示屏。本菜谱将带你完成在屏幕上渲染文本、图形和图像所需的一切步骤:

  • E1001 / E1002 / E1003 / E1004 的硬件概览与购买链接。
  • 四款型号通用的 Arduino IDE 环境配置(XIAO_ESP32S3 开发板、OPI PSRAM)。
  • 使用 Seeed_GFX 库在每个型号上实现第一个 Hello World(配合匹配的 BOARD_SCREEN_COMBO)。
  • 使用 Seeed_GFX 的面板专用进阶示例——E1001 上的 4 级灰度以及 E1003 上的 16 级灰度
  • 使用流行的 GxEPD2 库实现另一种 Hello World
  • 针对 ePaper 刷新问题和烧录失败的故障排查建议。

所需材料

要完成本教程,请准备以下任意一款 reTerminal E 系列设备:

reTerminal E1001reTerminal E1002reTerminal E1003reTerminal E1004

环境准备

要使用 Arduino 为 reTerminal E 系列 ePaper 显示屏编程,你需要在 Arduino IDE 中配置 ESP32 支持。

tip

如果这是你第一次使用 Arduino,我们强烈建议你参考 Arduino 入门指南

Arduino IDE 设置

步骤 1. 下载并安装 Arduino IDE,然后启动 Arduino 应用程序。


步骤 2. 在 Arduino IDE 中添加 ESP32 开发板支持。

在 Arduino IDE 中,依次点击 File > Preferences,并在 “Additional Boards Manager URLs” 字段中添加以下 URL:

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

步骤 3. 安装 ESP32 开发板包。

依次进入 Tools > Board > Boards Manager,搜索 “esp32”,并安装 Espressif Systems 提供的 ESP32 包。

步骤 4. 选择正确的开发板。

依次点击 Tools > Board > ESP32 Arduino,然后选择 XIAO_ESP32S3

步骤 5. 使用 USB-C 线将 reTerminal E 系列 ePaper 显示屏连接到电脑。

步骤 6.Tools > Port 中选择正确的端口。

ePaper 显示屏编程

reTerminal E1001 配备 7.5 英寸黑白 ePaper 显示屏,而 reTerminal E1002 则配备 7.3 英寸全彩 ePaper 显示屏。这两款显示屏在各种光照条件下都具有出色的可视性,并且功耗极低,非常适合需要始终点亮、但功耗要求极低的工业应用场景。

使用 Seeed_GFX 库

为了控制 ePaper 显示屏,我们将使用 Seeed_GFX 库,它为多种 Seeed Studio 显示设备提供了全面支持。

步骤 1. 从 GitHub 下载 Seeed_GFX 库:


步骤 2. 在 Arduino IDE 中通过添加 ZIP 文件来安装该库。依次点击 Sketch > Include Library > Add .ZIP Library,然后选择刚刚下载的 ZIP 文件。

note

如果你之前安装过 TFT_eSPI 库,可能需要暂时从 Arduino 库文件夹中移除或重命名它,以避免冲突,因为 Seeed_GFX 是在 TFT_eSPI 基础上进行分支并为 Seeed Studio 显示屏增加了额外特性。

Programming reTerminal E1001(7.5 英寸黑白 ePaper)

下面我们通过一个简单示例来演示在黑白 ePaper 显示屏上的基本绘图操作。

步骤 1. 从 Seeed_GFX 库中打开示例草图:File > Examples > Seeed_GFX > ePaper > Basic > HelloWorld

步骤 2. 在 Arduino IDE 中启用 OPI PSRAM:Tools > PSRAM > OPI PSRAM

步骤 3. 在与你的草图相同的文件夹中创建一个名为 driver.h 的新文件。你可以点击 Arduino IDE 中的箭头按钮并选择 “New Tab”,然后将其命名为 driver.h

步骤 4. 复制生成的配置代码并将其粘贴到 driver.h 文件中。代码应类似如下所示:

#define BOARD_SCREEN_COMBO 520 // reTerminal E1001 (UC8179)

步骤 5. 将草图上传到你的 reTerminal E1001。你应该会看到显示屏上出现多种图形,包括线条、文本和形状,用于展示基本绘图能力。

使用 Seeed_GFX 实现多级灰度

上面的 Hello World 草图刻意保持精简,以便适配每一种型号。E1001 和 E1003 上的单色面板实际上在纯黑白之上还支持多级灰度——E1001 支持 4 级,E1003 支持 16 级——并且 Seeed_GFX 通过 epaper.initGrayMode(...) 加上一组 TFT_GRAY_* 调色板常量同时暴露了这两种模式。下面的两个示例将分别进行讲解。

在 reTerminal E1001 上实现 4 级灰度

reTerminal E1001 的 7.5 英寸单色面板可以渲染4 级灰度,而不仅仅是纯黑白。Seeed_GFX 通过 epaper.initGrayMode(GRAY_LEVEL4) 和四个调色板常量来实现这一点:

常量显示的色阶
TFT_GRAY_0黑色
TFT_GRAY_1深灰色
TFT_GRAY_2浅灰色
TFT_GRAY_3白色

下面的示例首先绘制四条水平条纹——每条对应一个灰度级——以便你可以直观地验证调色板,然后将一个 800×480 的灰度位图块传输到屏幕上。Seeed_GFX 库已经将此示例作为可直接烧录的示例提供,其中包含预先转换好的 image.h,因此你无需自己生成任何位图数据。

步骤 1. 从 Seeed_GFX 库中打开示例草图:File > Examples > Seeed_GFX > ePaper > Gray > GrayLevel4。草图及其配套的 image.h 将在编辑器中打开。

步骤 2. 在 Arduino IDE 中启用 OPI PSRAM:Tools > PSRAM > OPI PSRAM

步骤 3. 在示例旁边添加一个 driver.h 文件(与 Hello World 相同的工作流程),并选择 E1001 的板卡–屏幕组合:

#define BOARD_SCREEN_COMBO 520 // reTerminal E1001 (UC8179)

步骤 4. 上传草图。显示屏首先会显示四条灰度条纹——顶部为黑色,然后是深灰、浅灰,底部为白色——随后清屏并渲染来自 image.h 的位图。

供参考,示例草图如下所示:

/*
* 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
}
想使用你自己的图片吗?

image.h 中的 L4_GRAY 数组只是一个 800×480 的灰度位图,预先转换成了 C 数组。若要替换为你自己的图片,请使用任意标准的“图像转 C 数组”转换工具,从一张 800×480 的灰度源图生成数组,并在 image.h 中替换 L4_GRAY。草图本身无需做任何修改。

tip

4 级灰度刷新大约比 1 位黑白更新慢 4 倍,因为控制器需要将每个像素驱动到四个目标电压,而不是两个。请将其用于照片、插图或细节丰富的仪表盘等静态内容,而对于需要快速更新的 UI,请坚持使用标准的 1 位模式。

使用 GxEPD2 库

除了 Seeed_GFX,你还可以使用 GxEPD2 库来驱动 reTerminal 的电子墨水屏显示。Seeed 基于流行的 GxEPD2 库进行了 fork,并为 reTerminal E10xx 系列添加了专用支持,因此它是 reTerminal 用户的推荐选择。

安装 Seeed_GxEPD2 库

要在 reTerminal 产品上使用此库,你需要安装 Seeed_GxEPD2——这是 Seeed 专门为 reTerminal E10xx 系列定制适配的 fork。

步骤 1. 访问 Seeed_GxEPD2 的 GitHub 仓库。点击 "Code" 按钮,然后选择 "Download ZIP" 将该库下载到你的电脑。


步骤 2. 在 Arduino IDE 中,从下载的文件安装该库。依次进入 Sketch > Include Library > Add .ZIP Library...,然后选择你刚刚下载的 ZIP 文件。

步骤 3. Seeed_GxEPD2 库需要 Adafruit GFX Library 才能工作,你也必须安装它。最简单的方式是通过库管理器安装:进入 Tools > Manage Libraries...,搜索 "Adafruit GFX Library",然后点击 "Install"。

note

Seeed_GxEPD2 是 Seeed 基于原始 GxEPD2 库的自定义 fork,针对 reTerminal E10xx 系列提供了专用驱动和优化。我们强烈建议使用这个 fork,而不是上游库,以确保与你的 reTerminal 设备完全兼容。

使用 GxEPD2 为 reTerminal E1001 编程(7.5" 黑白屏)

reTerminal E1001 配备了一块 7.5" 黑白电子墨水屏(800×480,GDEY075T7 面板,UC8179 控制器)。下面的示例演示了多个界面,包括启动画面、系统信息、排版、几何图形、图案以及仪表盘布局。

安装 Seeed_GxEPD2 库后,你可以在 Arduino IDE 中通过 File > Examples > Seeed_GxEPD2 > GxEPD2_reTerminal_E1001 找到此示例,或者在文件系统中手动定位到 Seeed_GxEPD2/examples/GxEPD2_reTerminal_E1001/GxEPD2_reTerminal_E1001.ino

点击此处查看完整代码
#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());
}

下图展示了 E1001 示例的实际显示效果:

使用 GxEPD2 库实现多级灰度显示

除了标准的黑白和 6 色模式外,一些 reTerminal 显示屏还支持多级灰度渲染。Seeed_GxEPD2 库包含专门的灰度示例,这些示例绕过常规的 GxEPD2_BW / GxEPD2_7C 驱动,而是使用自定义 LUT 波形直接驱动显示控制器,同时仍然利用 Adafruit_GFX 进行绘图。

  • reTerminal E1001(7.5" 黑白):UC8179 控制器通过专用的 VCOM/WW/KW/WK/KK LUT 表支持 4 级灰度 模式。使用 2bpp(96 KB)帧缓冲区,每个像素可以是黑色、深灰、浅灰或白色。
  • reTerminal E1003(10.3" 单色):IT8951 控制器通过其 GC16 波形模式原生支持 16 级灰度。在 PSRAM 中分配了 4bpp(约 1.25 MB)帧缓冲区,每个像素可以呈现 16 个灰度级之一。

编程 reTerminal E1001 — 4 级灰度

E1001 的 UC8179 控制器可以通过上传自定义 LUT 表(VCOM、LUTWW、LUTKW、LUTWK、LUTKK),从其正常的 1 位模式切换到 4 级灰度模式。该示例创建了一个 Gray4Canvas(2bpp,96 KB),并使用 Adafruit_GFX 进行绘图,然后将两个位平面上传到控制器以实现灰度渲染。

安装 Seeed_GxEPD2 库后,你可以在 Arduino IDE 中通过 File > Examples > Seeed_GxEPD2 > GxEPD2_reTerminal_E1001_Gray4 找到此示例,或在 Seeed_GxEPD2/examples/GxEPD2_reTerminal_E1001_Gray4/GxEPD2_reTerminal_E1001_Gray4.ino 中手动定位。

点击此处查看完整代码
#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() {}

下图展示了 E1001 4 级灰度示例的实际显示效果:

note

ePaper 显示屏的刷新率相对较慢。黑白显示屏(E1001、E1003)通常在 1–3 秒内完成刷新,而 6 色显示屏(E1002、E1004)进行一次完整刷新可能需要 25–40 秒。这是正常现象,是为了实现超低功耗以及在无背光条件下仍具备出色可视性的权衡。

故障排查

Q1:为什么在运行上述代码时,reTerminal 的 ePaper 显示屏没有任何显示或不刷新?

如果你在 reTerminal 中插入了 MicroSD 卡,可能会出现此问题。原因是 MicroSD 卡和 ePaper 显示屏在 reTerminal 上共用同一条 SPI 总线。如果插入了 MicroSD 卡,但其使能(片选)引脚未被正确管理,就会在 SPI 总线上产生冲突。具体来说,MicroSD 卡可能会将 BUSY 线保持为高电平,从而阻止 ePaper 显示屏正常工作——导致没有显示更新或刷新。

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

要解决这个问题,你必须确保使用上面提供的代码正确使能 MicroSD 卡。该代码通过设置正确的引脚状态来初始化并使能 MicroSD 卡,从而避免 SPI 总线冲突,使 SD 卡和 ePaper 显示屏都能正常协同工作。在 reTerminal 上使用 MicroSD 卡时,请务必使用推荐的初始化代码,以避免此类问题。

如果你的项目中不使用 MicroSD 卡,我们建议先关闭设备电源并取出卡片,再运行显示程序。如果卡片已经插入 reTerminal,则无论你是否实际使用 MicroSD 卡,都需要添加上述代码,以确保屏幕能够正常显示。

Q2:为什么我无法向 reTerminal 上传程序?

如果你在向 reTerminal 上传程序时遇到如下错误。

那么,很可能是你的 Arduino IDE 上传速率设置得过高。请将其修改为 115200 波特率以解决此问题。

技术支持与产品讨论

感谢你选择我们的产品!我们将为你提供多种支持,确保你在使用我们产品的过程中尽可能顺畅。我们提供多种沟通渠道,以满足不同的偏好和需求。

Loading Comments...