OpenHarmony海思WS63星闪平台:LVGL 9 + LittleFS:字库文件按需流式加载,减少内存占用的实践笔记

终于实现啦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.*


目录

  1. 背景:老方式为何撑爆内存
  2. 目标与新方式总览
  3. [磁盘格式与官方 loader 的关系](#磁盘格式与官方 loader 的关系)
  4. 代码实现拆解
  5. [老方式 vs 新方式对比](#老方式 vs 新方式对比)
  6. [文件系统与 LVGL 的衔接](#文件系统与 LVGL 的衔接)
  7. 应用层接入与演示
  8. 已知限制与后续可做
  9. 排障与误区
  10. 结语

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 等模块)在打开文件后的核心步骤是:

  1. head,得到度量、bpp、压缩方式、loca 格式等;
  2. cmap ,为每种 cmap 子表 lv_mallocunicode_list / glyph_id_ofs_list 等;
  3. loca ,得到每个 glyph 在 glyf 段内的偏移;
  4. load_glyph遍历每一个 glyph :先解析度量,再计算该字位图占多少字节,最后把所有字的位图 顺序拷进一块大的 glyph_bitmap 缓冲区 ------这块缓冲区通常就是 cur_bmp_size 累加后一次 lv_malloc
  5. 同时分配 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_bitsxy_bitswh_bits按位迭代器读度量;
  • index_to_loc_format0 为 loca 每项 uint161uint32
  • 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.binlv_fs 路径宏。
BUILD.gn / CMakeLists.txt lv_font_stream_bin.c 编入工程。

4.2 运行时对象布局

核心思路:一个包装结构 里内嵌 lv_font_t私有描述体 xh_stream_bin_dsc_tfont->dsc 指向后者;destroy 时用 offsetoflv_font_t* 还原包装指针,统一释放。

私有描述体中(逻辑上)包含:

  • lv_fs_file_t file :创建成功后 一直保持打开 ,后续度量/位图都靠它 seek/read
  • xh_font_header_bin_t fhhead 解析结果;
  • 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 做了什么

实现要点(与源码顺序一致):

  1. lv_malloc_zeroed 分配包装结构;lv_fs_open 只读打开路径。
  2. xh_read_label(..., "head") 校验标签并得 head 段长度;读入 xh_font_header_bin_t
  3. compression_id != 0 则失败返回------当前不实现压缩位图解压。
  4. xh_load_cmaps :与官方类似,为各子表分配 unicode_list / glyph_id_ofs_list 等。
  5. loca :读 loca_count,按 index_to_loc_format 填充 glyph_offset[]
  6. glyf 标签,得 glyf_length ,记录 glyf_start(文件内绝对偏移)。
  7. 初始化 lv_font_tget_glyph_dsc / get_glyph_bitmap 指向静态回调,line_heightbase_lineunderline 等来自 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:只算度量,不读整块位图

流程:

  1. Unicode → xh_get_glyph_id (逻辑等价于 lv_font_get_glyph_dsc_fmt_txt 所用的 cmap 查找;工程内自写二分查找替代 lv_utils_bsearch,避免额外头文件依赖)。
  2. xh_stream_read_glyph_metricslv_fs_seekglyf_start + glyph_offset[gid] ,用与官方一致的 bit iterator 读出 advance、ofs、box;并对 gid==0(.notdef)做与官方一致的清零处理。
  3. 填充 lv_font_glyph_dsc_t (含 tab 展开、adv_w 归一化等与 fmt_txt 对齐的细节)。

4.5 get_glyph_bitmap:按需读一个 glyph 的打包位图

流程:

  1. 再次 xh_stream_read_glyph_metrics 得到 nbitsbmp_size (由 下一 glyph 偏移 − 当前偏移 − 度量占用字节 推导,与官方 load_glyph 一致)。
  2. lv_malloc(bmp_size) 读入 打包位图 (当度量位数非 8 对齐时,走与官方相同的 逐位读取 慢路径)。
  3. 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)。
  4. lv_free(packed)lv_draw_buf_flush_cache

说明:这里 每个绘制到的字 可能触发 一次小堆分配 ;若需进一步压碎片,可改为 静态复用缓冲按最大 glyph 预分配一块,属于后续优化。

4.6 lv_font_stream_bin_destroy

释放 cmap 内各子表指针、glyph_offsetlv_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.hLV_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.cxh_lv_font_bin_init_demo 完成:

  1. xh_lv_fs_littlefs_register_drv()
  2. fs_adapt_stat("system/font.bin", &fsz) 做存在性与最小体积检查;
  3. lv_font_stream_bin_create(XIAOHONG_LVGL_FONT_BIN_LV_PATH)
  4. 对传入的 lv_label 设置 lv_obj_set_style_text_font 与 UTF-8 测试串(中文、数字、标点)。

工程里曾用 LV_MEM_SIZEfsz 比较跳过 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_fullLV_OBJ_FLAG_HIDDEN ,故 text_half 上的 demo 文案上电可见 ;长段若写在 text_full,需在进入对应 UI 状态时 去掉 HIDDEN 或改写到 text_half。这与字库实现无关,属 UI 状态机 设计。

8. 已知限制与后续可做

  1. 仅 PLAIN:压缩 bin 需接官方 RLE/解压路径或扩展流式解压状态机。
  2. 无 kern:若字库含 kern 表,当前未读入;需扩展创建阶段加载或流式查询(复杂度高)。
  3. 单文件句柄 + 多线程 :多任务并发排版需 互斥每线程独立文件描述符(若 FS 支持)。
  4. get_glyph_bitmap 临时 malloc :可改为 环形缓冲 / LRU 字形缓存 平衡 碎片读放大
  5. 度量读两次 :可缓存「当前 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 工具链

相关推荐
特立独行的猫a1 天前
OpenHarmony海思WS63星闪平台:移植Mongoose网络库到WS63平台指南
网络·openharmony·星闪·mongoose·海思·ws63
特立独行的猫a1 天前
OpenHarmony海思WS63星闪平台 LittleFS文件系统移植指南(使用外扩Flash驱动)
智能硬件·openharmony·星闪·littlefs·ws63
键盘鼓手苏苏2 天前
Flutter 三方库 p2plib 的鸿蒙化适配指南 - 实现高性能的端到端(P2P)加密通讯、支持分布式节点发现与去中心化数据流传输实战
flutter·harmonyos·鸿蒙·openharmony
加农炮手Jinx2 天前
Flutter 组件 heart 适配鸿蒙 HarmonyOS 实战:分布式心跳监控,构建全场景保活检测与链路哨兵架构
flutter·harmonyos·鸿蒙·openharmony
王码码20352 天前
Flutter 三方库 dns_client 的鸿蒙化适配指南 - 告别 DNS 劫持、探索 DNS-over-HTTPS (DoH) 技术、构建安全的鸿蒙网络请求环境
flutter·harmonyos·鸿蒙·openharmony·dns_client
键盘鼓手苏苏2 天前
Flutter 组件 highlighter 适配鸿蒙 HarmonyOS 实战:高性能语法高亮,构建大规模代码分析与文本染色架构
flutter·harmonyos·鸿蒙·openharmony
雷帝木木2 天前
Flutter 三方库 http_client_interceptor 的鸿蒙化适配指南 - 实现原生 HttpClient 的全量请求拦截、支持端侧动态 Headers 注入与网络流量审计实战
flutter·harmonyos·鸿蒙·openharmony·http_client_interceptor
坚果派·白晓明4 天前
三方库ada
harmonyos·鸿蒙·openharmony
特立独行的猫a5 天前
CMake与GN构建系统对比及GN使用指南
harmonyos·cmake·openharmony·构建·gn