「鲁班说:要做出东西,先把工具备齐。」这一章是终点站,也是真正的起点——给这台阅读器加电子书(16.1)、加字体(16.2)、换一块新屏幕(16.3),最后回顾这四条技术线里本项目的位置(16.4)。前 15 章学会"读代码",这一章学会"改代码":你手上已经有完整的工具链和模板。
核心方法论:工欲善其事——先备齐工具和模板,再做二次开发
「鲁班教徒弟,第一课不是教做柜子,而是教磨斧子。这一章也一样:换屏幕不是从头写驱动,而是『复制模板、填波形、改四个函数、加一行配置』;加书不是改代码,而是『知道书往哪放』;加字体不是写解析器,而是『往目录里丢一个文件』。工具备好了,活儿都是搭积木。我们把前一章留下的『待完善』清单,一件件变成『动手就能做』的活。」
EPUB 本质是一个 zip 压缩包(第 7 章讲过用 miniz 解包),体积从几百 KB 到几十 MB 不等。这台阅读器有两条取书通道:内置文件系统(随镜像打包进 Flash)和 TF 卡(microSD 存储卡,可随时插拔)。README 的「二次开发」开宗明义只有两句话:少量样书放入 disk/,随文件系统镜像打包;大量书籍建议使用 TF 卡。原因很朴素——镜像里的文件系统是只读的、空间有限,每改一次都要重新编译烧录;而 TF 卡容量大、内容随时可以换,不需要动固件。
disk/ 是怎么变成镜像里的文件的?答案在 project/SConstruct 里。FileSystemBuild("../disk", env) 把整个 disk/ 目录打包成一个文件系统镜像,AddCustomImg("fs_root", bin=[fs_bin]) 再以 fs_root 的名字把这个镜像烧进固件分区。所以你在 disk/ 里放的任何 EPUB,开机后都会出现在内置文件系统里——第 12 章书库页扫描的就是它(这就是"优先内置文件系统"的来源)。注意同一个文件如果还满足 EPD_WAVEFORM_USE_BIN(选了二进制波形),构建脚本还会把 ../waveform/epd_waveform.bin 单独打成 wave_table 分区——这也是模板里 #ifndef EPD_WAVEFORM_USE_BIN 那条分支存在的意义。
一个真实的小插曲能让你看清这套打包机制:disk/ 里现成的三本样书里,oebps.epub 和 pg43-images.epub 大小一模一样(都是 304004 字节),连 MD5 都完全相同——它们是同一个文件被复制成了两份,等于镜像里白白塞了两份一模一样的书(第 15 章那套"信号盘点"思路在这里立刻派上用场)。另一本 no_oebps.epub 则是有意的"压力测试书":它没有标准的 OEBPS/ 目录,content.opf 直接躺在包根目录,专门用来检验解析器对非标准结构的容错。这些真实细节告诉你:读一个项目的构建脚本,等于读它"怎么把资源变进固件"的全部真相。
# project/SConstruct:把 disk/ 打包成镜像里的只读文件系统 fs_root
fs_bin = FileSystemBuild( "../disk", env)
AddCustomImg("fs_root", bin=[fs_bin])
# 选了二进制波形(EPD_WAVEFORM_USE_BIN)时,额外把波形打成一个独立分区
if GetDepend('EPD_WAVEFORM_USE_BIN'):
epd_wave_bin = "../waveform/epd_waveform.bin"
AddCustomImg("wave_table", bin=[epd_wave_bin])
# 真实现状:disk/ 里三本样书,其中两本是同一个文件
$ ls -l disk/
oebps.epub 304004 字节
pg43-images.epub 304004 字节 ← 和 oebps.epub 大小一致
no_oebps.epub 96038 字节 ← 无 OEBPS/ 目录的非标准测试书
$ md5sum disk/oebps.epub disk/pg43-images.epub
18dbb997072703ff53471889f614b781 disk/oebps.epub
18dbb997072703ff53471889f614b781 disk/pg43-images.epub ← MD5 完全相同
| 取书通道 | 怎么加书 | 优点 | 代价 |
|---|---|---|---|
disk/(内置) | 把 EPUB 丢进 disk/,重新编译烧录 | 开机即有、不怕丢 | 只读、空间有限、改一次烧一次 |
| TF 卡 | 卡里建个目录放 EPUB,插卡即读 | 容量大、可热插拔、不动固件 | 依赖卡的存在与格式 |
「工欲善其事,先磨斧子」在这儿的落点是:先搞清"谁把书打包进镜像"这条构建管线,再决定书放哪。动手改任何"资源类"的东西(书、图、字体、波形)之前,先花两分钟在 SConstruct / Kconfig 里找到它被烧录的入口——你就能判断"改完要不要重新编译烧录",而这是最容易踩的坑。
第 8 章讲过内置中文字体是怎么从 Flash XIP(在 Flash 里直接取数据、不搬进 RAM 的执行方式)加载的。这一节把"系统怎么认识你自己的字体"讲完整。字体管理器 src/font_manager.c 的全部秘密就藏在一个上限、一个目录和一个后缀检查里。上限是三个宏:FONT_MAX_COUNT 64(内置 Default 占 1 个,外部字体最多 64 - 1 = 63 个)、FONT_NAME_MAX 32、FONT_PATH_MAX 64。后缀检查是 is_ttf_file():只认 .ttf / .TTF / .otf / .OTF 四种结尾。
启动时 font_manager_init("/fonts") 做两件事:先把内置 "Default" 登记到 index 0,然后 opendir 扫描目录、把每个命中后缀的文件登记进 g_font_list(撞到 FONT_MAX_COUNT 就停)。所以加字体的完整流程只有两步:在 TF 卡根目录建 /fonts,把 .ttf/.otf 放进去,重启——开机扫描后,新字体会自动出现在「阅读设置 → 字体」列表里(第 9 章讲过 reading_settings 页面)。选择时的切换逻辑在 font_manager_select():先清字形缓存、释放旧的 FreeType face,然后分两条路——内置字体走 epd_font_ft_init_builtin(从 Flash XIP 读),TF 卡字体走 epd_font_ft_init_from_file(FreeType 直接从 TF 卡文件读取,不占 PSRAM,第 8 章提过这个取舍);外部字体一旦加载失败,自动回退内置 Default 并返回 -4。看懂这段,你就明白 README 那句"若指定字体文件不可用,自动回退到内置默认字体"是从哪来的。
/* src/font_manager.c:字体列表的三个上限 + 扫描时认哪几种后缀 */
#define FONT_MAX_COUNT 64 // 列表上限(含内置 Default)
#define FONT_NAME_MAX 32
#define FONT_PATH_MAX 64
typedef struct {
char name[FONT_NAME_MAX];
char path[FONT_PATH_MAX];
int is_builtin;
} FontEntry;
static FontEntry g_font_list[FONT_MAX_COUNT];
static int is_ttf_file(const char *filename)
{
const char *dot = strrchr(filename, '.');
if (!dot) return 0;
return (strcmp(dot, ".ttf") == 0 || strcmp(dot, ".TTF") == 0 ||
strcmp(dot, ".otf") == 0 || strcmp(dot, ".OTF") == 0);
}
/* src/font_manager.c:初始化 = 内置占位 + 扫描 /fonts 目录 */
int font_manager_init(const char *font_dir)
{
strcpy(g_font_list[0].name, "Default"); // index 0 永远是内置字体
g_font_list[0].path[0] = '\0';
g_font_list[0].is_builtin = 1;
g_font_count = 1;
DIR *dir = opendir(font_dir);
if (!dir) return g_font_count; // 没有 /fonts 也能用(只有 Default)
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (g_font_count >= FONT_MAX_COUNT) break; // 撞上限就停
if (!is_ttf_file(entry->d_name)) continue; // 只认 .ttf/.otf
FontEntry *fe = &g_font_list[g_font_count];
snprintf(fe->path, FONT_PATH_MAX, "%s/%s", font_dir, entry->d_name);
extract_display_name(entry->d_name, fe->name, FONT_NAME_MAX);
fe->is_builtin = 0;
g_font_count++;
}
closedir(dir);
return g_font_count;
}
| 来源 | 加载函数 | 数据在哪 | 失败策略 |
|---|---|---|---|
| 内置 Default | epd_font_ft_init_builtin | Flash XIP,编译进固件 | —(永远可用) |
| TF 卡外部字体 | epd_font_ft_init_from_file | TF 卡 /fonts,FreeType 直接读文件 | 回退内置 Default,返回 -4 |
验证"字体加对了没"最快的方法,不是看代码,是看开机日志:font_manager_init 每扫到一个字体就打一行 [font_mgr] Found font: [N] 名字 (路径),结束再打 [font_mgr] Total N fonts available。第 2 章教你用串口/调试口看 RT-Thread 日志时讲过怎么读 log——现在它就是你的"磨斧子"。加完字体重启,扫一眼这两行,比猜代码快十倍。
这一节是第 3 章(板级与 Kconfig)和第 4 章(LUT、时序、VCOM、EPIC)知识的总装。README 的「二次开发」把"加一块新屏幕"压缩成了四步:复制模板 → 波形数组 → 关键函数 → Kconfig 配置。最要紧的一点是架构上的解耦——屏驱跟 EPUB 解析、UI 状态机互不相干,换屏完全不影响上层阅读逻辑(第 4 章已经讲过 DBI 驱动如何被渲染器调用)。所以移植新屏的本质不是"重写驱动",而是"照着已有模板填四个口子"。
第 1 步:复制模板改名。把 src/boards/display_dbi/epd_configs_opm060d.c 复制一份,按新屏型号命名(README 建议)。模板开头是 #if defined(LCD_USING_OPM060D),这个宏开关要换成你自己的屏宏。第 2 步:填波形数组。把屏幕厂商给的波形文档转成 C 数组:全刷 static const uint8_t xxx_wave_full[32][256] = {}、局刷 [12][256]。模板里真实的全刷数组叫 yzc085_wave_full_0_100[32][256],局刷叫 yzc085_wave_partial_0_100[12][256],每个元素取值 0/1/2——它本质是一张 256 级灰阶查找表,告诉屏幕在当前这一帧里对每个新旧像素组合施加多大的电压,这正是第 4 章讲的 LUT(查找表)概念。
第 3 步:改四个关键函数。(1)波形选择 epd_wave_table_get_frames(temperature, mode):按温度区间和全刷/局刷模式选波形、返回帧数;超温区自动回退全刷表。README 还给了一个多温区分段表的例子(te067xjhe_wave_forms_full[] 把波形按 0-5℃ / 5-10℃ / 50-100℃ 分段)。(2)LUT 转换 epd_wave_table_fill_lut:核心就一行 p_epic_lut[i] = p_frame_wave[i] << 3;,把 8bit 波形左移 3 位转成 32bit EPIC 加速器能用的值。(3)时序 epd_get_timing_config:像素时钟 sclk_freq = 24(MHz)、行时钟 fclk_freq = 83(KHz)、有效数据长度 LDL = LCD_HOR_RES_MAX / 4(8 位数据线要除 4)、FDL = LCD_VER_RES_MAX。(4)公共电极电压 epd_get_vcom_voltage:R7D005 屏返 2100(mV),其他屏返 1050。第 4 步:Kconfig。在 project/Kconfig.proj 的 choice "Custom LCD driver" 里加一项(select 对应触控芯片、驱动源文件宏和总线宏 BSP_LCDC_USING_EPD_8BIT),补上 LCD_HOR_RES_MAX / LCD_VER_RES_MAX / LCD_DPI 三组默认值,然后跑 menuconfig 确认新屏选项可见并默认选中。
/* 第 1-2 步:模板开关 + 波形数组(src/boards/display_dbi/epd_configs_opm060d.c) */
#if defined(LCD_USING_OPM060D) // 换成你自己的屏宏
#ifndef EPD_WAVEFORM_USE_BIN // 不用二进制波形时才走 C 数组
typedef struct {
int min_temp; // 温区下限
int max_temp; // 温区上限
uint32_t frame_count; // 这个波形共有几帧
const uint8_t (*wave_table)[256]; // 指向 [N][256] 二维数组
} WaveTableEntry;
// 全刷:32 帧 × 256 灰阶组合;局刷:12 帧 × 256
static const uint8_t yzc085_wave_full_0_100[32][256] = { ... };
static const uint8_t yzc085_wave_partial_0_100[12][256] = { ... };
static const WaveTableEntry full_wave_table =
{0, 100, 32, &yzc085_wave_full_0_100[0]};
static const WaveTableEntry partial_wave_table =
{0, 100, 12, &yzc085_wave_partial_0_100[0]};
/* 第 3 步-1:波形选择(温区 + 全刷/局刷),超温区回退全刷表 */
uint32_t epd_wave_table_get_frames(int temperature, EpdDrawMode mode)
{
const WaveTableEntry *wave_table = NULL;
if (EPD_DRAW_MODE_FULL == mode) wave_table = &full_wave_table;
else wave_table = &partial_wave_table;
if (temperature < wave_table->min_temp ||
temperature >= wave_table->max_temp) {
wave_table = &full_wave_table; // 超出温区一律用全刷表
}
p_current_wave_from = (const uint8_t *)&wave_table->wave_table[0][0];
return wave_table->frame_count;
}
/* 第 3 步-2:8bit 波形 → 32bit EPIC LUT(核心就这一行左移) */
void epd_wave_table_fill_lut(uint32_t *p_epic_lut, uint32_t frame_num)
{
const uint8_t *p_frame_wave = p_current_wave_from + (frame_num * 256);
for (uint16_t i = 0; i < 256; i++)
p_epic_lut[i] = p_frame_wave[i] << 3;
}
/* 第 3 步-3/4:时序 + VCOM(数值务必按新屏数据手册改) */
const EPD_TimingConfig *epd_get_timing_config(void)
{
static const EPD_TimingConfig timing_config = {
.sclk_freq = 24, // 像素时钟,单位 MHz
.SDMODE = 0, // R7D005 屏要改为 1
.LSL = 0, .LBL = 0,
.LDL = LCD_HOR_RES_MAX/4, // 有效数据 clock 数(8 线数据要除 4)
.LEL = 1,
.fclk_freq = 83, // 行时钟,单位 KHz
.FSL = 1, .FBL = 3,
.FDL = LCD_VER_RES_MAX,
.FEL = 5,
};
return &timing_config;
}
uint16_t epd_get_vcom_voltage(void)
{
#if defined(LCD_USING_EPD_R7D005)
return 2100; // 这块屏要 2.1V 公共电极电压
#else
return 1050; // YZC085 等其他屏用 1.05V
#endif
}
# 第 4 步:project/Kconfig.proj —— choice 里加一项 + 补分辨率/DPI 默认值
choice
prompt "Custom LCD driver"
default LCD_USING_EPD_YZC085_V100 if BSP_USING_BOARD_SF32_OED_EPD_V11
default LCD_USING_EPD_YZC085_V100_V12 if BSP_USING_BOARD_SF32_OED_EPD_V12
config LCD_USING_EPD_YZC085_V100_V12
bool "6.0 rect electronic paper display(EPD YZC085_V1.05 1032x758) for V1.2 board"
select TSC_USING_GT967 if BSP_USING_TOUCHD # 触控芯片
select LCD_USING_OPM060D # 驱动源文件开关
select BSP_LCDC_USING_EPD_8BIT # DBI 8-bit 并行总线
config LCD_USING_EPD_R7D005
bool "6.0 rect electronic paper display(EPD R7D005_-1.30 1448x1072)"
select TSC_USING_FT5446U_V01 if BSP_USING_TOUCHD
select LCD_USING_OPM060D
select BSP_LCDC_USING_EPD_8BIT
endchoice
# 新屏务必补上这三组默认值,否则编译期分辨率是错的
config LCD_HOR_RES_MAX
int
default 1448 if LCD_USING_EPD_R7D005
default 1032 if LCD_USING_EPD_YZC085_V100
default 240 if LCD_USING_EPD_OPMO37A3
config LCD_VER_RES_MAX
int
default 1072 if LCD_USING_EPD_R7D005
default 758 if LCD_USING_EPD_YZC085_V100
default 416 if LCD_USING_EPD_OPMO37A3
config LCD_DPI
int
default 315 if LCD_USING_EPD_R7D005
default 300 if LCD_USING_EPD_YZC085_V100
default 130 if LCD_USING_EPD_OPMO37A3
# 收尾验证:确认新屏选项可见并默认选中
$ scons --board=sf32-oed-epd_v12 --board_search_path=.. --menuconfig
# (也可按 README 的等价写法:)
$ menuconfig --board=sf32-oed-epd_v12 --board_search_path=..
| 关键函数 | 干什么 | 新屏要改什么 |
|---|---|---|
epd_wave_table_get_frames | 按温度/模式选波形并返回帧数 | 温区划分、帧数 |
epd_wave_table_fill_lut | 8bit 波形 → 32bit EPIC LUT | 一般不用改(左移 3 位是通用约定) |
epd_get_timing_config | 像素/行时钟与有效数据长度 | sclk_freq / fclk_freq / LDL / FDL |
epd_get_vcom_voltage | 公共电极电压(mV) | 按数据手册返回值 |
四步配方的顺序是有讲究的,每一步都有"验证点":第 2 步填完数组,可以先只用全刷表点亮看灰阶是否正常;第 3 步改完 VCOM 和时序,先测一次全刷再测局刷(局刷要额外验证残影,第 4 章讲过);第 4 步加完 Kconfig,必须跑一次 menuconfig 看选项是否真的出现在 Custom LCD driver 下。小步验证、逐级点亮——这是移植硬件驱动最稳的节奏。
波形数组的值(0/1/2)来自屏幕厂商的波形文档,不是你自己发明的。两个常见坑:一是数组维度搞反——全刷表必须是 [32][256]、局刷是 [12][256],少一行都会让 epd_wave_table_get_frames 越界读;二是时序里的单位——sclk_freq 是 MHz、fclk_freq 是 KHz,差 1000 倍会让整屏闪烁或白屏。动手前把波形文档和数据手册放在手边,逐行对照着填。
回头看这个站点:四条技术线,Kotlin 教你怎么从零写一个 Android 应用,LVGL 教你怎么在嵌入式上画界面,ESP32 电子墨水屏线教你解剖一个真实阅读器的完整实现,而 SF32 电子书移植实战是站在第三条线肩膀上的一课——把同一个 EPUB 阅读器从 ESP32 芯片整体搬到 SiFli 芯片。它回答的不是"怎么写一个新东西",而是"怎么把一个能跑的东西搬到另一个平台":第 3-6 章换硬件层,第 7-10 章沿用解析与渲染,第 11-13 章重做 UI 与状态机,第 14-16 章盘点与扩展。这个"移植"的视角,是前三条线都没直接给的——它训练的是另一种能力:读已有代码、辨别哪些该搬哪些该重写。
如果把本教程的十六章压缩成一张地图,它是这样的:第 1-2 章(认识项目)给你坐标系和工具——项目是什么、怎么编译烧录、日志怎么看;第 3-6 章(硬件平台)回答"这台机器由什么组成"——板级、显示、输入、电源四条硬件线;第 7-10 章(解析与渲染)回答"一本书怎么变成屏幕上的字"——EPUB 解包、字体、设置、渲染;第 11-13 章(UI 与状态机)回答"整台机器怎么被组织起来"——状态机、书库流程、触控操作层;第 14-16 章(现状与展望)回答"它做到哪一步了、还能怎么变"——已实现盘点、未实现清单、二次开发。从"能跑起来"到"看得懂"再到"改得动",是这条线递进的三级台阶。
你的下一步,就照 16.3 那四步配方动手:挑一块你手上真实存在的墨水屏(数据手册 + 波形文档齐全的那种),照着模板走一遍"复制 → 填波形 → 改四函数 → 加 Kconfig → menuconfig 验证"。这一步走通,你就不再是"读过移植代码的人",而是"真的移植过一块屏的人"——第 15 章那份待完善清单里,你也可以顺手挑一条(比如给 disk/ 里重复的样书去重,或给天气补上文档)当成第一个真实 issue。鲁班说的"工欲善其事",到这一步才算圆满:工具你已经全有了,接下来就是干活。
# 本章核心配方速记:加新屏四步走
1. cp src/boards/display_dbi/epd_configs_opm060d.c \
src/boards/display_dbi/epd_configs_myedp.c # 复制模板 + 改名
2. # 填波形数组:全刷 [32][256],局刷 [12][256]
3. # 改四函数:波形选择 / LUT 转换 / 时序 / VCOM
4. # project/Kconfig.proj 加选项 + LCD_HOR_RES_MAX / VER_RES_MAX / DPI
$ scons --board=sf32-oed-epd_v12 --board_search_path=.. --menuconfig # 验证
# 加字两句话:往 TF 卡放字体,重启即可
$ mkdir -p /fonts
$ cp /path/to/MyFont.ttf /fonts/ # .ttf/.TTF/.otf/.OTF 都认
# 开机日志确认:
# [font_mgr] Found font: [1] MyFont (fonts/MyFont.ttf)
# [font_mgr] Total 2 fonts available
| 技术线 | 核心问题 | 本教程的独特价值 |
|---|---|---|
| Kotlin | 怎么从零写 Android 应用 | 语言 + 架构的系统路径 |
| LVGL | 怎么在嵌入式上画界面 | 嵌入式图形库的完整机制 |
| ESP32 电子墨水屏 | 怎么解剖一个真实阅读器 | 完整产品的读码能力 |
| SF32 移植实战(本线) | 怎么把它搬到另一块芯片 | 跨平台移植:辨别该搬 / 该重写 |
毕业前最后一课,鲁班讲的是"磨斧子的终点就是动手"。这台阅读器从头到尾都是真实代码,而你现在的工具箱里有:SCons 构建(第 2 章)、menuconfig(第 3 章)、模板文件 epd_configs_opm060d.c、Kconfig 配置块,以及最重要的——一套"先 grep 再写"的求证习惯。接下来不管你是移植新屏、加功能还是修 bug,记住这一条:先找到证据,再动手改。这一章学完,你可以真正毕业了。
下面三种书,分别该走 disk/ 还是 TF 卡?给出理由:① 出厂时随镜像预装的两本样书;② 你私藏的 20 本正文电子书,想随时换;③ 打算一次拷 500 本进设备。
回到 16.1 的对比表:只读+空间 vs 可插拔+大容量,再想一下"要不要重新编译烧录"。
① 放 disk/:随镜像打包、开机即有,适合"出厂即带"的固定内容。② 建议 TF 卡:内容要频繁换,放镜像每次都要重新编译烧录。③ 必须 TF 卡:500 本体积远超镜像文件系统容量,且只读镜像根本塞不下;同时还要注意第 12 章讲的单页列表上限(MAX_EPUB_LIST_SIZE)对一次能列出的本数有限制,需要靠翻页解决。
读 src/font_manager.c:加外部字体的完整步骤是什么?FONT_MAX_COUNT = 64 这个上限意味着"外部字体最多几个"?外部字体文件加载失败时,font_manager_select 会做什么?
看 font_manager_init 的扫描循环、is_ttf_file 认的后缀、以及 font_manager_select 的失败分支(返回 -4 前后发生了什么)。
步骤:在 TF 卡根目录建 /fonts,把 .ttf/.TTF/.otf/.OTF 放进去,重启;开机时 font_manager_init("/fonts") 扫描登记,新字体会出现在「阅读设置 → 字体」里。由于 index 0 被内置 Default 占用,外部字体最多 64 - 1 = 63 个。加载失败时:先 epd_font_ft_cache_clear() 清缓存、释放旧 FT_Face,若 epd_font_ft_init_from_file 返回非 0,则回退调用 epd_font_ft_init_builtin、把 g_current_font 归 0 并返回 -4——即"自动回退到内置默认字体"。
假设你要给项目加一块 1234×567、DPI 300 的新墨水屏(数据手册要求 VCOM 1800mV、像素时钟 18MHz)。按 16.3 的四步配方,列出每一步要动哪个文件、改哪个值。
对照 16.3 的四个代码块:模板在 display_dbi/ 下,Kconfig 在 project/ 下,四函数在屏驱文件里,分辨率/DPI 在 LCD_HOR_RES_MAX 等宏。
① 复制 epd_configs_opm060d.c 为 epd_configs_myedp.c,把 #if defined(LCD_USING_OPM060D) 改成新宏 LCD_USING_EPD_MYEDP。② 填波形数组:全刷 [32][256]、局刷 [12][256],数据来自新屏波形文档。③ 改四函数:epd_wave_table_get_frames 按温区选表;epd_get_timing_config 把 sclk_freq 改为 18;epd_get_vcom_voltage 改为 return 1800;;epd_wave_table_fill_lut 一般不动。④ 在 project/Kconfig.proj 的 choice 里加 config LCD_USING_EPD_MYEDP(select 触控/EPD 宏/总线宏),并补 LCD_HOR_RES_MAX = 1234、LCD_VER_RES_MAX = 567、LCD_DPI = 300,最后跑 menuconfig 验证。
从下面挑一件真的动手做,并记录过程:① 用第 15 章的结论给 disk/ 里重复的样书去重(想一想:删掉哪一份、改不改 SConstruct);② 找一块真实的墨水屏,照 16.3 的四步配方移植;③ 给天气功能写一页 README 文档(它现在完全没被文档承认)。做完后在报告里说明:你改了什么、怎么验证的。
① 去重的关键是"打包入口"在 FileSystemBuild("../disk", env),删文件即可,不用改脚本;③ 天气的代码证据在第 15 章已经备齐(wheather.c、PAN 依赖、状态机)。
① 删除 disk/pg43-images.epub(它与 oebps.epub MD5 相同,是冗余副本),保留 oebps.epub 与 no_oebps.epub;因为 disk/ 是整目录打包,删文件即可让镜像瘦身,SConstruct 无需改动。② 移植按四步配方,验证点是"先只亮全刷、再验证局刷残影、最后 menuconfig 确认选项"。③ 给天气写文档时,把链路写清楚:设备无 WiFi,天气依赖手机蓝牙网关(PAN),未连接时显示"尚未获取天气";并如实标注 API key 是硬编码在固件里的风险(这是第 15 章盘点出的真实事实)。三项任选其一,重点是"改了什么 + 怎么验证"的闭环。