OpenHarmony 小鸿 AI 开发实战 08:LittleFS 与流式 binfont 的完整中文显示链
在 240×240 小屏上显示“正在连接”并不难,难的是让服务端随时返回的中文、标点和常用符号都能显示,同时又不把整套中文字形常驻 WS63 的 SRAM。当前小鸿工程采用的办法不是继续扩大内置 C 数组,而是把二进制字库放进外部 W25Q128,由 LittleFS 管理文件,通过 HTTP Range 支持续传,最后由 LVGL 按需读取字形。
本文依据当前 OpenHarmony mini、LiteOS-M 设备侧的外部 Flash、LittleFS、字库下载和 LVGL 流式加载源码整理。设备源码仓库处于干净的 OpenHarmony-6.1.0.31-Release 分支;本地服务端使用的 font.bin 为 881040 bytes,SHA-256 为 5D961B3466AB011429C2550027504A4EEC5F2CD9FB1E9ADDC22C137ED96AC386。本轮重新核对了文件和代码,没有重新让设备下载字库,也没有重新跑全部 Unicode 字形的实机显示验收。

外部 W25Q128 与内部 4 MiB Flash 必须分开
第 04 篇验证的 4 MiB 原始备份对应 WS63 内部 GD25Q32 烧录空间;本文的文件系统位于板上的外部 W25Q128。当前 w25q128.h 把 P0 分区设为 0x01000000,即 16 MiB,并给出 256-byte page 与 4096-byte sector。两种 Flash 不能因为都叫“固件存储”就混为一谈。
#define W25X_P0_ADDR (0x00000000U)
#define W25X_P0_SIZE (0x01000000U)
#define W25X_PAGE_SIZE (256U)
#define W25X_SECTOR_SIZE (4096U)
LittleFS 的起始地址和容量直接引用 P0 宏,读写粒度为 16 bytes,块大小引用扇区,cache 为一页,lookahead 为 32 bytes。这里没有把 16 MiB 全部复制进内存;这些参数描述的是外部介质布局和文件系统工作缓冲。

挂载失败不能悄悄格式化并假装数据还在
littlefs_adapt.c 在挂载前校验块大小、起始地址和容量是否越过 W25Q128 P0 边界。挂载函数维护全局 g_lfs,成功后 fs_adapt_get_lfs() 才会返回有效实例,供 LVGL 注册 L: 盘。字库加载代码在实例为空时直接跳过,并打印“LittleFS not mounted”,不会继续用无效文件句柄访问 Flash。
这条边界很重要:文件系统故障、字库缺失和字体解析失败是三个不同层次。排查方框字时,应先确认挂载,再确认文件,再确认 bin 格式和码点范围,而不是看到 UI 有文字就断定外部字库已经工作。
#define CONFIG_LFS_EXT_FLASH_START_ADDR W25X_P0_ADDR
#define CONFIG_LFS_EXT_FLASH_SIZE W25X_P0_SIZE
#define CONFIG_LFS_EXT_FLASH_READ_SIZE 16U
#define CONFIG_LFS_EXT_FLASH_PROG_SIZE 16U
#define CONFIG_LFS_EXT_FLASH_BLOCK_SIZE W25X_SECTOR_SIZE
#define CONFIG_LFS_EXT_FLASH_CACHE_SIZE W25X_PAGE_SIZE
字库路径在下载层与 LVGL 层使用同一个来源
下载器把远程文件保存为 system/font.bin;LVGL 文件系统路径为 L:/system/font.bin。xh_font_paths.h 同时维护这两个常量,避免一处写 /font.bin、另一处读 /system/font.bin。加载器仍保留根目录 font.bin 的兼容探测,但主路径明确指向 system/font.bin。
#define XIAOHONG_LVGL_FONT_BIN_LV_PATH "L:/system/font.bin"
#define XIAOHONG_FONT_LFS_REL_PATH_DEFAULT "system/font.bin"
font_download_run() 会拒绝包含 /、.. 或超过 48 字符的 basename,再用固定 /system 目录拼接目标。这个校验不是完整安全沙箱,但可以避免 OTA 返回的文件名越过预期目录。
Range 续传围绕临时文件大小建立
通用下载器在没有指定后缀时使用 .part,但当前字体任务显式把 part_suffix 设为 .tmp。若这个临时文件已经存在,就读取其大小作为 resume_offset,请求头增加 Range: bytes=<offset>-。服务端返回 206 时还要检查 Content-Range 的起点是否等于本地偏移;不一致就放弃本轮续传,回退到从零开始。
已有 system/font.bin.tmp
-> stat 得到 resume_offset
-> HTTP Range: bytes=resume_offset-
-> 206 且 Content-Range 起点一致
-> 从 offset 继续写
若服务器返回 416,或者忽略 Range 直接返回 200,代码不会把新内容接在旧内容后。416 触发 HTTP_LFS_RES_FALLBACK_FULL,200 则清空临时文件并重新从零写。这比“只要网络恢复就 append”多了一层完整性保护。

Content-Length、chunked 与连接关闭都有独立处理
http_lfs_download.c 同时处理固定 Content-Length、Transfer-Encoding 中包含 chunked,以及无长度直到连接关闭三种响应。chunked 判断不是简单比较整段字符串是否等于 "chunked",因为实际头可能是逗号分隔的编码列表。每段 payload 都通过 fs_adapt_write() 循环写完,短写会继续,不把一次 write 返回值等同于全部成功。
连接超时和完整任务超时也分开:connect 与 body idle 各 30 秒,overall 为 300 秒。字库层在一次下载失败后最多执行 8 轮,基础退避为 600 ms 乘以轮数;进度每 32 KiB 通知一次 UI。源码能证明这些上限存在,但不能据此声称任意弱网都能恢复。
#define FONT_DL_MAX_ROUNDS 8U
#define FONT_DL_RETRY_MS_BASE 600U
#define FONT_DL_PROGRESS_STRIDE (32U * 1024U)
for (unsigned round = 1U;
round <= FONT_DL_MAX_ROUNDS;
round++) {
int r = http_lfs_download_blocking(¶m);
if (r == 0) {
break;
}
osal_msleep(FONT_DL_RETRY_MS_BASE * round);
}
完成下载后先验大小,再进行原子替换
下载结束时先比较实际写入量和预期量,再 stat 最终临时文件的大小。通过后才关闭旧字体引用、把 .tmp 重命名成正式文件,并重新探测。任何中途断电都应该留下旧正式文件或新临时文件,而不是留下一个名字正确但内容只写了一半的 font.bin。
这里的“原子”是文件系统层 rename 语义,不代表跨所有掉电时刻都有数据库级事务保证。文章能确认实现顺序和检查点,不能替代真实断电注入测试。

版本标记与文件存在检查不是同一件事
下载成功后代码更新 font version,并写入 /font_dl_ok 标记,内容为固定 magic。启动时还会独立检查 system/font.bin 或兼容路径 font.bin 是否至少 64 bytes。也就是说,只有标记没有文件不能算安装成功;只有一个过小文件也不会被当作有效字库。
64 bytes 只是最低结构门槛,不是完整字库哈希验证。当前服务端 OTA 元数据包含 size 和 SHA-256,但设备侧这条下载通道主要以 HTTP 长度、文件大小、版本和 bin 解析结果建立信任。若要把字库更新提升到产品级,还应在设备端落地 SHA-256 校验,再允许 rename。
流式 binfont 只把必要索引留在 RAM
lv_font_lfs_binfont.c 先探测文件,再调用 lv_font_stream_bin_create()。流式加载器解析 header、cmap 与 loca 等索引结构,字形 bitmap 在 LVGL 请求具体 Unicode 时才从 L: 盘 seek/read。当前 glyph cache 总预算为 1024 bytes、6 槽,metrics cache 为 8 槽;这与把 881040-byte 文件整体加载进堆完全不同。
#define XH_STREAM_GLYPH_CACHE_TOTAL 1024U
#define XH_STREAM_GLYPH_CACHE_SLOTS 6U
#define XH_STREAM_METRICS_CACHE_SLOTS 8U
font->get_glyph_dsc = xh_stream_get_glyph_dsc;
font->get_glyph_bitmap = xh_stream_get_glyph_bitmap;
单个 glyph 的打包数据如果超过缓存槽容量仍可直接读取,只是不进入 LRU。取 bitmap 时还会检查模块 magic,避免字体已释放后继续把其他 dsc 当作流式字体解引用。代码里保留了针对 Load fault 和 UAF 风险的防护注释,说明这个适配层经历过实际内存问题排查。

外部字库失败时仍有内置字体回退
流式字体创建成功后把 lv_font_Chinese_18_UI 设为 fallback。若 LittleFS 未挂载、文件不存在或 bin 解析失败,界面仍可使用内置字体显示启动和关键状态文字,只是无法保证服务端任意回答的全部字形都存在。
外部 bin 必须由兼容的 lv_font_conv --format bin 生成,并包含实际需要的 Unicode range 或 symbols。文件大小正确不等于 cmap 一定覆盖某个生僻字;出现方框时应记录具体码点,再核对生成命令和 cmap,而不是盲目增大缓存。
验证要分成文件、解析、显示和故障恢复四层
文件层检查服务端 size/hash、Range 206、设备端 .tmp 和正式文件大小;解析层检查 header、cmap、loca 与创建返回值;显示层用包含常用字、标点、Emoji 和缺失字形的固定文本;恢复层测试中断下载、服务器忽略 Range、416、重启和旧字库回退。

本轮确认的是当前源码结构、常量、文件哈希和本地后端文件一致性,没有执行新的设备下载与断电测试。结论应写成“当前 OpenHarmony WS63 工程具备外部 LittleFS 字库、Range 续传和 LVGL 流式读取实现”,不能写成“所有中文字符与所有弱网场景都已实机通过”。
更多推荐


所有评论(0)