水墨屏阅读器

用 FreeRTOS 做一台墨水屏电子书阅读器

demo_pcb.jpg

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 文件系统,存放电子书
W25Q12816 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()
任务优先级栈大小职责
KeyTaskAboveNormal256 B每 10 ms 扫描按键、执行状态机并投递按键事件
AppTask(defaultTask)Normal4 KB主页、文件浏览、文本分页、书签保存等应用逻辑
EPDTaskNormal1 KB独占执行电子纸初始化、全刷、局刷和休眠
DBGTaskLow256 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(二值信号量)-- EPDTask

KeyTask 将 {按键编号, 事件类型} 写入 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 写入函数。

demo_reader.jpg

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 和有效载荷组成:

  1. 每次保存选择非活动扇区作为写入目标;
  2. 将序号加一,并对书签数据计算 CRC32;
  3. 完整写入成功后才将新扇区视为活动镜像;
  4. 上电初始化时校验两个扇区,选择 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 配置

建议按下面顺序阅读源码:

  1. Core/Src/main.c:初始化顺序、任务创建及调度器启动;
  2. User/App/ipc.[ch]、key_task.c、epd_task.c:任务通信和职责边界;
  3. User/App/app_task.c:主页、文件浏览、分页阅读的业务流程;
  4. User/BSP/spi_bsp.c:DMA 完成中断如何转换为任务同步;
  5. User/App/bookmark.c:书签双镜像和 CRC 校验;
  6. 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、文件系统与可靠性存储的完整练习。

工程开源

Github https://github.com/Gypsyold/E-ink-screen_Project

仅有一条评论

  1. iyzrdeufpd
    iyzrdeufpd

    prpmjzutnpjluypixzktttmymwzfmw

添加新评论