终于实现啦LVGL加载使用littlefs文件系统中的字库文件的流式加载方式,极大减少了本来就很吃紧的内存占用。本文记录在小内存嵌入式设备上,用 自研流式解码 替代官方
lv_binfont_create一次性加载 ,在 不改动lv_font_conv --format bin产物格式 的前提下,把字库主体留在 Flash 文件系统 ,仅将 cmap / loca 等表结构常驻 RAM 的方案。 之前使用老的方式,内存直接就撑爆了。
工程路径:src/display/lvgl/ 下 lv_font_stream_bin.*、lv_font_lfs_binfont.*,以及 lv_fs_lfs_adapt.*。
目录
- 背景:老方式为何撑爆内存
- 目标与新方式总览
- [磁盘格式与官方 loader 的关系](#磁盘格式与官方 loader 的关系)
- 代码实现拆解
- [老方式 vs 新方式对比](#老方式 vs 新方式对比)
- [文件系统与 LVGL 的衔接](#文件系统与 LVGL 的衔接)
- 应用层接入与演示
- 已知限制与后续可做
- 排障与误区
- 结语

1. 背景:老方式为何撑爆内存
1.1 典型诉求
- 字库存放在 SPI Flash 上的 LittleFS (例如挂载后应用内路径
system/font.bin)。 - LVGL 通过
lv_fs访问盘符(本工程为L:),与lv_font_conv生成的 bin 字库对接。 - 设备
LV_MEM_SIZE(或内置 malloc 池) 很小(几十~一百多 KB 很常见),但 子集中文 bin 往往 100KB~数百 KB。
1.2 官方 lv_binfont_create 在做什么(逻辑层面)
对照 LVGL 官方 src/font/binfont_loader/lv_binfont_loader.c 里的 load_glyph():
- 会先扫一遍 glyf 区,算出 cur_bmp_size(所有字形位图总字节数);
- 再执行 glyph_bmp = lv_malloc(cur_bmp_size),把整段位图一次性放进 LVGL 的 lv_malloc
池(LV_MEM_SIZE),然后从文件里读进这块内存。
也就是说:不是「字在 Flash 里按需读、几乎不占 RAM」,而是 整颗字库的位图要在 RAM 里有一块连续空间。
官方实现(各 LVGL 版本中可能位于 lv_binfont_loader.c 或并入 lv_bin_decoder 等模块)在打开文件后的核心步骤是:
- 读
head,得到度量、bpp、压缩方式、loca 格式等; - 读
cmap,为每种 cmap 子表lv_malloc出unicode_list/glyph_id_ofs_list等; - 读
loca,得到每个 glyph 在glyf段内的偏移; - 在
load_glyph里 遍历每一个 glyph :先解析度量,再计算该字位图占多少字节,最后把所有字的位图 顺序拷进一块大的glyph_bitmap缓冲区 ------这块缓冲区通常就是cur_bmp_size累加后一次lv_malloc; - 同时分配
glyph_dsc[],并挂到lv_font_fmt_txt_dsc_t上,字体回调指向lv_font_get_glyph_dsc_fmt_txt/lv_font_get_bitmap_fmt_txt。
因此:字库文件在磁盘上有多大,位图部分最终几乎要完整进堆一份 (外加 cmap、loca、glyph_dsc、kern 等)。当 font.bin 位图总量 + 元数据 > LV_MEM 时,会出现分配失败或 在创建过程内 HardFault ,现象上像「一加载字库就崩」,甚至来不及打印 lv_binfont_create 返回地址。
官方的使用方式如下(看似简单,但会撑爆内存):
使用的是lv_binfont_create。
c
void xh_lv_font_bin_init_demo(lv_obj_t *label_text_half, lv_obj_t *label_text_full)
{
lv_font_t *f;
//xh_lv_fs_littlefs_register_drv();
f = lv_binfont_create(XIAOHONG_LVGL_FONT_BIN_LV_PATH);
if (f == NULL) {
printf("[xh_lv_font] lv_binfont_create failed: %s (检查文件、LV_USE_FS、字库是否为 LVGL bin 格式)\r\n",
XIAOHONG_LVGL_FONT_BIN_LV_PATH);
return;
}
s_bin_font = f;
/* 勿在堆上字体上随意写 fallback:与 LVGL 9 内部布局/const 语义冲突时可能把 get_glyph_dsc 等函数指针破坏成 NULL(fault: mtval≈0x20) */
printf("[xh_lv_font] loaded bin font OK: %s\r\n", XIAOHONG_LVGL_FONT_BIN_LV_PATH);
if (label_text_half != NULL) {
lv_obj_set_style_text_font(label_text_half, s_bin_font, LV_PART_MAIN);
lv_label_set_text(label_text_half,
"字库测试 OK\n"
"简体中文 你好世界\n"
"OpenHarmony 开源鸿蒙\n"
"LittleFS: /system/font.bin");
}
if (label_text_full != NULL) {
lv_obj_set_style_text_font(label_text_full, s_bin_font, LV_PART_MAIN);
lv_label_set_text(label_text_full,
"全屏字库演示:智能设备欢迎界面。一二三四五六七八九十。"
"物联网 人工智能 嵌入式 LVGL 9.4");
}
}
1.3 小结
老方式的矛盾是:「文件在 Flash」≠「内存占用小」 ------只要实现是 整库位图进 RAM,堆上限就是硬天花板。
2. 目标与新方式总览
2.1 目标
- 兼容现有 bin 文件布局 (与官方 loader 一致),不重做字库流水线。
- 常驻 RAM :仅
head副本、cmap 表、loca 偏移数组、打开的文件句柄及少量包装结构。 - 非常驻 :整块
glyf位图 ;每个 glyph 在 排版/绘制 时再 seek + read。 - LVGL 9 字体接口:实现自定义的
get_glyph_dsc/get_glyph_bitmap(后者写入lv_draw_buf_t的 A8 数据)。
2.2 新方式数据流(概念)
text
创建阶段:
font.bin → 解析 head、cmap、loca → 内存中仅保留「找得到 gid、算得出偏移」所需表
→ 记录 glyf 段在文件中的起始与长度 → 保持 lv_fs 文件打开
运行阶段(每个参与绘制的字符):
Unicode → cmap → gid
→ seek(glyf + offset[gid]) → 按位读度量
→ 再读该 glyph 的打包位图 → 展开为 A8 → 写入 draw_buf
代价 :绘制时 Flash 读取次数增加 、单字可能有 临时 lv_malloc(bmp_size) (见下文实现)。收益 :堆占用与 字库文件总大小 解耦,小屏中文场景往往可接受。
3. 磁盘格式与官方 loader 的关系
本实现 刻意对齐 官方 binfont 的二进制布局(标签 head / cmap / loca / glyf 等),字段如:
advance_width_bits、xy_bits、wh_bits与 按位迭代器读度量;index_to_loc_format:0 为 loca 每项 uint16 ,1 为 uint32;compression_id == 0表示 PLAIN 位图(本工程当前 仅支持 该模式)。
这样 lv_font_conv --format bin 生成的文件 无需转换 即可被流式加载(在「非压缩」前提下)。
4. 代码实现拆解
4.1 文件与职责
| 文件 | 职责 |
|---|---|
src/display/lvgl/lv_font_stream_bin.h |
对外 API:lv_font_stream_bin_create / destroy;注释中说明 PLAIN、单任务、无 kern 等约束。 |
src/display/lvgl/lv_font_stream_bin.c |
解析 bin、注册回调、流式读 glyf、位图展开为 A8。 |
src/display/lvgl/lv_font_lfs_binfont.c |
注册 L:、stat 检查、create 流式字库、给 label 套字体与 demo 文案。 |
src/display/lvgl/lv_fs_lfs_adapt.c |
把 fs_adapt_get_lfs() 得到的 lfs_t* 绑到 LVGL LittleFS 驱动的 user_data,避免 drv 为 NULL 时写崩。 |
src/display/lvgl/xh_font_paths.h |
L:/system/font.bin 与 lv_fs 路径宏。 |
BUILD.gn / CMakeLists.txt |
将 lv_font_stream_bin.c 编入工程。 |
4.2 运行时对象布局
核心思路:一个包装结构 里内嵌 lv_font_t 与 私有描述体 xh_stream_bin_dsc_t,font->dsc 指向后者;destroy 时用 offsetof 从 lv_font_t* 还原包装指针,统一释放。
私有描述体中(逻辑上)包含:
lv_fs_file_t file:创建成功后 一直保持打开 ,后续度量/位图都靠它seek/read;xh_font_header_bin_t fh:head解析结果;lv_font_fmt_txt_cmap_t *cmaps+cmap_num:与官方加载器相同的 cmap 内存形态;uint32_t *glyph_offset+loca_count:loca 表;glyf_start/glyf_length:glyf 段边界,用于计算每个 glyph 的打包位图长度。
4.3 lv_font_stream_bin_create 做了什么
实现要点(与源码顺序一致):
lv_malloc_zeroed分配包装结构;lv_fs_open只读打开路径。xh_read_label(..., "head")校验标签并得head段长度;读入xh_font_header_bin_t。compression_id != 0则失败返回------当前不实现压缩位图解压。xh_load_cmaps:与官方类似,为各子表分配unicode_list/glyph_id_ofs_list等。- 读
loca:读loca_count,按index_to_loc_format填充glyph_offset[]。 - 读
glyf标签,得glyf_length,记录glyf_start(文件内绝对偏移)。 - 初始化
lv_font_t:get_glyph_dsc/get_glyph_bitmap指向静态回调,line_height、base_line、underline等来自fh。
对应源码锚点(节选,便于对照阅读):
c
/* 打开文件、读 head、拒绝非 PLAIN、加载 cmap/loca、定位 glyf、挂回调 --- lv_font_stream_bin.c */
lv_font_t *lv_font_stream_bin_create(const char *lv_fs_path)
{
// ...
lv_fs_res_t fs_res = lv_fs_open(&sd->file, lv_fs_path, LV_FS_MODE_RD);
// ...
if(sd->fh.compression_id != 0) { /* LV_FONT_FMT_TXT_PLAIN */
// ...
}
int32_t cmaps_length = xh_load_cmaps(&sd->file, sd, cmaps_start);
// ... loca ...
sd->glyf_start = loca_start + (uint32_t)loca_length;
int32_t glyf_len = xh_read_label(&sd->file, (int)sd->glyf_start, "glyf");
// ...
font->get_glyph_dsc = xh_stream_get_glyph_dsc;
font->get_glyph_bitmap = xh_stream_get_glyph_bitmap;
// ...
}
4.4 get_glyph_dsc:只算度量,不读整块位图
流程:
- Unicode →
xh_get_glyph_id(逻辑等价于lv_font_get_glyph_dsc_fmt_txt所用的 cmap 查找;工程内自写二分查找替代lv_utils_bsearch,避免额外头文件依赖)。 xh_stream_read_glyph_metrics:lv_fs_seek到glyf_start + glyph_offset[gid],用与官方一致的 bit iterator 读出 advance、ofs、box;并对 gid==0(.notdef)做与官方一致的清零处理。- 填充
lv_font_glyph_dsc_t(含 tab 展开、adv_w归一化等与 fmt_txt 对齐的细节)。
4.5 get_glyph_bitmap:按需读一个 glyph 的打包位图
流程:
- 再次
xh_stream_read_glyph_metrics得到nbits与bmp_size(由 下一 glyph 偏移 − 当前偏移 − 度量占用字节 推导,与官方load_glyph一致)。 lv_malloc(bmp_size)读入 打包位图 (当度量位数非 8 对齐时,走与官方相同的 逐位读取 慢路径)。xh_expand_plain_to_draw_buf:将 1/2/4/8 bpp 展开为 A8 ,写入draw_buf->data,逻辑对齐lv_font_get_bitmap_fmt_txt的 PLAIN 分支(含opa2_table/opa4_table)。lv_free(packed);lv_draw_buf_flush_cache。
说明:这里 每个绘制到的字 可能触发 一次小堆分配 ;若需进一步压碎片,可改为 静态复用缓冲 或 按最大 glyph 预分配一块,属于后续优化。
4.6 lv_font_stream_bin_destroy
释放 cmap 内各子表指针、glyph_offset 、lv_fs_close 、释放包装块。见 xh_stream_wrap_destroy。
5. 老方式 vs 新方式对比
5.1 内存模型
| 项目 | 官方 lv_binfont_create(老方式) |
lv_font_stream_bin_create(新方式) |
|---|---|---|
| cmap / loca | 常驻 RAM | 常驻 RAM(形式相近) |
glyph_dsc[] |
常驻 RAM(每个 glyph 一项) | 不分配 ;度量 现场从文件解析 |
glyph_bitmap |
整库位图连续块,常驻 RAM | 无 ;仅绘制时 单字临时缓冲 |
| kern | 可加载 kern 表 | 当前 未加载;字距为 0 |
| 文件句柄 | 通常加载完即关闭 | 保持打开 直到 destroy |
结论 :老方式的堆峰值近似 「位图总和 + 表 + 描述」 ;新方式近似 「表 + 单字 bmp_size 峰值 + draw_buf(LVGL 管理)」 ,与 font.bin 文件总大小 弱相关。
5.2 CPU / IO
| 项目 | 老方式 | 新方式 |
|---|---|---|
| 首次加载 | 读整个 glyf,CPU 解析累加 | 只读 head/cmap/loca,IO 分散但总量小 |
| 绘制字符 | 内存访问 | 每次 seek/read(度量可能读两遍:dsc 一次、bitmap 一次) |
5.3 功能与兼容性
| 项目 | 老方式 | 新方式 |
|---|---|---|
| PLAIN bin | 支持 | 支持 |
压缩 bin + LV_USE_FONT_COMPRESSED |
支持 | 未实现 (compression_id!=0 创建失败) |
req_raw_bitmap |
可返回 glyph_bitmap 内指针 |
不支持 (返回 NULL 并打日志) |
| 多任务同字体 | 一般安全(只读内存) | 需保证对同一 lv_fs_file_t 的互斥 或单任务使用 |
6. 文件系统与 LVGL 的衔接
6.1 路径约定
xh_font_paths.h 中默认:
c
#define XIAOHONG_LVGL_FONT_BIN_LV_PATH "L:/system/font.bin"
与 LittleFS 应用侧路径 system/font.bin 对应,盘符 L: 须与 lv_conf.h 里 LV_FS_LITTLEFS_LETTER 一致。
6.2 lv_fs_lfs_adapt:避免绑定崩溃
部分 SDK 的 lv_littlefs_set_handler 未校验 lv_fs_get_drv 是否为 NULL,若在驱动未注册时写入 user_data ,会直接导致 a0=0 类异常 。本工程在 lv_fs_lfs_adapt.c 中:
- 先
lv_fs_get_drv,必要时lv_fs_littlefs_init(); - 仅当 drv 非空 时
drv->user_data = lfs。
并在 fs_adapt_get_lfs() 非空、已挂载的前提下完成绑定,打印 bound lfs_t* 便于串口确认。
7. 应用层接入与演示
7.1 初始化入口
lv_font_lfs_binfont.c 中 xh_lv_font_bin_init_demo 完成:
xh_lv_fs_littlefs_register_drv();fs_adapt_stat("system/font.bin", &fsz)做存在性与最小体积检查;lv_font_stream_bin_create(XIAOHONG_LVGL_FONT_BIN_LV_PATH);- 对传入的
lv_label设置lv_obj_set_style_text_font与 UTF-8 测试串(中文、数字、标点)。
工程里曾用 LV_MEM_SIZE 与 fsz 比较跳过 binfont 的逻辑,在流式方案下 已移除 ------因为 不再整库进堆 ;若 cmap 极大,仍应关注 表结构本身 的 RAM 上限。
c
void xh_lv_font_bin_init_demo(lv_obj_t *label_text_half, lv_obj_t *label_text_full)
{
lv_font_t *f;
if (xh_lv_fs_littlefs_register_drv() != 0) {
printf("[xh_lv_font] skip binfont: L: drive not bound\r\n");
return;
}
{
unsigned int fsz = 0U;
if (fs_adapt_stat("system/font.bin", &fsz) != 0) {
printf("[xh_lv_font] skip: fs_adapt_stat(system/font.bin) failed (no file or not mounted)\r\n");
return;
}
if (fsz < 64U) {
printf("[xh_lv_font] skip: font.bin too small (%u bytes)\r\n", fsz);
return;
}
printf("[xh_lv_font] system/font.bin size=%u, stream load (cmap/loca in RAM, glyf on-demand)...\r\n", fsz);
}
f = lv_font_stream_bin_create(XIAOHONG_LVGL_FONT_BIN_LV_PATH);
printf("[xh_lv_font] lv_font_stream_bin_create returned %p\r\n", (void *)f);
if (f == NULL) {
printf("[xh_lv_font] stream bin font failed: %s (须非压缩 bin,lv_font_conv --format bin)\r\n",
XIAOHONG_LVGL_FONT_BIN_LV_PATH);
return;
}
s_bin_font = f;
printf("[xh_lv_font] stream bin font OK: %s\r\n", XIAOHONG_LVGL_FONT_BIN_LV_PATH);
/* 中文/数字/标点:缺字时 LVGL 会显示方框或空白,可对照 lv_font_conv 子集 */
if (label_text_half != NULL) {
lv_obj_set_style_text_font(label_text_half, s_bin_font, LV_PART_MAIN);
lv_label_set_text(label_text_half,
"流式 bin 字库\n"
"加载成功\n"
"L:/system/font.bin");
}
if (label_text_full != NULL) {
lv_obj_set_style_text_font(label_text_full, s_bin_font, LV_PART_MAIN);
lv_label_set_text(label_text_full,
"中文显示测试\n"
"一二三四五六七八九十\n"
"常用标点:,。!?;:\n"
"流式从 Flash 读字形,常驻 cmap/loca,glyf 按需读取。");
}
}

7.2 与 LvglTask、UI 的交互
- 字库加载可在
lv_init之后 、首帧 timer 中延迟执行,避免与初始化抢堆(工程里已有相关注释与xh_lv_font_bin_schedule_demo思路)。 - 界面上有
text_half/text_full两个 label :默认text_full带LV_OBJ_FLAG_HIDDEN,故 仅text_half上的 demo 文案上电可见 ;长段若写在text_full,需在进入对应 UI 状态时 去掉 HIDDEN 或改写到text_half。这与字库实现无关,属 UI 状态机 设计。
8. 已知限制与后续可做
- 仅 PLAIN:压缩 bin 需接官方 RLE/解压路径或扩展流式解压状态机。
- 无 kern:若字库含 kern 表,当前未读入;需扩展创建阶段加载或流式查询(复杂度高)。
- 单文件句柄 + 多线程 :多任务并发排版需 互斥 或 每线程独立文件描述符(若 FS 支持)。
get_glyph_bitmap临时 malloc :可改为 环形缓冲 / LRU 字形缓存 平衡 碎片 与 读放大。- 度量读两次 :可缓存「当前 gid」的度量到
xh_stream_bin_dsc_t减少一次 seek(注意线程安全)。
9. 排障与误区
| 现象 | 可能原因 |
|---|---|
lv_font_stream_bin_create 返回 NULL,compression_id 日志 |
使用了 压缩 bin,需改 lv_font_conv 为非压缩或实现解压。 |
| 打开失败 | L: 未注册 、路径与 LittleFS 实际挂载不一致、font.bin 未写入镜像。 |
| 中文方框 | 子集未包含该字 ;检查 lv_font_conv 的 range / 字符列表。 |
| 绑定 L: 即崩 | drv 为 NULL 仍写 user_data ;检查是否已用本工程的 lv_fs_lfs_adapt 修复路径。 |
| 老方式「文件在 Flash 却仍 OOM」 | 正常:官方 loader 仍会整库位图进 RAM;与「文件在不在 Flash」无关。 |
10. 结语
在 LVGL 9 上,通过 自研 lv_font_stream_bin ,在 不改变 bin 磁盘格式 的前提下,把 内存瓶颈 从 「整库位图」 换成 「cmap/loca + 单字临时缓冲」 ,使 LittleFS 上的大子集中文字库 在 小 LV_MEM 设备上成为可行方案。代价是 绘制路径上的 Flash 读取与少量临时分配 ,需在具体产品上按 屏刷频率、字表大小、CPU 主频 做一次实测权衡。
若后续将 压缩 、缓存 、kern 逐步补齐,可在同一架构上继续演进,而无需推翻 lv_font_conv 工具链。