找回密码
立即注册
搜索
发回帖 发新帖

6396

积分

0

好友

806

主题
发表于 10 小时前 | 查看: 4| 回复: 0

一、引言

在日常嵌入式 GUI 开发中,我们最常做的事情之一就是在屏幕上显示文字。LVGL 里一句简单的:

lv_label_set_text(label, "你好,世界!");

就能在屏幕上渲染出字符串。但背后涉及了:UTF-8 解码 → 字体查找 → 字形描述符获取 → 位图解压 → Alpha 混合 → 像素写入 一整套流程。

本文将完整拆解这个管道(Pipeline)。


二、整体架构

LVGL 文字渲染分为四层:

应用层      lv_label_set_text("Hello")
               │
布局层      计算行宽、自动换行、对齐
               │
字形查找层  Unicode → 字体链 → 字形描述符 → 位图
               │
像素绘制层  A8 蒙版 → Alpha 混合 → Framebuffer

三、标签控件:lv_label_t

lv_label_t 是文字渲染的入口。其核心字段包括:

  • text:保存原始 UTF-8 字符串
  • size_cache:缓存的文字宽高,避免重复测量
  • offset:滚动偏移量
  • long_mode:LV_LABEL_LONG_WRAP/SCROLL/DOTS/CLIP
  • hint:滚动模式下缓存的 Y 坐标锚点(后文详解)

3.1 设置文本

调用 lv_label_set_text() 后,内部流程为:

lv_label_set_text()
  → 去掉翻译标记
  → copy_text_to_label()       // 深拷贝文本
  → lv_label_mark_need_refr_text()
      → invalid_size_cache = true
      → lv_obj_invalidate()     // 标记区域为"脏"
      → 注册布局完成回调

这个流程并不立即渲染,而是标记控件为“需要重绘”,等显示驱动下一次 LV_EVENT_DRAW_MAIN 事件触发时才真正执行绘制。

3.2 布局刷新

在布局阶段,lv_label_refr_text() 会测量所有行的宽高:

lv_text_get_next_line()  →  按单词/字符拆分行
    ↓
lv_text_get_width()      →  计算每行的像素宽度(含字距)
    ↓
每行高度累加            →  总文本高度

根据 long_mode 的不同,结果会触发:

  • WRAP:自动换行后的完整文本尺寸
  • SCROLL:创建滚动动画
  • DOTS:在超出位置插入 ...

四、UTF-8 处理

LVGL 通过函数指针表实现编码无关的抽象:

// 所有编码操作通过函数指针调用
uint32_t (*lv_text_encoded_next)(const char *, uint32_t *);
uint32_t (*lv_text_encoded_prev)(const char *, uint32_t *);

UTF-8 解码规则(lv_text_utf8_next()):

| 字节 1 前缀 | 字节数 | 数据位 | 可编码范围 |
| 0xxxxxxx | 1 | 7 bits | U+0000 - U+007F |
| 110xxxxx | 2 | 11 bits | U+0080 - U+07FF |
| 1110xxxx | 3 | 16 bits | U+0800 - U+FFFF |
| 11110xxx | 4 | 21 bits | U+10000 - U+10FFFF |

无效的后续字节(非 10xxxxxx 格式)会被检测并返回 0。


五、字体系统

字体是文字渲染的核心。LVGL 使用函数指针 + 多态的方式抽象字体:

struct _lv_font_t {
    // 三个虚函数
    bool (*get_glyph_dsc)(font, dsc, letter, letter_next);
    const void *(*get_glyph_bitmap)(dsc, draw_buf);
    void (*release_glyph)(font, dsc);

    int32_t line_height;      // 行高
    int32_t base_line;        // 基线位置

    const void *dsc;          // 实现私有数据
    const lv_font_t *fallback;  // 后备字体链
};

每个字体只要实现这三个函数指针,就能接入 LVGL 的文字渲染引擎。

5.1 字形描述符

字形查找的结果保存在 lv_font_glyph_dsc_t 中:

typedef struct {
    const lv_font_t *resolved_font;  // 实际找到字形的字体
    uint16_t adv_w;                  // 前进宽度(下一个字的位置增量)
    uint16_t box_w, box_h;           // 包围盒宽高
    int16_t ofs_x, ofs_y;            // 偏移量
    lv_font_glyph_format_t format;   // A1/A2/A4/A8/IMAGE/VECTOR...
    uint32_t index;                  // 字形在字体中的索引
} lv_font_glyph_dsc_t;

5.2 后备字体机制(Fallback)

字体的 fallback 指针形成了一个单向链表:

Primary font → Fallback 1 → Fallback 2 → ... → NULL

字形查找时,引擎会深度优先遍历整个链:

bool lv_font_get_glyph_dsc(font, dsc_out, letter, letter_next)
{
    f = font_p;
    while (f) {
        found = f->get_glyph_dsc(f, dsc_out, letter, ...);
        if (found && !dsc_out->is_placeholder) {
            dsc_out->resolved_font = f;   // 记录来源字体
            return true;
        }
        f = f->fallback;   // 链式前进
    }
    // 返回占位符或空字形
}

这就是为什么通常在 get_fixed_width_font() 中构造这样一个链:

iosevka_bold_16 [英文字符]
  → heiti_16 [中文字符]
    → lv_font_montserrat_16 [最后保底]

渲染 "Hello 你好" 时,英文字符由 iosevka 提供,中文字符由 heiti 提供,所有字符都能找到对应的字形。


六、位图字体格式(fmt_txt)

LVGL 内置的位图字体使用 lv_font_fmt_txt 格式。其描述符为:

typedef struct {
    const uint8_t *glyph_bitmap;               // 所有字形的位图数据
    const lv_font_fmt_txt_glyph_dsc_t *glyph_dsc;  // 字形描述符数组
    const lv_font_fmt_txt_cmap_t *cmaps;       // Unicode 映射表
    const void *kern_dsc;                      // 字距数据
    uint16_t cmap_num : 9;                     // 映射表数量
    uint16_t bpp : 4;                          // 每像素位数(1/2/4/8)
    uint16_t bitmap_format : 2;                // 0=不压缩, 1=RLE压缩
    uint8_t stride;                            // 行对齐(0/1/4/8/...)
    // ...
};

6.1 字形描述符

typedef struct {
    uint32_t bitmap_index : 20;   // 位图偏移
    uint32_t adv_w : 12;          // 前进宽度(8.4定点数)
    uint8_t box_w;                // 包围盒宽度
    uint8_t box_h;                // 包围盒高度
    int8_t ofs_x;                 // X 偏移
    int8_t ofs_y;                 // Y 偏移
} lv_font_fmt_txt_glyph_dsc_t;

注意 adv_w 使用 8.4 定点数格式(实际值 ×16 存储),精度为 1/16 像素,用于精确的字距控制。

6.2 Unicode 映射表(cmap)

LVGL 提供了四种映射表格式,在查找速度和内存占用之间做权衡:

| 类型 | unicode_list | offset_list | 查找方式 | 适用场景 |
| FORMAT0_TINY | NULL | NULL | glyph_id = start + (cp - range_start) | 连续密集的字符区间 |
| FORMAT0_FULL | NULL | uint8_t | glyph_id = start + offset[rcp] | 有缺字的密集区间 |
| SPARSE_TINY | uint16_t
| NULL | glyph_id = start + binsearch(list, rcp) | 稀疏字符集 |
| SPARSE_FULL | uint16_t | uint16_t | glyph_id = start + offset[binsearch(...)] | 带偏移的稀疏集 |

查找过程(get_glyph_dsc_id()):

1. 遍历所有 cmap 表
2. 计算相对码点 rcp = letter - cmap->range_start
3. 如果 rcp >= range_length,跳过
4. 根据 cmap 类型计算 glyph_id
5. 返回后,用 glyph_id 索引 glyph_dsc 数组获取字形描述符
6. 再从 glyph_bitmap 中按 bitmap_index 偏移读取位图

6.3 位图解码

LVGL 的位图以 bpp 位(1/2/4/8)存储,渲染时统一扩展为 A8(8位 Alpha)格式:

bpp=1: 每 bit → 0x00 或 0xFF
bpp=2: 每 2bit → [0, 85, 170, 255] 查表
bpp=4: 每 4bit → 16 级查表扩展
bpp=8: 直接复制

查表方式:

static const uint8_t opa4_table[16] = {
    0,  17, 34, 51,  68, 85, 102, 119,
    136, 153, 170, 187, 204, 221, 238, 255
};

压缩格式

当 bitmap_format 为压缩模式时,使用 RLE(游程编码)+ 行间 XOR 预滤波:

  • RLE:连续相同像素压缩为 (值, 重复次数) 对,重复超过 11 次时使用扩展计数器
  • 行间 XOR:第 N 行与第 N-1 行异或,减少重复数据 —— 对上下连续的笔画效果显著

解码时先解 RLE,再 XOR 恢复原始行数据,最后按 bpp 扩展为 A8。

6.4 字距(Kerning)

LVGL 支持两种字距格式:

Pair 模式:存储 (left_gid, right_gid, value) 三元组,按 left_gid 排序,查找时二分搜索。

Class 模式:将字形分为若干类,存储 class_cnt_left × class_cnt_right 的二维矩阵:

kern = class_pair_values[(left_class-1) * right_class_cnt + (right_class-1)]

Class 模式显著减少存储量:对一个有 200 个字符的字体,Pair 模式可能有数千对,而 Class 模式只需几十个类+几百个矩阵值。

字距应用时经过缩放并取整:

kv = (kvalue * kern_scale) >> 4;
adv_w = (adv_w_raw + kv + (1 << 3)) >> 4;   // 从8.4定点转整数

七、绘制管道

7.1 字符迭代

lv_draw_label_iterate_characters() 是整个渲染管道的核心协调者:

for each line:
    lv_text_get_next_line()         // 获取行边界
    计算对齐偏移                     // LEFT/CENTER/RIGHT

for each char in line:
    lv_text_encoded_next()          // UTF-8 解码
    if recolor: 解析 #rrggbb
    lv_font_get_glyph_dsc()         // 字形查找 + 字距
    lv_draw_unit_draw_letter()      // 获取位图 + 绘制
    更新光标位置 += adv_w + letter_space

7.2 单字形绘制

lv_draw_unit_draw_letter() 负责:

  1. 计算字母坐标(box 偏移 + 基线调整)
  2. 通过 lv_font_get_glyph_bitmap() 解压位图到 A8 draw buffer
  3. 设置 lv_draw_glyph_dsc_t 描述符
  4. 回调 draw_letter_cb() 进行实际像素绘制

7.3 最终像素混合

draw_letter_cb() 在软件渲染器中执行最后的 Alpha 混合:

        A8 蒙版(字形灰度)
            │
文字颜色 ──→  Alpha 混合 ──→ Framebuffer
            │
        文字透明度(全局 opa)

具体来说,它构造一个 lv_draw_sw_blend_dsc_t:

blend_dsc.color     = text_color;       // 文字颜色(RGB565/888)
blend_dsc.opa       = text_opa;         // 全局透明度
blend_dsc.mask_buf  = glyph_a8_data;    // 字形 A8 蒙版
blend_dsc.mask_area = letter_coords;    // 字母包围盒区域

然后调用 lv_draw_sw_blend(),逐像素计算:

output = src_color × (mask_alpha × global_opa / 255) + bg × (1 - mask_alpha × global_opa / 255)

对于旋转、矢量字体等情况,则走图像渲染路径。


八、自动换行逻辑

换行(Line Breaking)由 lv_text_get_next_line() 实现:

1. 如果标记了 EXPAND/FIT,只按 \n/\r 换行
2. 否则反复调用 lv_text_get_next_word() 直到:
   - 遇到换行符 \n
   - 累积宽度超过 max_width
3. 自动换行后,下一行前导空格被去掉

8.1 单词边界

“单词”以断字符界定(默认 ,.-:_)。更重要的是,CJK 字符被视作独立单词,每个中文字符后都可自动换行:

bool lv_text_is_a_word(uint32_t letter)
{
    return (letter >= 0x4E00 && letter <= 0x9FFF)   // CJK 统一表意文字
        || (letter >= 0x3000 && letter <= 0x303F)   // CJK 符号
        || (letter >= 0xFF01 && letter <= 0xFF5E)   // 全角 ASCII
        || // ... 其他东亚字符区间
}

8.2 超长单词处理

当一个单词超出 LV_TXT_LINE_BREAK_LONG_LEN 字符且一行容不下时,LVGL 会强制断词,当前行至少保留 PRE_MIN_LEN 个字符,下一行至少保留 POST_MIN_LEN 个字符。

LV_TEXT_FLAG_BREAK_ALL 标志位强制在任何字符处都可断行(DOTS 模式下使用,防止死循环)。


九、对齐方式

对齐在每行级别处理(lv_draw_label_iterate_characters()):

LEFT:   pos.x = line_start_x
CENTER: pos.x = line_start_x + (available_width - line_width) / 2
RIGHT:  pos.x = line_start_x + available_width - line_width

AUTO 模式根据 base_dir 决定:

  • base_dir == RTL → RIGHT
  • 否则 → LEFT

十、双向文本(Bidi)

LVGL 实现了 Unicode 双向算法的核心功能:

  1. 字符方向分类:Strong(LTR/RTL)、Weak(数字)、Neutral(空格标点)
  2. 运行分析:将文本按方向分割为连续的 Runs
  3. 顺序调整:
    • LTR 基地:LTR Run 正序,RTL Run 反转
    • RTL 基地:Run 顺序翻转,LTR Run 内部正序
  4. 括号配对:跟踪成对括号(深度 ≤4)来决定括号区域的方向

RTL 语言如阿拉伯语的处理还会激活字体塑形(Arabic Shaping),在 copy_text_to_label() 时通过 lv_text_ap_proc() 将字符转为相应的呈现形式(初始形、中间形、尾形、独立形)。


十一、缓存机制

11.1 尺寸缓存

标签控件在 size_cache 中缓存测量结果。当文本或样式变化时,invalid_size_cache 标记失效,下次渲染前重新测量。

11.2 行锚点缓存(Hint)

对于包含数百行的大文本(比如日志界面),每次重绘都要重新扫描所有行是不现实的。LVGL 提供 lv_draw_label_hint_t:

struct _lv_draw_label_hint_t {
    int32_t line_start;   // 缓存行的字节偏移
    int32_t y;            // 该行的 Y 坐标
};

当可视区域滚动超过 1024 像素时更新缓存的锚点行,下次从缓存行开始往后扫描,避免从头开始。

11.3 Draw Buffer 复用

在 lv_draw_unit_draw_letter() 中,lv_draw_glyph_dsc_t 内嵌一个 draw buffer,在同一个 label 的不同字符间复用,只在大字需要更多空间时增长。


十二、完整调用链一览

应用: lv_label_set_text("你好")
  │
  ├─ 标签控件层 ──────────────────────────────
  │  set_text_internal()
  │    → copy_text_to_label()
  │    → lv_label_mark_need_refr_text()
  │    → lv_obj_invalidate()
  │
  ├─ 布局测量层 ──────────────────────────────
  │  lv_label_refr_text()
  │    → lv_text_get_next_line()    // 逐行拆分
  │    → lv_text_get_next_word()    // 逐词拆分
  │    → lv_text_get_width()        // 行宽+字距
  │
  ├─ 绘制触发层 ──────────────────────────────
  │  draw_main()
  │    → lv_draw_label()
  │    → lv_draw_sw_label()
  │
  ├─ 字符迭代层 ──────────────────────────────
  │  lv_draw_label_iterate_characters()
  │    → UTF-8 解码
  │    → 解析 #rrggbb 重着色指令
  │    → lv_font_get_glyph_dsc()
  │        ├─ 遍历 fallback 链
  │        ├─ cmap 查找 → glyph_id
  │        ├─ 字距查询与应用
  │        └─ 返回字形描述符(adv_w/box/offset/index)
  │    → lv_draw_unit_draw_letter()
  │
  ├─ 字形位图层 ──────────────────────────────
  │  lv_font_get_glyph_bitmap()
  │    → lv_font_get_bitmap_fmt_txt()
  │        ├─ 按 bitmap_index 定位
  │        ├─ RLE 解压缩(压缩格式)
  │        ├─ 行间 XOR 恢复
  │        └─ bpp 查表扩展 → A8 draw buffer
  │
  └─ 像素混合层 ──────────────────────────────
       draw_letter_cb()
         → lv_draw_sw_blend()
             ├─ 读取 A8 蒙版
             ├─ 文字颜色 + 透明度
             └─ Alpha 混合 → Framebuffer

十三、小结

LVGL 的文字渲染看似简单(一行 API 调用),背后却是一项系统性的工程:

  • 编码层:通过函数指针表抽象 UTF-8/ASCII,上层统一使用 Unicode 码点
  • 字体层:函数指针实现多态,后备字体链支持任意数量字体的组合
  • 存储层:4 种 cmap 格式 + RLE 压缩 + bpp 可变 = 极致的内存压缩
  • 绘制层:A8 蒙版 + Alpha 混合,支持透明度、旋转、着色

了解这些原理后,你就能回答“为什么我设置的中文字体没生效?”(fallback 链断裂)或“为什么阿拉伯文字显示不对?”(Bidi/Shaping 没开)等问题了。如果你对这类底层渲染机制感兴趣,也欢迎到 云栈社区 一起讨论。下一次我们将继续深入 LVGL,剖析对象树与事件系统。




上一篇:LVGL嵌入式GUI移植详解:显示缓冲触摸与RTOS多线程避坑
下一篇:嵌入式 GUI 选型:LVGL 资源占用、组件生态与实战上手全解析
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-28 13:48 , Processed in 0.125212 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表