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。你只需要:

  1. 把.c文件放到工程目录
  2. 在代码里extern lv_font_t my_font_16;
  3. 在创建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字体的完整流程,以及各个结构体之间的关系:

TTF到LVGL字体转换流程与结构体关系图 TTF → LVGL字体转换流程与结构体关系 TTF字体文件 lv_font_conv 转换工具 生成的 .c 字体文件 lv_font_t 结构体 glyph_dsc 数组 fmt_dsc 格式描述 关键点:bpp决定抗锯齿质量,fallback实现多字体回退,glyph_dsc是字符查找的核心索引

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目录下)直接查看每个字符的位图数据。这样能快速定位是转换问题还是渲染问题。