2. lv_disp_drv_t 结构体深度解析:结构体成员详解、回调函数注册、缓冲区配置
上一章我们聊了 LVGL 显示接口的整体架构。今天,咱们把目光聚焦到最核心的一个结构体——lv_disp_drv_t 上。
说白了,这个结构体就是 LVGL 和你的硬件屏幕之间的「翻译官」。你告诉它屏幕长什么样、怎么刷新、用多大内存,它就能帮你把 LVGL 内部的绘图命令,变成屏幕上能看到的像素。
我刚开始接触 LVGL 时,觉得这个结构体成员太多,看着就头疼。后来踩了几个坑才明白——其实核心就三块:结构体成员、回调函数、缓冲区配置。今天咱们一个一个拆开讲。
2.1 结构体成员详解
先看一张总览图,把 lv_disp_drv_t 的成员按功能分组,心里有个谱。
2.1.1 核心参数
这部分告诉 LVGL 你的屏幕物理规格。我见过不少新手在这里填错,导致画面偏移或撕裂。
| 成员名 | 类型 | 说明 |
|---|---|---|
hor_res |
lv_coord_t |
水平分辨率(像素)。比如 480 |
ver_res |
lv_coord_t |
垂直分辨率(像素)。比如 320 |
physical_hor_res |
lv_coord_t |
物理水平分辨率。通常和 hor_res 一样,旋转时有用 |
physical_ver_res |
lv_coord_t |
物理垂直分辨率 |
offset_x |
lv_coord_t |
X 轴偏移。用于双屏或部分显示场景 |
offset_y |
lv_coord_t |
Y 轴偏移 |
rotated |
uint32_t |
旋转标志。0=正常,1=90°,2=180°,3=270° |
color_depth |
uint8_t |
颜色深度。常见 16、32 |
screen_transp |
uint8_t |
屏幕是否支持透明。1=支持,0=不支持 |
我的习惯: 我会把
hor_res 和 ver_res 定义成宏,方便后期移植。比如 #define MY_LCD_HOR 480,这样换屏幕时改一处就行。
2.1.2 回调函数
这部分是驱动和 LVGL 的「握手协议」。LVGL 画完一帧后,会调用你注册的回调函数,把数据送到屏幕上。
我重点说几个常用的:
flush_cb:最核心的回调。LVGL 把绘制好的缓冲区数据传给你,你需要把数据刷到屏幕上。这个函数必须实现。rounder_cb:坐标对齐回调。如果你的屏幕只能按 2 的倍数刷新(比如某些 OLED),可以用这个回调把坐标对齐。set_px_cb:逐像素设置回调。用于没有帧缓冲的极简场景,性能较差,不推荐。monitor_cb:性能监控回调。每帧刷新完成后调用,可以统计 FPS、CPU 占用等。clean_dcache_cb:清理 CPU 数据缓存。如果你用 DMA2D 或 GPU,这个回调很重要。user_data:用户自定义数据指针。可以传一些上下文信息给回调函数。
我曾经踩过的坑: 在 STM32F429 上做项目时,忘了注册
clean_dcache_cb,结果画面一直有残影。折腾了两天才发现是 DMA2D 和 CPU 缓存不一致导致的。从那以后,只要用 DMA,我必注册这个回调。
2.1.3 缓冲区配置
缓冲区配置是性能的关键。LVGL 支持多种缓冲模式,我建议你根据硬件资源来选择。
| 成员名 | 类型 | 说明 |
|---|---|---|
draw_buf |
lv_draw_buf_t * |
绘图缓冲区指针(v9 新接口) |
buf1 |
void * |
缓冲区 1 地址(v8 旧接口) |
buf2 |
void * |
缓冲区 2 地址(双缓冲时使用) |
buf_size_byte |
uint32_t |
缓冲区大小(字节) |
full_refresh |
uint32_t |
是否全屏刷新。1=每次刷新整个屏幕 |
direct_mode |
uint32_t |
直接模式。1=LVGL 直接写入帧缓冲 |
antialiasing |
uint32_t |
抗锯齿。1=开启,0=关闭 |
2.2 回调函数注册
注册回调函数其实很简单,就是给结构体成员赋值。但要注意顺序——先初始化结构体,再注册回调。
/* 1. 定义显示驱动结构体 */
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv); // 先初始化,把成员设为默认值
/* 2. 设置分辨率 */
disp_drv.hor_res = 480;
disp_drv.ver_res = 320;
/* 3. 注册回调函数 */
disp_drv.flush_cb = my_flush_callback;
disp_drv.rounder_cb = my_rounder_callback; // 可选
disp_drv.monitor_cb = my_monitor_callback; // 可选
/* 4. 配置缓冲区 */
static lv_color_t buf1[480 * 10]; // 10 行缓冲区
disp_drv.draw_buf = &draw_buf;
lv_draw_buf_init(&draw_buf, 480, 10, LV_COLOR_FORMAT_RGB565);
/* 5. 注册显示驱动 */
lv_disp_t * disp = lv_disp_drv_register(&disp_drv);
关键点:
lv_disp_drv_init() 会把所有成员设为默认值。如果你不调用它,有些成员可能是随机值,导致驱动工作异常。我习惯在定义结构体后立刻调用它。
2.3 缓冲区配置
缓冲区配置直接影响刷新率和内存占用。LVGL 支持三种缓冲模式,我分别说一下:
2.3.1 单缓冲模式
只有一个缓冲区。LVGL 绘制时,屏幕可能显示一半新数据一半旧数据,造成撕裂。适合对画面要求不高的场景。
static lv_color_t buf[LV_HOR_RES_MAX * 10];
lv_draw_buf_init(&draw_buf, LV_HOR_RES_MAX, 10, LV_COLOR_FORMAT_RGB565);
disp_drv.draw_buf = &draw_buf;
2.3.2 双缓冲模式
两个缓冲区交替使用。一个用于 LVGL 绘制,另一个用于屏幕刷新。没有撕裂,性能最好,但内存占用翻倍。
static lv_color_t buf1[LV_HOR_RES_MAX * 10];
static lv_color_t buf2[LV_HOR_RES_MAX * 10];
lv_draw_buf_init(&draw_buf, LV_HOR_RES_MAX, 10, LV_COLOR_FORMAT_RGB565);
lv_draw_buf_set_buf(&draw_buf, buf1, buf2); // 设置双缓冲
2.3.3 全屏缓冲模式
缓冲区大小等于整个屏幕。LVGL 绘制完成后一次性刷新。内存占用最大,但刷新逻辑最简单。
static lv_color_t buf[LV_HOR_RES_MAX * LV_VER_RES_MAX];
lv_draw_buf_init(&draw_buf, LV_HOR_RES_MAX, LV_VER_RES_MAX, LV_COLOR_FORMAT_RGB565);
我的建议: 如果内存够用,优先选双缓冲模式。如果内存紧张,单缓冲 + 垂直同步也能接受。全屏缓冲模式除非屏幕很小(比如 128x64),否则不推荐。
2.4 完整配置示例
最后,给一个完整的配置示例。这是我个人项目里常用的模板,你可以直接拿来改。
/* 定义缓冲区 */
#define MY_HOR_RES 480
#define MY_VER_RES 320
#define MY_BUF_LINES 10
static lv_color_t buf1[MY_HOR_RES * MY_BUF_LINES];
static lv_color_t buf2[MY_HOR_RES * MY_BUF_LINES];
static lv_draw_buf_t draw_buf;
/* flush 回调实现 */
static void my_flush_callback(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p)
{
/* 将 color_p 指向的数据刷到屏幕 area 区域 */
lcd_draw_bitmap(area->x1, area->y1, area->x2 - area->x1 + 1, area->y2 - area->y1 + 1, color_p);
lv_disp_flush_ready(drv); // 通知 LVGL 刷新完成
}
/* 驱动初始化 */
void my_display_init(void)
{
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
/* 分辨率 */
disp_drv.hor_res = MY_HOR_RES;
disp_drv.ver_res = MY_VER_RES;
/* 回调 */
disp_drv.flush_cb = my_flush_callback;
/* 缓冲区 */
lv_draw_buf_init(&draw_buf, MY_HOR_RES, MY_BUF_LINES, LV_COLOR_FORMAT_RGB565);
lv_draw_buf_set_buf(&draw_buf, buf1, buf2); // 双缓冲
disp_drv.draw_buf = &draw_buf;
/* 注册 */
lv_disp_drv_register(&disp_drv);
}
注意: 在
flush_cb 中,一定要在数据传输完成后调用 lv_disp_flush_ready(drv)。否则 LVGL 会认为刷新还没完成,不会开始下一帧的绘制。我见过有人忘了加这行,结果画面卡死。