1、LVGL概述与环境搭建:LVGL是什么、为什么选择LVGL、LVGL在MCU上的优势、开发环境(Keil/IAR/STM32CubeIDE)搭建与验证
1.1 LVGL到底是什么?
LVGL,全称是Light and Versatile Graphics Library。说白了,它就是一个专门为嵌入式系统打造的图形库。
我刚开始接触嵌入式GUI时,用过uCGUI,也折腾过emWin。后来遇到LVGL,第一感觉就是——这玩意儿真轻。它不像Linux下的Qt那么重,也不像某些商业库那样藏着掖着。LVGL完全开源,用C语言写的,移植起来特别顺手。
你想想看,一个只有几百KB Flash的单片机,居然能跑出带动画、带触摸、带多语言的界面。这就是LVGL的厉害之处。
我个人习惯把LVGL比作「嵌入式界的Qt」。它提供了丰富的控件:按钮、标签、滑块、图表、键盘……基本上你能想到的UI元素,它都有。
1.2 为什么选择LVGL?
市面上嵌入式GUI方案不少,我列几个常见的:
| 方案 | 特点 | 适合场景 |
|---|---|---|
| LVGL | 开源、轻量、控件丰富、社区活跃 | 资源受限的MCU,如STM32、ESP32 |
| emWin | 商业库、稳定、但收费 | 有预算的商业产品 |
| TouchGFX | 界面华丽、但资源消耗大 | 高性能MCU,如STM32F7/H7 |
| uGUI | 极简、但功能有限 | 超低内存场景 |
我在项目中遇到过几次选型纠结。有一次客户要求做一个小家电,MCU只有64KB RAM,还要跑动画。emWin试了,内存不够。TouchGFX更别提了。最后用LVGL,配合帧缓冲优化,稳稳跑起来了。
为什么会这样?因为LVGL在设计之初就考虑到了低内存场景。它支持多种显示缓冲模式:单缓冲、双缓冲、甚至局部刷新。你内存小,就用单缓冲;内存够,就用双缓冲防撕裂。
核心优势总结:
- 内存占用极低:最低只需几KB RAM就能跑
- 控件丰富:30+内置控件,省去自己造轮子
- 开源免费:MIT协议,商用无压力
- 跨平台:支持裸机、FreeRTOS、RT-Thread等
- 社区活跃:遇到问题,GitHub上基本能找到答案
1.3 LVGL在MCU上的优势
嗯,这里要重点说说。很多初学者会问:「MCU那么弱,跑GUI靠谱吗?」
我的回答是:靠谱,但要有技巧。
LVGL在MCU上的优势,主要体现在三个方面:
第一,资源占用可控。 LVGL允许你裁剪功能。不需要动画?关掉。不需要抗锯齿?关掉。不需要某些控件?也可以关掉。我见过一个项目,把LVGL裁剪到只占30KB Flash,照样跑出了漂亮的界面。
第二,刷新机制高效。 LVGL默认只刷新变化区域,而不是全屏刷新。这一点在低帧率的MCU上特别重要。你想想看,如果每次按键都全屏刷新,CPU基本就废了。LVGL的局部刷新机制,让CPU有更多时间处理业务逻辑。
第三,硬件加速可选。 如果你的MCU带硬件加速(比如STM32的LTDC、DMA2D),LVGL可以配合使用。我在一个项目里用DMA2D做颜色格式转换,帧率直接从15fps提到了30fps。
小提示: 如果你的MCU没有硬件加速,也别慌。LVGL的软件渲染效率也很高。我实测过,在72MHz的STM32F103上,跑一个带按钮和滑块的界面,帧率能稳定在20fps以上。
1.4 开发环境搭建与验证
好了,理论说完了,咱们动手。这里我以Keil为例,带你走一遍LVGL的移植流程。IAR和STM32CubeIDE的步骤类似,我就不重复了。
1.4.1 下载LVGL源码
去GitHub搜「lvgl」,下载最新稳定版。我个人习惯用v8.x系列,因为文档最全,社区支持最好。v9.x虽然新,但有些API变了,新手容易踩坑。
下载后,你会看到这样的目录结构:
lvgl/
├── src/ # 核心源码
├── examples/ # 示例代码
├── docs/ # 文档
└── lv_conf_template.h # 配置文件模板
1.4.2 添加源码到工程
把 src/ 目录下的所有 .c 文件添加到你的Keil工程里。注意,不要漏掉 lvgl/src/ 下的子目录,每个子目录都有用。
然后,把 lv_conf_template.h 复制到工程根目录,重命名为 lv_conf.h。这个文件是LVGL的配置中心,所有功能开关都在这里。
1.4.3 配置 lv_conf.h
打开 lv_conf.h,找到下面几个关键配置:
#define LV_COLOR_DEPTH 16 // 颜色深度,16位色最常见
#define LV_HOR_RES_MAX 240 // 屏幕宽度
#define LV_VER_RES_MAX 320 // 屏幕高度
#define LV_USE_DEMO_WIDGETS 1 // 打开示例,方便验证
我曾经犯过一个低级错误:颜色深度设成了32位,结果屏幕显示全是花的。后来才发现,我的屏幕只支持16位色。嗯,这里要注意,颜色深度一定要和屏幕匹配。
1.4.4 实现显示驱动
LVGL需要知道怎么往屏幕上画点。你需要实现三个函数:
// 1. 初始化屏幕
void my_disp_init(void) {
// 你的屏幕初始化代码
}
// 2. 画一个像素点
void my_disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
// 把 color_p 中的数据刷到 area 指定的区域
// 比如用SPI发送数据
lv_disp_flush_ready(disp_drv); // 通知LVGL刷新完成
}
// 3. 注册到LVGL
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = my_disp_flush;
disp_drv.hor_res = 240;
disp_drv.ver_res = 320;
lv_disp_drv_register(&disp_drv);
注意: 一定要在刷新完成后调用 lv_disp_flush_ready()。如果不调用,LVGL会一直等待,界面就卡死了。我刚开始移植时忘了这步,调试了一整天。
1.4.5 实现触摸驱动(可选)
如果你的屏幕支持触摸,还需要实现触摸读取函数:
void my_touch_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) {
// 读取触摸坐标
data->point.x = touch_x;
data->point.y = touch_y;
data->state = touch_pressed ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
}
1.4.6 验证移植是否成功
在 main() 函数中,加入以下代码:
int main(void) {
my_disp_init(); // 初始化屏幕
lv_init(); // 初始化LVGL
my_disp_flush_init(); // 注册显示驱动
my_touch_init(); // 注册触摸驱动
// 创建一个示例界面
lv_demo_widgets(); // LVGL自带的控件示例
while(1) {
lv_timer_handler(); // LVGL的心跳函数,必须周期性调用
my_delay_ms(5); // 延时5ms
}
}
如果一切顺利,屏幕上应该会出现一个带按钮、滑块、进度条的示例界面。你可以点一点、滑一滑,看看响应是否流畅。
验证小技巧: 如果屏幕没显示,先检查硬件连接。用逻辑分析仪抓一下SPI信号,看看数据有没有发出去。如果数据发了但屏幕没反应,大概率是初始化序列不对。我曾经被一个屏幕的初始化序列坑过,折腾了两天才发现是复位时序的问题。
1.5 常见问题与避坑指南
最后,分享几个我踩过的坑:
- 编译报错: 如果提示
lv_conf.h找不到,检查一下头文件路径有没有添加。Keil里要在「C/C++」选项卡的「Include Paths」里加上lvgl/目录。 - 屏幕闪烁: 大概率是刷新频率太低。把
lv_timer_handler()的调用间隔缩短到5ms以内,或者改用双缓冲。 - 触摸不灵: 检查触摸坐标是否需要校准。有些屏幕的触摸坐标和显示坐标是反的,需要做映射。
- 内存不足: 如果编译提示RAM不够,可以减小
LV_MEM_SIZE的值。默认是32KB,对于小内存MCU,可以降到8KB甚至4KB。
好了,第一章的内容就到这里。环境搭好了,下一章我们开始讲LVGL的核心机制——显示缓冲与刷新策略。到时候我会详细说说怎么在低内存下把性能榨到极致。