M5Stack官方推出了一款 Xiaozhi Card Kit(小智墨伴),是ESP32-S3主控 + 2.7 英寸触控电子墨水屏,已经接入了小智。

我也想试试,给 M5Paper 适配开源的“小智 AI"项目,打造一个带电子纸屏幕的 AI 对话终端。
然而,当我准备在 ESP-IDF v5.5 环境下驱动这块屏幕(驱动芯片为 IT8951E)时,却发现并没有现成的、开箱即用的驱动。从最初尝试手写 SPI 驱动踩坑,到最终发现 M5Unified 这一优雅方案,我经历了一番折腾。
本文将详细记录这段历程,希望能帮到同样想在 ESP-IDF 下驱动 M5Paper 的开发者们。
一、硬件透视:M5Paper 与 IT8951E
M5Paper 的屏幕并非普通的 SPI 屏幕,它内部集成了一颗 IT8951E 显示控制器。这颗芯片负责管理电子纸的复杂刷新时序(如全局刷新、局部刷新、16 级灰度抖动等)。
关键参数主控:ESP32-D0WDQ6 (双核 240MHz)
屏幕:4.7" E-ink (ED047TC1)
分辨率:960 x 540 (横屏) / 540 x 960 (竖屏)
色彩:16 级灰度 (4-bit)
显存:IT8951E 内部自带,但需要 ESP32 提供 PSRAM 作为帧缓冲
IT8951E 通过 SPI 接口与 ESP32 通信,关键引脚如下:
| MOSI | GPIO 12 | SPI 数据输出 |
| MISO | GPIO 13 | SPI 数据输入 |
| SCLK | GPIO 14 | SPI 时钟 |
| CS | GPIO 15 | 片选信号 |
| RST | GPIO 23 | 复位信号 |
| BUSY | GPIO 27 | 忙信号 (低电平忙,高电平闲) |
从下图中,可以看到IT8951的关键引脚连接:
从下图中,可以看到IT8951的原理图:

二、方案选型:为什么放弃手写驱动?
起初,我尝试参考 IT8951 的数据手册手写驱动,结果踩了不少坑:
SPI 字节序混乱:IT8951 的命令和数据帧格式特殊,高低字节顺序容易搞反。
BUSY 信号逻辑:忙信号是低电平有效,但初始化时序中需要等待它变高,极易死锁。
复杂的刷新命令:局部刷新需要发送 7 个参数(坐标、宽高、模式、显存地址等),错一个就不显示。
PSRAM 依赖:960x540 的 4bpp 图像需要约 259KB 内存,ESP32 内部 SRAM 根本不够,必须配置 PSRAM。
后来我发现了 M5Unified 库。它是 M5Stack 官方推出的统一硬件抽象库,底层封装了 M5GFX 图形库。
方案对比:
| 代码量 | 500+ 行 | 10 行 |
| 灰度转换 | 需手动实现 Bayer 抖动 | 内置自动转换 |
| 刷新模式 | 需手动管理全局/局部刷新 | 自动节流优化 |
| 稳定性 | 容易 Watchdog 重启 | 经过大量验证 |
三、环境搭建1. 引入依赖
在 ESP-IDF v5.5 中,我们可以通过 Component Manager 轻松引入 M5Unified。在项目根目录的 idf_component.yml 中添加:
dependencies: m5stack/m5unified: "*"2. 开启 PSRAM (关键!)
M5Paper 的 ESP32 芯片外挂有 PSRAM,必须在 sdkconfig.defaults 中显式开启,否则分配显存时会直接崩溃。
# sdkconfig.defaults CONFIG_IDF_TARGET_ESP32=y CONFIG_SPIRAM=y CONFIG_SPIRAM_BOOT_INIT=y CONFIG_SPIRAM_USE_MALLOC=y CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096 CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768

四、核心代码解析
使用 M5Unified 驱动 M5Paper 的代码非常简洁。
1. 初始化只需调用 M5.begin(),库会自动检测硬件型号并初始化 IT8951。
#include <M5Unified.h>void setup() {
auto cfg = M5.config();
// 开启电源、IMU、RTC 等外设
cfg.output_power = true;
cfg.internal_imu = true;
cfg.internal_rtc = true;
M5.begin(cfg);
// 设置电子纸刷新模式 (epd_fastest 适合 UI 交互)
M5.Display.setEpdMode(m5gfx::epd_mode_t::epd_fastest);}2. 屏幕方向与文字适配IT8951 的旋转方向是“逆时针”的,这与常规屏幕不同。
setRotation(0):竖屏 (540x960)
setRotation(1):横屏 (960x540)
为了在不同方向下保持文字大小一致,建议根据短边动态计算字号:
void loop() {
// 设置竖屏
M5.Display.setRotation(0);
// 根据短边计算字号,保证横竖屏视觉一致
int short_side = M5.Display.width() < M5.Display.height()
? M5.Display.width() : M5.Display.height();
int text_size = short_side / 160;
if (text_size == 0) text_size = 1;
M5.Display.setTextSize(text_size);
M5.Display.fillScreen(TFT_WHITE);
M5.Display.setTextColor(TFT_BLACK);
M5.Display.setCursor(20, 20);
M5.Display.println("Hello M5Paper!");
M5.Display.printf("Resolution: %dx%d\n", M5.Display.width(), M5.Display.height());
// 触发刷新
M5.Display.display();
while(1) { vTaskDelay(1000); }}

五、踩坑记录1. SPI 命令字节序错误
在尝试手写驱动时,我发现发送 0x0302(获取设备信息)命令时,IT8951 毫无反应,MISO 线上全是高电平。
原因:IT8951 的 SPI 协议比较特殊。命令帧是 32 位的,格式为 [Cmd_Low][Cmd_High][0x00][0x60]。
而我最初是按照常规的大端序发送的 [0x03][0x02][0x00][0x60],导致 IT8951 无法识别命令。
解决:M5GFX 内部通过 getSwap16 宏处理了这个问题,自动将 0x0302 转换为正确的字节序发送。这也是为什么推荐使用 M5GFX/M5Unified 的原因之一——它屏蔽了这些底层协议细节。
IT8951 的 BUSY 引脚是 低电平有效(Low = Busy, High = Ready)。
但在初始化阶段,如果复位后没有等待足够的时间(约 800ms)或者没有正确等待 BUSY 变高就去读取数据,会导致 SPI 总线死锁,进而触发 ESP32 的 Task Watchdog Timeout。
这是最容易被忽视的一点。M5Paper 的屏幕分辨率高达 960x540,即使使用 4-bit 灰度,一帧图像也需要约 259KB 的内存。ESP32 的内部 SRAM 只有 520KB,根本不够用。
如果在 sdkconfig 中没有开启 PSRAM (CONFIG_SPIRAM=y),程序在尝试分配显存时会直接崩溃或返回空指针。
六、总结与展望
经过一番折腾,我最终确认:在 ESP-IDF 下驱动 M5Paper,直接使用 M5Unified 是最优解。
为什么推荐 M5Unified?开箱即用:无需手动配置 SPI 引脚、时序和 IT8951 寄存器,M5.begin() 自动搞定一切。
内置灰度转换:电子纸显示需要特殊的 Bayer 抖动算法来模拟灰度,M5GFX 内部已经完美实现,我们只需要传入 RGB565 图像即可。
刷新优化:它自动处理了电子纸的刷新节流(Throttling)和局部刷新逻辑,避免了频繁刷新导致的屏幕闪烁和残影。
生态整合:配合 LVGL 使用非常顺滑,可以轻松实现复杂的 UI 界面。
既然屏幕驱动问题已经解决,接下来的目标就很明确了:
适配小智 AI:将 M5Paper 接入小智 AI 的对话系统,利用电子纸低功耗的特性,做一个桌面的 AI 助手。
触摸支持:目前只搞定了显示,M5Paper 的 GT911 触摸驱动也需要在 ESP-IDF 下调试。
七、参考资料
M5GFX 源码:https://github.com/m5stack/M5GFX
M5Unified 源码:https://github.com/m5stack/M5Unified
IT8951 数据手册:https://www.waveshare.net/w/upload/8/89/IT8951E_v2.4.4.2.pdf
ESP-IDF 编程指南:https://docs.espressif.com/projects/esp-idf/en/latest/esp32/
我要赚赏金
