2. LVGL 显示驱动与基础框架
好,咱们正式开始动手了。这一章的目标很明确——在 STM32 上把 LVGL 跑起来,看到第一个 Demo 界面。说白了,就是打通显示驱动和输入设备这两条路,让 LVGL 能画、你能点。
我刚开始接触 LVGL 时,觉得移植是个大工程。后来发现,只要把显示驱动和触摸驱动的接口搞清楚,剩下的就是配置几个宏的事。嗯,咱们一步步来。
2.1 LVGL 显示驱动原理
LVGL 的显示驱动,核心是一个叫 flush_cb 的回调函数。你想想看,LVGL 内部维护了一个显存缓冲区,每次绘制完成后,它会调用这个回调,把缓冲区的内容刷到物理屏幕上。
核心流程:
- LVGL 绘制内容到
lv_disp_drv_t指定的缓冲区 - 缓冲区填满后,触发
flush_cb回调 - 回调函数将缓冲区数据通过 SPI/并行接口发送到 LCD
- 发送完成后调用
lv_disp_flush_ready()通知 LVGL 继续绘制
我在项目中遇到过一个问题:如果忘记调用 lv_disp_flush_ready(),屏幕会一直卡在第一帧。这个坑踩过一次就再也不会忘了。
2.2 Framebuffer 配置
LVGL 支持三种缓冲区模式,我习惯根据硬件资源来选择:
| 模式 | 缓冲区大小 | 特点 | 适用场景 |
|---|---|---|---|
| 单缓冲 | 1/10 屏幕大小 | 内存占用小,但可能有撕裂 | RAM 紧张的 MCU |
| 双缓冲 | 1/6 屏幕大小 × 2 | 无撕裂,性能均衡 | 大多数 STM32 项目 |
| 全屏缓冲 | 全屏大小 | 性能最好,但吃内存 | 带外部 SRAM 的板子 |
我的建议:STM32F4 系列,如果屏幕分辨率不超过 320×240,用双缓冲模式。颜色深度选 16 位(RGB565),一个缓冲区只需要 320×240×2 = 153.6KB,两个加起来约 307KB,大部分 F4 都能扛住。
2.3 显示接口实现
以 STM32F429 + ILI9341 为例,显示驱动的核心代码就这几步:
// 1. 定义缓冲区
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf_1[MY_DISP_HOR_RES * 100];
static lv_color_t buf_2[MY_DISP_HOR_RES * 100];
// 2. 初始化显示驱动
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = 320;
disp_drv.ver_res = 240;
disp_drv.flush_cb = my_flush_callback;
disp_drv.draw_buf = &draw_buf;
// 3. 注册驱动
lv_disp_t * disp = lv_disp_drv_register(&disp_drv);
// 4. 实现 flush 回调
static void my_flush_callback(lv_disp_drv_t * drv,
const lv_area_t * area,
lv_color_t * color_p) {
// 将 color_p 中的数据通过 SPI 发送到 LCD
LCD_SetWindow(area->x1, area->y1, area->x2, area->y2);
LCD_WriteDataMultiple(color_p, lv_area_get_size(area));
// 重要:通知 LVGL 刷新完成
lv_disp_flush_ready(drv);
}
注意:SPI 发送数据时,如果使用 DMA,一定要在 DMA 传输完成中断中调用 lv_disp_flush_ready()。我曾经在 DMA 还没传完时就调用了这个函数,结果屏幕出现花屏——因为 LVGL 以为缓冲区可以复用了,实际上数据还在路上。
2.4 触摸/输入设备驱动
触摸驱动的原理和显示驱动类似,也是注册一个回调函数。LVGL 会定期调用这个回调,读取触摸坐标。
// 1. 初始化输入设备驱动
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;
// 2. 注册驱动
lv_indev_t * indev = lv_indev_drv_register(&indev_drv);
// 3. 实现读取回调
static void my_touchpad_read(lv_indev_drv_t * drv, lv_indev_data_t * data) {
static lv_coord_t last_x = 0;
static lv_coord_t last_y = 0;
if (touch_pressed()) {
data->point.x = get_touch_x();
data->point.y = get_touch_y();
data->state = LV_INDEV_STATE_PRESSED;
last_x = data->point.x;
last_y = data->point.y;
} else {
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_RELEASED;
}
}
这里有个细节:触摸坐标可能需要做旋转或镜像映射。比如你的屏幕是横屏,但触摸芯片默认是竖屏坐标。我习惯在 read_cb 里做一次坐标变换,而不是改硬件配置。
2.5 LVGL 初始化流程
整个初始化流程,我总结了一个清晰的顺序:
- 硬件初始化:时钟、GPIO、SPI/I2C、DMA
- LCD 初始化:发送初始化序列,设置显示方向
- 触摸初始化:复位触摸芯片,校准(如果需要)
- LVGL 核心初始化:调用
lv_init() - 配置显示缓冲区:分配并初始化
lv_disp_draw_buf_t - 注册显示驱动:设置分辨率、颜色深度、flush 回调
- 注册输入驱动:设置类型、读取回调
- 创建 UI:创建第一个界面元素
- 进入主循环:周期性调用
lv_timer_handler()
主循环的典型写法:
while(1) {
lv_timer_handler(); // 处理 LVGL 任务
my_delay_ms(5); // 适当延时,避免 CPU 满载
}
2.6 在 STM32 上跑通第一个 Demo
好了,理论说完了,咱们直接上代码。下面是一个完整的 main.c 骨架:
#include "lvgl.h"
#include "lcd_driver.h"
#include "touch_driver.h"
#define DISP_BUF_SIZE (320 * 100)
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf_1[DISP_BUF_SIZE];
static lv_color_t buf_2[DISP_BUF_SIZE];
int main(void) {
// 硬件初始化
HAL_Init();
SystemClock_Config();
LCD_Init();
Touch_Init();
// LVGL 初始化
lv_init();
// 显示缓冲区初始化
lv_disp_draw_buf_init(&draw_buf, buf_1, buf_2, DISP_BUF_SIZE);
// 显示驱动注册
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = 320;
disp_drv.ver_res = 240;
disp_drv.flush_cb = lcd_flush_callback;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register(&disp_drv);
// 触摸驱动注册
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touch_read_callback;
lv_indev_drv_register(&indev_drv);
// 创建第一个界面:一个简单的按钮
lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 120, 50);
lv_obj_center(btn);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "Hello LVGL!");
lv_obj_center(label);
// 主循环
while(1) {
lv_timer_handler();
HAL_Delay(5);
}
}
这段代码跑通后,你会在屏幕上看到一个居中的按钮,上面写着 "Hello LVGL!"。嗯,这就是你的第一个 LVGL Demo。
2.7 本章知识结构图
下面这张图总结了 LVGL 显示驱动与基础框架的完整链路:
2.8 常见问题与避坑
- 屏幕不亮:先检查硬件初始化序列是否正确。我遇到过 LCD 的复位引脚没拉高,导致屏幕一直处于复位状态。
- 花屏:大概率是缓冲区大小不对,或者 DMA 传输时序有问题。试试降低 SPI 时钟频率。
- 触摸没反应:检查 I2C 地址是否正确。有些触摸芯片的地址可以通过引脚电平配置,和 datasheet 上的默认值不一样。
- 界面卡顿:看看
lv_timer_handler()的调用频率。我建议每 5-10ms 调用一次,太频繁反而会增加开销。
调试小技巧:在 flush_cb 里加一个 GPIO 翻转,用示波器看波形。如果翻转频率稳定,说明 LVGL 的绘制节奏是正常的。我曾经用这个方法定位到一个 DMA 优先级配置错误的问题。
好了,这一章的内容就到这里。你手头如果有 STM32 开发板,现在就可以把代码烧进去试试。看到屏幕上出现按钮的那一刻,你会觉得前面的配置都是值得的。