用 FreeRTOS 做一台墨水屏电子书阅读器
![]()
1. 项目要解决什么问题
这是一个运行在 STM32F407VGT6 上的离线墨水屏阅读器。系统从 SD 卡的 0:/ebooks 目录读取文本文件,在 2.66 英寸、152 × 296 黑白电子纸上显示,并提供文件浏览、翻页、断点续读和最近阅读等功能。
它的重点不只是“把文字显示出来”,而是把会阻塞很久的外设操作拆开:电子纸全刷、SPI DMA 传输、SD 卡文件访问和按键扫描不再挤在一个超级循环里。FreeRTOS 负责调度,任务之间通过消息队列和信号量协作,因此在刷屏或读文件期间,按键仍能被持续采样和保存。
主要能力如下:
- 浏览 SD 卡中的文本文件,最多缓存 50 个文件名;
- 按页读取文件,支持 ASCII 与 GBK 双字节文本的显示、自动换行和分页;
- 首次进入页面使用全刷,光标移动与翻页使用局刷;
- 四按键支持 30 ms 消抖、单击、双击和 1.5 s 长按识别;
- 将书签和最近打开文件保存到 W25Q128 外置 Flash;
- 使用双扇区镜像、序号和 CRC32 校验,降低写入过程中断电导致书签整体损坏的风险;
- 通过 USART1(115200 bps)输出异步调试日志,且日志任务不会抢占核心业务。
2. 硬件与软件栈
| 模块 | 作用 |
|---|---|
| STM32F407VGT6 | 主控,HSE 8 MHz 经 PLL 配置到 168 MHz |
| 2.66 英寸电子纸(SSD1681 兼容) | 152 × 296 黑白显示,帧缓冲为 5624 B |
| SD 卡 | 经 SDIO 挂载为 FatFs 文件系统,存放电子书 |
| W25Q128 | 16 MB SPI NOR Flash,用于书签和最近阅读记录 |
| 4 个按键 | 上下选择、确认、返回/长按返回主页等交互输入 |
| SPI1 / SPI2 + DMA | 分别服务于外设传输;DMA 完成后通过信号量唤醒等待任务 |
| FreeRTOS + CMSIS-RTOS v2 | 抢占式任务调度及队列、信号量抽象 |
CubeMX 配置中启用了 SDIO、SPI1、SPI2、USART1、DMA、FatFs 和 FreeRTOS。内核节拍为 1 kHz,FreeRTOS 堆大小为 20 KB,并使用 heap_4 动态内存实现。
3. 从“超级循环”到任务分工
程序启动后先完成 HAL、时钟、GPIO、DMA、SPI、串口、SDIO 和 FatFs 初始化;随后初始化 FreeRTOS 内核、SPI DMA 信号量及应用层 IPC 对象,创建任务并启动调度器。
main()
├─ 外设初始化 / SPI_BSP_Init()
├─ osKernelInitialize()
├─ IPC_Init()
├─ 创建 AppTask、KeyTask、EPDTask、DBGTask
└─ osKernelStart()| 任务 | 优先级 | 栈大小 | 职责 |
|---|---|---|---|
KeyTask | AboveNormal | 256 B | 每 10 ms 扫描按键、执行状态机并投递按键事件 |
AppTask(defaultTask) | Normal | 4 KB | 主页、文件浏览、文本分页、书签保存等应用逻辑 |
EPDTask | Normal | 1 KB | 独占执行电子纸初始化、全刷、局刷和休眠 |
DBGTask | Low | 256 B | 异步取出日志队列并经串口发送;关闭 DBG_ENABLE 后可在编译期消除 |
这种拆分的核心原则是:应用任务决定“显示什么”,显示任务决定“怎样操作屏幕”,按键任务只负责可靠地产生输入事件。 每个硬件资源的调用边界足够清楚,后期增加 Wi-Fi、下载或电量管理任务时也不会把业务逻辑重新塞回主循环。
4. 任务通信:队列传递意图,信号量传递完成
项目在 User/App/ipc.c 中统一创建了三个同步对象:
KeyTask -- g_key_queue(深度 8)--> AppTask
AppTask -- g_epd_queue(深度 2)--> EPDTask
AppTask <-- g_epd_done(二值信号量)-- EPDTaskKeyTask 将 {按键编号, 事件类型} 写入 g_key_queue,而 AppTask 使用无限期等待接收事件。这样应用处于等待输入时不消耗 CPU;当它正在等待一次电子纸刷新结束时,按键任务仍然可以继续工作,事件先暂存在队列中。
电子纸刷新则是一次“命令 + 完成确认”的同步调用:AppTask 将 WAKE、DISP_FULL、DISP_PART 或 SLEEP 命令放进队列,随后等待 g_epd_done。EPDTask 完成实际硬件访问后释放信号量。帧缓冲只在收到完成通知后被再次修改,因此避免了“DMA 还在读缓冲区,应用已经开始画下一帧”的数据竞争。
5. 电子纸刷新与 SPI DMA
电子纸刷新时间明显长于普通 LCD,项目据此设计了两类刷新策略:
- 全刷:首次显示主页、文件列表,或从阅读器返回列表时使用。完整更新当前帧,并同步控制器的前一帧 RAM(
0x26),画面更干净。 - 局刷:在文件列表中移动光标、阅读器翻页时使用。只更新变化区域,交互速度更好。
每一帧的大小为 (152 / 8) × 296 = 5624 B。电子纸和 Flash 的大块 SPI 传输使用 DMA:发起 DMA 后,调用任务阻塞等待相应 SPI 总线的二值信号量;DMA 中断回调 HAL_SPI_TxCpltCallback() 释放该信号量。于是等待传输完成的是任务而不是 CPU 的忙等循环,调度器可以运行其他就绪任务。
注意:电子纸驱动中的等待忙信号和 DMA 传输都依赖任务上下文,因此不能直接从中断服务程序调用这些显示或 Flash 写入函数。
![]()
6. 按键:小状态机换来稳定输入
按键不是在应用任务中轮询,而是由高优先级 KeyTask 每 10 ms 执行一次 Key_Tick()。驱动内部有 IDLE → DEBOUNCE → HELD → WAIT_DOUBLE 等状态,并设定:
- 消抖窗口:连续 3 个采样周期,即 30 ms;
- 长按阈值:150 个周期,即 1.5 s,达到阈值立即上报;
- 双击窗口:50 个周期,即 500 ms;
- 长按触发后不会再额外产生单击或双击事件。
这种方式把机械抖动和手势判定集中在驱动层,应用层只需处理 KEY_EVT_SHORT、KEY_EVT_DOUBLE、KEY_EVT_LONG,而不必关心引脚电平与计时细节。
当前交互以单击为主:主页可进入书架或继续上次阅读;书架中可上下移动并打开文件;阅读器中可翻页、退出并保存当前阅读位置;在文件浏览器中长按 K4 返回主页。具体按键提示会绘制在对应界面与状态栏中。
7. 文本阅读器:按需分页,而不是一次性读完整本书
AppTask 在打开文件后使用 512 B 读取缓冲区顺序读取内容,最多在屏幕上绘制 8 行正文,第 9 行显示页码与按键提示。程序处理换行、ASCII 和 GBK 双字节字符,并根据字体宽度自动折行。
这里有一个很实用的设计:系统不缓存整本书,而是缓存每一页起始位置的文件偏移量,最多 200 页,仅约 800 B。首次向后翻页时才计算并记录下一页偏移;回退时直接跳转到已经记录的偏移。这样在 RAM 资源有限的 MCU 上仍能阅读较大的文本文件。
视频占位:文件浏览与打开电子书
8. 书签为何要做成双扇区镜像
阅读位置保存在 W25Q128 中,每条记录由完整路径和页起始字节偏移组成;系统最多保存 50 条书签,并另存一条“最近打开文件”路径。
Flash 写入不具备事务性:如果刚擦除并写入到一半就掉电,单个扇区可能损坏。为此,项目用两个 4 KB 扇区交替保存同一份书签镜像。镜像由 magic、递增序号 seq、CRC32 和有效载荷组成:
- 每次保存选择非活动扇区作为写入目标;
- 将序号加一,并对书签数据计算 CRC32;
- 完整写入成功后才将新扇区视为活动镜像;
- 上电初始化时校验两个扇区,选择 CRC 正确且序号更新的一份。
因此,即使最新一次写入异常,旧镜像通常仍然有效,系统可以回退到上一次保存的阅读进度。这是嵌入式持久化数据中成本很低、收益很高的可靠性设计。
9. 工程目录与阅读入口
Wash_Ink_Screen/
├─ Core/ # CubeMX 生成的启动、时钟、中断与 FreeRTOS 配置
├─ FATFS/ # FatFs 适配层
├─ User/
│ ├─ App/ # 任务、IPC、阅读器、书签、调试日志
│ ├─ BSP/ # 板级 GPIO、SPI DMA、串口、延时封装
│ ├─ Drivers/ # EPD、按键、SD 卡、W25Q128 驱动
│ └─ DriverTests/ # 各外设独立测试
├─ Middlewares/ # FreeRTOS 与 FatFs 源码
└─ Wash_Ink_Screen.ioc # STM32CubeMX 配置建议按下面顺序阅读源码:
Core/Src/main.c:初始化顺序、任务创建及调度器启动;User/App/ipc.[ch]、key_task.c、epd_task.c:任务通信和职责边界;User/App/app_task.c:主页、文件浏览、分页阅读的业务流程;User/BSP/spi_bsp.c:DMA 完成中断如何转换为任务同步;User/App/bookmark.c:书签双镜像和 CRC 校验;User/Drivers/:从应用逻辑下沉到具体硬件协议的实现。
10. 构建与运行提示
工程提供 CMakeLists.txt 和 CMakePresets.json,也保留 Wash_Ink_Screen.ioc 以便使用 STM32CubeMX 修改引脚或中间件配置。编译链为 GCC ARM Embedded;下载后,在 SD 卡根目录创建 ebooks 文件夹并放入可显示的文本文件,即可从主页进入书架。
如果修改了 CubeMX 配置,建议保留 main.c 中的 USER CODE BEGIN/END 区域,避免重新生成代码时覆盖应用初始化、IPC 创建和任务启动代码。
视频占位:翻页、局刷与断点续读
11. 联网扩展版与后续方向
同级 Wash_Ink_Screen_Internet 保留了这套 FreeRTOS 分层,并新增 wifi_task.[ch] 和 server/api/,为后续通过网络获取书籍、同步设备状态或上传内容预留了入口。基础版本身已经把输入、显示、存储和应用逻辑分成独立模块,因此网络能力可以作为新的任务接入,而不需要推翻阅读器的主体结构。
后续可继续完善的方向包括:为 SD 卡访问增加互斥保护、为队列满/外设异常补充错误恢复、统计任务栈余量、按文件类型过滤书架,以及加入电池电量与低功耗唤醒策略。
结语
这个项目的价值在于把一个很直观的“墨水屏阅读器”需求,落成了可维护的实时系统:按键输入可持续响应,电子纸刷新被串行化管理,DMA 等待不占用 CPU,文件读取按需分页,关键阅读数据还能在异常掉电后保持可恢复。对于从裸机超级循环过渡到 FreeRTOS 的开发者来说,它是一个覆盖驱动、任务、IPC、文件系统与可靠性存储的完整练习。
prpmjzutnpjluypixzktttmymwzfmw