3、LVGL基础入门:LVGL架构概览、源码移植步骤、初始化流程、显示缓冲区配置、输入设备驱动注册
好,咱们进入第三章。这一章我打算把LVGL的底裤扒开给你看。说白了,就是让你知道LVGL到底怎么跑起来的,以及你该怎么把它塞进你的板子里。
很多初学者一上来就急着画界面,结果卡在移植上,一卡就是好几天。我当年也干过这事——对着官方文档翻来覆去,就是跑不出一个Hello World。后来才发现,其实就那么几个关键点,搞通了就通了。
3.1 LVGL架构概览:它到底长什么样?
先看一张图,把LVGL的骨架看清楚。
你看,LVGL不是一个大泥球。它分了好几层。你写应用代码,调用控件API,控件再依赖核心层去管理对象和事件。核心层往下,就是驱动抽象层——这一层是你需要动手的地方。
我个人习惯把LVGL比作一个「积木工厂」。核心层是生产积木的机器,控件层是各种形状的积木块,驱动层就是传送带,把你的积木送到屏幕上。你作为工程师,只需要把传送带接好就行。
3.2 源码移植步骤:怎么把它塞进你的工程?
移植LVGL,说白了就是三步:拿源码、配参数、写接口。别想复杂了。
3.2.1 获取源码
去GitHub上拉LVGL的官方仓库。我建议你直接用release版本,别追master。我在项目里吃过这个亏——追master追到一半,API变了,整个界面重写,那叫一个酸爽。
3.2.2 文件结构
把源码丢进你的工程后,目录结构大概长这样:
project/
├── lvgl/
│ ├── src/ # LVGL核心源码
│ │ ├── core/ # 核心对象、事件、动画
│ │ ├── draw/ # 绘图引擎
│ │ ├── fonts/ # 字体
│ │ ├── widgets/ # 控件
│ │ └── misc/ # 杂项
│ ├── examples/ # 示例(可删)
│ └── lv_conf.h # 配置文件(关键!)
├── lv_port_disp.c # 显示驱动接口(你写)
├── lv_port_indev.c # 输入设备驱动接口(你写)
└── main.c # 你的主程序
lv_conf.h 单独拎出来放在工程根目录,而不是藏在lvgl文件夹里。这样改配置的时候不用翻半天目录。
3.2.3 配置 lv_conf.h
这个文件是LVGL的「总开关」。里面几百个宏定义,但你别慌——大部分保持默认就行。你需要关注的就这几个:
| 宏定义 | 说明 | 我常用的值 |
|---|---|---|
LV_COLOR_DEPTH |
颜色深度,跟你的屏幕有关 | 16(RGB565)或 32(ARGB8888) |
LV_HOR_RES_MAX |
屏幕水平分辨率 | 320、480、800 等 |
LV_VER_RES_MAX |
屏幕垂直分辨率 | 240、320、480 等 |
LV_MEM_SIZE |
LVGL动态内存大小 | 32 * 1024(32KB起步) |
LV_USE_DEMO_WIDGETS |
是否启用内置demo | 调试时开,发布时关 |
LV_MEM_SIZE 别设太小。我曾经在一个STM32F103项目上只给了16KB,结果界面稍微复杂一点就分配失败,画面花屏。后来改成48KB才稳定。你想想看,一个按钮加个标签就吃掉好几KB,太小了真不行。
3.3 初始化流程:LVGL是怎么启动的?
初始化流程其实就五步,我把它写成了一个口诀:
- 调 lv_init() —— 初始化LVGL内部核心模块
- 调 lv_port_disp_init() —— 注册显示驱动
- 调 lv_port_indev_init() —— 注册输入设备驱动
- 调 lv_tick_inc(x) —— 告诉LVGL时间过去了多少毫秒(通常在定时器中断里调)
- 在while(1)里调 lv_task_handler() —— 让LVGL处理任务、刷新界面
代码写出来就是这样:
#include "lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"
int main(void)
{
/* 硬件初始化:时钟、GPIO、SPI、定时器等 */
hardware_init();
/* 第一步:初始化LVGL核心 */
lv_init();
/* 第二步:注册显示驱动 */
lv_port_disp_init();
/* 第三步:注册输入设备驱动 */
lv_port_indev_init();
/* 第四步:创建一个简单的界面 */
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);
/* 主循环 */
while(1)
{
/* 第五步:定期调用LVGL任务处理 */
lv_task_handler();
/* 延时,别让CPU空转 */
delay_ms(5);
}
}
lv_tick_inc() 必须在定时器中断里周期性调用,频率通常是1ms一次。LVGL内部靠这个时间戳来做动画、长按检测、闪烁光标等。如果你忘了调这个,动画会卡住,触摸长按也没反应。我刚开始就踩过这个坑,动画死活不动,查了半天才发现是定时器没开。
3.4 显示缓冲区配置:画面流畅的关键
显示缓冲区,说白了就是一块内存,LVGL先把画面画到这块内存里,然后通过你的驱动刷到屏幕上。缓冲区怎么配,直接影响画面流畅度。
3.4.1 三种缓冲模式
| 模式 | 缓冲区大小 | 特点 | 适用场景 |
|---|---|---|---|
| 单缓冲 | 1个全屏 | 简单,但会有撕裂 | 低要求、低刷新率 |
| 双缓冲 | 2个全屏 | 无撕裂,流畅 | 大部分项目 |
| 部分缓冲 | 几行到半屏 | 省内存,但刷新慢 | 内存紧张时 |
我个人最推荐双缓冲。虽然多占一份内存,但画面是真的丝滑。你想想看,用户按个按钮,界面卡一下,那体验多糟糕。
3.4.2 配置代码示例
在 lv_port_disp.c 里,你会看到类似这样的配置:
/* 定义两个全屏缓冲区 */
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf_1[LV_HOR_RES_MAX * LV_VER_RES_MAX];
static lv_color_t buf_2[LV_HOR_RES_MAX * LV_VER_RES_MAX];
/* 初始化显示缓冲区 */
void lv_port_disp_init(void)
{
/* 设置为双缓冲模式 */
lv_disp_draw_buf_init(&draw_buf, buf_1, buf_2,
LV_HOR_RES_MAX * LV_VER_RES_MAX);
/* 注册显示驱动 */
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &draw_buf;
disp_drv.flush_cb = my_display_flush; /* 你的刷屏函数 */
disp_drv.hor_res = LV_HOR_RES_MAX;
disp_drv.ver_res = LV_VER_RES_MAX;
lv_disp_drv_register(&disp_drv);
}
3.5 输入设备驱动注册:让触摸屏和按键活起来
输入设备包括触摸屏、物理按键、编码器、鼠标等。LVGL把它们统一抽象成「输入设备」,你只需要注册一个读取函数就行。
3.5.1 注册流程
在 lv_port_indev.c 里,核心代码就这几行:
static lv_indev_drv_t indev_drv;
void lv_port_indev_init(void)
{
/* 初始化输入设备驱动 */
lv_indev_drv_init(&indev_drv);
/* 指定设备类型:LV_INDEV_TYPE_POINTER 是触摸屏 */
indev_drv.type = LV_INDEV_TYPE_POINTER;
/* 注册读取函数——LVGL会周期性调用它 */
indev_drv.read_cb = my_touchpad_read;
/* 注册设备 */
lv_indev_drv_register(&indev_drv);
}
/* 你的触摸读取函数 */
void my_touchpad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
/* 读取触摸坐标 */
int16_t x, y;
uint8_t pressed = touchpad_get_xy(&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.2 设备类型一览
| 类型 | 说明 | 典型硬件 |
|---|---|---|
LV_INDEV_TYPE_POINTER |
触摸屏/鼠标 | 电阻屏、电容屏 |
LV_INDEV_TYPE_KEYPAD |
矩阵键盘 | 4x4按键矩阵 |
LV_INDEV_TYPE_ENCODER |
旋转编码器 | EC11旋钮 |
LV_INDEV_TYPE_BUTTON |
独立按键 | GPIO直连按键 |
read_cb 里绝对不能加阻塞延时。正确的做法是:在外部用定时器或DMA读取触摸数据,read_cb 只负责把最新数据交给LVGL。
3.6 小结:移植其实就这几件事
嗯,到这里,LVGL的移植骨架就搭完了。你回头看看,其实就干了三件事:
- 配好 lv_conf.h —— 告诉LVGL你的屏幕多大、颜色多深、内存给多少
- 写好显示驱动 —— 把LVGL画好的缓冲区刷到屏幕上
- 写好输入驱动 —— 把触摸或按键数据喂给LVGL
剩下的,就是LVGL自己的事了。你只管调用API创建控件,它帮你画、帮你响应触摸、帮你做动画。说白了,LVGL就是个「高级打工仔」,你给它配好干活的环境,它就能帮你把界面撑起来。