跳到主要内容

E-Paper(墨水屏)

微雪 ESP32-S3-ePaper-3.97 模块(双色墨水屏)

双色(黑白):ESP32-S3-ePaper-3.97

微雪 ESP32-S3-ePaper-1.54G 模块(四色墨水屏)

四色(黑白黄红):ESP32-S3-ePaper-1.54G

微雪 ESP32-S3-PhotoPainter(全彩墨水屏相框)

全彩(E Ink Spectra 6):ESP32-S3-PhotoPainter

E-Paper(电子纸,俗称墨水屏)与其他屏幕有显著差异:画面断电也能保持,只有刷新期间才耗电。配合类纸的观感和阳光下的可读性,它适合电子价签、电子日历、阅读器等画面长期不变的场景;代价是刷新以秒计,不适合动画。

本章介绍墨水屏的成像原理,重点讲解它独有的全刷 / 局刷机制与残影问题,最后给出在 ESP32 上的驱动方法。

1. 显示原理

墨水屏基于 电泳(Electrophoresis) 原理。以最常见的黑白屏为例:

电泳微胶囊显示原理与双稳态特性

  • 显示层是一层微胶囊薄膜:无数微胶囊(或微杯)固定在黏合剂中,夹在上下两层电极之间;每个胶囊内悬浮着带负电的黑色粒子带正电的白色粒子
  • 上下电极间施加电场时,胶囊本身不动,只有内部粒子沿电场向两极泳动:白色粒子浮到表面,该处显示为白;反之显示为黑。
  • 粒子到位后,撤去电场粒子仍停留在原处,这就是墨水屏的双稳态特性:画面保持不需要任何电力,断电后内容依然存在。

它同样是反射式显示:依靠反射环境光成像,无背光,光线越强越清晰,暗处则需要外部照明(阅读器上的前光即为此增加的照明层)。

彩色墨水屏在此基础上扩展:

  • 三色屏(黑白红 / 黑白黄):胶囊内增加第三种带电粒子,驱动波形更复杂,刷新时间明显变长。
  • 全彩屏:如 ACeP、Spectra 6 等方案,通过多种颜色粒子的精细驱动实现彩色。面板只能显示少数几种原生颜色(如 Spectra 6 为黑、白、红、黄、绿、蓝六色),照片中的其他颜色由抖动算法转换为原生色像素的空间混合来呈现,近距离观察有颗粒感。这类屏刷新更慢,色彩饱和度低于 LCD,适合海报、相框类静态展示(如采用 Spectra 6 屏的 微雪 ESP32-S3-PhotoPainter 相框)。

2. 特性与局限

优势:

  • 静态零功耗:只有刷新耗电,画面保持不耗电、断电不丢失。以天为单位更新的电子价签类设备可依靠纽扣电池运行数年。
  • 类纸观感:反射式成像,无频闪、不直射眼睛,长时间阅读舒适。
  • 阳光下清晰可读,可视角接近 180°。

局限:

  • 刷新慢:黑白屏全刷约 2~4 秒,三色及以上的彩色屏可达 10~30 秒,刷新过程画面会闪烁数次。
  • 有残影:快速刷新(局刷)后,前一画面可能留下轻微的痕迹,需要定期全刷清除。
  • 低温性能下降:电泳粒子在低温下泳动变慢,多数屏幕的工作温度下限在 0°C 左右,户外冬季使用需确认规格。
  • 刷新寿命有限:刷新次数有上限,低频更新不受影响,但不适合动画、视频等高频刷新的内容,具体限制见面板规格。
与 RLCD 的选型区别

两者同为反射式低功耗屏幕。区别在于:画面基本不变、待机时间优先的场景选墨水屏;需要实时刷新(时钟秒针、动态数据)的场景选 RLCD

3. 刷新机制:全刷与局刷

刷新机制是墨水屏与其他屏幕差别最大的部分,也是使用中问题最集中的部分。

主控发给墨水屏的图像数据只规定每个像素的目标颜色,以黑白屏为例即最终显示黑色还是白色;但电泳粒子在液体中的迁移需要时间,无法像 LCD 像素那样通过一次简单的电压切换快速完成状态变化。驱动 IC 需要按照一套多阶段驱动波形,依次施加不同极性、不同时长的电压脉冲,才能将粒子可靠地驱动到目标位置。这套波形参数以 LUT(Look-Up Table,查找表) 的形式预存在驱动 IC 内,或由驱动库在初始化时写入。波形不同,刷新效果和速度也不同,由此分为两种刷新方式:

全刷与局刷的刷新过程及残影累积对比

  • 全刷(Full Refresh):刷新前先将全屏在黑白之间往复驱动数次(表现为整屏闪烁),使所有粒子充分复位后再写入新画面。速度慢,但能彻底清除残影,画面质量最好。部分驱动 IC 另外提供快速全刷,通过缩短波形时长减少耗时,整屏只闪烁一次,代价是粒子复位不充分,清除残影的能力弱于标准全刷。

  • 局刷(Partial Refresh):只把指定区域内变化的像素驱动到目标状态,不做全屏闪烁。在支持快速局刷的黑白屏上速度可达亚秒级,适合更新时钟数字、传感器读数等小块内容;但短促的波形无法把粒子推到两极的稳定极限位置,部分粒子停在中间态,多次局刷后这种偏差不断累积,形成残影,需要靠全刷的往复驱动把粒子重新压回两极才能清除。面板是否支持快速局刷以产品资料为准,部分面板虽然可以设置局部窗口,刷新速度和表现却与全刷接近。

    从单个胶囊看,残影可以理解为粒子没有回到两极的极限位置

    残影成因:黑色粒子未充分复位、混在表面未沉底导致画面发灰

实际使用的通用策略:

  1. 上电后先做一次全刷,建立干净的基准画面。
  2. 高频的局部区域更新用局刷
  3. 每隔若干次局刷(经验值 5~10 次)或定期(如每小时)做一次全刷,清除累积的残影。
  4. 刷新期间驱动 IC 通过 BUSY 引脚指示忙碌状态,程序必须等待 BUSY 释放后才能发起下一次操作,不可打断。
使用注意
  • 不要以过高频率连续刷新墨水屏,这会加速屏幕老化;三色及以上的彩色屏两次刷新的间隔建议不低于官方产品页标注的最小值。
  • 设备长期不用时,建议先把屏幕刷白再存放,避免长时间保持同一画面形成难以恢复的残影。
  • 刷新过程中不要断电,否则画面可能停留在中间态,需要全刷恢复。

4. 常见驱动 IC 与接口

墨水屏常用 SPI 接口,DIN 传输数据,CLK 提供时钟,CS 片选,DC 区分命令与数据,RST 复位驱动 IC;与常见的 SPI 屏幕相比多一根 BUSY,由驱动 IC 输出、主控读取,用于指示刷新是否结束。常见驱动 IC 有 Solomon Systech SSD168x 系列和 UltraChip UC81xx 系列,微雪各尺寸墨水屏使用的具体型号见对应产品页。

墨水屏的显存格式取决于颜色数量:

  • 黑白屏通常按 1bpp(1 bit per pixel,每像素 1 位)存放。
  • 三色屏通常使用两个 1bpp 显存平面,分别记录黑白区域和红色 / 黄色区域。
  • 四色屏可按 2bpp 存放,每个字节记录 4 个像素。

以 ESP32-S3-ePaper-1.54G 板载的 200×200 四色屏为例,一帧数据为 200 × 200 × 2 ÷ 8 = 10000 字节。SPI 带宽足以传输这些数据;墨水屏刷新慢的原因在于粒子泳动的物理过程,而非数据传输。

5. Arduino + GxEPD2 驱动示例

本节使用 GxEPD2 分别驱动黑白、四色和全彩墨水屏。以下案例按屏幕颜色类型组织,采用相同的绘图接口;显示类、面板驱动类、显存格式和刷新特性均需与具体面板匹配。

5.1 通用准备

  1. 参考 Arduino IDE 开发环境搭建教程 安装 Arduino IDE,并安装 arduino-esp32 v3.2.0 或更高版本。
  2. 在库管理器(工具 > 管理库)中搜索并安装 GxEPD2。安装依赖时,选择安装全部依赖。其他安装方式参考 Arduino 库管理教程

本节示例使用 GxEPD2 v1.6.9、Adafruit GFX Library v1.12.6 和 arduino-esp32 v3.3.10 验证。不同版本支持的面板可能不同,使用其他版本时先查看 GxEPD2 支持列表

与前述 LCD、OLED 等显示库主要按驱动 IC 选择驱动不同,GxEPD2 的适配单位是具体墨水屏面板型号,而不是驱动 IC 型号。墨水屏的初始化参数、颜色编码和刷新波形需要与面板匹配,即使驱动 IC 相同,也不能据此直接使用其他面板的驱动类。使用 GxEPD2 时,应先确认面板型号,再选择对应的驱动类。

5.2 黑白屏:ESP32-S3-ePaper-3.97

示例开发板

本例使用 ESP32-S3-ePaper-3.97。该开发板板载 3.97 英寸 800×480 黑白墨水屏,驱动 IC 为 SSD1677。该屏支持快速局刷,本节的第二个示例即基于此。

ESP32-S3-ePaper-3.97 的 Arduino 工程参数设置 配置开发板选项。GxEPD2 自 v1.6.5 起支持该屏使用的 GDEM0397T81 面板。

板载接口

屏幕已直连 ESP32-S3,信号引脚固定如下:

信号GPIO说明
EPD_BUSYGPIO3刷新忙状态
EPD_DCGPIO9命令 / 数据选择
EPD_CSGPIO10SPI 片选
EPD_SCLKGPIO11SPI 时钟
EPD_MOSIGPIO12SPI 数据
EPD_RSTGPIO46复位

板载屏幕不需要外部接线。与 ESP32-S3-ePaper-1.54G 不同,该开发板没有独立的屏幕电源控制引脚,屏幕供电随系统上电,代码中无需操作电源引脚。

使用 GxEPD2 绘制静态画面

黑白屏按 1bpp 存放显存,一帧数据为 800 × 480 ÷ 8 = 48000 字节,可在 ESP32-S3 的内部 RAM 中保存完整画面。下面的示例绘制一屏静态内容,包含几何图形、四种字号的文字,以及一组网点抖动色块。

#include <SPI.h>
#include <GxEPD2_BW.h>

constexpr int EPD_BUSY = 3;
constexpr int EPD_DC = 9;
constexpr int EPD_CS = 10;
constexpr int EPD_SCLK = 11;
constexpr int EPD_MOSI = 12;
constexpr int EPD_RST = 46;

using EpdDriver = GxEPD2_397_GDEM0397T81;
GxEPD2_BW<EpdDriver, EpdDriver::HEIGHT> display(
EpdDriver(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));

// 以 step 为周期填充网点,用黑白像素的疏密模拟灰度
void fillDither(int x, int y, int w, int h, int step) {
for (int j = 0; j < h; ++j) {
for (int i = 0; i < w; ++i) {
if ((i % step == 0) && (j % step == 0)) {
display.drawPixel(x + i, y + j, GxEPD_BLACK);
}
}
}
}

void drawStatic() {
display.fillScreen(GxEPD_WHITE);
display.setTextWrap(false);

// 标题栏
display.fillRect(8, 8, 784, 64, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
display.setTextSize(4);
display.setCursor(40, 28);
display.print("ESP32-S3-ePaper-3.97");

// 左侧:几何图形
display.setTextColor(GxEPD_BLACK);
display.setTextSize(2);
display.setCursor(40, 96);
display.print("GRAPHICS");

display.drawRect(40, 128, 160, 120, GxEPD_BLACK);
display.fillRect(216, 128, 160, 120, GxEPD_BLACK);
display.drawCircle(120, 320, 56, GxEPD_BLACK);
display.fillCircle(296, 320, 56, GxEPD_BLACK);
display.drawLine(40, 128, 200, 248, GxEPD_BLACK);
display.drawLine(40, 248, 200, 128, GxEPD_BLACK);

// 右侧:不同字号
display.setTextSize(2);
display.setCursor(432, 96);
display.print("TEXT SIZE");

display.setTextSize(1);
display.setCursor(432, 128);
display.print("setTextSize(1) 6 x 8 px");
display.setTextSize(2);
display.setCursor(432, 152);
display.print("setTextSize(2)");
display.setTextSize(3);
display.setCursor(432, 184);
display.print("setTextSize(3)");
display.setTextSize(4);
display.setCursor(432, 224);
display.print("SIZE(4)");

// 右下:1bpp 只有黑白两级,中间调由网点疏密模拟
display.setTextSize(2);
display.setCursor(432, 288);
display.print("1 BPP DITHER");

const int patchX[] = {432, 528, 624, 720};
const char *patchLabel[] = {"BLACK", "DENSE", "LIGHT", "WHITE"};
for (int i = 0; i < 4; ++i) {
display.drawRect(patchX[i], 320, 72, 72, GxEPD_BLACK);
display.setTextSize(1);
display.setCursor(patchX[i] + 12, 400);
display.print(patchLabel[i]);
}
display.fillRect(433, 321, 70, 70, GxEPD_BLACK);
fillDither(529, 321, 70, 70, 2);
fillDither(625, 321, 70, 70, 4);

// 底栏
display.fillRect(8, 424, 784, 48, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
display.setTextSize(2);
display.setCursor(40, 440);
display.print("800 x 480 1 BPP SSD1677 FULL REFRESH");

// 外框最后绘制,避免被色块覆盖
display.drawRect(0, 0, 800, 480, GxEPD_BLACK);
display.drawRect(1, 1, 798, 478, GxEPD_BLACK);
}

void setup() {
// 将硬件 SPI 映射到板载屏幕使用的引脚
SPI.begin(EPD_SCLK, -1, EPD_MOSI, EPD_CS);

display.init(115200);
// 使用标准全刷波形,彻底清除上一画面的残留
display.epd2.selectFastFullUpdate(false);
display.setRotation(0);
display.setFullWindow();

uint32_t start = millis();
display.firstPage();
do {
drawStatic();
} while (display.nextPage());

Serial.printf("full refresh: %lu ms\n", millis() - start);

display.hibernate();
}

void loop() {
}

GxEPD2_BW 是黑白显示类,GxEPD2_397_GDEM0397T81 是对应面板的驱动类,模板参数使用 EpdDriver::HEIGHT,在内存中保存完整画面。setRotation(0) 对应 800×480 横向,坐标原点在左上角。

GxEPD2 对该面板默认启用快速全刷,速度快但会保留上一画面的痕迹,因此本例调用 selectFastFullUpdate(false) 改用标准全刷,确保一次成像干净。三种刷新方式的差别见全刷与局刷对比。与四色屏示例相同,必须在 display.init() 之前调用 SPI.begin(EPD_SCLK, -1, EPD_MOSI, EPD_CS),否则 GxEPD2 不会使用该开发板的板载屏幕引脚。

右下角的四个色块说明 1bpp 的显示能力:面板只有黑、白两级,fillDither() 按不同周期填充黑色像素,在观看距离上呈现为深浅不同的灰色。同样的方法也用于黑白屏显示照片,即把灰度图转换为黑白网点,原理与全彩屏的抖动一致。

全刷与局刷对比

下面的示例在屏幕中央显示一个持续递增的计数器:默认用局刷更新计数器所在的窗口,每 10 次更新中的最后一次改用全刷,两种刷新的耗时打印到串口。程序不额外延时,每次刷新完成后立即更新计数值,因此计数器的递增速度即刷新速度。

#include <SPI.h>
#include <GxEPD2_BW.h>

constexpr int EPD_BUSY = 3;
constexpr int EPD_DC = 9;
constexpr int EPD_CS = 10;
constexpr int EPD_SCLK = 11;
constexpr int EPD_MOSI = 12;
constexpr int EPD_RST = 46;

using EpdDriver = GxEPD2_397_GDEM0397T81;
GxEPD2_BW<EpdDriver, EpdDriver::HEIGHT> display(
EpdDriver(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));

// 局刷窗口,x 和宽度取 8 的整数倍
constexpr int WIN_X = 280;
constexpr int WIN_Y = 180;
constexpr int WIN_W = 240;
constexpr int WIN_H = 120;

// 每 10 次更新做一次全刷,其余 9 次为局刷
constexpr int UPDATES_PER_FULL = 10;

uint32_t counter = 0;

// 静态背景,只在全刷时绘制
void drawFrame() {
display.fillScreen(GxEPD_WHITE);
display.setTextWrap(false);

display.fillRect(8, 8, 784, 56, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
display.setTextSize(3);
display.setCursor(40, 26);
display.print("FULL vs PARTIAL REFRESH");

display.setTextColor(GxEPD_BLACK);
display.setTextSize(2);
display.setCursor(WIN_X, WIN_Y - 40);
display.print("UPDATE COUNT");

// 边框画在局刷窗口之外,局刷时不会被重绘
display.drawRect(
WIN_X - 8, WIN_Y - 8, WIN_W + 16, WIN_H + 16, GxEPD_BLACK);

display.setTextSize(2);
display.setCursor(40, 400);
display.print("full refresh every 10 updates");

display.drawRect(0, 0, 800, 480, GxEPD_BLACK);
}

// 计数器,局刷和全刷都会绘制
void drawCounter() {
display.setTextColor(GxEPD_BLACK);
display.setTextSize(7);
display.setCursor(WIN_X + 24, WIN_Y + 32);
display.print(counter);
}

// 全刷:重绘整屏
uint32_t refreshFull() {
uint32_t start = millis();
display.setFullWindow();
display.firstPage();
do {
drawFrame();
drawCounter();
} while (display.nextPage());
return millis() - start;
}

// 局刷:只重绘计数器所在的窗口
uint32_t refreshPartial() {
uint32_t start = millis();
display.setPartialWindow(WIN_X, WIN_Y, WIN_W, WIN_H);
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
drawCounter();
} while (display.nextPage());
return millis() - start;
}

void setup() {
SPI.begin(EPD_SCLK, -1, EPD_MOSI, EPD_CS);

display.init(115200);
display.setRotation(0);

// 上电后先做一次全刷,建立干净的基准画面
Serial.printf("full refresh #%lu: %lu ms\n", counter, refreshFull());
}

void loop() {
// 不额外延时,连续刷新以体现两种方式的速度差异
++counter;

if (counter % UPDATES_PER_FULL == 0) {
Serial.printf("full refresh #%lu: %lu ms\n", counter, refreshFull());
} else {
Serial.printf("partial refresh #%lu: %lu ms\n", counter, refreshPartial());
}
}

代码对应刷新机制一节给出的通用策略:

  • setFullWindow()setPartialWindow(x, y, w, h) 决定本次刷新的方式和范围。局刷窗口的 x 和宽度应取 8 的整数倍,与显存中每字节 8 个像素的排布对齐。
  • 局刷时 fillScreen() 只作用于当前窗口,因此计数器的边框绘制在窗口之外,不会被反复刷新。
  • 局刷依赖驱动 IC 中保存的上一帧数据计算差异,因此上电后先做一次全刷建立基准画面。
  • 每 10 次更新中的最后一次改用全刷,重绘完整画面。本例局刷范围小、间隔短,默认的快速全刷即可维持画面干净;残影已经明显时,应改用标准全刷。

GxEPD2 对该面板默认启用快速全刷(驱动类中 useFastFullUpdatetrue)。实测快速全刷的波形耗时为 1.70 秒、局刷为 370 ms;加上图像数据传输和驱动上电,本例串口打印的单次整体耗时为全刷约 2.46 秒、局刷约 420 ms,局刷约为全刷的六分之一。display.init(115200) 会打开 GxEPD2 的诊断输出,串口中的 _Update_Part_Update_Full 即波形耗时,单位为微秒;整体耗时另受局刷窗口大小和 SPI 时钟影响。

display.init() 之后调用 display.epd2.selectFastFullUpdate(false),可切换为标准全刷,实测其波形耗时为 2.10 秒。三种刷新方式的差别汇总如下:

刷新方式GxEPD2 用法波形耗时刷新时的表现残影
标准全刷selectFastFullUpdate(false) + setFullWindow()2.10 秒整屏往复闪烁多次彻底清除
快速全刷默认 + setFullWindow()1.70 秒整屏闪烁一次可能残留前一画面的痕迹
局刷setPartialWindow()370 ms无闪烁,内容直接替换逐次累积

上电后的第一次刷新会执行两次全刷:GxEPD2 在首次写入显存时先将整屏刷白,再刷新实际画面,以建立干净的基准。因此串口中 #0 的整体耗时约为后续全刷的两倍(本例实测 4.52 秒,示例一使用标准全刷时为 5.39 秒),属正常现象。

残影的可见程度取决于局刷的累积次数和画面翻转的面积。本例中数字区域面积小,且每 10 次更新即全刷一次,通常看不出明显残影;反复翻转大面积黑白区域时,残影表现为翻转区域边缘的浅色残留。此时默认的快速全刷不足以清除残留,需要切换为标准全刷,靠往复驱动把粒子压回两极。

前一画面的痕迹是否被带入新画面,主要取决于上一次刷新把粒子留在什么状态:标准全刷后粒子已被驱动到两极,此前累积的偏差已经清除,后续刷新不会再显现旧画面的痕迹;大量局刷后粒子停在中间态,此时若只做快速全刷,前一画面的痕迹会留在新画面中,在大面积纯黑或纯白区域上尤其明显。

使用微雪官方库

微雪官方示例包含 EPD_3in97 驱动、GUI_Paint 绘图库、字体和图片数据。基本调用流程如下:

DEV_Module_Init();
EPD_3IN97_Init();
EPD_3IN97_Display_Base(image); // 全刷,同时写入局刷所需的基准帧
EPD_3IN97_Display_Partial(image, x_start, y_start, x_end, y_end);
EPD_3IN97_Sleep();

EPD_3IN97_Init_Fast()EPD_3IN97_Display_Fast() 选择快速全刷波形。该驱动另外提供 EPD_3IN97_Init_4GRAY()EPD_3IN97_Display_4Gray(),通过多阶段波形使同一面板显示四级灰度,代价是刷新时间更长且不支持局刷。完整程序见 ESP32-S3-ePaper-3.97 官方示例;开发环境和烧录参数见 ESP32-S3-ePaper-3.97 Arduino 教程

刷新与休眠

display.hibernate() 使屏幕驱动进入深度休眠,功耗最低,但驱动 IC 中的上一帧数据随之失效。休眠后再次刷新,需要重新初始化驱动并先做一次全刷,之后才能继续局刷。需要持续局刷的场景(如时钟、传感器读数)应保持驱动上电,仅在长时间不更新时调用 hibernate()

局刷使用注意
  • 局刷窗口之外的内容不会被更新,切换画面布局时应改用全刷。
  • 本例为体现速度差异而连续刷新,实际产品应按所需的更新频率控制刷新间隔。
  • 长期不使用时,先将屏幕刷白,再调用 hibernate()

5.3 四色屏:ESP32-S3-ePaper-1.54G

示例开发板

本例使用 ESP32-S3-ePaper-1.54G。该开发板板载 1.54 英寸 200×200 四色墨水屏,可显示黑、白、黄、红四种颜色。带 G 的型号是四色版本;不带 G 的 ESP32-S3-ePaper-1.54 是黑白版本,两者的驱动和刷新时间不同。

ESP32-S3-ePaper-1.54G 的 Arduino 工程参数设置 配置开发板选项。GxEPD2 自 v1.6.6 起支持该屏使用的 1.54 英寸 200×200 四色面板。

板载接口

屏幕已直连 ESP32-S3,信号和供电控制引脚固定如下:

信号GPIO说明
EPD_PWRGPIO6屏幕电源控制,低电平开启、高电平关闭
EPD_BUSYGPIO8刷新忙状态
EPD_RSTGPIO9复位
EPD_DCGPIO10命令 / 数据选择
EPD_CSGPIO11SPI 片选
EPD_SCLKGPIO12SPI 时钟
EPD_SDIGPIO13SPI 数据(即 MOSI,代码中记作 EPD_MOSI

板载屏幕不需要外部接线。EPD_PWR 用于控制屏幕供电,刷新完成并进入休眠后应将其拉高,关闭屏幕电源。

使用 GxEPD2 绘制四色价签

GxEPD2 使用 Adafruit GFX 的绘图 API,可调用 fillRect()fillCircle()drawRect()print() 等函数组合画面。下面的示例将屏幕绘制成电子价签,同时展示红、黄、黑、白四种原生颜色、不同字号和几何图形。

#include <SPI.h>
#include <GxEPD2_4C.h>

constexpr int EPD_PWR = 6;
constexpr int EPD_BUSY = 8;
constexpr int EPD_RST = 9;
constexpr int EPD_DC = 10;
constexpr int EPD_CS = 11;
constexpr int EPD_SCLK = 12;
constexpr int EPD_MOSI = 13;

using EpdDriver = GxEPD2_154c_GDEM0154F51H;
GxEPD2_4C<EpdDriver, EpdDriver::HEIGHT> display(
EpdDriver(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));

void drawShowcase() {
display.fillScreen(GxEPD_WHITE);
display.setTextWrap(false);

// 红色标题栏
display.fillRect(2, 2, 196, 32, GxEPD_RED);
display.setTextColor(GxEPD_WHITE);
display.setTextSize(2);
display.setCursor(22, 10);
display.print("1.54G E-PAPER");

// 黄色价签主体
display.fillRect(4, 38, 192, 73, GxEPD_YELLOW);
display.setTextColor(GxEPD_BLACK);
display.setTextSize(1);
display.setCursor(12, 45);
display.print("SPECIAL PRICE");

display.setTextSize(4);
display.setCursor(12, 62);
display.print("29.90");

display.setTextSize(1);
display.setCursor(100, 99);
display.print("USD");

// 红色折扣徽章
display.fillCircle(165, 73, 25, GxEPD_RED);
display.setTextColor(GxEPD_WHITE);
display.setTextSize(1);
display.setCursor(153, 62);
display.print("SAVE");
display.setTextSize(2);
display.setCursor(147, 76);
display.print("25%");

// 黑色规格栏
display.fillRect(4, 116, 192, 27, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
display.setTextSize(1);
display.setCursor(58, 121);
display.print("4 COLORS / 2 BPP");
display.setCursor(73, 132);
display.print("200 x 200");

// 四种原生颜色
const int swatchY = 149;
const int swatchW = 43;
const int swatchH = 43;
const int swatchX[] = {4, 53, 102, 151};
const uint16_t swatchColor[] = {
GxEPD_RED, GxEPD_YELLOW, GxEPD_BLACK, GxEPD_WHITE
};
const uint16_t labelColor[] = {
GxEPD_WHITE, GxEPD_BLACK, GxEPD_WHITE, GxEPD_BLACK
};
const char *label[] = {"RED", "YEL", "BLK", "WHT"};

display.setTextSize(1);
for (int i = 0; i < 4; ++i) {
display.fillRect(
swatchX[i], swatchY, swatchW, swatchH, swatchColor[i]);
display.drawRect(
swatchX[i], swatchY, swatchW, swatchH, GxEPD_BLACK);
display.setTextColor(labelColor[i]);
display.setCursor(swatchX[i] + 12, swatchY + 18);
display.print(label[i]);
}

// 外框最后绘制,避免被色块覆盖
display.drawRect(0, 0, 200, 200, GxEPD_BLACK);
display.drawRect(1, 1, 198, 198, GxEPD_BLACK);
}

void setup() {
pinMode(EPD_PWR, OUTPUT);
digitalWrite(EPD_PWR, LOW); // 开启屏幕电源
delay(10);

// 将硬件 SPI 映射到板载屏幕使用的引脚
SPI.begin(EPD_SCLK, -1, EPD_MOSI, EPD_CS);

display.init(115200);
display.setRotation(0);
display.setFullWindow();

display.firstPage();
do {
drawShowcase();
} while (display.nextPage());

display.hibernate();
digitalWrite(EPD_PWR, HIGH); // 关闭屏幕电源
}

void loop() {
}

GxEPD2_4C 是四色显示类,GxEPD2_154c_GDEM0154F51H 是对应面板的驱动类。该屏一帧缓冲区仅占 10000 字节,因此模板参数使用 EpdDriver::HEIGHT,在内存中保存完整画面。firstPage()nextPage() 组成 GxEPD2 的分页绘图循环;使用完整缓冲区时,循环只执行一次,但保留这种写法后也可改用分页缓冲区。

ESP32-S3 的 SPI 信号可映射到不同 GPIO。必须在 display.init() 之前调用 SPI.begin(EPD_SCLK, -1, EPD_MOSI, EPD_CS),否则 GxEPD2 不会使用该开发板的板载屏幕引脚。GPIO6 是屏幕电源控制引脚,不由 GxEPD2 管理,需要在初始化前拉低,并在 display.hibernate() 后拉高。

四色像素编码

ESP32-S3-ePaper-1.54G 使用 2bpp 图像数据,一个字节按从高位到低位的顺序记录 4 个像素:

颜色编码
00
01
10
11

例如,连续 4 个像素依次为黑、白、黄、红时,对应的数据为 00 01 10 11,即 0x1B。GxEPD2 已处理像素打包,使用 Adafruit GFX 绘图 API 时直接传入 GxEPD_BLACKGxEPD_WHITEGxEPD_YELLOWGxEPD_RED 即可。

使用微雪官方库

微雪官方示例包含 EPD_1in54g 驱动、GUI_Paint 绘图库、字体和图片数据,适合核对硬件状态、查看底层初始化流程,或在现有微雪示例工程中继续开发。基本调用流程如下:

DEV_Module_Init();
EPD_1IN54G_Init_Fast();
EPD_1IN54G_Display(image);
EPD_1IN54G_Sleep();

使用时应保留 .ino、驱动和资源文件所在的完整工程目录。完整程序见 ESP32-S3-ePaper-1.54G 官方示例;开发环境和烧录参数见 ESP32-S3-ePaper-1.54G Arduino 教程

刷新与休眠

该屏的标准全刷约需 20 秒,快速全刷约需 15 秒。GxEPD2 驱动支持设置局部窗口,但该面板不支持黑白屏常见的快速局刷(见全刷与局刷对比),不适合高频更新动态内容。微雪官方库中的 EPD_1IN54G_Init_Fast() 选择快速波形,EPD_1IN54G_Display() 仍会刷新完整画面。

四色屏刷新限制
  • 两次刷新之间建议至少间隔 180 秒;频繁刷新可能影响显示效果和屏幕寿命。
  • 完成显示后使屏幕驱动进入深度休眠:GxEPD2 调用 display.hibernate(),微雪官方库调用 EPD_1IN54G_Sleep();之后将 EPD_PWR (GPIO6) 拉高,关闭屏幕电源。
  • 屏幕休眠后不再处理图像数据,再次刷新前需要重新拉低 EPD_PWR 并初始化屏幕驱动。

5.4 全彩屏:ESP32-S3-PhotoPainter

示例开发板

本例使用 ESP32-S3-PhotoPainter。该产品板载 7.3 英寸 800×480 E Ink Spectra 6 墨水屏,可直接显示黑、白、绿、蓝、红、黄六种原生颜色;其他颜色需要通过抖动显示,见通过抖动显示混合颜色

该屏对应 GxEPD2 的 GxEPD2_730c_GDEP073E01 面板驱动类,需要包含 GxEPD2_7C.h。GxEPD2 自 v1.5.9 起支持该面板;本例使用 GxEPD2 v1.6.9 和 arduino-esp32 v3.3.10 编译及实物验证。

在 Arduino IDE 的库管理器中搜索并安装 XPowersLib。该库用于配置板载 AXP2101 电源管理芯片,使 ALDO4 为墨水屏提供 3.3 V 电源。

屏幕供电电压

ESP32-S3-PhotoPainter 的 EPD_VCC 连接到 AXP2101 的 ALDO4,使用屏幕前必须将其设置为 3.3 V 并开启输出。

Arduino 开发板设置与板载接口

在 Arduino IDE 中选择 工具 > 开发板 > esp32 > ESP32S3 Dev Module,并设置以下选项:

  • Flash Size:16MB (128Mb)
  • PSRAM:OPI PSRAM
  • USB CDC On Boot:Enabled

屏幕和 AXP2101 已直连 ESP32-S3,不需要外部接线。板载接口如下:

信号GPIO说明
EPD_DCGPIO8命令 / 数据选择
EPD_CSGPIO9SPI 片选
EPD_SCKGPIO10SPI 时钟
EPD_DINGPIO11SPI 数据
EPD_RSTGPIO12复位
EPD_BUSYGPIO13刷新忙状态
PMU_SDAGPIO47AXP2101 I2C 数据
PMU_SCLGPIO48AXP2101 I2C 时钟

使用 GxEPD2 绘制六色同心圆构图

下面的示例只使用 GxEPD2 和 Adafruit GFX 的图形接口,不读取 SD 卡或外部图片。画面主体是 5 × 3 的同心圆阵列:每格由背景色和三层同心圆组成,颜色组合由表格定义,画面由双重循环生成;底部色卡列出六种原生颜色。这类纯色块构图不需要抖动,也便于在实物上直观比较各颜色组合的对比度。

#include <SPI.h>
#include <Wire.h>

#define XPOWERS_CHIP_AXP2101
#include <XPowersLib.h>
#include <GxEPD2_7C.h>

constexpr int PMU_SDA = 47;
constexpr int PMU_SCL = 48;

constexpr int EPD_DC = 8;
constexpr int EPD_CS = 9;
constexpr int EPD_SCK = 10;
constexpr int EPD_MOSI = 11;
constexpr int EPD_RST = 12;
constexpr int EPD_BUSY = 13;

using EpdDriver = GxEPD2_730c_GDEP073E01;

// 每次缓冲 40 行:800 × 40 × 4 ÷ 8 = 16000 字节
GxEPD2_7C<EpdDriver, 40> display(
EpdDriver(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));

XPowersPMU power;

bool initEpaperPower() {
if (!power.begin(
Wire, AXP2101_SLAVE_ADDRESS, PMU_SDA, PMU_SCL)) {
Serial.println("AXP2101 initialization failed.");
return false;
}

// 设置并开启电子纸的 3.3 V 供电
if (!power.setALDO4Voltage(3300)) {
Serial.println("Failed to set ALDO4 to 3.3 V.");
return false;
}

if (!power.enableALDO4()) {
Serial.println("Failed to enable ALDO4.");
return false;
}

delay(10);
return true;
}

void drawPalette() {
const uint16_t colors[] = {
GxEPD_BLACK, GxEPD_WHITE, GxEPD_GREEN,
GxEPD_BLUE, GxEPD_RED, GxEPD_YELLOW
};
const uint16_t labelColors[] = {
GxEPD_WHITE, GxEPD_BLACK, GxEPD_BLACK,
GxEPD_WHITE, GxEPD_WHITE, GxEPD_BLACK
};
const char *labels[] = {
"BLACK", "WHITE", "GREEN", "BLUE", "RED", "YELLOW"
};

display.setTextSize(2);
for (int i = 0; i < 6; ++i) {
const int16_t x = 20 + i * 128;
display.fillRect(x, 420, 112, 40, colors[i]);
display.drawRect(x, 420, 112, 40, GxEPD_BLACK);
display.setTextColor(labelColors[i]);
display.setCursor(x + 20, 433);
display.print(labels[i]);
}
}

void drawCirclesGrid() {
// 每格的颜色组合:背景、外环、中环、内圆
const uint16_t cellColors[15][4] = {
{GxEPD_YELLOW, GxEPD_BLUE, GxEPD_RED, GxEPD_WHITE},
{GxEPD_WHITE, GxEPD_GREEN, GxEPD_YELLOW, GxEPD_BLACK},
{GxEPD_RED, GxEPD_WHITE, GxEPD_BLUE, GxEPD_YELLOW},
{GxEPD_BLUE, GxEPD_YELLOW, GxEPD_WHITE, GxEPD_RED},
{GxEPD_BLACK, GxEPD_RED, GxEPD_YELLOW, GxEPD_GREEN},
{GxEPD_GREEN, GxEPD_WHITE, GxEPD_RED, GxEPD_BLACK},
{GxEPD_RED, GxEPD_YELLOW, GxEPD_GREEN, GxEPD_WHITE},
{GxEPD_BLUE, GxEPD_WHITE, GxEPD_YELLOW, GxEPD_RED},
{GxEPD_YELLOW, GxEPD_BLACK, GxEPD_WHITE, GxEPD_BLUE},
{GxEPD_WHITE, GxEPD_RED, GxEPD_BLUE, GxEPD_YELLOW},
{GxEPD_GREEN, GxEPD_YELLOW, GxEPD_BLACK, GxEPD_WHITE},
{GxEPD_WHITE, GxEPD_BLUE, GxEPD_GREEN, GxEPD_YELLOW},
{GxEPD_YELLOW, GxEPD_RED, GxEPD_WHITE, GxEPD_GREEN},
{GxEPD_BLACK, GxEPD_WHITE, GxEPD_RED, GxEPD_YELLOW},
{GxEPD_BLUE, GxEPD_WHITE, GxEPD_RED, GxEPD_YELLOW},
};

const int16_t originX = 20;
const int16_t originY = 70;
const int16_t cellW = 152;
const int16_t cellH = 110;
const int16_t radius[] = {48, 32, 16};

for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 5; ++col) {
const uint16_t *colors = cellColors[row * 5 + col];
const int16_t x = originX + col * cellW;
const int16_t y = originY + row * cellH;
const int16_t cx = x + cellW / 2;
const int16_t cy = y + cellH / 2;

display.fillRect(x, y, cellW, cellH, colors[0]);
for (int ring = 0; ring < 3; ++ring) {
display.fillCircle(cx, cy, radius[ring], colors[ring + 1]);
}
display.drawRect(x, y, cellW, cellH, GxEPD_BLACK);
}
}
}

void drawColorStudy() {
display.fillScreen(GxEPD_WHITE);
display.setTextWrap(false);

// 黑色标题栏
display.fillRect(12, 12, 776, 44, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
display.setTextSize(3);
display.setCursor(200, 24);
display.print("SPECTRA 6 COLOR DISPLAY");

// 5 × 3 同心圆阵列
drawCirclesGrid();

// 外框与色卡
display.drawRect(0, 0, 800, 480, GxEPD_BLACK);
drawPalette();
}

void setup() {
Serial.begin(115200);
delay(100);

if (!initEpaperPower()) {
Serial.println("EPD power initialization failed.");
while (true) {
delay(1000);
}
}

// 使用板载 SPI 引脚初始化电子纸
SPI.begin(EPD_SCK, -1, EPD_MOSI, EPD_CS);
display.epd2.selectSPI(
SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0));

display.init(115200);
// 使显示内容与开发板的观看方向一致
display.setRotation(2);
display.setFullWindow();

// 分页绘制并刷新整屏
display.firstPage();
do {
drawColorStudy();
} while (display.nextPage());

// 刷新完成后使屏幕进入休眠
display.hibernate();
Serial.println("PhotoPainter refresh completed.");
}

void loop() {
}

setup() 会先初始化 AXP2101,将 ALDO4 设置为 3.3 V 并开启输出,然后再初始化 SPI 和墨水屏。若 PMIC 初始化、调压或输出使能失败,程序会停止在屏幕初始化之前,避免在 EPD_VCC 状态不确定时访问屏幕。为使显示内容与开发板的观看方向一致,示例通过 setRotation(2) 将画面旋转 180°。

六色像素编码

GxEPD2_7C 的分页缓冲区按 4bpp 保存像素,一个字节可记录两个像素:前一个像素位于高 4 位,后一个像素位于低 4 位。GxEPD2 先将颜色常量转换为分页缓冲区编码,再由 GxEPD2_730c_GDEP073E01 驱动转换为面板原生编码。

颜色GxEPD2 颜色常量分页缓冲区编码面板原生编码
GxEPD_BLACK0000 (0x0)0000 (0x0)
GxEPD_WHITE0001 (0x1)0001 (0x1)
绿GxEPD_GREEN0010 (0x2)0110 (0x6)
GxEPD_BLUE0011 (0x3)0101 (0x5)
GxEPD_RED0100 (0x4)0011 (0x3)
GxEPD_YELLOW0101 (0x5)0010 (0x2)

以上映射以本例使用的 GxEPD2 v1.6.9 中 GxEPD2_7C.hGxEPD2_730c_GDEP073E01.cpp 为准,表格只列出本例使用的六种颜色。例如,连续两个像素依次为绿、红时,分页缓冲区中的数据为 0010 0100,即 0x24;驱动发送到面板前会将其转换为 0110 0011,即 0x63。使用 Adafruit GFX 绘图接口时不需要手动处理该转换;直接写入原生图像数据时,才需要按面板原生编码组织像素。

完整的 800×480 帧缓冲区需要 800 × 480 × 4 ÷ 8 = 192000 字节。示例将模板参数设置为 40,每次只缓冲 40 行,占用 16000 字节;firstPage()nextPage() 会依次完成各页绘制和整屏刷新。

六种原生颜色可直接绘制,不需要抖动;橙色、紫色、灰阶等其他颜色的显示方式见通过抖动显示混合颜色

通过抖动显示混合颜色

抖动利用人眼的空间混色特性:将若干种原生颜色按一定比例交错排布在相邻像素上,在正常观看距离下即呈现为原生色之外的颜色。最简单的情形是两色混合:红色与黄色按 1:1 棋盘格排布呈现为橙色,黑色与白色按不同比例排布呈现为不同深浅的灰色,比例连续变化即可近似渐变。全彩墨水屏显示照片正是以此方式实现的:转换算法将照片的每个像素映射为原生色的空间组合,近距离观察可以看到颗粒,观看距离越远混色越自然。

常用的抖动算法分为两类:

  • 有序抖动(ordered dithering):用一个固定的阈值矩阵(如 Bayer 矩阵)逐像素查表,决定每个像素取哪种原生色。实现简单、可在单片机上实时计算,生成的图案规则,适合色块、渐变等程序绘制的内容。
  • 误差扩散(error diffusion):如 Floyd–Steinberg 算法,将每个像素颜色近似后的误差按比例分摊给相邻像素,颗粒分布更自然,适合照片;计算量较大,通常在 PC 上离线转换后再写入设备。

显示照片时不需要自行实现这些算法:ESP32-S3-PhotoPainter 的官方示例包含照片转换和 SD 卡读图的完整流程,建议直接使用,见 产品 Wiki

下面的程序使用有序抖动绘制一幅混色示意画面,可直接烧录体验混色效果。画面上半部分为六组混合色块,每格上方并排显示参与混合的两种原生纯色,下方为两者的 1:1 抖动混合结果,六组配对覆盖全部六种原生颜色;下半部分为黑到白、红到黄两条渐变条,首末两段为纯色。开发板设置、板载接口、供电与初始化流程均与上例相同。

#include <SPI.h>
#include <Wire.h>

#define XPOWERS_CHIP_AXP2101
#include <XPowersLib.h>
#include <GxEPD2_7C.h>

constexpr int PMU_SDA = 47;
constexpr int PMU_SCL = 48;

constexpr int EPD_DC = 8;
constexpr int EPD_CS = 9;
constexpr int EPD_SCK = 10;
constexpr int EPD_MOSI = 11;
constexpr int EPD_RST = 12;
constexpr int EPD_BUSY = 13;

using EpdDriver = GxEPD2_730c_GDEP073E01;

// 每次缓冲 40 行:800 × 40 × 4 ÷ 8 = 16000 字节
GxEPD2_7C<EpdDriver, 40> display(
EpdDriver(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));

XPowersPMU power;

bool initEpaperPower() {
if (!power.begin(
Wire, AXP2101_SLAVE_ADDRESS, PMU_SDA, PMU_SCL)) {
Serial.println("AXP2101 initialization failed.");
return false;
}

// 设置并开启电子纸的 3.3 V 供电
if (!power.setALDO4Voltage(3300)) {
Serial.println("Failed to set ALDO4 to 3.3 V.");
return false;
}

if (!power.enableALDO4()) {
Serial.println("Failed to enable ALDO4.");
return false;
}

delay(10);
return true;
}

// 标准 4×4 Bayer 阈值矩阵,16 个位置取遍 0~15
const uint8_t kBayer4x4[4][4] = {
{ 0, 8, 2, 10},
{12, 4, 14, 6},
{ 3, 11, 1, 9},
{15, 7, 13, 5},
};

// 以 level/16 的比例将 colorB 混入 colorA,抖动填充矩形
void fillRectDithered(int16_t x, int16_t y, int16_t w, int16_t h,
uint16_t colorA, uint16_t colorB, uint8_t level) {
for (int16_t dy = 0; dy < h; ++dy) {
for (int16_t dx = 0; dx < w; ++dx) {
const uint16_t color =
level > kBayer4x4[dy & 3][dx & 3] ? colorB : colorA;
display.drawPixel(x + dx, y + dy, color);
}
}
}

// 六组混合色块:每格上方为参与混合的两种原生纯色,下方为 1:1 抖动混合结果
void drawMixedSwatches() {
const uint16_t mixes[6][2] = {
{GxEPD_RED, GxEPD_YELLOW}, // 橙
{GxEPD_RED, GxEPD_BLUE}, // 紫
{GxEPD_RED, GxEPD_WHITE}, // 粉
{GxEPD_BLACK, GxEPD_WHITE}, // 灰
{GxEPD_BLUE, GxEPD_WHITE}, // 浅蓝
{GxEPD_YELLOW, GxEPD_GREEN}, // 黄绿
};
const char *labels[6] = {
"RED+YEL", "RED+BLU", "RED+WHT",
"BLK+WHT", "BLU+WHT", "YEL+GRN"
};

display.setTextSize(2);
display.setTextColor(GxEPD_BLACK);
for (int i = 0; i < 6; ++i) {
const int16_t x = 20 + i * 128;
display.fillRect(x, 90, 56, 28, mixes[i][0]);
display.fillRect(x + 56, 90, 56, 28, mixes[i][1]);
fillRectDithered(x, 118, 112, 84, mixes[i][0], mixes[i][1], 8);
display.drawRect(x, 90, 112, 112, GxEPD_BLACK);
display.drawFastHLine(x, 118, 112, GxEPD_BLACK);
display.setCursor(x + 8, 212);
display.print(labels[i]);
}
}

// 16 段抖动渐变条,首段为纯 colorA,末段为纯 colorB
void drawGradientBar(int16_t x, int16_t y, int16_t w, int16_t h,
uint16_t colorA, uint16_t colorB) {
const int16_t stepW = w / 16;
for (int i = 0; i < 16; ++i) {
// 将第 0~15 段映射到抖动等级 0~16
fillRectDithered(x + i * stepW, y, stepW, h,
colorA, colorB, i * 16 / 15);
}
display.drawRect(x, y, w, h, GxEPD_BLACK);
}

void drawDitherStudy() {
display.fillScreen(GxEPD_WHITE);
display.setTextWrap(false);

// 黑色标题栏
display.fillRect(12, 12, 776, 44, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
display.setTextSize(3);
display.setCursor(202, 24);
display.print("SPECTRA 6 DITHER STUDY");

drawMixedSwatches();

display.setTextSize(2);
display.setTextColor(GxEPD_BLACK);
display.setCursor(16, 252);
display.print("BLACK -> WHITE");
drawGradientBar(16, 274, 768, 64, GxEPD_BLACK, GxEPD_WHITE);

display.setCursor(16, 366);
display.print("RED -> YELLOW");
drawGradientBar(16, 388, 768, 64, GxEPD_RED, GxEPD_YELLOW);

display.drawRect(0, 0, 800, 480, GxEPD_BLACK);
}

void setup() {
Serial.begin(115200);
delay(100);

if (!initEpaperPower()) {
Serial.println("EPD power initialization failed.");
while (true) {
delay(1000);
}
}

// 使用板载 SPI 引脚初始化电子纸
SPI.begin(EPD_SCK, -1, EPD_MOSI, EPD_CS);
display.epd2.selectSPI(
SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0));

display.init(115200);
// 使显示内容与开发板的观看方向一致
display.setRotation(2);
display.setFullWindow();

// 分页绘制并刷新整屏
display.firstPage();
do {
drawDitherStudy();
} while (display.nextPage());

// 刷新完成后使屏幕进入休眠
display.hibernate();
Serial.println("Dither study refresh completed.");
}

void loop() {
}

刷新完成后,先贴近观察:色块上方的纯色区没有颗粒,下方混合区和渐变条中段可以看到规则的像素排布图案;再逐渐拉远距离,颗粒感消失,混合区呈现为均匀的中间色。这正是全彩墨水屏显示照片的方式。

刷新与休眠

刷新期间应等待 EPD_BUSY 释放,不要复位设备或断开电源。nextPage() 完成整屏刷新后,调用 display.hibernate() 使屏幕驱动进入深度休眠。该函数只管理屏幕驱动,整机低功耗和电源管理仍需按 ESP32-S3-PhotoPainter 的电源电路另行处理。

如果使用其他墨水屏,屏幕是否支持局刷、局刷范围以及最小刷新间隔均以对应产品资料为准。

5.5 常见问题

现象常见原因处理
烧录后屏幕仍显示上一次的画面墨水屏断电保持画面,程序未正常运行时屏幕不会有任何变化按对应产品页的 Arduino 工程参数设置(开发板型号、USB CDC On Boot、Flash Size、Partition Scheme、PSRAM)重新烧录,完成后复位或重新插拔 USB;串口无输出可作为程序未运行的判断依据
屏幕无任何反应屏幕供电未开启;SPI 引脚未重映射按开发板资料开启屏幕供电(可能由 GPIO 或电源管理芯片控制);确认在 display.init() 之前调用了 SPI.begin() 并传入正确引脚
程序卡在初始化或刷新不结束BUSY 引脚接错,驱动一直等待忙状态释放核对 BUSY 引脚号;GxEPD2 等待超时会在串口输出 Busy Timeout,可作为判断依据
编译时找不到驱动类GxEPD2 版本过低,未包含该面板支持升级 GxEPD2(ESP32-S3-ePaper-3.97 的黑白面板需 v1.6.5 以上,ESP32-S3-ePaper-1.54G 的四色面板需 v1.6.6 以上,ESP32-S3-PhotoPainter 的六色面板需 v1.5.9 以上)
画面能刷新但内容异常(发灰、颜色错乱、镜像)面板驱动类与实际面板不匹配按面板型号(而非驱动 IC 型号)重新选择驱动类
全彩屏显示照片近看颗粒明显、色彩不如样图鲜艳六色面板通过抖动模拟原生色之外的颜色,属正常显示原理通过抖动显示混合颜色;适当增大观看距离,混色更自然
局刷若干次后出现残影局刷偏差累积,属正常现象执行一次全刷清除;按刷新机制的策略定期全刷,代码见全刷与局刷对比
全刷后仍能看到前一画面的痕迹面板驱动默认使用快速全刷,粒子复位不充分对提供快速全刷的面板,调用 display.epd2.selectFastFullUpdate(false) 切换为标准全刷
休眠后无法再次刷新hibernate() 后驱动处于深度休眠,不再响应数据重新开启屏幕电源(如此前已关闭)并再次初始化屏幕驱动
低温环境下刷新变慢、残影加重电泳粒子在低温下泳动变慢确认环境温度在屏幕规格的工作范围内