先说结论,这套方案我已经跑通了:ESP32-S3 + LVGL9 + FreeType,在ILI9488的480x320屏幕上,不用预编译一份中文点阵字库,直接运行时加载TTF文件,中文字体、字号、颜色随便切,显示流畅,内存也hold得住。整个过程踩了不少坑,尤其是LVGL9的API变化、FreeType路径带盘符、PSRAM做DMA缓冲这三个点,网上资料东一句西一句,我是硬生生排查了两天才全部理顺。这篇就把整个链路完整写下来,从工程搭建到字体动态渲染,再到性能调优和问题排查,给被中文多字体卡住的朋友一个可以直接照抄的参考。
1. 项目背景与整体方案设计
1.1 为什么放弃静态字库,选择FreeType动态渲染
做嵌入式屏幕显示中文,最传统的方式是用LVGL官方提供的字体转换工具,把TTF字体生成一个C语言数组,编译进固件里。这个方法对于显示少量英文和数字、字体字号固定不变的项目完全够用。可一旦涉及中文,问题就来了:中文字符数量太多,常用的GB2312一级字库就有3755个字,加上二级字库一共6763个,用一个常见的中文字体生成一个24px的字库C文件,轻轻松松1MB以上。如果你还需要多个字号、多个字体风格,组合起来就是好几MB甚至十几MB的字库,一片16MB Flash的芯片直接被吃穿了一半。更痛苦的是,每次调整字号、换字体、加字符集,都必须重新生成字库、重新编译、重新烧录,产品迭代效率极低。
FreeType动态渲染的思路是反过来的:把TTF/OTF字体文件当作“数据”存放在存储介质上,比如SD卡或Flash分区,运行时由FreeType引擎解析字形轮廓,实时渲染成像素位图再交给LVGL显示。这样字体文件只有一份,你想显示多少字号就渲染多少字号,想切换字体就换一个文件,想支持繁体、日文、韩文、emoji,只要字体文件里包含这些字形就行。代价是FreeType库本身会占用一些Flash和RAM,而且实时渲染字形需要消耗CPU时间,但ESP32-S3这颗240MHz双核芯片加上PSRAM配置,完全扛得住。
1.2 整体架构和硬件选型
我手上的核心硬件配置如下:
- 主控:ESP32-S3-WROOM-1 N16R8,16MB Flash + 8MB PSRAM
- 屏幕:ILI9488驱动IC,480x320分辨率,SPI接口,RGB565色深
- 字体存储:TF卡(SD卡),FAT32格式,用于存放多个TTF字体文件
- 电源:5V供电,板载3.3V LDO
软件栈方面,开发环境用的是VSCode + ESP-IDF v5.2.2插件,LVGL通过git submodule直接拉取v9.2分支放到components目录,FreeType则使用LVGL自带的LV_USE_FREETYPE集成方式。显示驱动没有用现成的第三方库,而是直接用ESP-IDF的esp_lcd组件加自定义flush回调接入LVGL的display接口。
选择ESP32-S3而不是普通ESP32,主要看中它更高频率的双核、更宽的内部总线,以及对SPI外设DMA的支持更完善。做GUI项目时,CPU性能和传输带宽决定了UI流畅度的上限,S3在这方面的余量明显更大。ILI9488虽然不算最新最快的驱动IC,但胜在便宜、资料多、市面上所有常见方案都有现成初始化代码,做中低端HMI面板非常常见。
1.3 整体数据流设计
整个系统的运行流程可以这样概括:
- 上电后ESP-IDF先初始化NVS、日志、PSRAM分配器
- 初始化SD卡,用VFS挂载到"/sdcard"路径
- 读取字体目录,确认字体文件可访问
- 初始化ILI9488屏幕,通过esp_lcd配置SPI通信并启动DMA传输
- 调用lv_init()、lv_freetype_init(),创建多个不同字号和风格的FreeType字体实例
- 构建UI界面,把不同控件分别挂上对应的lv_font_t字体指针
- 进入LVGL主循环,定时器驱动UI刷新,LVGL的脏矩形机制只重绘变化区域
2. 环境准备:ESP32-S3 + LVGL9 显示链路搭建
2.1 工程初始化与LVGL9引入
我用VSCode的ESP-IDF插件创建了一个空白工程,然后手动引入LVGL。注意这里一定要拉v9版本的分支,不能直接用master,也不能用v8.x。LVGL9相对v8改动相当大,网上大量教程和示例代码都是基于v8写的,如果版本对不上,API调用会到处报错,新手很容易被带偏。
拉取LVGL:
cd components git clone --branch v9.2 https://github.com/lvgl/lvgl.git接下来需要从LVGL源码目录拷贝一份lv_conf_template.h到项目根目录,重命名为lv_conf.h,并确保#if 1这个总开关打开。很多人在这一步直接跑默认配置,结果LVGL开不起来,屏幕上什么都显示不了,就是因为lv_conf.h没有正确生效。LVGL9用了一套很严格的条件编译机制,头文件路径要配置好,宏定义要打开,缺一不可。
在lv_conf.h里,有几个必须重点确认的宏:
| 宏定义 | 我设置的值 | 作用 |
|---|---|---|
| LV_USE_FREETYPE | 1 | 启用FreeType字体引擎 |
| LV_FREETYPE_CACHE_SIZE | 512KB | FreeType字形缓存总大小 |
| LV_FREETYPE_CACHE_FT_GLYPH_CNT | 64 | 缓存字形数量上限 |
| LV_USE_FS_POSIX | 1 | 允许LVGL通过POSIX接口读取文件 |
| LV_FS_POSIX_LETTER | 'A' | 指定LVGL文件系统盘符前缀 |
| LV_COLOR_DEPTH | 16 | RGB565,匹配ILI9488色深 |
2.2 ILI9488驱动接入的重点
ILI9488通过SPI方式连接,我使用的引脚分配是:SCLK、MOSI、CS、DC、RST和背光PWM。在ESP-IDF里,我用SPI2_HOST驱动屏幕,另外单独用SPI3_HOST驱动SD卡,这样两根总线的时钟速率可以分别独立调节,避免互相干扰。
初始化序列这里不贴全了,网上到处都是,我只说三个容易坑到人的细节:
第一,SPI时钟不要一开始就拉太高。ILI9488屏幕标称最高支持到几十MHz,实际上很多屏幕的排线、FPC转接板质量参差不齐,尤其是在面包板或者杜邦线连接的情况下,80MHz必然花屏。我最初用40MHz调试稳定,后来又降回26.7MHz,后续优化再慢慢拉高。
第二,初始化时序中的延时不能省。ILI9488从Sleep Out到进入Normal Mode需要等待120ms以上,Display On之后也需要短暂延时。很多初始化代码是从网上复制来的,少了这几个延时,可能会出现屏幕一直无显示、背光亮但没画面的诡异问题。
第三,SPI写入像素数据时,建议使用DMA方式。ILI9488一帧全屏数据是4803202等于307200字节,如果逐字节或逐行塞进SPI FIFO,CPU会被卡到完全干不了别的。用ESP-IDF的SPI驱动加DMA传输,发送一个大buffer的像素数据只需要一条命令,CPU可以趁这段时间去跑LVGL的绘制逻辑。
2.3 字体文件存储方案:SD卡完胜内嵌Flash
字体文件放哪里,我对比过三种方案:
| 存储方式 | 优点 | 缺点 | 我的结论 |
|---|---|---|---|
| 内嵌Flash | 读取快,无需文件系统 | 浪费Flash容量,换字体要重刷固件 | 不推荐,除非字体固定且Flash富余 |
| SPIFFS/LittleFS分区 | 可在线OTA更新字体,无需外设 | 大文件读写效率一般,有擦写损耗 | 适合产品量产,字体需远程更新的场景 |
| TF卡 | 容量大,换字体就是拷贝文件,调试效率极高 | 需要SD卡驱动,稳定性受卡质量影响 | 我最终的选择,最适合开发阶段 |
我最终把字体文件放在了TF卡的/font目录下,使用FAT32格式。这样调试时想换字体,把新字体文件拷进卡里,重新上电就生效,完全不用重新编译固件。这个效率优势在开发阶段是决定性的,因为字体渲染效果需要反复肉眼验证,每次改动都编译烧录会让人崩溃。
需要注意,SD卡上的字体文件名建议全部使用英文字母和数字,不要用中文文件名。LVGL的FreeType模块读取文件时对中文路径的支持并不友好,一旦路径编码和文件系统编码不一致,就会加载失败,排查起来非常麻烦。
3. FreeType在LVGL9中的开启与核心配置
3.1 lv_conf.h里那5个关键宏,每一个都有代价
FreeType能不能在LVGL9里跑起来,lv_conf.h的配置是第一道门槛。我把重点宏和它们背后的内存代价讲清楚。
LV_USE_FREETYPE这个宏打开后,LVGL会把FreeType库编译进来,代码段会增加大约几十KB的Flash占用,同时会为FreeType分配缓存空间。LV_FREETYPE_CACHE_SIZE控制的就是这个缓存,单位是字节。它用来缓存已经渲染好的字形位图,中文页面切换时,如果大部分字都在缓存里,显示速度会非常快;如果缓存太小,每次都要重新渲染字形,页面会明显卡顿。我设置了512KB,对于8MB PSRAM的配置来说完全可接受。
LV_FREETYPE_CACHE_FT_GLYPH_CNT是缓存字形数量的上限。中文字符多,即使在同一个页面里,也可能同时出现几十个不同的汉字。这个值设太小,会让缓存频繁失效,建议不低于64。
LV_USE_FS_POSIX和LV_FS_POSIX_LETTER这两个宏是我重点要强调的。LVGL的FreeType模块读取字体文件,走的是LVGL自己的文件系统接口。要让它能访问SD卡上的文件,你需要给LVGL挂一个文件系统驱动。我选择的是POSIX驱动,因为ESP-IDF的VFS天然兼容POSIX接口。LV_FS_POSIX_LETTER设置为'A',意味着所有LVGL的文件路径必须以"A:"开头,LVGL会去掉这个前缀,把剩余部分交给标准C库的fopen函数打开。
3.2 盘符前缀A:这个坑,无数人在这里卡了两天
接上文,这个问题太关键了,单独拿出来说。LVGL的文件系统接口设计了一个“盘符”概念,类似Windows的C盘D盘。你用lv_fs_open或者直接让lv_freetype_font_create去读文件时,路径不能直接写成"/sdcard/font/a.ttf",而要写成"A:/sdcard/font/a.ttf"。其中A就是你在lv_conf.h里配置的LV_FS_POSIX_LETTER。LVGL会解析这个路径,看到前缀A,就知道交给注册在A盘符下的POSIX驱动去处理,驱动再调用fopen("/sdcard/font/a.ttf")完成真正的文件打开。
我一开始没注意这个前缀,直接在代码里写"/sdcard/font/a.ttf",结果lv_freetype_font_create返回NULL,FreeType报错说文件打不开。排查了半天,SD卡挂载、文件拷贝、路径权限全检查过一遍都没问题,最后翻LVGL源码才发现是盘符前缀的问题。
所以加载字体之前,建议先用C标准库确认一次文件可读,把问题同文件系统层切开:
FILE *fp = fopen("/sdcard/font/HarmonyOS_Sans_SC_Medium.ttf", "rb"); if (fp == NULL) { ESP_LOGE("FONT", "SD卡文件打开失败,请检查路径和文件"); } else { fseek(fp, 0, SEEK_END); ESP_LOGI("FONT", "字体文件大小: %ld", ftell(fp)); fclose(fp); }如果这段代码能打印出文件大小,说明SD卡挂载和文件路径没有问题,接下来才放心大胆去创建LVGL字体实例。
3.3 lv_freetype_font_create参数全解析
LVGL9中动态加载FreeType字体的核心函数是lv_freetype_font_create,完整代码示例如下:
#include "lvgl.h" #include "lv_freetype.h" static lv_font_t *font_title; static lv_font_t *font_body; static lv_font_t *font_num; void app_font_init(void) { lv_freetype_init(); font_title = lv_freetype_font_create( "A:/sdcard/font/HarmonyOS_Sans_SC_Medium.ttf", LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 32, LV_FREETYPE_FONT_STYLE_NORMAL ); font_body = lv_freetype_font_create( "A:/sdcard/font/HarmonyOS_Sans_SC_Regular.ttf", LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 18, LV_FREETYPE_FONT_STYLE_NORMAL ); font_num = lv_freetype_font_create( "A:/sdcard/font/Montserrat-SemiBold.ttf", LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 24, LV_FREETYPE_FONT_STYLE_NORMAL ); if (font_title == NULL || font_body == NULL || font_num == NULL) { ESP_LOGE("FONT", "字体创建失败,请检查路径或内存"); return; } }函数有四个关键参数:
第一个是字体文件路径,必须带盘符前缀,前面已经强调过。第二个是渲染模式,我用的是LV_FREETYPE_FONT_RENDER_MODE_BITMAP,适合嵌入式LCD屏;LVGL9还支持OUTLINE模式,用于矢量渲染,但在普通LCD上并不适用。第三个是字号,单位是像素,FreeType允许运行时任意指定,不需要像静态字库那样预先生成好固定字号。第四个是字体风格,可以选NORMAL、BOLD、ITALIC,如果你用的字体文件本身包含对应的字重和斜体变体,就会自动匹配。
字体创建成功后,直接把这个lv_font_t指针赋给控件的样式即可:
lv_obj_t *label = lv_label_create(screen); lv_obj_set_style_text_font(label, font_body, 0); lv_label_set_text(label, "你好,ESP32-S3");这里有一个很重要的点,label在设置文本之前,必须先确保字体实例创建成功,否则LVGL会使用默认字体,中文字符一律显示成方块。所以每次上电初始化时,我都在界面创建之前调用app_font_init,如果字体创建失败就直接打印日志,绝不让UI带着错误字体继续跑。
4. 中文多字体动态渲染的完整实现
4.1 乱码问题:先解决编码,再谈渲染
中文字体动态渲染遇到的第一个“玄学”问题就是乱码。明明字体文件是中文的,代码里写的中文字符串,编译也沒报错,为什么显示出来全是乱码或者方块?
核心原因是编码格式不匹配。LVGL9内部处理字符串时,一律按UTF-8解析。如果你的源代码文件保存的是GBK或者GB2312编码,那么字符串"你好"在内存里就是GBK字节序列,LVGL拿到这串字节后再按UTF-8去解析,自然解析不出正确字符,显示出来就是乱码。
解决办法并不复杂:用VSCode打开所有包含中文字符串的C文件,点击右下角编码按钮,改成UTF-8保存。同时建议在编译选项里强制指定字符编码为UTF-8,避免不同平台默认编码不一致。
另外要确认字体文件本身包含你需要的汉字。很多英文字体文件只包含Latin字符集,中文显示出来必然是空白或者方块。网上有一些精简版中文字体只包含常用汉字,如果你显示的生僻字不在字体文件里,同样会显示方块。我在项目中用的是HarmonyOS Sans系列的中文TTF,字符集覆盖比较全,常见中文和拉丁字符混合显示没有问题。
4.2 同一页面多字体多字号动态切换的实现
“动态渲染”这个词听起来玄乎,落地到代码层面,其实就两件事:一是控件内容动态变化,二是不同控件可以挂载不同字体实例,甚至同一个控件在运行过程中也能切换不同字体。
先看内容动态变化的场景。比如一个仪表盘页面,温度值每秒更新一次,这种label天然就是动态的。FreeType处理这种场景非常自然,因为第一次显示某个字符时,FreeType从字体文件里解析字形并缓存;后续再显示相同字符时,直接从缓存取位图,速度极快。我实测下来,页面切换时第一帧可能会有十几毫秒的卡顿,但之后的操作完全顺滑,这就是缓存起的作用。
再看字体动态切换的场景。比如一个新闻列表,标题用32px中黑体,正文用18px常规体,数字指标用24px英文字体,时间戳用16px字体。这就需要在UI初始化时为每个控件设置不同的字体。LVGL9的样式机制很容易实现,关键是字体实例要提前创建好并统一管理。
我还封装了一个按需获取字体的函数,避免重复创建相同参数的字体实例造成内存泄漏:
typedef struct { uint16_t size; bool bold; lv_font_t *font; } font_cache_entry_t; static lv_font_t *get_cached_font(uint16_t size, bool bold) { for (int i = 0; i < font_cache_cnt; i++) { if (font_cache[i].size == size && font_cache[i].bold == bold) { return font_cache[i].font; } } // 未命中则创建新字体实例并写入缓存 lv_font_t *f = lv_freetype_font_create( bold ? FONT_PATH_MEDIUM : FONT_PATH_REGULAR, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, size, LV_FREETYPE_FONT_STYLE_NORMAL ); if (f == NULL) { return NULL; } font_cache[font_cache_cnt].size = size; font_cache[font_cache_cnt].bold = bold; font_cache[font_cache_cnt].font = f; font_cache_cnt++; return f; }这样在业务代码里,不管哪个页面需要什么字号,一行代码就能拿到对应的字体实例,完全不用担心创建太多实例拖垮内存。
4.3 混排中英文时的字体选择策略
中文UI几乎必然会遇到中英文混排的情况。这里有一个细节值得注意:LVGL的字体对象本身没有“fallback链”的概念,一个字体实例要么能渲染某个字符,要么不能。如果你创建的是中文字体实例,而这个字体文件恰好也包含拉丁字母(绝大多数中文字体都包含ASCII字形),那中英文混排完全没问题,同一个label的汉字和英文都能正常显示。
但如果你希望英文数字用专门设计的西文字体显示,视觉上更精致,那就不能靠单个字体实例。我在项目里对数字和单位信息做了单独处理:把需要特殊字体显示的数值放在单独的label里,挂载英文字体实例,中文标题放在另一个label,挂载中文字体实例。页面布局上用分组和间距来协调两者的对齐,视觉效果比混排更好,而且代码逻辑也更清晰。
从性能角度看,中英文分别用不同label还有一个好处:英文数字的字体文件通常很小,字形也简单,FreeType渲染开销远低于中文字体;整个页面的刷新效率会更高。
4.4 渲染效果与资源占用实测
我搭建了一个测试页面,包含32px中文标题、18px中文字体正文、24px英文数字指标和底部菜单文字。实际显示效果完全满足需求,中文字形清晰,没有明显的锯齿,颜色切换和透明度效果也都能正常表现。
资源占用方面,我统计过实测数据:
| 资源项 | 实测值 |
|---|---|
| FreeType字形缓存 | 512KB(PSRAM) |
| LVGL显示缓冲(双缓冲) | 约37.5KB(片上RAM) |
| LVGL对象堆 | 约120KB左右 |
| 三个FreeType字体实例 | 合计约10KB |
| 首次渲染单个复杂汉字耗时 | 约15ms |
| 缓存命中后渲染耗时 | 接近0ms |
| 页面局部刷新帧率 | 20~30 FPS |
| 全屏重绘帧率 | 约10 FPS |
这个数据说明,只要缓存配置得当,FreeType动态渲染在ESP32-S3上不仅能跑,还能跑得比较顺。传统静态字库虽然省去了实时渲染的CPU开销,但灵活性太差,且多字体多字号的Flash占用是几何级增长。两相对比,动态渲染显然更适合中文多字体场景。
5. 实战中的性能优化与问题排查
5.1 内存占用分析:钱要花在刀刃上
ESP32-S3的片上SRAM总共有512KB,但系统启动后,协议栈、VFS、驱动、日志系统会占用一部分,留给LVGL的空间并没有想象中那么多。如果你的芯片不带PSRAM,跑LVGL9加FreeType会非常紧张,甚至直接分配不出足够缓存。所以我强烈建议做GUI项目选用带PSRAM的S3型号。
内存分配需要区分用途。显示缓冲如果用于DMA传输,建议分配在片上RAM或者使用ESP-IDF提供的esp_dma_capable_malloc函数分配。原因是SPI外设做DMA传输时,对源地址有特殊要求,普通的malloc如果落到PSRAM,在某些线材和驱动配置下会出现花屏或者传输不稳定的问题。我最初直接把显示缓冲用heap_caps_malloc(MALLOC_CAP_SPIRAM)分配,结果DMA传输时屏幕画面偶发撕裂,排查了很久才意识到是DMA和PSRAM的兼容性问题。换成esp_dma_capable_malloc后问题消失。
FreeType的字形缓存就不需要DMA支持了,可以放心放到PSRAM。字形缓存越大,中文字形的命中率越高,显示越流畅。我实测512KB缓存能覆盖一个完整页面里绝大多数常用汉字,基本达到了“页面切换后继续操作几乎无感”的效果。
5.2 刷新速度优化:SPI时钟、双缓冲和局部刷新
ILI9488这种480x320的SPI屏,刷新速度优化的核心就三条路:提高SPI时钟、使用双缓冲、利用LVGL的局部刷新机制。
SPI时钟是影响全屏刷新时间的决定性因素。同样发送307200字节像素数据,26.7MHz和40MHz的差距肉眼可见。我的最终选择是40MHz,再往上提升时屏幕出现轻微雪花点,判断是杜邦线太长的原因,如果打板走短线应该还能继续压榨。
双缓冲的收益在于CPU和SPI可以并行。LVGL在绘制缓冲A时,SPI正在刷缓冲B,两个动作重叠后,UI的响应速度会明显提升。代价是显示缓冲内存翻倍,我采用了480x20大小的两个缓冲,总共才37.5KB,对ESP32-S3来说完全可以接受。注意这个尺寸不是全屏,而是部分缓冲,LVGL9会基于脏矩形机制,把需要重绘的区域切分成小矩形块,分批送入显示缓冲。对于变化不频繁的UI,实际刷新区域远小于全屏,帧率自然就上去了。
这里还建议开启LVGL9自带的刷新模式管理功能,比如设置合适的刷新周期和脏矩形合并策略,避免高频重绘时SPI总线被小碎块请求淹没。
5.3 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 中文全部显示为方块 | 源文件不是UTF-8编码,或字符不在字体文件中 | 源码保存为UTF-8,换字符集覆盖全的中文字体文件 |
| lv_freetype_font_create返回NULL | 路径没带盘符前缀,文件系统未挂载,内存不足 | 确认路径带"A:"前缀,先用fopen验证文件可读,开启PSRAM |
| 打开字体文件报错 | SD卡初始化失败或文件路径不对 | 检查SD卡挂载日志,用listdir打印目录内容 |
| 屏幕花屏/雪花点 | SPI时钟过高、排线过长 | 降低SPI时钟,检查接线,优先使用短杜邦线或PCB走线 |
| 画面偶发撕裂 | DMA使用PSRAM地址 | 显示缓冲改用esp_dma_capable_malloc |
| 页面切换时卡顿一下 | FreeType首次渲染字形耗时 | 保持字形缓存开启,可在页面加载时预热常用字符 |
| 刷新率低 | 全屏缓冲或SPI时钟过慢 | 改局部缓冲,提升SPI时钟,启用双缓冲 |
| 背光亮但屏幕无内容 | ILI9488初始化时序缺少延时 | Sleep Out后延时120ms以上 |
| 内存不足导致无法创建字体 | 片上SRAM不够,或缓存配置过大 | 配置PSRAM,调低LV_FREETYPE_CACHE_SIZE |
5.4 排查心法:日志打印优先级高于一切
遇到问题不要上来就怀疑硬件和驱动,优先打印日志确认每一层是否成功。个人经验是分四步排查:
第一步,确认文件系统层。用fopen直接读取字体文件,看能否打印出文件大小。这一步能挡掉一半以上的问题。
第二步,确认FreeType字体创建层。lv_freetype_font_create返回NULL后,用ESP_LOG打印错误码,LVGL9的日志会给出比较具体的失败原因,比如文件打开失败还是内存分配失败。
第三步,确认显示层。Label创建好、字体挂上之后,先用英文数字测试显示是否正常,再用中文测试编码问题。如果英文正常中文乱码,几乎可以断定是UTF-8编码问题。
第四步,确认性能层。使用LVGL自带的监控函数和ESP-IDF的heap_caps_get_free_size,实时观察剩余内存和LVGL堆的使用情况,排查是否存在内存泄漏。
按这个顺序排查,大多数问题都能在半小时内定位。
6. 最后再分享几个实战心得
字体文件的选择上,我做过一次对比实验:同一个中文字体族,OTF格式和TTF格式都能被FreeType加载,但实测OTF格式首次渲染复杂汉字时耗时偏高,后来我换成了TTF版本,字体加载速度和渲染速度都有改善。如果你的项目对首帧渲染时间敏感,尽量优先选TTF格式的字体文件。
另外,FreeType动态渲染并不代表完全不需要预热。页面切换时首次出现新字符,依然会有一次几十毫秒级别的渲染耗时。我现在的做法是在页面onLoad回调里,先调用一次lv_label_set_text把页面全部文本刷一遍,让常用字形提前进入缓存,用户实际看到页面时几乎感受不到卡顿。
还有一个关于Flash容量的建议。虽然字体文件放在SD卡上,但LVGL9加FreeType本身会占用一定Flash空间,加上ESP-IDF的各个组件、WiFi协议栈等,整体固件体积大概在1.5MB到2MB之间。16MB Flash对这类项目是非常从容的选择,如果你只有4MB Flash,建议仔细裁剪LVGL和ESP-IDF的组件,膨胀功能尽量不开。
这套方案的下一阶段,我准备把语言包也搬到SD卡上,做成中英文动态切换。做法很直接:字体文件路径做成配置文件里的一个字段,切换语言时销毁当前字体实例,再根据配置文件重新创建新字体的实例,UI内部逻辑完全不用改。后续有空还会试试LVGL9的矢量图形和更多动画效果,在GUI这条路上一路玩下去。