3. LVGL基础架构:软件架构、对象树模型、消息循环与刷新机制
学LVGL,很多人一上来就画控件、调样式,觉得挺爽。但真到了项目里,界面卡了、内存爆了、触摸没反应了,才发现根本不知道底层怎么跑的。
我刚开始做嵌入式GUI那会儿,也踩过这个坑。后来花了两个晚上把LVGL的架构啃了一遍,才明白——搞懂架构,比背100个API都管用。
这一章,咱们就把LVGL的骨架拆开看看。说白了就三件事:软件怎么分层、对象怎么组织、屏幕怎么刷新。
3.1 软件架构:三层分工,各司其职
LVGL的代码结构非常清晰,我习惯把它分成三层:
| 层级 | 名称 | 职责 | 典型文件 |
|---|---|---|---|
| 应用层 | Application | 用户界面逻辑、业务代码 | main.c, user_app.c |
| 中间层 | LVGL Core | 对象管理、事件分发、样式计算、布局 | lv_obj.c, lv_event.c, lv_style.c |
| 底层 | Driver & Porting | 显示驱动、触摸驱动、内存管理、系统时钟 | lv_port_disp.c, lv_port_indev.c |
你写应用代码的时候,其实是在最上层调用中间层的API。中间层帮你处理完逻辑,最后通过底层驱动把画面刷到屏幕上。
这里有个关键点——LVGL不依赖操作系统。它自己有一套完整的事件循环和内存管理。当然,如果你想跑在FreeRTOS上,它也支持得很好。我有个项目就是在RTOS里给LVGL单独开了一个任务,优先级设得比通信任务低一点,效果不错。
3.2 对象树模型:一切皆对象,对象皆父子
LVGL里所有的界面元素都是对象(Object)。按钮是对象,标签是对象,面板是对象,甚至屏幕本身也是对象。
这些对象不是散落的,它们组织成一棵树。
lv_obj_t *scr = lv_scr_act(); // 获取当前活动屏幕
lv_obj_t *panel = lv_obj_create(scr); // 在屏幕上创建面板
lv_obj_t *btn = lv_btn_create(panel); // 在面板上创建按钮
lv_obj_t *label = lv_label_create(btn); // 在按钮上创建标签
你看,每个对象创建时都要指定一个父对象。这样就形成了一棵从屏幕到具体控件的树。
为什么会这样设计?
你想想看,如果所有对象都是平级的,那移动一个面板的时候,面板上的按钮、标签、滑块全都要手动跟着移,多麻烦。有了父子关系,父对象移动,子对象自动跟着走。父对象隐藏,子对象也看不见。父对象销毁,子对象也一起清理。
我在项目中遇到过一个问题:创建了一个弹窗,弹窗上有个关闭按钮。结果弹窗移位置了,按钮还留在原地。后来一查,按钮的父对象设成了屏幕,不是弹窗。嗯,这种低级错误,犯过一次就记住了。
3.3 对象树的可视化
下面这张图,是我用SVG画的一个典型对象树结构。你可以直观地看到层级关系。
这棵树,你可以在运行时随时遍历。LVGL提供了lv_obj_get_child()和lv_obj_get_parent()来上下查找。调试的时候特别有用。
3.4 消息循环:LVGL的心脏
LVGL能跑起来,全靠一个死循环。这个循环在lv_task_handler()里,你需要在主循环中反复调用它。
while(1) {
lv_task_handler(); // LVGL 核心处理
my_delay(5); // 适当延时,让出CPU
}
这个函数每次调用,LVGL会做以下几件事:
- 处理输入事件:检查触摸、按键、编码器有没有新数据。
- 触发事件回调:如果有点击、滑动、长按,调用你注册的事件处理函数。
- 执行动画:更新所有正在运行的动画帧。
- 处理定时器:检查lv_timer创建的任务是否到期。
- 标记脏区域:记录哪些区域需要重绘。
我刚开始用LVGL的时候,以为lv_task_handler()调用越快越好。结果CPU占用率飙到90%,界面反而卡。后来发现,5ms调用一次就足够了,太快了反而浪费。
3.5 刷新机制:脏区域与双缓冲
LVGL不会每帧都重绘整个屏幕。那样太慢了。它只重绘发生变化的部分,这部分叫做脏区域(Dirty Area)。
举个例子:你点击了一个按钮,按钮颜色变了。LVGL会把这个按钮所在的矩形区域标记为“脏”。下一次刷新时,只重绘这个矩形,其他区域保持不变。
具体流程是这样的:
// 伪代码:LVGL内部刷新流程
1. 用户操作 → 对象属性变化
2. lv_obj_invalidate(btn) → 标记按钮区域为脏
3. lv_refr_join() → 合并相邻的脏区域
4. lv_refr_area() → 调用 flush_callback 刷新合并后的区域
5. flush_callback → 把数据写入显示缓冲区 → 送到屏幕
这里有个重要的配置——显示缓冲区。LVGL支持三种模式:
| 模式 | 缓冲区大小 | 特点 | 适用场景 |
|---|---|---|---|
| 单缓冲 | 1/10 屏幕 | 内存占用小,但可能有撕裂 | 资源紧张的MCU |
| 双缓冲 | 1/6 屏幕 × 2 | 无撕裂,流畅 | 大多数项目 |
| 全屏双缓冲 | 全屏 × 2 | 最流畅,内存占用大 | 带外部RAM的设备 |
我个人建议,能用双缓冲就用双缓冲。单缓冲在快速滑动时容易出现画面撕裂,用户体验很差。我曾经在一个STM32F429的项目上,硬是抠出了两个1/6屏幕的缓冲区,效果比单缓冲好太多了。
3.6 刷新流程全景图
下面这张SVG图,把整个刷新流程串起来了。你可以看到从用户操作到画面显示的全链路。
你看,整个流程其实不复杂。关键就两个地方:标记脏区域要准,flush回调要快。
我曾经在一个项目里,flush_callback里加了个延时,结果整个界面卡成PPT。后来把延时去掉,用DMA传输数据,瞬间流畅了。嗯,这种坑,踩过一次就记住了。
3.7 本章小结
这一章我们拆了LVGL的骨架:
- 软件架构:应用层、中间层、驱动层,各层职责清晰。
- 对象树模型:父子层级管理,移动、隐藏、销毁都自动联动。
- 消息循环:lv_task_handler()是心脏,5ms调用一次刚刚好。
- 刷新机制:脏区域 + 双缓冲,只重绘变化的部分,高效省资源。