3. LVGL 基础概念:架构、对象、驱动与内存

好,咱们正式开始接触 LVGL 的核心了。这一章我会把 LVGL 的骨架给你拆开来看。说白了,你理解了这几个概念,后面写界面就像搭积木一样自然。

3.1 LVGL 架构概览:它到底是怎么工作的?

LVGL 不是一个大黑盒。它的内部结构其实很清晰。我习惯把它分成三层:

  • 应用层:你写的代码,创建按钮、滑块、标签这些。
  • 核心层:LVGL 自己的对象管理、事件分发、样式计算。
  • 驱动层:连接你的硬件——屏幕怎么刷、触摸怎么读。

你想想看,你在应用层点了一个按钮,核心层会判断这个点按在了哪个对象上,然后通过驱动层把变化画到屏幕上。整个过程是单向流动的。

核心要点:LVGL 是一个事件驱动的图形框架。它有一个主循环(lv_task_handler),不断处理输入、更新动画、重绘屏幕。你的应用代码只是在这个循环里注册回调。

下面这张图能帮你把这三层关系看明白:

LVGL 三层架构图 LVGL 软件架构 应用层(Application Layer) 你的 UI 代码:创建按钮、标签、滑块,绑定事件回调 核心层(Core Layer) 对象管理 · 事件分发 · 样式计算 · 动画调度 驱动层(Driver Layer) 显示驱动(flush) · 输入设备驱动(read) · 硬件抽象 数据流方向:应用层 → 核心层 → 驱动层 → 硬件

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 的大门就算真正推开了。后面我们会用这些知识,一步步搭出完整的界面。