3. LVGL 基础概念:架构、对象、驱动与内存
好,咱们正式开始接触 LVGL 的核心了。这一章我会把 LVGL 的骨架给你拆开来看。说白了,你理解了这几个概念,后面写界面就像搭积木一样自然。
3.1 LVGL 架构概览:它到底是怎么工作的?
LVGL 不是一个大黑盒。它的内部结构其实很清晰。我习惯把它分成三层:
- 应用层:你写的代码,创建按钮、滑块、标签这些。
- 核心层:LVGL 自己的对象管理、事件分发、样式计算。
- 驱动层:连接你的硬件——屏幕怎么刷、触摸怎么读。
你想想看,你在应用层点了一个按钮,核心层会判断这个点按在了哪个对象上,然后通过驱动层把变化画到屏幕上。整个过程是单向流动的。
核心要点:LVGL 是一个事件驱动的图形框架。它有一个主循环(lv_task_handler),不断处理输入、更新动画、重绘屏幕。你的应用代码只是在这个循环里注册回调。
下面这张图能帮你把这三层关系看明白:
3.2 对象(Object)机制:一切皆对象
LVGL 里,屏幕上能看到的所有东西都是对象。按钮是对象,标签是对象,甚至屏幕本身也是对象。它们都继承自一个基础类——lv_obj_t。
我记得刚开始用 LVGL 时,总搞不清父子关系。这里给你说清楚:
- 父对象:负责管理子对象的生命周期和位置。删除父对象,所有子对象一起消失。
- 子对象:坐标是相对于父对象的。父对象移动,子对象跟着动。
我的习惯:创建对象时,先创建屏幕(lv_scr_act()),然后在屏幕上创建容器(lv_obj_create),最后在容器里放控件。这样层级清晰,后期改布局也方便。
来看一个最简单的对象创建例子:
/* 创建一个按钮,父对象是当前活动屏幕 */
lv_obj_t *btn = lv_btn_create(lv_scr_act());
/* 设置按钮位置和大小 */
lv_obj_set_pos(btn, 50, 50);
lv_obj_set_size(btn, 120, 40);
/* 在按钮上创建一个标签 */
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, "点击我");
你发现没有?创建标签时,父对象传的是 btn 而不是屏幕。这样标签就自动成为按钮的子对象,位置也是相对于按钮的。
3.3 显示驱动与输入设备驱动
驱动层是 LVGL 和硬件之间的桥梁。没有它,你代码写得再漂亮,屏幕也是黑的。
显示驱动
显示驱动的核心是一个回调函数——flush_cb。LVGL 计算好要显示的内容后,会调用这个函数,把像素数据发送到屏幕。
我曾经在一个项目里踩过坑:刷新回调里忘了调用 lv_disp_flush_ready(),结果屏幕一直卡在第一帧。嗯,这个函数是告诉 LVGL「我已经刷完了,你可以继续了」。
/* 显示驱动初始化示例 */
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
/* 设置刷新回调 */
disp_drv.flush_cb = my_flush_callback;
/* 设置缓冲区 */
static lv_color_t buf[LV_HOR_RES_MAX * 10];
disp_drv.draw_buf = &draw_buf;
lv_disp_draw_buf_init(&draw_buf, buf, NULL, LV_HOR_RES_MAX * 10);
/* 注册显示驱动 */
lv_disp_t *disp = lv_disp_drv_register(&disp_drv);
避坑指南:缓冲区大小直接影响性能。太小会导致频繁刷新,CPU 占用高;太大又浪费 RAM。我一般用「一行像素 × 10」作为起步值,然后根据实际帧率调整。
输入设备驱动
输入设备驱动负责把用户的触摸、按键等操作告诉 LVGL。核心也是一个回调——read_cb。
LVGL 支持多种输入设备:触摸屏、鼠标、键盘、编码器。每种设备类型用 lv_indev_type_t 枚举区分。
| 设备类型 | 枚举值 | 典型硬件 | 回调返回的数据 |
|---|---|---|---|
| 触摸屏 | LV_INDEV_TYPE_POINTER |
电阻屏、电容屏 | 坐标 (x, y) + 按下状态 |
| 键盘 | LV_INDEV_TYPE_KEYPAD |
矩阵键盘、PS/2 键盘 | 按键编码 |
| 编码器 | LV_INDEV_TYPE_ENCODER |
旋转编码器 | 旋转方向 + 按键 |
| 鼠标 | LV_INDEV_TYPE_POINTER |
USB 鼠标 | 坐标 + 按键状态 |
/* 触摸屏驱动注册示例 */
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = my_touchpad_read;
lv_indev_t *my_indev = lv_indev_drv_register(&indev_drv);
3.4 内存管理策略
嵌入式系统的 RAM 是金贵的。LVGL 提供了两种内存管理方式,你得根据项目选对。
动态内存分配
默认情况下,LVGL 使用标准 C 库的 malloc/free。但说实话,在裸机或 RTOS 环境下,我建议你换成 LVGL 自带的内存分配器。
配置方式很简单,在 lv_conf.h 里设置:
/* 使用 LVGL 内置内存分配器 */
#define LV_MEM_CUSTOM 0
/* 分配一个 32KB 的内存池 */
#define LV_MEM_SIZE (32 * 1024)
为什么推荐内置分配器? 它不会产生内存碎片,而且速度比标准 malloc 快。我在一个只有 64KB RAM 的 STM32 项目上跑过,连续运行一个月没出过内存问题。
内存池与碎片管理
LVGL 的内存池是一个大数组,所有对象、样式、图片都从这里分配。它使用了一种叫「dlmalloc」的算法,专门针对嵌入式场景优化。
你可能会问:内存碎片怎么办?
嗯,LVGL 有一个小技巧——它会在空闲时自动整理碎片。但前提是你得定期调用 lv_timer_handler()。我见过有人把主循环写成死等,结果内存越用越少,最后崩溃。
我的经验:在 main 函数的主循环里,保证每 5-10 毫秒调用一次 lv_timer_handler()。这样既能保证 UI 流畅,又能让内存管理器及时回收碎片。
缓冲区策略
显示缓冲区的大小直接影响内存占用和刷新效率。LVGL 支持三种模式:
| 模式 | 缓冲区数量 | 内存占用 | 适用场景 |
|---|---|---|---|
| 单缓冲 | 1 个 | 低 | RAM 极少的 MCU(如 Cortex-M0) |
| 双缓冲 | 2 个 | 中 | 大多数项目,平衡性能与内存 |
| 全屏缓冲 | 1 个(全屏大小) | 高 | 高性能 MCU 或带外部 RAM |
我个人最常用双缓冲模式。一个缓冲区在刷屏,另一个在渲染下一帧,这样 CPU 和 DMA 可以并行工作,帧率能提升不少。
/* 双缓冲配置示例 */
static lv_color_t buf1[LV_HOR_RES_MAX * 10];
static lv_color_t buf2[LV_HOR_RES_MAX * 10];
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LV_HOR_RES_MAX * 10);
好了,这一章的内容就到这儿。你理解了架构、对象、驱动和内存管理这四个概念,LVGL 的大门就算真正推开了。后面我们会用这些知识,一步步搭出完整的界面。