3、LVGL显示驱动与触摸驱动:显示驱动接口实现(flush_cb)、触摸驱动接口实现(read_cb)、双缓冲机制、帧率优化

驱动层是LVGL和硬件之间的桥梁。说白了,没有驱动,你的界面再漂亮也出不来。

这一章我们聚焦四个核心点:显示驱动的 flush_cb、触摸驱动的 read_cb、双缓冲机制,以及帧率优化。我会结合自己踩过的坑,把每个环节讲透。

核心认知:LVGL本身不直接操作硬件,它通过回调函数与驱动交互。你只需要实现这几个回调,剩下的刷新、事件分发都由框架搞定。

3.1 显示驱动接口:flush_cb

flush_cb 是显示驱动的灵魂。每次LVGL需要更新屏幕上的某个区域时,就会调用这个函数,把像素数据送过去。

3.1.1 接口原型

static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
  • disp_drv:显示驱动结构体指针,里面存着你的驱动上下文。
  • area:需要刷新的矩形区域,包含 x1/y1/x2/y2。
  • color_p:像素数据缓冲区指针,LVGL已经把颜色算好了,你只管送。

3.1.2 实现要点

我在项目里用过几种常见的硬件接口,这里以SPI屏为例,给你看一个完整的实现:

static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    int32_t x, y;
    uint16_t * buf = (uint16_t *)color_p;

    /* 设置显示窗口 */
    LCD_SetWindow(area->x1, area->y1, area->x2, area->y2);

    /* 逐行发送数据 */
    for(y = area->y1; y <= area->y2; y++) {
        LCD_WriteDataMultiple(&buf[(y - area->y1) * (area->x2 - area->x1 + 1)],
                              (area->x2 - area->x1 + 1) * 2);
    }

    /* 通知LVGL刷新完成 */
    lv_disp_flush_ready(disp_drv);
}
注意:一定要在数据发送完成后调用 lv_disp_flush_ready()。如果不调用,LVGL会一直等待,整个界面卡死。我曾经因为这个函数漏调,排查了整整一个下午。

3.2 触摸驱动接口:read_cb

触摸驱动比显示驱动简单一些。LVGL只需要知道:当前有没有触摸?触摸点在哪里?

3.2.1 接口原型

static void my_touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
  • indev_drv:输入设备驱动结构体。
  • data:存放触摸状态和坐标的结构体。

3.2.2 实现示例

static void my_touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
    static int16_t last_x = 0, last_y = 0;
    uint8_t touch_pressed;
    uint16_t touch_x, touch_y;

    /* 读取触摸控制器 */
    Touch_Read(&touch_pressed, &touch_x, &touch_y);

    if(touch_pressed) {
        data->point.x = touch_x;
        data->point.y = touch_y;
        data->state = LV_INDEV_STATE_PR;
        last_x = touch_x;
        last_y = touch_y;
    } else {
        data->point.x = last_x;
        data->point.y = last_y;
        data->state = LV_INDEV_STATE_REL;
    }
}
小技巧:触摸坐标可能需要做校准。我习惯在初始化时读取触摸控制器的校准参数,或者直接写死一组偏移值。如果你的屏幕触摸不准,先检查坐标转换公式。

3.3 双缓冲机制

双缓冲是提升显示流畅度的关键。没有它,屏幕会出现撕裂、闪烁。

3.3.1 什么是双缓冲?

简单说,就是准备两个缓冲区。一个用于LVGL绘制,另一个用于硬件刷新。两者交替工作,互不干扰。

双缓冲工作流程 缓冲区 A 缓冲区 B LVGL 绘制引擎 硬件刷新引擎 写入 读取 交换 绘制和刷新并行工作,互不阻塞,帧率翻倍

3.3.2 配置方法

在 lv_disp_drv_t 中设置:

static lv_disp_drv_t disp_drv;
static lv_color_t buf_1[MY_DISP_HOR_RES * 40];
static lv_color_t buf_2[MY_DISP_HOR_RES * 40];

lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = my_flush_cb;
disp_drv.buffer->buf1 = buf_1;
disp_drv.buffer->buf2 = buf_2;
disp_drv.buffer->size = MY_DISP_HOR_RES * 40;
lv_disp_drv_register(&disp_drv);
缓冲模式 缓冲区数量 内存占用 流畅度 适用场景
单缓冲 1 低 低(可能撕裂) 低分辨率、静态界面
部分双缓冲 2(小尺寸) 中 中 大多数嵌入式项目
全双缓冲 2(全屏) 高 高 高性能、动画密集
我的经验:对于320x240的屏幕,用两个40行高的缓冲区就够了。全屏双缓冲虽然流畅,但内存开销太大,很多MCU扛不住。

3.4 帧率优化

帧率直接决定用户体验。60fps和30fps的差别,你滑动一下列表就能感受到。

3.4.1 影响帧率的因素

  • 缓冲区大小:缓冲区越大,单次刷新的数据量越大,但刷新次数减少。
  • 硬件接口速度:SPI、I2C、并行接口的吞吐量差异很大。
  • LVGL刷新策略:全屏刷新 vs 局部刷新。
  • CPU负载:绘制复杂图形时,CPU占用率飙升。

3.4.2 优化手段

1. 调整刷新周期

lv_disp_drv_t disp_drv;
disp_drv.refr_period = 16;  /* 约60fps,单位ms */

2. 启用局部刷新

LVGL默认只刷新变化区域。如果你的驱动支持,可以关闭全屏刷新:

disp_drv.full_refresh = 0;  /* 0=局部刷新,1=全屏刷新 */

3. 优化flush_cb实现

  • 使用DMA传输数据,释放CPU。
  • 减少每次传输的数据量,分多次发送。
  • 避免在flush_cb中做复杂计算。
避坑:我曾经为了追求极致帧率,把缓冲区设得特别大,结果内存溢出,系统频繁重启。后来改用部分双缓冲 + DMA,帧率稳定在55fps左右。记住:不是缓冲区越大越好,够用就行。

3.4.3 实测帧率方法

LVGL内置了帧率监控功能:

lv_disp_t * disp = lv_disp_get_default();
lv_theme_t * th = lv_theme_default_init(disp, lv_palette_main(LV_PALETTE_BLUE),
                                         lv_palette_main(LV_PALETTE_RED),
                                         LV_THEME_DEFAULT_DARK, NULL);
lv_disp_set_theme(disp, th);

/* 在初始化后调用 */
lv_disp_t * d = lv_disp_get_default();
LV_LOG_USER("Current FPS: %d", lv_disp_get_fps(d));

或者直接在终端看日志:LVGL会定期打印帧率信息。

总结一下:显示驱动和触摸驱动是LVGL的底层基石。双缓冲解决撕裂问题,帧率优化解决流畅问题。我建议你先跑通单缓冲,再逐步升级到双缓冲,每一步都验证效果,不要一口气全上。