2. lv_disp_drv_t 结构体深度解析:结构体成员详解、回调函数注册、缓冲区配置

上一章我们聊了 LVGL 显示接口的整体架构。今天,咱们把目光聚焦到最核心的一个结构体——lv_disp_drv_t 上。

说白了,这个结构体就是 LVGL 和你的硬件屏幕之间的「翻译官」。你告诉它屏幕长什么样、怎么刷新、用多大内存,它就能帮你把 LVGL 内部的绘图命令,变成屏幕上能看到的像素。

我刚开始接触 LVGL 时,觉得这个结构体成员太多,看着就头疼。后来踩了几个坑才明白——其实核心就三块:结构体成员、回调函数、缓冲区配置。今天咱们一个一个拆开讲。

2.1 结构体成员详解

先看一张总览图,把 lv_disp_drv_t 的成员按功能分组,心里有个谱。

lv_disp_drv_t 结构体成员分组 核心参数 • hor_res / ver_res • physical_hor_res / ver • offset_x / offset_y • rotated • color_depth • screen_transp 回调函数 • flush_cb • rounder_cb • set_px_cb • monitor_cb • clean_dcache_cb • user_data 缓冲区配置 • draw_buf • buf1 / buf2 • buf_size_byte • full_refresh • direct_mode • antialiasing 提示:这三个分组是配置显示驱动的核心,缺一不可。 实际项目中,90% 的问题都出在回调函数和缓冲区配置上。

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 会认为刷新还没完成,不会开始下一帧的绘制。我见过有人忘了加这行,结果画面卡死。