返回 00-项目导航 · 数据发送见 02-墨水屏驱动与串行通信 · 绘制算法见 03-画布像素与文字显示
本文精读
moqian 驱动演示工程,依据 2026-10-25 静态快照。工程使用 ESP-IDF,目标esp32s3;当前应用是顺序执行的墨水屏演示,没有在这份主程序中实现网络服务、天气、人工智能或多应用终端。本次不修改、编译或烧录固件。
一、启动入口为何是这个函数
裸机课程里常从 main() 开始,ESP-IDF 应用则实现 app_main()。进入它之前,框架已经完成必要启动,并启动实时操作系统调度器。框架创建主任务,再由主任务调用用户的 app_main(),所以这里可以使用 vTaskDelay() 等任务接口,无需自己再次启动调度器。
main.c:202–231 就是本工程的应用入口。它先调用 epd_init(),检查返回值;再取得画布,检查是否为空;最后按顺序执行五个演示函数。源码没有在这条应用流程里调用 xTaskCreate()。因此,准确说法是“应用在框架主任务中串行执行”,而不是“工程没有运行实时操作系统”。系统仍有空闲任务和框架任务。乐鑫启动流程文档说明了主任务与应用入口的关系。
esp_err_t ret = epd_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "E-Paper init failed! Check wiring.");
return;
}
uint8_t *buf = epd_get_buffer();
if (!buf) {
ESP_LOGE(TAG, "Failed to allocate frame buffer!");
return;
}
这段检查有边界:总线初始化和添加设备失败能经 ret 传回;忙信号等待超时并没有返回错误,详见 02-墨水屏驱动与串行通信。不能把 ret==ESP_OK 解读为所有硬件交互已经验证成功。
二、按执行顺序拆开五个演示阶段
| 阶段 | 源码入口 | 主要学习内容 | 静态能确认的操作 |
|---|---|---|---|
| 全屏刷新 | demo_full_refresh():38–65 |
画布合成、背景与文字层次 | 棋盘格、标题横幅、边框,调用全刷后延时 |
| 快速刷新 | demo_fast_refresh():67–104 |
文本布局和几何图形 | 清白画布、绘制说明与图形,调用快刷 |
| 计数更新 | demo_fast_refresh_2():106–133 |
循环、格式化文本、进度条 | 计数 0~4,共五次整帧快刷 |
| 局部刷新 | demo_partial_refresh():135–181 |
静态区域与变化区域 | 先全刷底图,再更新局部文本区域 |
| 显示休眠 | demo_sleep():183–198 |
最后画面与显示器件状态 | 全刷告别画面,再调用 epd_sleep() |
五个函数在 app_main():224–228 依次调用。函数返回后才会开始下一个,所以计数器循环不会与局刷演示并行。屏幕上的 Status: OK、MCU: ESP32-S3-N16R8 等是固定绘制的字符串,不是运行时状态检测和芯片容量探测结果。当前实际配置应读 sdkconfig,实物型号还要核对模组丝印。
三、棋盘格是理解二维图像的好入口
draw_checkerboard():19–34 遍历整个 400×300 画布。它先计算 bx=x/cell_size、by=y/cell_size,把像素坐标转换成棋盘格编号;格编号之和为偶数时设白,为奇数时设黑。对 cell_size=20 而言,画面横向 20 格、纵向 15 格。
int bx = x / cell_size;
int by = y / cell_size;
uint32_t byte_idx = (y * EPD_WIDTH + x) / 8;
uint8_t bit_pos = 7 - (x % 8);
例如 (19,19) 仍在第 (0,0) 格,(20,19) 已进入第 (1,0) 格,因此两点颜色相反。这里直接操作传入画布,没有经过 epd_draw_pixel() 的坐标旋转。之后 demo_full_refresh():49 明确设为零度,再绘制横幅和边框,所以当前演示保持了一致方向。若以后在非零旋转下复用背景生成函数,需要重新核对两种坐标系统。
四、计数器如何把数值变成画面
demo_fast_refresh_2():112–132 中每次循环先清白画布,再使用 snprintf(line,sizeof(line),...) 把整数格式化为字符串。line[64] 是栈上的临时文本数组,绘制函数立即读取它并把笔画写入长期画布,不会保存 line 指针供稍后使用。
进度条宽度为 (i+1)×70,外框宽 350,所以五次填充宽度依次为 70、140、210、280、350;百分比依次为 20%、40%、60%、80%、100%。计数文本使用 i / 4 的视觉形式,虽然从 0 开始,但进度条从 20% 开始,这是当前演示的设计,不应误认为两者都从零百分比起步。
epd_display_fast(buf) 会执行数据写入和忙信号等待,完成后才执行 vTaskDelay(2000毫秒对应节拍)。因此,真实两次计数显示的间隔近似为:绘图耗时+初始化和串行传输耗时+控制器刷新等待+主动等待 2 秒+调度误差,而非严格每 2 秒更新一次。
五、任务延时与死等有什么不同
当前 sdkconfig:1187 为 CONFIG_FREERTOS_HZ=100,即一个节拍为 10 毫秒。pdMS_TO_TICKS() 把毫秒转换成节拍数,而 vTaskDelay() 的参数单位是节拍。任务进入阻塞态等待时间到达,处理器可以调度其他就绪任务;这与用空循环持续占用处理器不同。
| 毫秒参数 | 当前转换结果 | 应如何理解 |
|---|---|---|
| 10 | 1 节拍 | 忙信号轮询的基本间隔 |
| 20 | 2 节拍 | 复位前后延时使用的量级 |
| 1500 | 150 节拍 | 局刷循环末尾的主动等待 |
| 2000 | 200 节拍 | 计数器循环末尾的主动等待 |
| 3000 | 300 节拍 | 前两个阶段结束后的主动等待 |
| 2 | 0 节拍 | 不能保证获得两毫秒的阻塞等待 |
最后一行有真实源码依据:epd_ssd1683.c:194 使用 vTaskDelay(pdMS_TO_TICKS(2)) 生成复位低电平间隔;本机 ESP-IDF 5.1.2 的 components/freertos/FreeRTOS-Kernel/include/freertos/projdefs.h:48 使用整数除法,2×100/1000=0。因此,这段写法不能保证注释暗示的两毫秒脉宽;具体 GPIO 波形需要仪器测量。这里没有改用其他延时函数,也没有断言复位已经失败。
vTaskDelay() 是相对当前调用时刻计时,循环工作时间会累加到周期里。对严格周期采样,应理解以绝对节拍为基准的 xTaskDelayUntil();但即使使用它,执行时间超过周期仍会造成超时,不能靠换接口消除显示刷新耗时。乐鑫任务接口文档对此有说明。
六、局部更新需要协调画布和控制器状态
局刷演示先清白画布、绘制静态外框和“正在更新”文字,然后调用一次全刷建立底图。之后每次循环只清白缓冲区中内部区域,绘制新字符串,再发送局部刷新。这说明应用层的局部重绘和驱动层的局部传输是两个环节:只在内存里改一小块,不会自动减少全屏发送;反过来,只发送局部区域,也要求该区域内容完整正确。
当前 main.c:177 传递 (81,101,238,98),而驱动头文件要求横坐标、宽度按 8 像素对齐。驱动又直接使用 w/8,没有自动补齐、合并边界位或报错。问题计算见 02-墨水屏驱动与串行通信。因此,这段演示可用于学习预期调用过程,同时必须保留“局刷参数与驱动约束不一致”的事实。
weixue 对照工程采用的旧帧、新帧和刷新后同步逻辑,也不是简单给本程序加一个任务就会自动出现。两个工程的数据所有权、刷新协议和功能范围应分别阅读。
七、为什么显示休眠不等于整机低功耗
demo_sleep():193–197 先显示告别画面,再调用 epd_sleep()。后者在 epd_ssd1683.c:395–401 发送 0x10 命令和 0x01 参数,随后延时。它操纵的是显示控制器;源码没有在这条流程调用 esp_deep_sleep_start(),也没有配置 ESP32 的唤醒源。
app_main() 最终在 main.c:231 返回。ESP-IDF 允许应用入口返回,框架会清理主任务,其他系统任务继续调度。不能把函数返回理解为 MCU 断电或深睡眠。整机静态电流还涉及 MCU、显示驱动板、稳压器、指示灯及外设;需要实际供电测量。
乐鑫将轻度睡眠和深度睡眠作为 MCU 电源管理机制,分别提供 esp_light_sleep_start()、esp_deep_sleep_start() 等入口,深度睡眠会关闭 CPU 等电源域,见 ESP32-S3 睡眠模式文档。若以后要做电池终端,应单独设计完成刷屏、显示休眠、关闭不需要的外设、配置唤醒、进入 MCU 睡眠的流程;当前尚未实现。
八、未来需要多任务时,先明确谁拥有显示资源
当前一个主任务串行使用全局画布和显示句柄,没有在应用层增加互斥量。这个结构容易跟踪:绘图时不会有另一个应用任务同时改同一帧。
若以后增加传感器或网络功能,一种可研究的结构是由采集任务产生数据,由显示任务独占画布和刷新操作,其他任务通过队列发送更新请求。这是后续设计建议,不是对现有工程的描述。需要先定义请求频率、数据生命周期、积压时如何取舍,以及刷新失败如何报告,再决定是否真正拆任务。
不能让多个任务分别调用底层命令发送后,就认为实时操作系统会自动保证整段显示协议原子执行。一次 SPI 事务完成,只保证该次事务;窗口命令、地址参数、图像数据和激活命令组成的多次调用仍需要一致的所有权。官方串行总线文档也要求同一设备的多任务访问使用明确的串行化机制。
九、可验证练习与验收
- 绘制调用时序。 从
app_main()写出五阶段顺序,标注每次绘图、传输、忙信号等待和主动延时的位置。验收:不把五个演示误写成五个并行任务。 - 计算更新间隔。 假设一轮绘图与刷新共用时 800 毫秒,再主动延时 2000 毫秒,估计两轮开始间隔。验收:得到约 2800 毫秒,并能说明调度仍有误差;800 毫秒只是练习给定值,不是项目实测。
- 检查一次计数迭代。 取
i=2,推导显示文本、进度条宽度和百分比。验收:计数为2 / 4,宽度为 210,百分比为 60%。 - 区分三种等待。 分别说明任务阻塞延时、等待显示忙信号、MCU 深度睡眠的主体与作用。验收:能明确显示休眠函数没有让 MCU 进入深度睡眠。
- 后续上板测量,尚未执行。 在可复现验证版本中记录各阶段开始结束时间,观察复位低电平脉宽、忙信号时长以及告别画面后整机电流。验收记录必须同时写软件配置、仪器接法和实物型号,不能只写“运行正常”。
十、继续阅读的官方资料
- 乐鑫应用启动流程:应用入口、主任务、返回后的行为。
- 乐鑫实时任务接口:任务状态、节拍延时、周期调度;该版本创建任务时栈大小单位为字节。
- 乐鑫睡眠模式:MCU 电源域、睡眠与唤醒。
- 乐鑫串行总线并发访问说明:同一设备的多任务访问与同步要求。
来源与关联索引
- 来源
- 嵌入式比赛
- 原笔记
- 项目/墨嵌墨水屏/知识讲解/04-主程序与实时任务.md
- 资料性质
- 实践指南。原文的版本、来源与验证边界继续保留。