LVGL设计原理
# 目录介绍
- 1.UI系统架构设计
- 1.1 LVGL框架定位
- 1.2 LVGL核心组件
- 2.LVGL基础使用
- 2.1 对象的模型
- 2.2 常用控件创建
- 2.3 样式系统设计
- 3.UI启动与退出
- 3.1 LVGL启动
- 3.2 LVGL退出
- 4.LVGL绘制原理
- 4.1 渲染管线
- 4.2 脏区机制
- 4.3 双缓冲原理
- 4.4 flush_cb原理
- 5.LVGL线程模型
- 5.1 单线程
- 5.2 跨线程机制
- 5.3 渲染和事件
- 5.4 lv_timer_handler
- 5.5 lv_timer
- 6.资源设计原理
- 6.1 文件系统抽象
- 6.2 图片加载机制
- 6.3 字体系统
- 6.4 系统层优先级
- 7.常见UI操作
- 7.1 文字操作
- 7.2 图片操作
- 7.3 屏幕操作
- 7.4 布局操作
- 7.5 动画操作
- 7.6 对象安全删除
- 7.7 强制刷新
- 8.LVGL封装设计
- 8.1 DisplayService
- 8.2 整体架构
- 8.3 类层次设计
- 8.4 生命周期流程
# 1.UI系统架构设计
# 1.1 LVGL框架定位
LVGL (Light and Versatile Graphics Library) 是一个开源的嵌入式图形库。
提供了创建 GUI 所需的一切要素:可视控件、高级图形效果、低内存占用、支持多种输入设备、多语言等。
其核心设计理念是硬件无关——LVGL 本身不直接操作任何硬件,而是通过抽象的驱动接口(Display Driver、Input Driver)与底层硬件通信。
# 1.2 LVGL核心组件
LVGL 内部由以下核心子系统构成:
| 子系统 | 职责 | 项目中的体现 |
|---|---|---|
| Object System (lv_obj) | 所有可视元素的基类,树形父子结构 | lv_obj_create(nullptr) 创建 Screen,lv_obj_create(parent) 创建子对象 |
| Display Driver (lv_disp_drv) | 硬件显示抽象层,定义分辨率、flush 回调、旋转等 | DisplayService::InitHal() 中注册 |
| Draw Buffer (lv_disp_draw_buf) | 渲染缓冲区管理,支持单/双/全屏缓冲 | 双全屏缓冲 disp_buf_ + disp_buf2_ |
| Timer System (lv_timer) | 内置定时器,驱动动画、脏区刷新等周期任务 | lv_timer_handler() 每 5ms 调用一次 |
| Style System (lv_style) | 类 CSS 的样式属性系统,支持 Part + State 组合 | lv_obj_set_style_*() 系列内联样式 API |
| File System (lv_fs) | 虚拟文件系统抽象,支持多种后端 | POSIX 后端 LV_FS_POSIX_LETTER='P' |
| Font Engine | 位图字体 + FreeType 矢量字体 | 内置 lv_font_montserrat_* + FreeType 动态加载 |
| Image Decoder | 图片解码管线(PNG/JPG/BMP/内存描述符) | lv_img_set_src() 支持文件路径和 lv_img_dsc_t |
| Layout Engine | Flex/Grid 布局系统 | lv_obj_set_flex_flow() 在 ListViewWidget 中使用 |
# 2.LVGL基础使用
# 2.1 对象的模型
LVGL 的一切可视元素都是 lv_obj_t 对象。对象通过父子关系形成树状结构,子对象的坐标相对于父对象,父对象被删除时子对象自动递归删除。
Screen (lv_obj_create(nullptr)) ← 独立屏幕,无父对象
├── bg_image (lv_img_create(screen)) ← 背景图片
├── container (lv_obj_create(screen)) ← 容器
│ ├── label (lv_label_create(container))
│ └── image (lv_img_create(container))
└── spinner (lv_spinner_create(screen))
2
3
4
5
6
Screen 是特殊的 lv_obj_t:通过 lv_obj_create(nullptr) 创建(传入 nullptr 表示无父对象),通过 lv_scr_load(screen) 切换为当前活跃屏幕。LVGL 同一时刻只有一个活跃 Screen,但可以创建多个 Screen 在内存中待命。
# 2.2 常用控件创建
// 1. 创建标签
lv_obj_t* label = lv_label_create(parent); // 创建
lv_label_set_text(label, "Hello"); // 设置文本
lv_obj_set_style_text_font(label, &lv_font_montserrat_24, LV_PART_MAIN);
lv_obj_set_style_text_color(label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); // 居中对齐
// 2. 创建图片
lv_obj_t* img = lv_img_create(parent);
std::string path = LvRes("image/icon.png"); // 转为 LVGL 文件路径
lv_img_set_src(img, path.c_str());
lv_img_set_size_mode(img, LV_IMG_SIZE_MODE_REAL);
lv_obj_align(img, LV_ALIGN_CENTER, 0, 0);
// 3. 创建 Spinner 加载动画
lv_obj_t* spinner = lv_spinner_create(parent, 1000, 60); // 1000ms周期, 60度弧长
lv_obj_set_size(spinner, 80, 80);
lv_obj_set_style_arc_color(spinner, lv_color_hex(0x07C160), LV_PART_MAIN);
lv_obj_set_style_arc_color(spinner, lv_color_hex(0x333333), LV_PART_INDICATOR);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2.3 样式系统设计
LVGL v8 的样式通过 Part + State 二维选择器精确控制。Part 指对象的子部分(主体、指示器、旋钮等),State 指交互状态(默认、按下、聚焦等)。
┌─────────────────────────────────────┐
│ lv_obj_set_style_xxx(obj, value, │
│ selector) │
│ │
│ selector = LV_PART_xxx | LV_STATE_xxx
│ │
│ 常用 Part: │
│ LV_PART_MAIN - 对象主体 │
│ LV_PART_INDICATOR - 指示器部分 │
│ LV_PART_KNOB - 旋钮/滑块 │
│ LV_PART_SCROLLBAR - 滚动条 │
│ │
│ 常用 State: │
│ LV_STATE_DEFAULT - 默认(0) │
│ LV_STATE_PRESSED - 按下 │
│ LV_STATE_FOCUSED - 聚焦 │
└─────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
本项目全部使用 内联样式(lv_obj_set_style_* 系列 API),不使用 lv_style_t 样式对象。
这种方式的优势是简洁直观,劣势是无法样式复用。对于嵌入式小屏幕 UI 场景(控件数量少),内联样式是更实用的选择。
# 3.UI启动与退出
# 3.1 LVGL启动
LVGL 的启动是一个严格有序的多步过程,任何步骤的缺失或顺序颠倒都会导致黑屏或崩溃。
sequenceDiagram
participant App as Application
participant DS as DisplayService
participant LVGL as LVGL Core
participant FB as /dev/fb0
participant BU as BaseUi
participant Sub as 子类(OperationUi等)
Note over App,FB: 阶段一:硬件初始化 (Init)
App->>DS: Init()
DS->>DS: InitDisplayInfo() 读取 /sys/class/graphics/fb0/virtual_size
DS->>LVGL: lv_init() — 初始化核心数据结构
DS->>LVGL: lv_extra_init() — 初始化扩展模块(FreeType等)
DS->>FB: fbdev_init() — 打开 /dev/fb0, mmap 映射
DS->>DS: 分配双缓冲 disp_buf_ + disp_buf2_
DS->>LVGL: lv_disp_draw_buf_init() — 注册缓冲区
DS->>LVGL: lv_disp_drv_init() + lv_disp_drv_register()
Note right of DS: flush_cb=fbdev_flush<br/>full_refresh=1<br/>rotated=270°
DS->>LVGL: InitLvScr() — 设黑色背景, 关滚动条
Note over BU,Sub: 阶段二:Screen 创建
BU->>DS: Init() [如果未初始化]
BU->>LVGL: lv_obj_create(nullptr) — 创建独立 Screen
BU->>LVGL: lv_obj_set_style_bg_color(0x1E1E1E)
BU->>Sub: CreateScreen() — 虚函数, 子类创建控件
Note over BU,DS: 阶段三:屏幕加载与渲染启动 (Show + Start)
BU->>LVGL: lv_scr_load(screen_) — 切换活跃屏幕
BU->>DS: Start()
DS->>DS: 创建 refresh_timer_ (asio::steady_timer)
DS->>DS: Post(Tick) 到主线程
loop 渲染循环 (每5ms)
DS->>LVGL: lv_timer_handler() — 处理定时器+脏区+渲染
LVGL->>FB: fbdev_flush() — 写入 framebuffer
end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
关键细节:
lv_init()是 LVGL 所有功能的前提,它初始化内部内存池、对象链表、定时器链表等核心数据结构lv_extra_init()加载扩展模块,包括 FreeType 字体引擎、额外的控件(spinner 等)fbdev_init()打开/dev/fb0设备,通过mmap将 framebuffer 映射到用户空间lv_disp_drv_register()是让 LVGL "知道"有一块屏幕存在的关键步骤,注册后 LVGL 自动创建默认 Screenlv_scr_load()切换活跃屏幕时,LVGL 会标记整个屏幕为"脏区",触发全屏重绘
# 3.2 LVGL退出
sequenceDiagram
participant BU as BaseUi
participant DS as DisplayService
participant LVGL as LVGL Core
BU->>DS: Stop() — 取消 refresh_timer_
Note right of DS: 渲染循环停止, lv_timer_handler 不再被调用
BU->>BU: HideBackground() — 删除背景图对象
BU->>BU: CleanupScreen() — 子类清理控件
BU->>DS: Deinit()
DS->>DS: Stop()
DS->>LVGL: lv_obj_clean(lv_scr_act()) — 清理屏幕子对象
DS->>LVGL: lv_disp_remove(disp) — 注销显示驱动
DS->>DS: 释放 disp_buf_ / disp_buf2_
BU->>LVGL: lv_obj_del(screen_) — 删除 Screen 对象
2
3
4
5
6
7
8
9
10
11
12
13
14
15
退出顺序至关重要:
- 先停渲染循环 — 防止
lv_timer_handler在对象已删除后继续访问 - 再清子对象 — 通过子类虚函数
CleanupScreen()清理业务控件 - 注销驱动前清理屏幕 —
lv_obj_clean在驱动还存在时执行,避免 UAF - 注销驱动 —
lv_disp_remove让 LVGL 忘记这块屏幕 - 释放缓冲 — 在驱动已注销后安全释放
- 最后删 Screen — Screen 对象在驱动注销后删除
如果顺序颠倒(如先删 Screen 再停渲染),会导致 lv_timer_handler 访问已释放的 lv_obj_t 指针,产生 Use-After-Free 崩溃。
# 4.LVGL绘制原理
# 4.1 渲染管线
LVGL 的渲染不是即时的。当调用 lv_label_set_text 等 API 时,LVGL 仅标记对象为"脏",真正的绘制在 lv_timer_handler() 被调用时才发生。
graph LR
subgraph "API调用阶段"
A[lv_label_set_text] --> B[标记对象脏区<br/>lv_obj_invalidate]
C[lv_obj_set_style_*] --> B
D[lv_obj_set_pos] --> B
end
subgraph "lv_timer_handler() 内部"
B --> E[1. 处理 lv_timer 链表<br/>执行到期的定时器回调]
E --> F[2. 布局计算<br/>Flex/Grid 布局求解]
F --> G[3. 脏区合并<br/>合并重叠的无效矩形区域]
G --> H[4. 逐区域光栅化<br/>遍历对象树, 绘制到 draw_buf]
H --> I[5. 调用 flush_cb<br/>将 draw_buf 写入硬件]
end
subgraph "硬件输出"
I --> J[fbdev_flush<br/>memcpy 到 /dev/fb0 mmap 区域]
J --> K[LCD 控制器<br/>读取 framebuffer 驱动屏幕]
end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 4.2 脏区机制
脏区是 LVGL 性能优化的核心。当对象的属性(位置、大小、文本、样式等)发生变化时,LVGL 计算该对象在屏幕上的覆盖矩形,加入无效区域列表。
下次 lv_timer_handler 执行时,只重绘这些脏矩形区域,而非整个屏幕。
但在本项目中,由于配置了 full_refresh = 1:
disp_drv_.full_refresh = 1; // display_service.cpp:54
这意味着每帧都全屏刷新,脏区优化被禁用。原因是小尺寸屏幕(240×280 = 67200 像素 × 2 字节/像素 ≈ 131KB)全屏刷新的开销很小,且硬件旋转(270°)要求一次性写入完整帧。
# 4.3 双缓冲原理
┌──────────────────────────────────────────────┐
│ LVGL 渲染到 draw_buf 的过程 │
│ │
│ disp_buf_ ┌─────────┐ ← LVGL 正在绘制 │
│ │ Buffer A │ │
│ └─────────┘ │
│ │
│ disp_buf2_ ┌─────────┐ ← fbdev_flush 正在 │
│ │ Buffer B │ 写入 framebuffer │
│ └─────────┘ │
│ │
│ 每帧绘制完成后 A/B 角色互换 │
│ 实现渲染和输出的流水线并行 │
└──────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
本项目分配了两块全屏大小的缓冲区:
auto buf_px_cnt = screen_width_ * screen_height_; // 240 * 280 = 67200 像素
disp_buf_ = std::make_unique<lv_color_t[]>(buf_px_cnt); // ~131 KB
disp_buf2_ = std::make_unique<lv_color_t[]>(buf_px_cnt); // ~131 KB
2
3
双缓冲的优势:LVGL 可以在绘制下一帧的同时,将上一帧通过 flush_cb 输出到硬件,避免撕裂和卡顿。
# 4.4 flush_cb原理
fbdev_flush 是 LVGL 官方提供的 Linux framebuffer 刷新函数:
lv_timer_handler()
└─ 渲染完成后调用 flush_cb(drv, area, color_p)
│
├─ area: 需要刷新的屏幕矩形区域 {x1, y1, x2, y2}
├─ color_p: 指向 draw_buf 中该区域的像素数据
│
└─ fbdev_flush 的实现:
1. 计算目标在 mmap 区域中的偏移
2. 逐行 memcpy color_p → framebuffer mmap 区域
3. 调用 lv_disp_flush_ready(drv) 通知 LVGL 刷新完成
2
3
4
5
6
7
8
9
10
lv_disp_flush_ready() 是必须调用的——它告知 LVGL 缓冲区已经安全地输出到硬件,可以切换到另一个缓冲区继续渲染下一帧。如果不调用,LVGL 会永远等待,屏幕冻结。
# 5.LVGL线程模型
# 5.1 单线程
LVGL 不是线程安全的。 这是使用 LVGL 最重要的约束。所有 lv_* API 调用必须在同一个线程中进行——本项目中是 MainThread。
┌─────────────────────────────────────────────────────┐
│ MainThread (= UI Thread) │
│ │
│ boost::asio::io_context.run() │
│ │ │
│ ├── Tick() → lv_timer_handler() [每5ms] │
│ │ └── 处理 lv_timer 回调 │
│ │ └── 布局计算 │
│ │ └── 渲染 + flush │
│ │ │
│ ├── 业务逻辑回调 (Post 过来的) │
│ │ └── lv_label_set_text(...) │
│ │ └── lv_obj_set_style_*(...) │
│ │ └── lv_obj_del(...) │
│ │ │
│ └── asio::steady_timer 回调 │
│ └── 倒计时更新、自动关闭等 │
└─────────────────────────────────────────────────────┘
┌─────────────────────┐ ┌─────────────────────┐
│ IoT Thread │ │ Network Thread │
│ │ │ │
│ 设备指令处理 │ │ HTTP 请求 │
│ 操作码验签 │ │ 图片下载 │
│ │ │ │
│ ⚠️ 不能直接调 │ │ ⚠️ 不能直接调 │
│ lv_* API │ │ lv_* API │
│ │ │ │
│ 必须 Post 到 │ │ 必须 Post 到 │
│ MainThread │ │ MainThread │
└─────────────────────┘ └─────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 5.2 跨线程机制
当非 UI 线程需要更新 UI 时,必须通过 Threads::MainThread()->Post() 将操作投递到主线程:
// IoT 线程中:设备信息获取完成
DeviceOpcodeManager::Instance()->GetFirmwareInfo(
[this, callback](error_code ec, const FirmwareInfo& info) {
// ⚠️ 当前在 IoT 线程,不能直接操作 UI
Threads::MainThread()->Post([this, ec, info, callback]() {
// ✅ 现在在 MainThread 中,安全操作 UI
if (ec) {
operation_ui_->ShowFailed("Error", std::to_string(ec.value()));
} else {
operation_ui_->ShowListView(BuildFirmwareItems(info));
}
});
});
2
3
4
5
6
7
8
9
10
11
12
13
# 5.3 渲染和事件
本项目的一个重要设计决策是:LVGL 渲染循环与 Boost.Asio 事件循环运行在同一个 io_context 上。
io_context.run()
│
├── [t=0ms] Tick() → lv_timer_handler() → flush
├── [t=1ms] 处理网络响应回调
├── [t=3ms] 处理 Post 过来的 UI 更新
├── [t=5ms] Tick() → lv_timer_handler() → flush
├── [t=7ms] 倒计时 timer 到期回调
├── [t=10ms] Tick() → lv_timer_handler() → flush
└── ...
2
3
4
5
6
7
8
9
这意味着:
- 优势:不需要额外的锁和线程同步,所有 UI 操作天然串行化
- 风险:如果某个 Post 回调或 timer 回调执行时间过长,会阻塞渲染循环,导致 UI 卡顿
- 实践:耗时操作(网络请求、文件IO)必须在其他线程执行,只在回调中做轻量 UI 更新
# 5.4 lv_timer_handler
void DisplayService::Tick() {
auto interval_ms = lv_timer_handler(); // 返回值是建议的下次调用间隔
refresh_timer_->expires_after(std::chrono::milliseconds(5)); // 固定 5ms
refresh_timer_->async_wait([this](const auto& ec) {
if (ec == asio::error::operation_aborted) return;
Tick();
});
}
2
3
4
5
6
7
8
lv_timer_handler() 返回值是 LVGL 建议的下次调用间隔(通常 1~30ms),但项目中固定使用 5ms。这意味着理论最大帧率为 200 FPS,但实际帧率取决于:
- 脏区大小和渲染复杂度
fbdev_flush的 memcpy 耗时- 主线程上其他任务的抢占时间
对于 240×280 全屏刷新,实际帧率通常在 30~60 FPS。
# 5.5 lv_timer
LVGL 自带的定时器系统运行在 lv_timer_handler() 内部,与渲染同步执行:
// 创建 LVGL 定时器(100ms 周期的圆点动画)
dot_anim_timer_ = lv_timer_create(DotAnimTimerCallback, 100, this);
// 删除定时器
lv_timer_del(dot_anim_timer_);
2
3
4
5
lv_timer 的回调在 lv_timer_handler() 中被检查和执行,因此:
- 它天然在 UI 线程中运行,可安全调用
lv_*API - 它的精度受
Tick()调度频率限制(5ms 粒度) - 它不是独立线程,不会与渲染产生竞争
# 6.资源设计原理
# 6.1 文件系统抽象
LVGL 通过虚拟文件系统(lv_fs)屏蔽不同平台的文件访问差异。每个文件系统后端注册一个驱动字母,文件路径的第一个字符标识使用哪个后端。
LVGL 文件路径格式: <驱动字母><绝对路径>
P/opt/app/res/image/icon.png
↑
POSIX 文件系统驱动字母
2
3
4
LvRes() 函数完成路径转换:
std::string LvRes(std::string_view res_path) {
std::string lv_path;
lv_path.push_back(LV_FS_POSIX_LETTER); // 'P'
lv_path.append(Application::GetAppResPath(res_path)); // 拼接绝对路径
return lv_path;
}
// 使用:
// LvRes("image/icon.png")
// → "P/opt/app/res/image/icon.png"
2
3
4
5
6
7
8
9
10
# 6.2 图片加载机制
项目中有三种图片加载方式:
graph TD
subgraph "方式A: 本地文件路径"
A1[LvRes 转换路径] --> A2["lv_img_set_src(img, 'P/path/to/file.png')"]
A2 --> A3[LVGL 内部: lv_fs_open → read → 图片解码器]
end
subgraph "方式B: 网络URL异步加载"
B1[Ui::LvImgSetUrl] --> B2[协程: HTTP 下载]
B2 --> B3{文件缓存}
B3 -->|命中| B4[读取缓存文件]
B3 -->|未命中| B5[HTTP GET]
B5 --> B6[保存缓存]
B4 --> B7[构造 lv_img_dsc_t]
B6 --> B7
B7 --> B8["lv_img_set_src(img, img_dsc)"]
B8 --> B9[LvImgAutoZoom 自动缩放]
end
subgraph "方式C: 内存Buffer"
C1[Ui::LvImgSetBuffer] --> C2[构造 lv_img_dsc_t]
C2 --> C3["lv_img_set_src(img, img_dsc)"]
C3 --> C4[LvImgAutoZoom 自动缩放]
end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
网络图片生命周期管理(方式B/C)特别精妙:
// 1. 创建共享状态,持有图片数据
auto share_state = boost::make_local_shared<LvImgShareState>();
// 2. 注册 LVGL 对象的 DELETE 事件回调
lv_obj_add_event_cb(img_obj, [](lv_event_t* ev) {
auto* state = reinterpret_cast<...>(lv_event_get_user_data(ev));
(*state)->obj_deleted = true; // 标记对象已删除
delete state; // 释放 shared_ptr 副本
}, LV_EVENT_DELETE, new shared_ptr<LvImgShareState>(share_state));
// 3. 异步下载图片(协程可能在任意时刻恢复)
auto img_buf = co_await RequestImage(url, timeout);
// 4. 恢复时检查对象是否还存在
if (share_state->obj_deleted) {
co_return kLvImgObjStateErr; // 对象已被删除,安全退出
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这解决了一个经典的异步资源管理问题:HTTP 请求发出后,如果 UI 页面在下载完成前被关闭(lv_obj_del 删除了 img 对象),下载完成的回调必须感知到这一点,否则会向已释放的对象写入数据。
# 6.3 字体系统
// 预定义字体枚举
enum UiFont {
kUiFontFangZhengLanTingGBK = 0, // 方正兰亭字体
kUiFontWeChatSansSS, // 微信字体
};
// 获取字体(线程安全,仅在UI线程调用)
lv_font_t *font = Ui::GetFont(Ui::kUiFontFangZhengLanTingGBK, 24);
// 便捷函数
lv_font_t *font_sc = FontSc(24); // 中文字体
lv_font_t *font_en = FontWeChatSs(18); // 英文字体
2
3
4
5
6
7
8
9
10
11
12
字体加载机制
static lv_font_t *GetFont(UiFont font, uint16_t weight);
设计特点:
- 缓存机制: 使用
std::map<std::pair<Ui::UiFont, uint16_t>, lv_font_t *>缓存已加载字体 - 延迟加载: 首次使用时才加载字体文件
- 权重支持: 支持不同字体权重(粗细)
- 错误处理: 完善的错误检查和日志记录
实现原理:
- 检查字体缓存是否存在
- 如不存在,通过Application获取字体文件路径
- 使用LVGL的FreeType接口加载字体
- 将加载的字体存入缓存并返回
# 6.4 系统层优先级
LVGL 提供 lv_layer_sys() 全局系统层(位于所有 Screen 之上),多个业务模块可能同时想使用它。项目通过优先级枚举实现互斥访问:
enum class SysLayerPriority {
kUpgrade = 0, // 最高优先级:升级
kDeviceInspect, // 设备检查
kSettings, // 设置
kNetworkStatus, // 网络状态
kLowest, // 默认(无人持有)
};
2
3
4
5
6
7
低优先级业务无法抢占高优先级的系统层。这避免了多个业务同时向系统层写入导致的 UI 冲突。
# 7.常见UI操作
# 7.1 文字操作
// 创建标签
lv_obj_t* label = lv_label_create(parent);
// 设置文本内容
lv_label_set_text(label, "Hello World");
// ⚠️ lv_label_create 后默认文本是 "Text",必须立即设置
// 设置字体(根据语言自动选择)
lv_obj_set_style_text_font(label, Ui::GetDisplayFont(), LV_PART_MAIN);
// 或指定内置字体
lv_obj_set_style_text_font(label, &lv_font_montserrat_24, LV_PART_MAIN);
// 设置颜色
lv_obj_set_style_text_color(label, lv_color_white(), LV_PART_MAIN); // 白色
lv_obj_set_style_text_color(label, lv_color_hex(0xFAD000), LV_PART_MAIN); // 黄色
lv_obj_set_style_text_color(label, lv_color_hex(0xFF0000), LV_PART_MAIN); // 红色
// 文本对齐
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
// 控制宽度和换行
lv_obj_set_width(label, LV_PCT(90)); // 90% 父容器宽度
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); // 自动换行
// 对齐定位
lv_obj_align(label, LV_ALIGN_CENTER, 0, -30); // 居中偏上
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 7.2 图片操作
// 本地图片
lv_obj_t* img = lv_img_create(parent);
std::string path = LvRes("image/icon_result_success.png");
lv_img_set_src(img, path.c_str());
lv_img_set_size_mode(img, LV_IMG_SIZE_MODE_REAL); // 原始尺寸
lv_obj_align(img, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_img_opa(img, LV_OPA_COVER, LV_PART_MAIN); // 完全不透明
// 网络图片(协程异步加载)
co_await Ui::LvImgSetUrl(img, "https://example.com/photo.png");
// 内存图片(如二维码)
Ui::LvImgSetBuffer(img, qrcode_buffer);
// 图片自动缩放
// LvImgAutoZoom 根据 lv_obj 设置的宽高计算缩放因子
lv_obj_set_size(img, 200, 200); // 先设目标尺寸
Ui::LvImgAutoZoom(img); // 自动等比缩放
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 7.3 屏幕操作
// 创建独立屏幕
lv_obj_t* screen = lv_obj_create(nullptr); // nullptr 父对象 = 独立屏幕
// 设置屏幕背景
lv_obj_set_style_bg_color(screen, lv_color_hex(0x1E1E1E), LV_PART_MAIN);
// 切换活跃屏幕
lv_scr_load(screen);
// 获取当前活跃屏幕
lv_obj_t* active = lv_scr_act();
// 隐藏屏幕
lv_obj_add_flag(screen, LV_OBJ_FLAG_HIDDEN);
// 清理屏幕上所有子对象
lv_obj_clean(screen);
// 删除屏幕
lv_obj_del(screen);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 7.4 布局操作
// Flex 布局(垂直列表)
lv_obj_t* container = lv_obj_create(parent);
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN); // 垂直排列
lv_obj_set_style_pad_row(container, 0, LV_PART_MAIN); // 行间距
lv_obj_set_style_pad_top(container, 5, LV_PART_MAIN); // 上内边距
lv_obj_set_style_pad_left(container, 20, LV_PART_MAIN); // 左内边距
// 透明容器(纯布局用途)
lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(container, 0, LV_PART_MAIN);
// 滚动设置
lv_obj_set_scroll_dir(container, LV_DIR_VER); // 仅垂直滚动
lv_obj_set_scrollbar_mode(container, LV_SCROLLBAR_MODE_OFF); // 隐藏滚动条
2
3
4
5
6
7
8
9
10
11
12
13
14
# 7.5 动画操作
// LVGL 内置定时器实现动画
lv_timer_t* timer = lv_timer_create(TimerCallback, 100, user_data); // 100ms 周期
void TimerCallback(lv_timer_t* timer) {
auto* self = static_cast<MyWidget*>(timer->user_data);
// 更新圆点位置、透明度等
lv_obj_set_style_bg_opa(dot, new_opa, LV_PART_MAIN);
lv_obj_set_size(dot, new_size, new_size);
}
// 停止动画
lv_timer_del(timer);
// Spinner 内置旋转动画
lv_obj_t* spinner = lv_spinner_create(parent, 1000, 60);
// 1000ms 旋转一周,弧长 60 度
// 动画由 LVGL 内部 lv_timer 驱动,无需手动管理
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 7.6 对象安全删除
// 项目封装的安全删除方法
class BaseWidget {
protected:
static void DeleteLvObj(lv_obj_t*& obj) {
if (obj) {
lv_obj_del(obj); // 递归删除所有子对象
obj = nullptr; // 置空防止野指针
}
}
};
// 使用:
DeleteLvObj(label_);
DeleteLvObj(container_); // 容器内的子对象也会被自动删除
// ⚠️ 直接用 lv_obj_del 的陷阱:
// 1. 删除后指针变野,必须手动置空
// 2. 删除父对象会递归删除子对象,子对象指针也变野
// 3. 在 lv_timer 回调中删除自身可能导致 UAF
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 7.7 强制刷新
正常情况下,UI 更新由 lv_timer_handler 在 5ms 后自动渲染。但某些场景需要立即刷新(如重启前最后一帧):
lv_timer_handler(); // 处理待刷新的脏区
lv_refr_now(nullptr); // 立即执行 flush_cb,将画面输出到屏幕
// 之后可以安全执行重启/关机等操作
2
3
# 8.LVGL封装设计
# 8.1 DisplayService
static int InitDisplayInfo(int *width, int *height) {
std::ifstream ifs("/sys/class/graphics/fb0/virtual_size");
if (!ifs.is_open()) return -1;
int w, h;
char sep;
if (!(ifs >> w >> sep >> h) || sep != ',') return -2;
*width = w;
*height = h;
return 0;
}
void DisplayService::InitHal() {
lv_log_register_print_cb([](const char *buf) { LOG_I(buf); });
/*LVGL init*/
// 1. 初始化LVGL核心
lv_init();
lv_extra_init();
/*Linux frame buffer device init*/
// 2. 初始化Linux FrameBuffer驱动
fbdev_init();
/*A small buffer for LittlevGL to draw the screen's content*/
// 3. 创建双缓冲区
auto buf_px_cnt = screen_width_ * screen_height_;
disp_buf_ = std::make_unique<lv_color_t[]>(buf_px_cnt);
disp_buf2_ = std::make_unique<lv_color_t[]>(buf_px_cnt);
/*Initialize a descriptor for the buffer*/
// 4. 初始化显示驱动
lv_disp_draw_buf_init(&draw_buf_, disp_buf_.get(), disp_buf2_.get(), buf_px_cnt);
/*Initialize and register a display driver*/
lv_disp_drv_init(&disp_drv_);
disp_drv_.draw_buf = &draw_buf_;
disp_drv_.flush_cb = fbdev_flush;
disp_drv_.hor_res = screen_width_;
disp_drv_.ver_res = screen_height_;
// 临时使用软件旋转
// disp_drv_.sw_rotate = 1;
disp_drv_.full_refresh = 1;
disp_drv_.rotated = LV_DISP_ROT_270;
lv_disp_drv_register(&disp_drv_);
}
void DisplayService::InitLvScr() {
auto *scr = lv_scr_act();
if (!scr) return;
// 默认使用黑色底色,不显示滚动条
lv_obj_set_style_bg_color(scr, lv_color_black(), LV_PART_MAIN);
lv_obj_set_scrollbar_mode(scr, LV_SCROLLBAR_MODE_OFF);
}
void DisplayService::Tick() {
if (!running_) {
return;
}
// 1. 处理LVGL定时器和绘制任务
auto interval_ms = lv_timer_handler();
// 2. 设置下次刷新时间(5ms间隔)
refresh_timer_->expires_after(std::chrono::milliseconds(5));
// 3. 异步等待下次刷新
refresh_timer_->async_wait([this](const auto &ec) {
if (ec == asio::error::operation_aborted) {
return;
} else if (ec) {
LOG_E("refresh timer error: {}", ec.value());
return;
}
Tick();
});
}
int DisplayService::Init() {
if (initialized_) {
LOG_I("DisplayService already initialized");
return 0;
}
if (Device::HasCapacity(Device::Capacity::kDisplay)) {
// 1. 读取屏幕分辨率信息
auto ret = InitDisplayInfo(&screen_width_, &screen_height_);
if (ret) {
LOG_E("read screen info fail, ret: {}", ret);
return ret; // make O3 runable
}
}
LOG_I("read screen info width: {} height: {}", screen_width_, screen_height_);
// 2. 初始化LVGL HAL层
InitHal();
// 3. 初始化LVGL屏幕
InitLvScr();
initialized_ = true;
return 0;
}
void DisplayService::Deinit() {
if (!initialized_) {
LOG_I("DisplayService not initialized, skip Deinit");
return;
}
LOG_I("DisplayService: Deinit start");
// 1. Stop refresh timer
Stop();
// 2. Clean LVGL screen objects
lv_obj_t* scr = lv_scr_act();
if (scr) {
lv_obj_clean(scr);
}
// 3. Unregister display driver
lv_disp_t* disp = lv_disp_get_default();
if (disp) {
lv_disp_remove(disp);
}
// 4. Release display buffers
disp_buf_.reset();
disp_buf2_.reset();
// 5. Close framebuffer device (fbdev_exit if available)
// Note: LVGL's fbdev driver may not provide exit function,
// framebuffer will be released by kernel when process exits
// 6. Reset state
initialized_ = false;
LOG_I("DisplayService: Deinit complete");
}
void DisplayService::Start() {
if (!initialized_) {
LOG_I("DisplayService: Not initialized, skip Start");
return;
}
if (running_) {
LOG_I("DisplayService: Tick loop already running, skip Start");
return;
}
LOG_I("DisplayService: Starting UI Looper");
if (!refresh_timer_) {
refresh_timer_ = std::make_unique<asio::steady_timer>(Threads::MainThread()->Executor());
}
running_ = true;
Threads::MainThread()->Post([this]() { Tick(); });
LOG_I("DisplayService: UI Looper started");
}
void DisplayService::Stop() {
if (!initialized_) {
LOG_I("DisplayService: Not initialized, skip Stop");
return;
}
LOG_I("DisplayService: Stopping UI Looper");
running_ = false;
if (refresh_timer_) {
refresh_timer_->cancel();
LOG_I("DisplayService: UI Looper stopped");
}
}
// screen is rotated 270 degree, so we swap width an height
int DisplayService::GetScreenWidth() {
if (!Instance()->initialized_) {
return 280; // 返回默认值
}
return GetPhysicalScreenHeight();
}
int DisplayService::GetScreenHeight() {
if (!Instance()->initialized_) {
return 240; // 返回默认值
}
return GetPhysicalScreenWidth();
}
DisplayService::~DisplayService() {
Deinit();
}
DisplayService::DisplayService() = default;
DisplayService *DisplayService::Instance() {
static DisplayService inst;
return &inst;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# 8.2 整体架构
┌─────────────────────────────────────────────────────────┐
│ Application │
│ RunMainThread() → 启动 boost::asio::io_context 事件循环 │
└───────────────┬─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ DisplayService (单例) │
│ │
│ Init() → lv_init + fbdev_init + 双缓冲 + 驱动注册 │
│ Start() → 启动 Tick 循环(5ms 周期 lv_timer_handler) │
│ Stop() → 取消 refresh_timer_ │
│ Deinit() → 清理 screen + 注销驱动 + 释放缓冲区 │
│ │
│ 核心原理:将 LVGL 的 lv_timer_handler() 挂载到 │
│ boost::asio::steady_timer 上,与主线程事件循环融合 │
└───────────────┬─────────────────────────────────────────┘
│ Tick 循环运行在 MainThread (= UiThread)
▼
┌─────────────────────────────────────────────────────────┐
│ BaseUi (抽象基类) │
│ │
│ Init() → DisplayService::Init() + lv_obj_create │
│ Show() → lv_scr_load + DisplayService::Start() │
│ Hide() → DisplayService::Stop() │
│ Deinit() → Stop + CleanupScreen + DisplayService::Deinit│
│ │
│ 纯虚接口: CreateScreen() / CleanupScreen() │
└───────┬──────────┬──────────────┬───────────────────────┘
│ │ │
ActivationUi UpgradeUi OperationUi
│
┌─────┴─────┐
│ Widgets │
├────────────┤
│LoadingWidget│
│CountdownWidget│
│TwoLineTextWidget│
│ListViewWidget│
│ImageTextWidget│
└────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 8.3 类层次设计
DisplayService ← 单例,管理 LVGL 底层(fbdev + 双缓冲 + Tick 循环)
│
BaseUi ← 抽象基类,管理 screen 生命周期 + DisplayService 启停
│
├── ActivationUi ← 激活流程 UI
├── UpgradeUi ← 升级 UI
└── OperationUi ← 设备操作 UI(组合多个 Widget)
│
BaseWidget ← Widget 基类(active 状态 + DeleteLvObj 工具方法)
│
├── LoadingWidget ← 加载动画(圆点/Spinner)
├── CountdownWidget ← 倒计时
├── TwoLineTextWidget ← 双行文本
├── ImageTextWidget ← 图文组合
└── ListViewWidget ← 列表视图
2
3
4
5
6
7
8
9
10
11
12
13
14
15
设计思想:
DisplayService只管底层驱动,不关心具体 UI 内容BaseUi管理页面级生命周期(一个 screen 对象 = 一个完整页面)BaseWidget管理组件级生命周期(一个 Widget = 页面内的一个独立功能块)OperationUi通过组合模式持有多个 Widget,按状态机切换显示
# 8.4 生命周期流程
Init 阶段:
BaseUi::Init()
→ DisplayService::Instance()->Init() // 幂等,只初始化一次
→ screen_ = lv_obj_create(nullptr) // 创建独立 screen
→ CreateScreen() // 子类创建 UI 元素
Show 阶段:
BaseUi::Show()
→ lv_scr_load(screen_) // 切换 LVGL 活动屏幕
→ DisplayService::Instance()->Start() // 启动 Tick 刷新循环
Hide 阶段:
BaseUi::Hide()
→ lv_obj_add_flag(screen_, HIDDEN) // 隐藏 screen
→ DisplayService::Instance()->Stop() // 停止 Tick 循环(省电)
Deinit 阶段:
BaseUi::Deinit()
→ DisplayService::Stop()
→ CleanupScreen() // 子类清理 Widget
→ DisplayService::Deinit() // 注销驱动、释放缓冲
→ lv_obj_del(screen_) // 销毁 screen
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22