3. 字体创建流程:从ttf文件到LVGL字体的完整转换步骤,字体结构体lv_font_t剖析
做嵌入式UI,字体这块儿是绕不开的坎儿。我见过不少项目,界面画得挺漂亮,一显示中文就崩了——要么乱码,要么内存爆掉。说白了,LVGL不认识Windows里的那些ttf字体文件,你得先把它“翻译”成LVGL能吃的格式。
这一章,我就带你走一遍完整的转换流程。从ttf文件开始,到生成LVGL的.c字体文件,最后再剖析一下那个核心结构体lv_font_t。嗯,看完你就知道字体在LVGL里到底是怎么“活”起来的。
3.1 从ttf到LVGL字体:三步走
转换流程其实不复杂,就三步:选字库、跑工具、集成代码。我习惯用一个叫LVGL Font Converter的在线工具,当然你也可以用官方的lv_font_conv命令行工具,效果一样。
核心流程: ttf文件 → 提取字形 → 生成位图/矢量数据 → 打包成lv_font_t结构体
步骤一:准备ttf文件
选字体时要注意:不是所有ttf都适合嵌入式。我踩过坑,选了个艺术字体,结果小字号下笔画糊成一团。建议用思源黑体或Noto Sans CJK,笔画清晰,字重丰富。
步骤二:使用lv_font_conv工具
命令行操作其实很直观。下面是我常用的转换命令:
lv_font_conv --font NotoSansSC-Regular.ttf \
--size 16 \
--bpp 4 \
--range 0x20-0x7F \
--range 0x4E00-0x9FFF \
--format lvgl \
--output my_font_16.c
参数说明:
--size:字体像素高度,16px适合小屏,32px以上适合标题--bpp:每个像素的位深,可选1/2/4/8。bpp越大,抗锯齿越好,但内存也越大--range:Unicode范围。0x20-0x7F是ASCII,0x4E00-0x9FFF是常用汉字--format lvgl:输出LVGL专用的.c文件
我的经验: 如果只显示常用汉字,别把整个CJK区都转进去。我一般只转0x4E00-0x9FA5,大概2万多个字,16px下生成的.c文件大约800KB。够用,又不浪费Flash。
步骤三:集成到项目
生成的.c文件里包含一个lv_font_t结构体变量,比如my_font_16。你只需要:
- 把.c文件放到工程目录
- 在代码里
extern lv_font_t my_font_16; - 在创建label时设置字体:
lv_obj_set_style_text_font(label, &my_font_16, 0);
3.2 字体结构体lv_font_t深度剖析
转换工具生成的.c文件里,核心就是那个lv_font_t结构体。我刚开始学LVGL时,以为它就是个简单的“字体描述”,后来才发现里面门道不少。
直接看定义:
typedef struct _lv_font_t {
uint32_t dsc_size; // 字形描述符的大小
uint16_t line_height; // 行高(像素)
uint16_t base_line; // 基线偏移
uint8_t bpp; // 位深(1/2/4/8)
lv_font_glyph_dsc_t *glyph_dsc; // 字形描述符数组
lv_font_fmt_t *fmt_dsc; // 字体格式描述符
const lv_font_t *fallback; // 后备字体
lv_font_get_glyph_cb_t get_glyph_cb; // 获取字形回调
lv_font_get_glyph_bitmap_cb_t get_glyph_bitmap_cb; // 获取字形位图回调
} lv_font_t;
这里我挑几个重点说:
| 成员 | 作用 | 我的理解 |
|---|---|---|
line_height |
行高,决定两行文字之间的间距 | 如果显示不全,多半是这个值设小了 |
bpp |
位深,影响抗锯齿效果 | bpp=1是纯黑白,bpp=4有16级灰度,边缘平滑很多 |
glyph_dsc |
每个字符的描述信息,包括宽高、偏移、位置 | 这是查找字符的核心索引 |
fallback |
后备字体,当前字体找不到字符时就用它 | 我常用它做“中英文混排”,英文用Arial,中文用思源 |
注意: fallback指针不能形成循环引用。我曾经不小心把字体A的fallback设为B,B的fallback又设为A,结果渲染时直接死循环。嗯,调试了半天才发现。
3.3 字形描述符:每个字符的“身份证”
glyph_dsc指向的是一个lv_font_glyph_dsc_t数组,每个字符对应一个描述符。结构体长这样:
typedef struct {
uint32_t unicode; // 字符的Unicode编码
uint16_t adv_w; // 字符的宽度(包括左右间距)
uint16_t box_w; // 字形位图的宽度
uint16_t box_h; // 字形位图的高度
int16_t ofs_x; // 水平偏移
int16_t ofs_y; // 垂直偏移
uint8_t bpp; // 该字符的位深
uint8_t bitmap_index;// 位图数据在bitmap数组中的索引
} lv_font_glyph_dsc_t;
这里有个容易搞混的地方:adv_w和box_w的区别。
adv_w:这个字符占的总宽度,包括左右留白。下一个字符从这个宽度之后开始画。box_w:实际位图的宽度。如果字符是“一”,box_w可能只有几个像素,但adv_w会大一些,保证字间距。
我在做自定义图标字体时,就靠调整ofs_x和ofs_y来对齐图标位置。比如一个箭头图标,默认偏移不对,显示出来偏上,手动改一下ofs_y就能完美居中。
3.4 字体格式描述符:位图数据的组织方式
fmt_dsc指向lv_font_fmt_t,它描述了位图数据是怎么存的。LVGL支持两种格式:
| 格式 | 说明 | 适用场景 |
|---|---|---|
LV_FONT_FMT_BITMAP |
位图格式,每个像素按bpp存储 | 小字号、低分辨率屏 |
LV_FONT_FMT_VECTOR |
矢量格式,存储轮廓数据 | 大字号、高分辨率屏,缩放不失真 |
我个人的建议:16px以下用位图,内存占用小,渲染快;32px以上用矢量,不然位图锯齿太明显。中间尺寸看情况,如果Flash够用,我倾向于用bpp=4的位图,效果和性能平衡得最好。
3.5 完整转换流程图
下面这张图总结了从ttf到LVGL字体的完整流程,以及各个结构体之间的关系:
3.6 避坑指南:我踩过的几个坑
做字体转换时,有几个地方特别容易出问题。我列出来,你遇到了可以直接对照排查:
- 字体显示不全:检查
line_height是否足够大。有些字体默认行高偏小,中文显示时底部被裁掉。我一般手动设到size * 1.4。 - 内存占用爆炸:bpp=8虽然效果好,但一个16px的汉字位图要占16×16=256字节。2万多个字就是5MB+。小内存MCU根本扛不住。我建议用bpp=4,效果接近,内存减半。
- 字符间距异常:检查
adv_w是否合理。如果字符挤在一起,可能是ttf本身的字距信息在转换时丢失了。可以手动在glyph_dsc里调整adv_w。 - 后备字体不生效:记得在代码里显式设置
fallback指针。我见过有人只设了主字体,没设fallback,结果英文显示正常,中文全变成方框。
我的小技巧: 调试字体时,可以用lvgl_font_debug工具(在LVGL的demo目录下)直接查看每个字符的位图数据。这样能快速定位是转换问题还是渲染问题。