第16章:二次开发与移植新屏

「鲁班说:要做出东西,先把工具备齐。」这一章是终点站,也是真正的起点——给这台阅读器加电子书(16.1)、加字体(16.2)、换一块新屏幕(16.3),最后回顾这四条技术线里本项目的位置(16.4)。前 15 章学会"读代码",这一章学会"改代码":你手上已经有完整的工具链和模板。

🔨

本章导师:鲁班

核心方法论:工欲善其事——先备齐工具和模板,再做二次开发

「鲁班教徒弟,第一课不是教做柜子,而是教磨斧子。这一章也一样:换屏幕不是从头写驱动,而是『复制模板、填波形、改四个函数、加一行配置』;加书不是改代码,而是『知道书往哪放』;加字体不是写解析器,而是『往目录里丢一个文件』。工具备好了,活儿都是搭积木。我们把前一章留下的『待完善』清单,一件件变成『动手就能做』的活。」

16.1 加 EPUB:少量进镜像,大量走 TF 卡

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.epubpg43-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 里找到它被烧录的入口——你就能判断"改完要不要重新编译烧录",而这是最容易踩的坑。

16.2 加外部字体:/fonts 即插即用

第 8 章讲过内置中文字体是怎么从 Flash XIP(在 Flash 里直接取数据、不搬进 RAM 的执行方式)加载的。这一节把"系统怎么认识你自己的字体"讲完整。字体管理器 src/font_manager.c 的全部秘密就藏在一个上限、一个目录和一个后缀检查里。上限是三个宏:FONT_MAX_COUNT 64(内置 Default 占 1 个,外部字体最多 64 - 1 = 63 个)、FONT_NAME_MAX 32FONT_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;
}
来源加载函数数据在哪失败策略
内置 Defaultepd_font_ft_init_builtinFlash XIP,编译进固件—(永远可用)
TF 卡外部字体epd_font_ft_init_from_fileTF 卡 /fonts,FreeType 直接读文件回退内置 Default,返回 -4
鲁班提示

验证"字体加对了没"最快的方法,不是看代码,是看开机日志font_manager_init 每扫到一个字体就打一行 [font_mgr] Found font: [N] 名字 (路径),结束再打 [font_mgr] Total N fonts available。第 2 章教你用串口/调试口看 RT-Thread 日志时讲过怎么读 log——现在它就是你的"磨斧子"。加完字体重启,扫一眼这两行,比猜代码快十倍。

16.3 移植新屏:四步配方

这一节是第 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.projchoice "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_lut8bit 波形 → 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 倍会让整屏闪烁或白屏。动手前把波形文档和数据手册放在手边,逐行对照着填。

16.4 本教程回顾:从三线到四线,你的下一步

回头看这个站点:四条技术线,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.cKconfig 配置块,以及最重要的——一套"先 grep 再写"的求证习惯。接下来不管你是移植新屏、加功能还是修 bug,记住这一条:先找到证据,再动手改。这一章学完,你可以真正毕业了。

章末练习

练习 1:书放哪 入门

下面三种书,分别该走 disk/ 还是 TF 卡?给出理由:① 出厂时随镜像预装的两本样书;② 你私藏的 20 本正文电子书,想随时换;③ 打算一次拷 500 本进设备。

提示

回到 16.1 的对比表:只读+空间 vs 可插拔+大容量,再想一下"要不要重新编译烧录"。

参考答案

① 放 disk/:随镜像打包、开机即有,适合"出厂即带"的固定内容。② 建议 TF 卡:内容要频繁换,放镜像每次都要重新编译烧录。③ 必须 TF 卡:500 本体积远超镜像文件系统容量,且只读镜像根本塞不下;同时还要注意第 12 章讲的单页列表上限(MAX_EPUB_LIST_SIZE)对一次能列出的本数有限制,需要靠翻页解决。

练习 2:加外部字体走一遍流程 进阶

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——即"自动回退到内置默认字体"。

练习 3:移植新屏四步走 进阶

假设你要给项目加一块 1234×567、DPI 300 的新墨水屏(数据手册要求 VCOM 1800mV、像素时钟 18MHz)。按 16.3 的四步配方,列出每一步要动哪个文件、改哪个值。

提示

对照 16.3 的四个代码块:模板在 display_dbi/ 下,Kconfig 在 project/ 下,四函数在屏驱文件里,分辨率/DPI 在 LCD_HOR_RES_MAX 等宏。

参考答案

① 复制 epd_configs_opm060d.cepd_configs_myedp.c,把 #if defined(LCD_USING_OPM060D) 改成新宏 LCD_USING_EPD_MYEDP。② 填波形数组:全刷 [32][256]、局刷 [12][256],数据来自新屏波形文档。③ 改四函数:epd_wave_table_get_frames 按温区选表;epd_get_timing_configsclk_freq 改为 18;epd_get_vcom_voltage 改为 return 1800;epd_wave_table_fill_lut 一般不动。④ 在 project/Kconfig.proj 的 choice 里加 config LCD_USING_EPD_MYEDPselect 触控/EPD 宏/总线宏),并补 LCD_HOR_RES_MAX = 1234LCD_VER_RES_MAX = 567LCD_DPI = 300,最后跑 menuconfig 验证。

练习 4:为项目做一次真实的二次开发 挑战

从下面挑一件真的动手做,并记录过程:① 用第 15 章的结论给 disk/ 里重复的样书去重(想一想:删掉哪一份、改不改 SConstruct);② 找一块真实的墨水屏,照 16.3 的四步配方移植;③ 给天气功能写一页 README 文档(它现在完全没被文档承认)。做完后在报告里说明:你改了什么、怎么验证的。

提示

① 去重的关键是"打包入口"在 FileSystemBuild("../disk", env),删文件即可,不用改脚本;③ 天气的代码证据在第 15 章已经备齐(wheather.c、PAN 依赖、状态机)。

参考答案

① 删除 disk/pg43-images.epub(它与 oebps.epub MD5 相同,是冗余副本),保留 oebps.epubno_oebps.epub;因为 disk/ 是整目录打包,删文件即可让镜像瘦身,SConstruct 无需改动。② 移植按四步配方,验证点是"先只亮全刷、再验证局刷残影、最后 menuconfig 确认选项"。③ 给天气写文档时,把链路写清楚:设备无 WiFi,天气依赖手机蓝牙网关(PAN),未连接时显示"尚未获取天气";并如实标注 API key 是硬编码在固件里的风险(这是第 15 章盘点出的真实事实)。三项任选其一,重点是"改了什么 + 怎么验证"的闭环。