E-Paper(墨水屏)
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。
3. 刷新机制:全刷与局刷
刷新机制是墨水屏与其他屏幕差别最大的部分,也是使用中问题最集中的部分。
主控发给墨水屏的图像数据只规定每个像素最终显示黑还是白;但电泳粒子有惯性,无法像 LCD 那样切换一次电压就到位。驱动 IC 需要按一套多阶段驱动波形,依次施加不同极性、不同时长的电压脉冲,才能把粒子可靠地驱动到目标位置。这套波形参数以 LUT(Look-Up Table,查找表) 的形式预存在驱动 IC 内,或由驱动库在初始化时写入。波形不同,刷新效果和速度也不同,由此分为两种刷新方式:
-
全刷(Full Refresh):刷新前先将全屏在黑白之间往复驱动数次(表现为整屏闪烁),使所有粒子充分复位后再写入新画面。速度慢,但能彻底清除残影,画面质量最好。
-
局刷(Partial Refresh):只把变化的像素直接驱动到目标状态,不做全屏闪烁。速度快(可达亚秒级)、无闪烁,适合更新时钟数字、传感器读数等小块内容;但短促的波形无法把粒子推到两极的稳定极限位置,部分粒子停在中间态,多次局刷后这种偏差不断累积,形成残影,需要靠全刷的往复驱动把粒子重新压回两极才能清除。
从单个胶囊看,残影的本质就是粒子没有回到两极的极限位置:
实际使用的通用策略:
- 上电后先做一次全刷,建立干净的基准画面。
- 高频的局部区域更新用局刷。
- 每隔若干次局刷(经验值 5~10 次)或定期(如每小时)做一次全刷,清除累积的残影。
- 刷新期间驱动 IC 通过 BUSY 引脚指示忙碌状态,程序必须等待 BUSY 释放后才能发起下一次操作,不可打断。
- 不要以过高频率连续刷新墨水屏,这会加速屏幕老化;三色及以上的彩色屏两次刷新的间隔建议不低于官方产品页标注的最小值。
- 设备长期不用时,建议先把屏幕刷白再存放,避免长时间保持同一画面造成永久性残影。
- 刷新过程中不要断电,否则画面可能停留在中间态,需要全刷恢复。
4. 常见驱动 IC 与接口
墨水屏对主控的接口是标准 SPI,在 显示基础与接口 的引脚基础上多一根 BUSY(忙碌指示,输入):DIN、CLK、CS、DC、RST、BUSY。常见驱动 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 通用准备
- 参考 Arduino IDE 开发环境搭建教程 安装 Arduino IDE,并安装 arduino-esp32 v3.2.0 或更高版本。
- 在库管理器(工具 > 管理库)中搜索并安装
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-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_PWR | GPIO6 | 屏幕电源控制,低电平开启、高电平关闭 |
| EPD_BUSY | GPIO8 | 刷新忙状态 |
| EPD_RST | GPIO9 | 复位 |
| EPD_DC | GPIO10 | 命令 / 数据选择 |
| EPD_CS | GPIO11 | SPI 片选 |
| EPD_SCLK | GPIO12 | SPI 时钟 |
| EPD_SDI | GPIO13 | SPI 数据(即 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_BLACK、GxEPD_WHITE、GxEPD_YELLOW 或 GxEPD_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(),使屏幕驱动进入深度休眠。如果开发板提供独立的屏幕电源控制引脚,再按对应原理图关闭屏幕电源。 - 使用微雪官方库时,应调用对应型号的休眠函数。如果开发板提供独立的屏幕电源控制引脚,也应按对应原理图关闭屏幕电源。
- 屏幕进入休眠后不会处理新的图像数据。再次刷新前,需要重新开启屏幕电源(如果此前已关闭)并初始化屏幕驱动。
- 长期不使用时,先将屏幕刷白,再进入休眠并断开屏幕电源。
5.3 全彩屏: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_DC | GPIO8 | 命令 / 数据选择 |
| EPD_CS | GPIO9 | SPI 片选 |
| EPD_SCK | GPIO10 | SPI 时钟 |
| EPD_DIN | GPIO11 | SPI 数据 |
| EPD_RST | GPIO12 | 复位 |
| EPD_BUSY | GPIO13 | 刷新忙状态 |
| PMU_SDA | GPIO47 | AXP2101 I2C 数据 |
| PMU_SCL | GPIO48 | AXP2101 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_BLACK | 0000(0x0) | 0000(0x0) |
| 白 | GxEPD_WHITE | 0001(0x1) | 0001(0x1) |
| 绿 | GxEPD_GREEN | 0010(0x2) | 0110(0x6) |
| 蓝 | GxEPD_BLUE | 0011(0x3) | 0101(0x5) |
| 红 | GxEPD_RED | 0100(0x4) | 0011(0x3) |
| 黄 | GxEPD_YELLOW | 0101(0x5) | 0010(0x2) |
以上映射以本例使用的 GxEPD2 v1.6.9 中 GxEPD2_7C.h 和 GxEPD2_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 的电源电路另行处理。
如果使用其他墨水屏,应先确认面板型号,再从 GxEPD2 支持列表中选择对应的显示类和驱动类;使用微雪官方库时,则应改用对应型号的驱动文件和刷新 API。屏幕是否支持局刷、局刷范围以及最小刷新间隔均以对应产品资料为准。
5.4 常见问题
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 屏幕无任何反应 | 屏幕供电未开启;SPI 引脚未重映射 | 按开发板资料开启屏幕供电(可能由 GPIO 或电源管理芯片控制);确认在 display.init() 之前调用了 SPI.begin() 并传入正确引脚 |
| 程序卡在初始化或刷新不结束 | BUSY 引脚接错,驱动一直等待忙状态释放 | 核对 BUSY 引脚号;GxEPD2 等待超时会在串口输出 Busy Timeout,可作为判断依据 |
| 编译时找不到驱动类 | GxEPD2 版本过低,未包含该面板支持 | 升级 GxEPD2(ESP32-S3-ePaper-1.54G 的四色面板需 v1.6.6 以上,ESP32-S3-PhotoPainter 的六色面板需 v1.5.9 以上) |
| 画面能刷新但内容异常(发灰、颜色错乱、镜像) | 面板驱动类与实际面板不匹配 | 按面板型号(而非驱动 IC 型号)重新选择驱动类 |
| 全彩屏显示照片近看颗粒明显、色彩不如样图鲜艳 | 六色面板通过抖动模拟原生色之外的颜色,属正常显示原理 | 见通过抖动显示混合颜色;适当增大观看距离,混色更自然 |
| 局刷若干次后出现残影 | 局刷偏差累积,属正常现象 | 执行一次全刷清除;按刷新机制的策略定期全刷 |
| 休眠后无法再次刷新 | hibernate() 后驱动处于深度休眠,不再响应数据 | 重新开启屏幕电源(如此前已关闭)并再次初始化屏幕驱动 |
| 低温环境下刷新变慢、残影加重 | 电泳粒子在低温下泳动变慢 | 确认环境温度在屏幕规格的工作范围内 |