第16章:移植、优化与展望

最后一章不教你新代码——把移植、局限、生态与学习路线串成一张地图,再把它们讲给下一个人

🔬

本章导师:费曼

核心方法论:教是最好的学

「学一个东西的最好方式,是把它讲给一个完全不懂的人听。十五个章节下来,你已经陪着这台阅读器从'拆开一个 zip'走到'睡一觉还能自己醒'——现在,试着把这一切讲给下一个人听吧。讲一遍,你才知道自己哪里还没真懂。移植、优化、加入社区,说到底都是在'讲':用代码、用 PR、用一篇文章,把你学到的东西还给这个世界。」

16.1 移植新板:Board 子类 + platformio.ini env

移植是检验理解程度的试金石。第 4 章已经拆过 Board 工厂的"两条腿",这里站在终点的位置回看一遍:读完整本书,移植一块新板其实只是回答一串问题——这块屏是 EPDiy 并行屏还是独立驱动(第 3 章)?按键是 GPIO 直连还是走 IO 扩展芯片、按下是什么电平(第 5 章)?SD 卡的四根 SPI 引脚在哪(第 6 章)?电池分压接在哪个 ADC 通道(第 15 章)?每个问题的答案,最后都变成 platformio.ini 里的一行 -D 宏。答案填对了,Board::factory() 就会在编译期替你实例化出正确的板类。

README 的 "Porting to other boards" 一节把步骤讲得很克制,共两步:(1) 在 src/boards 新建一个实现 Board.h 的板类,把它加进 Board::factory()(2) 在 platformio.ini 新增一个 env。但作者立刻补了一条"快捷通道":如果你的屏幕本身就是 EPDiy 并行屏,板类这一步可以整个省略——只需定义 -DBOARD_TYPE_EPDIY 加上屏幕型号与板卡修订两个宏,其余全由预处理指令接管。真正需要手写 Board 子类的是那些"既非 EPDiy 屏、也无现成板类"的全新板,最低限度要实现 get_renderer()get_button_controls()(README 原话:"Have a look at M5Paper.h for inspiration")。第 4 章那张"扩展新板四步表"在这里依然成立。

新增一块板之后,有一道免费的"体检"在等你:仓库根 .github/workflows/build-test-on-push.yml 会在每次 push 上跑三块板的构建 pio run -e lilygo_t5_47 -e epdiy -e m5_paper 和本机单元测试 pio test -e nativetest/ 目录里是 EPUB 解析器的原生测试,[env:native]lib_ignore 排掉 epdiy / sd_card / spiffs 等硬件依赖)。所以你移植的新 env 能不能过 CI(持续集成,说人话:每次提交代码后自动跑一遍构建和测试的流程)、能不能进 PR,既是门槛也是自检清单——把 pio run -e my_board 在本地先跑绿,再发 Pull Request,就是对一个开源项目最大的尊重。

; platformio.ini:为一块全新板子新增 env(最小模板,对照 [env:epdiy])
[env:my_board]
extends = esp32_common
build_flags =
  ${common.build_flags}
  ; ① 工厂选板:EPDiy 兼容屏可复用 Epdiy 板类
  -DBOARD_TYPE_EPDIY
  ; ② 屏幕型号 + 板卡修订(来自 epdiy 库,第 3 章)
  -DCONFIG_EPD_DISPLAY_TYPE_ED060XC3
  -DCONFIG_EPD_BOARD_REVISION_V6
  ; ③ 按键极性:0 = 按下为低(ULP 唤醒),1 = 按下为高(EXT1 唤醒)
  -DBUTONS_ACTIVE_LEVEL=0
  ; ④ SD 卡四根 SPI 引脚(第 6 章)
  -DSD_CARD_PIN_NUM_MISO=GPIO_NUM_36
  -DSD_CARD_PIN_NUM_MOSI=GPIO_NUM_0
  -DSD_CARD_PIN_NUM_CLK=GPIO_NUM_13
  -DSD_CARD_PIN_NUM_CS=GPIO_NUM_14
  ; ⑤ 电池 ADC(第 15 章,不定义则不显示电量)
  -DBATTERY_ADC_CHANNEL=ADC1_CHANNEL_0
# .github/workflows/build-test-on-push.yml:移植者的免费体检
name: Build and Test
on: [push]
jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: recursive          # 别忘了 --recursive,见第 1 章
      - uses: actions/setup-python@v2
      - run: pip install --upgrade platformio
      - run: pio run -e lilygo_t5_47 -e epdiy -e m5_paper   # 三块板都要能编
      - run: pio test -e native                              # 本机单测(EPUB 解析)
意义对应本书章节
-DBOARD_TYPE_*编译期选板,驱动 Board::factory()第 4 章
-DCONFIG_EPD_DISPLAY_TYPE_* / -DCONFIG_EPD_BOARD_REVISION_*屏幕型号与板卡修订(EPDiy 库)第 3 章
-DBUTONS_ACTIVE_LEVEL按键极性,决定 ULP / EXT1 唤醒第 5、15 章
-DSD_CARD_PIN_NUM_MISO/MOSI/CLK/CSSD 卡四根 SPI 引脚第 6 章
-DBATTERY_ADC_CHANNEL电池电压采样通道(不定义则不显示电量)第 15 章
-DUSE_SPIFFS / -DUSE_L58_TOUCH文件系统与触摸开关第 6、5 章
费曼提示

移植 = 一次"讲解":把每块新板的每一个宏选择理由讲给别人听(或者写进 PR 描述),讲不顺的地方,就是你还没真正搞懂的地方。费曼技巧在这里非常好用——先假设自己要带一个新人完成移植,你会立刻发现哪些细节是自己含糊的。

16.2 已知局限与改进空间

README 的 "How well does it work?" 一节,作者对自己作品的自评相当坦诚:"Surprisingly, it works pretty well"(意外地好用)。但紧接着就列了硬伤。最该被点名的是断页不看位置——"The code makes no attempt to break pages at suitable places"(代码完全不做在合适位置断页的努力),而 XHTML 文件里其实藏着断页的线索、也还有 CSS 文件可用。这正打在本书第 11 章的分页算法上:目前的策略是"页面装不下就翻下一页"的朴素逻辑,遇到段落、图片跨页时,行会被硬生生切断。

排版能力也受限于作者的取舍。开头那句 "It has limited support for formating - the CSS content of the ePub file is not parsed" 说得很直白:CSS 完全不解析,只认 <h1><h2><b><i> 等少数标签(正是第 9 章 RubbishHtmlParser 白名单的由来)。字体方面 "I've only included 4 font styles - regular, bold, italic and bold-italic. I've also only generated glyphs for Latin characters and punctuation"——只有 4 种字重、且只生成了拉丁字符和标点。这对中文读者是个硬约束:想看中文 EPUB,得先扩展第 14 章的 scripts/generate_fonts.sh 生成 CJK 字形。好在这是一个范围明确、可度量的改造入口。

最后一类局限是性能。README 原话:书单的显示取决于每本书的封面图,SD 卡上读封面 "can take a few seconds",翻书单也会偏慢;倒是"Rendering the actual pages is pretty reasonable, even when they have images on them"(正文页渲染相当稳,哪怕带图)。改进方向很自然:封面缩略图缓存、异步预加载、把书单数据缓存进内存。README 在「Improvements」一节用一句话收尾:"There's a lot of room for improvement. This is a very basic e-reader and I'm more than happy for people to contribute to this project."——"非常基础的阅读器" + "欢迎任何人来贡献",这就是一份摆在明面上的"招工启事"。下面这张表把局限、现状与改进方向摊开,每一行都是一道能写进简历的题。

# README "How well does it work?"(节选,作者自评)
# - "Surprisingly, it works pretty well. Layout is reasonable, but there are
#    a lot of improvements that could be made."
# - "The code makes no attempt to break pages at suitable places - there are
#    hints that can be extracted from the XHTML files and there are also
#    CSS files that could be used."
# - "Depending on the images ... displaying the list of ePub files on the SD
#    Card can take a few seconds and moving through the pages of ePub files
#    can be a bit slow."
# 一张"阅读器改造清单"(示例选项,每个都能写成一个独立项目)
1. 断页优化       在 <h1>/<div> 等块级标签边界优先断页 → 第 11 章
2. 子集 CSS      解析 margin / text-align 等少量样式 → 第 9 章
3. CJK 字体       扩展 generate_fonts.sh 生成中文字形 → 第 14 章
4. 封面缩略图     缓存/预加载,让书单秒开 → 第 13 章
5. 统一睡眠超时   把 120 秒做成可配置,对齐 README 的 30 秒 → 第 15 章
局限现状(代码事实)改进方向
断页位置不做任何"合适位置断页",行被硬切用 XHTML 块级标签 / <pagebreak> 提示优先断页
CSS 排版完全不解析,只认 h1/h2/b/i 白名单实现少量 CSS(居中、页边距、段首缩进)
字体4 种字重、仅拉丁字符 + 标点扩展 generate_fonts.sh 生成 CJK 字形
封面加载书单显示要读 SD 卡封面,需数秒封面缩略图缓存、异步预加载
睡眠超时代码 120 秒 vs README 30 秒参数化、可配置、文档对齐
费曼提示

"已知局限"是开源项目里最值钱的学习素材:每个局限都是一个范围明确、自带验收标准的小项目。挑一个,先写下"改完什么样算好"(例如"书单封面加载 <1 秒"),再去动代码——比起漫无目的地读源码,这样学得快十倍。更妙的是,作者已经在 README 里说了"more than happy for people to contribute",你的成果随时可以回馈社区。

16.3 生态:Wiki、Hackaday、社区聊天室

一个 DIY 项目的生命力,很大程度写在它的生态入口里。README「Improvements」一节的末尾,作者把三个外部资源并列摆出:项目 Wiki(说人话:Wiki 就是项目的文档/wiki 页;原文 "Check additional notes and research in our esp-32 epub reader Wiki",即官方 Wiki 里存着额外笔记与研究)、Hackaday 项目页(说人话:Hackaday 是一个硬件创客社区网站,很多 DIY 电子项目在这里展示;原文 "you can find the DIY-ePUB reader project in Hackaday")、以及公开聊天室("public chat-room")。这几条链接是作者留给世界的"后门"——文档没写透的东西,藏在 Wiki、项目页与聊天记录里。注意:本教程只转述 README 里的链接文字与用途,不展开外部站点的具体内容——想深入了解,请自己点开链接核实。

仓库本身的"可持续性"也值得一提。根目录的 LICENSE 是一份开源许可证(说人话:规定代码可以怎么被使用的法律条款),具体是 MIT(版权 Copyright (c) 2021 Chris Greening),意味着任何人都可以自由使用、修改、分发——这也是"贡献文化"的土壤。配套文件还有 .github/FUNDING.yml(给作者的打赏通道)、README 顶部那块 build status 徽章(指向 GitHub Actions)、以及「Can you contribute/help?」一节的号召:原文 "please try the project out on any e-paper boards that you have and open up pull requests if you get it working with any fixes. And if you find bugs, feel free to report (or better yet, fix!) them :)"——试试你的板子、修好了就发 PR、发现 bug 就报(能修更好)。这套"用 MIT + CI + 明确的贡献入口"的组合,就是个小而完整的新型开源协作样板。

还有一串藏在 README 各处的视频:顶部是三支演示(三块支持环境 + LilyGo 上的运行 + M5Paper 上的运行),Deep sleep 一节引了一支「ESP32 Deep Dive into Deep Sleep」的深入讲解,SD 卡接线也有一支 hack 视频。它们的价值不只是"看效果"——作者把难讲的硬件细节(深睡眠、飞线接 SD)做成了视频,这本身就是一种"费曼式"的文档策略:能用嘴讲清楚的,不塞进代码注释。你在 16.4 写自己的改造计划时,也不妨学着用"给别人讲得明白"作为交付标准。

# README:三个生态入口 + 贡献号召(节选)
# Check aditional notes and research in our esp-32 epub reader Wiki.
# Here you can find the DIY-ePUB reader project in Hackaday and the
# public chat-room.

# Can you contribute/help?
# "please try the project out on any e-paper boards that you have and open
#  up pull requests if you get it working with any fixes. And if you find
#  bugs, feel free to report (or better yet, fix!) them :)"
# 仓库"可持续性"文件一览
LICENSE                  MIT · Copyright (c) 2021 Chris Greening
.github/FUNDING.yml      打赏通道
.github/workflows/build-test-on-push.yml   每次 push 构建三板 + 跑单测
test/                    native 环境的 EPUB 解析单测(fixtures/ 配样本)
资源类型README 里的用途描述
esp-32 epub reader Wiki文档 / 研究笔记"additional notes and research"(额外笔记与研究)
Hackaday 项目页项目展示"DIY-ePUB reader project in Hackaday"
公开聊天室即时交流"public chat-room"
build status 徽章CI 状态指向 GitHub Actions
演示 / 讲解视频视频3 块板演示 + 深睡眠深度讲解 + SD 接线 hack
注意

本教程提到 Wiki、Hackaday、聊天室,都是转述 README 原文的链接文字与用途,并没有去核实或复述外部站点的具体内容。外部链接是"活的",今天的内容明天可能变。想看一手资料,请以 README 里的原始链接为准自行打开。

费曼提示

参与开源的正确姿势是渐进的:先在 Issue 里问清楚 → 通读相关代码 → 做最小复现 → 提交小而完整的 PR。别一上来就甩一大坨改动。作者在 README 里已经把门开好了——"try it out on any e-paper boards you have",这是给移植者发的邀请函。你的第一个 PR 不必完美,能编译、有说明、聚焦一件事,就够了。

16.4 学习路线回顾:三条线串起来

站在十六章的终点回望,这套教程其实铺了三条并行的技术线:Kotlin(应用层:Android App 开发)、LVGL(库层:嵌入式图形库本身)、以及本项目(整机层:用一堆现成库组装出一台能用的设备)。本项目在嵌入式坐标系里的位置很独特——它不是库,而是一台"由现成库 + 自己的胶水逻辑拼起来的完整产品":EPDiy 负责刷屏、miniz 负责解包、TinyXML2 负责 XML、tjpgd3 / PNGdec 负责解码,而项目自己写的是状态机、换行、分页与电源管理。学会"用库做产品"和"写库"是两种不同能力:LVGL 线教你"写库",本项目教你"用库做出一个能被手捧着的产品"。

把十六个章节摊开,是一张可以复用到任何嵌入式产品的骨架图:解析(第 7–9 章 zip / XML / HTML)→ 排版(第 10–11 章换行与分页)→ 渲染(第 12–14 章帧缓冲、图像、字体)→ 低功耗(第 15 章深睡眠与唤醒)→ 移植与展望(第 16 章)。存储、解析、布局、输出、电源、适配——一台有屏幕的嵌入式设备,几乎都逃不出这六个模块。你在这十六章里练就的"读源码、grep 宏、算单位、量功耗"的功夫,比任何单一知识点都值钱,因为它们才是跨项目通用的元技能。

接下来往哪走?费曼的方法论是"教是最好的学",所以下一步建议也围绕"产出"展开:(1) 挑 16.2 的一个局限,定一个可度量的目标(比如"书单封面加载 <1 秒"、"中文 EPUB 能正常渲染"),写成你的"阅读器改造计划"并动手;(2) 把项目移植到你手头任意一块 e-ink 板,走一遍 16.1 的流程,能过 CI 就发 PR;(3) 把"如何解析 EPUB"或"电子墨水屏怎么翻页"讲给一个外行听——写成文章、录成视频、或只是讲给朋友,讲不清楚的地方就是你回去复习的起点。记住 16.2 那张改造清单上的每一行,都是别人为你铺好的下一站。

# 本书十六章节地图:一台嵌入式阅读器的最小骨架
认识项目       第 1–2 章     环境、仓库地图           知道这是什么
解析           第 7–9 章     zip / XML / HTML         把"书"变成"数据"
排版           第 10–11 章   换行 / 分页              把"数据"变成"页面"
渲染           第 12–14 章   帧缓冲 / 图像 / 字体     把"页面"变成"画面"
低功耗         第 15 章      深睡眠 / ULP / EXT1      让它能"停下来"
移植与展望     第 16 章      配置 / 生态 / 成长路线   让它在别处也能活
# 你的"阅读器改造计划"模板(交付物驱动,可自行替换)
目标:<一句可度量的话>  例:中文 EPUB 能正常渲染,10 页样例零乱码
范围:<动哪些文件 / 涉及本书哪一章>
      例:scripts/generate_fonts.sh(第 14 章)+ Fonts/ 目录
验收:<怎么算成功>  例:pg 样例中文书名在书单正确显示、翻页正常
风险:<可能翻车的地方> 例:CJK 字形体积大,可能挤爆帧缓冲
第 1 步:<最小可验证动作>  例:先用 generate_fonts.sh 生成一张测试字形图
第 2 步:<…>
验收方式:把改造成果讲给一个外行听,讲不明白 = 还没真懂
阶段章节核心技能一句话价值
认识项目第 1–2 章环境搭建、仓库解剖知道这是台什么东西
解析第 7–9 章zip / XML / HTML 解析把"书"变成"数据"
排版第 10–11 章动态规划换行、分页把"数据"变成"页面"
渲染第 12–14 章帧缓冲、图像、字体把"页面"变成"画面"
低功耗第 15 章深睡眠、ULP/EXT1、电量让它能"停下来"
移植与展望第 16 章Board 配置、生态参与让它在别处也能活
费曼提示

十六章读完,最大的敌人是"伪完成"——看了一遍源码就以为自己懂了。检验标准永远是那个老办法:能不能把它讲给一个完全不懂的人。这本书对这台阅读器的叙述到此为止,但你的学习才刚拐弯:去移植、去改 16.2 的某个局限、去给作者发个 PR。把这本书"讲"给下一个人,你的十六个章节才算真正闭环。

章末练习

练习 1:移植盘点 入门

给你一块板:EPDiy 并行屏、按键按下为高电平、SD 四脚已知、没有电池。请列出这块板 platformio.ini 需要的宏,并说明哪一项可以省略、为什么。

提示

复用 Epdiy 板类可省 Board 子类;没电池就把电池 ADC 宏省掉。对照 16.1 的模板。

参考答案

需要:-DBOARD_TYPE_EPDIY(省掉手写 Board 子类)、-DCONFIG_EPD_DISPLAY_TYPE_* + -DCONFIG_EPD_BOARD_REVISION_*(屏幕)、-DBUTONS_ACTIVE_LEVEL=1(高有效 → EXT1 唤醒)、-DSD_CARD_PIN_NUM_MISO/MOSI/CLK/CS 四行。可省略:BATTERY_ADC_CHANNEL——Board::get_battery() 未定义该宏时返回 nullptr,主程序会跳过电量采样与绘制,不影响正常阅读。若你的屏不是 EPDiy 兼容屏,则必须新建 Board 子类并加入 factory()

练习 2:局限分析 进阶

从 16.2 的改造清单里任选两个局限,各回答:(a) 它卡在本书哪一章的哪段逻辑上;(b) 给出一个你想到的最小改进方案。要求方案具体到"动哪个文件 / 哪个函数"。

提示

断页 → 第 11 章分页分配页面的那段;CJK 字体 → 第 14 章 generate_fonts.sh;封面慢 → 第 13 章图像读取路径。

参考答案

示例一(断页):卡在第 11 章 RubbishHtmlParser::layout 把 blocks 分配到页面时——目前"页面放不下就新建页",可先扫描块级标签(<h1>/<div>)并优先在它们边界断页,改动集中在布局分配段。示例二(CJK 字体):卡在第 14 章 scripts/generate_fonts.sh——它只生成拉丁字符 + 标点,可加入一套 CJK 字形并重新生成 Fonts/ 下的数组,随后在渲染器里为中文分配字号。两个方案都满足"范围小、可度量"的标准,也都值得回馈 PR。

练习 3:制定你的阅读器改造计划 进阶

套用 16.4 的模板,为这台阅读器制定一份你自己的改造计划。要求包含:可度量的目标、明确的范围(文件 / 函数 / 涉及章节)、验收标准、风险点,以及第一步的最小可验证动作。可以选 16.2 的任一行,也可以是你自己的灵感。

提示

目标要可测量("封面加载 <1 秒"优于"更快");第一步要小到一天内能验证;验收标准最好能口述给外行。

参考答案

合格示例(可自由替换):目标——"书单从打开到可翻页 ≤1 秒";范围——lib/Epub/EpubList/EpubList.cpp(书单渲染与封面读取,第 13 章),给封面图加一个缩略图缓存文件 /fs/covers/;验收——放 10 本带封面书实测,书单首屏时间从数秒降到 1 秒内;风险——缩略图缓存占 SD 空间、首次生成仍慢,需考虑"首读生成 + 之后命中"策略;第一步——写一个把首张封面压成 64×64 缩略图并落盘的函数,单测验证尺寸与耗时。核心评判标准:目标可度量、第一步可验证、改造完能讲给外行听。

练习 4:费曼收官——把它讲出去 挑战

五句话向一个完全不懂硬件的人解释"这台 ESP32 电子书阅读器是怎么工作的",再为它写一份 3 分钟口头讲解提纲。写完后,标出你自己讲得最不顺、最含糊的一句话——那里就是你回去重新复习的章节。这是本书最后一次、也是最重要的练习。

提示

别用术语;按"书 → 数据 → 页面 → 画面 → 睡眠"的骨架讲;讲不顺的地方往往是"你只是看过、没真正理解"的地方。

参考答案

五句示例(一)这台机器读的是电子书文件,它们其实是压缩包,里面装着文字和排版信息;(二)开机后它把压缩包解开、把文字排版成一页一页,存进一块很小的缓存;(三)屏幕是电子墨水屏——跟纸张一样,画面停住后就不耗电了;(四)所以你翻页时才花电,不翻时它就睡大觉,还能靠按键把自己叫醒;(五)最后它把所有进度记在一小块不掉电的"记忆"里,下次开机接着读。3 分钟提纲可按这五句各展开 30 秒;讲不顺的句子(比如"排版成一页一页"背后的换行与分页)就该回到第 10、11 章。真正学会的标志,是讲完这 3 分钟,听众的眼睛是亮的。