3、LVGL基础入门:LVGL简介与架构、下载与移植LVGL库、创建第一个LVGL工程(Hello World界面)
好,咱们正式开始接触LVGL。说实话,我第一次看到这个库的时候,心里想的是:「又一个GUI框架,能有多特别?」结果用下来,它确实有点东西。这一章,我带你把LVGL的底裤扒开看看——它到底是什么、怎么拿过来用、怎么让它跑起来。
3.1 LVGL是什么?为什么选它?
LVGL,全称Light and Versatile Graphics Library,翻译过来就是「轻量且通用的图形库」。它专门为嵌入式系统设计,目标是在资源受限的MCU上跑出像样的图形界面。
我最早接触LVGL是在一个温控器项目上。当时MCU只有256KB Flash、64KB RAM,还要显示曲线、按钮、滑动条。用emWin要授权费,用TouchGFX又跑不动。最后选了LVGL,嗯,刚刚好。
它的核心特点,我列个表给你看:
| 特性 | 说明 | 我的评价 |
|---|---|---|
| 开源免费 | MIT协议,商用无限制 | 老板最爱,没有之一 |
| 资源占用低 | 最小配置:Flash 64KB、RAM 8KB | STM32F103都能跑 |
| 控件丰富 | 按钮、滑块、图表、键盘、动画…30+种 | 够用,不够还能自己画 |
| 跨平台 | 支持裸机、FreeRTOS、Linux等 | 一套代码,到处编译 |
| 硬件加速 | 支持STM32 DMA2D、NXP PXP等 | 开了加速,帧率翻倍 |
一句话总结:LVGL就是嵌入式界的「Qt」。你想想看,在资源那么紧的情况下,还能做出手机App级别的界面,是不是有点意思?
3.2 LVGL的软件架构
搞懂架构,你才知道怎么用它。LVGL的分层设计很清晰,我画了张图,你一看就明白:
你看,从上到下分五层。你写应用代码,调用控件API,核心层帮你处理渲染和事件,硬件抽象层把显示和触摸接到底层MCU上。
这里有个关键点:你只需要改最下面那层——硬件抽象层。其他层,LVGL已经写好了,你直接调用就行。
我的习惯:拿到一个新板子,先花半天把HAL层调通。显示能刷出颜色、触摸能读到坐标,后面的事就顺了。千万别一上来就写界面,底没打好,上面全是坑。
3.3 下载LVGL库
下载LVGL,我推荐两种方式:
- 从GitHub直接下载ZIP — 适合新手,一次搞定
- 用Git Clone — 适合后续更新,我习惯用这个
LVGL的GitHub地址是:https://github.com/lvgl/lvgl
我个人习惯用Git,命令很简单:
git clone https://github.com/lvgl/lvgl.git
cd lvgl
git checkout v8.3.0 # 切到稳定版本
下载完后,目录结构长这样:
lvgl/
├── src/ # 核心源码
│ ├── core/ # 核心对象、事件、样式
│ ├── draw/ # 渲染引擎
│ ├── extra/ # 扩展控件
│ ├── hal/ # 硬件抽象层接口
│ └── misc/ # 工具函数
├── examples/ # 示例代码
├── demos/ # 演示工程
├── lv_conf_template.h # 配置文件模板
└── lvgl.h # 主头文件
注意:千万不要直接把整个lvgl文件夹扔进你的工程。你只需要src目录和lv_conf.h配置文件。我曾经见过有人把examples和demos也编译进去,结果Flash直接爆了。
3.4 移植LVGL到你的工程
移植LVGL,说白了就是让LVGL知道「怎么往屏幕上画点」和「怎么知道时间过去了多久」。我把它拆成5步:
| 步骤 | 做什么 | 关键函数/文件 |
|---|---|---|
| 1. 复制源码 | 把lvgl/src复制到工程 | — |
| 2. 配置lv_conf.h | 打开宏开关,设置颜色深度、内存大小 | LV_COLOR_DEPTH、LV_MEM_SIZE |
| 3. 实现显示驱动 | 告诉LVGL怎么刷屏 | disp_flush() |
| 4. 实现触摸驱动 | 告诉LVGL触摸坐标 | touchpad_read() |
| 5. 提供时钟节拍 | 每1ms调用一次lv_tick_inc() |
lv_tick_inc(1) |
咱们重点看显示驱动怎么写。这是最核心的部分:
/* 显示刷新回调函数 */
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
int32_t x, y;
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
/* 把color_p指向的颜色写到屏幕(x,y)位置 */
set_pixel(x, y, color_p->full);
color_p++;
}
}
/* 通知LVGL刷新完成 */
lv_disp_flush_ready(disp_drv);
}
/* 初始化显示驱动 */
void lv_port_disp_init(void)
{
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = disp_flush;
disp_drv.hor_res = 320; /* 屏幕宽度 */
disp_drv.ver_res = 240; /* 屏幕高度 */
lv_disp_drv_register(&disp_drv);
}
避坑指南:我曾经在移植时忘了调用lv_disp_flush_ready(),结果屏幕死活不刷新。查了半天,原来是LVGL在等这个信号才继续送下一帧数据。记住,刷完屏一定要喊一声「我好了」。
触摸驱动也类似,核心就是告诉LVGL「手指按在哪」:
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
int16_t x, y;
uint8_t pressed;
/* 从硬件读取触摸状态 */
read_touch(&pressed, &x, &y);
if(pressed) {
data->state = LV_INDEV_STATE_PR;
data->point.x = x;
data->point.y = y;
} else {
data->state = LV_INDEV_STATE_REL;
}
}
3.5 创建第一个LVGL工程:Hello World
好,移植完了,咱们来写第一个界面。目标很简单:屏幕上显示一行「Hello, LVGL!」文字。
先看完整代码:
#include "lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"
int main(void)
{
/* 1. 初始化硬件 */
hardware_init();
/* 2. 初始化LVGL */
lv_init();
/* 3. 初始化显示和触摸驱动 */
lv_port_disp_init();
lv_port_indev_init();
/* 4. 创建Hello World界面 */
lv_obj_t * label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Hello, LVGL!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
/* 5. 主循环 */
while(1) {
lv_timer_handler(); /* 处理LVGL任务 */
delay_ms(5); /* 适当延时 */
}
}
这段代码,我一行行给你拆开讲:
lv_init()— 初始化LVGL内部模块,必须第一个调用lv_port_disp_init()— 注册显示驱动,告诉LVGL怎么画屏lv_port_indev_init()— 注册输入设备驱动,告诉LVGL触摸信息lv_label_create(lv_scr_act())— 在当前活动屏幕上创建一个标签控件lv_label_set_text()— 设置标签显示的文本lv_obj_align()— 把标签居中放置lv_timer_handler()— LVGL的心跳函数,处理动画、刷新、事件
核心要点:lv_timer_handler()必须周期性调用,否则LVGL不会做任何事。我见过有人把它放在中断里,结果系统卡死。正确的做法是放在主循环或RTOS的低优先级任务里,每5~10ms调用一次。
3.6 编译与运行
编译前,检查一下你的lv_conf.h配置:
/* 关键配置项 */
#define LV_COLOR_DEPTH 16 /* 16位色,RGB565 */
#define LV_MEM_SIZE (32U * 1024U) /* 32KB内存池 */
#define LV_TICK_CUSTOM 1 /* 使用自定义时钟节拍 */
#define LV_USE_DEMO_RENDER 0 /* 关闭演示,节省空间 */
编译通过后,下载到板子。如果一切正常,屏幕中央会显示:
Hello, LVGL!
如果屏幕没显示,别慌。先检查这几样:
- 显示驱动里的
set_pixel()函数是否真的能点亮像素?单独写个测试函数验证一下。 lv_tick_inc()是否在定时器中断里被调用?频率对不对?lv_conf.h里的LV_COLOR_DEPTH是否和屏幕硬件匹配?
我曾经在16位色屏上配了32位色,结果颜色全乱套。调了一下午才发现是这里的问题。
3.7 本章小结
这一章,咱们干了三件事:
- 搞懂了LVGL是什么、为什么选它
- 理清了LVGL的五层架构,知道改哪层、不动哪层
- 亲手移植了LVGL,并跑通了第一个Hello World界面
说实话,第一次看到「Hello, LVGL!」出现在自己板子上的时候,还是有点小激动的。你想想看,从裸机画点,到能显示文字,这一步跨出去,后面就是一片新天地。
嗯,基础打好了,后面咱们就可以玩点真的了。