4、显示驱动移植详解:配置显示缓冲区、实现flush回调函数、处理颜色格式转换、优化刷新性能
显示驱动移植,说白了就是让 LVGL 能跟你的屏幕「说上话」。很多初学者卡在这一步,觉得代码跑飞了、花屏了、刷新慢得像蜗牛。其实没那么玄乎——你只要搞定三件事:缓冲区怎么配、数据怎么送、颜色怎么转。今天我就把这套流程掰开揉碎讲清楚。
核心要点:显示驱动移植 = 配置缓冲区 + 实现 flush 回调 + 处理颜色格式 + 优化刷新性能。四步走完,屏幕就能跑起来。
4.1 显示缓冲区配置:选对策略,性能翻倍
LVGL 的显示缓冲区,是它跟底层驱动之间的「中转站」。你想想看,如果每次画一个像素都直接操作硬件,那效率得多低?所以 LVGL 先把内容画到内存里,然后一次性刷到屏幕。
缓冲区有三种配置方式,我直接给你列个表对比:
| 配置方式 | 内存占用 | 刷新性能 | 适用场景 |
|---|---|---|---|
| 单缓冲区 | 1 × 屏幕像素数 × 字节/像素 | 低(有撕裂风险) | 内存极小的 MCU(如 STM32F103 8KB RAM) |
| 双缓冲区 | 2 × 屏幕像素数 × 字节/像素 | 高(无撕裂) | 内存充足的平台(如 ESP32、Linux) |
| 部分缓冲区 | 可自定义(如 1/10 屏幕大小) | 中等(刷新次数增多) | 内存有限但需要复杂 UI 的场景 |
我个人习惯在项目初期先用双缓冲区。为什么?因为调试阶段最怕花屏,双缓冲能保证画面完整。等性能调优时再考虑换成部分缓冲区来省内存。
代码配置很简单,就这几行:
/* 定义缓冲区大小:假设屏幕是 320x240,颜色深度 16bit */
#define MY_DISP_HOR_RES 320
#define MY_DISP_VER_RES 240
#define MY_DISP_BUF_SIZE (MY_DISP_HOR_RES * MY_DISP_VER_RES)
/* 分配内存 */
static lv_color_t buf1[MY_DISP_BUF_SIZE];
static lv_color_t buf2[MY_DISP_BUF_SIZE];
/* 初始化缓冲区 */
static lv_disp_draw_buf_t draw_buf;
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, MY_DISP_BUF_SIZE);
小技巧:如果 RAM 实在紧张,可以把 buf2 设为 NULL,LVGL 会自动退化为单缓冲区模式。但记得开启垂直同步(VSync)来减少撕裂。
4.2 实现 flush 回调函数:数据怎么送到屏幕?
flush 回调是显示驱动的「最后一公里」。LVGL 画完一帧后,会调用这个函数,把缓冲区里的数据刷到屏幕上。
函数签名长这样:
static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
三个参数分别代表:显示驱动句柄、需要刷新的矩形区域、像素数据指针。你只需要把 color_p 指向的数据,按 area 指定的坐标写入屏幕即可。
我拿一个常见的 ILI9341 驱动举例:
static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
/* 设置屏幕写入窗口 */
ili9341_set_window(area->x1, area->y1, area->x2, area->y2);
/* 计算像素数量 */
uint32_t size = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1);
/* 通过 SPI/DMA 发送数据 */
ili9341_write_data((uint8_t *)color_p, size * sizeof(lv_color_t));
/* 通知 LVGL 刷新完成 */
lv_disp_flush_ready(disp_drv);
}
注意最后一行 lv_disp_flush_ready(),这个必须调用。我曾经见过有人忘了加,结果屏幕死活不刷新——LVGL 以为驱动还在忙,就一直等着。
避坑指南:flush 回调里不要做复杂计算或延时。它应该在尽可能短的时间内返回,否则会阻塞 LVGL 的渲染循环。如果硬件传输慢(比如 SPI 时钟只有 10MHz),建议用 DMA + 中断的方式异步处理。
4.3 颜色格式转换:别让颜色「失真」
颜色格式不匹配,是花屏的头号元凶。LVGL 内部默认用 lv_color_t 表示颜色,但这个类型的大小取决于你配置的 LV_COLOR_DEPTH。
常见的颜色深度有:
- 16 位(RGB565):最常用,占 2 字节,红 5 位 + 绿 6 位 + 蓝 5 位
- 32 位(ARGB8888):占 4 字节,带透明度通道
- 8 位(L8):索引色,占 1 字节,需要调色板
如果你的屏幕硬件支持 RGB565,但 LVGL 配置成了 32 位,那每次刷新都要做格式转换,性能会大打折扣。反过来,如果屏幕只支持 RGB565,LVGL 也配成 16 位,那就完美匹配,零开销。
但现实往往没那么理想。比如你的屏幕是 RGB565,但 UI 需要透明度效果(比如圆角、阴影),那就得用 32 位。这时候你需要在 flush 回调里做转换:
static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
uint32_t size = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1);
uint16_t * buf_rgb565 = malloc(size * 2); /* 临时缓冲区 */
/* 逐像素转换:ARGB8888 -> RGB565 */
for (uint32_t i = 0; i < size; i++) {
lv_color32_t * c32 = (lv_color32_t *)&color_p[i];
uint8_t r = c32->ch.red >> 3; /* 8 位转 5 位 */
uint8_t g = c32->ch.green >> 2; /* 8 位转 6 位 */
uint8_t b = c32->ch.blue >> 3; /* 8 位转 5 位 */
buf_rgb565[i] = (r << 11) | (g << 5) | b;
}
/* 发送转换后的数据 */
ili9341_write_data((uint8_t *)buf_rgb565, size * 2);
free(buf_rgb565);
lv_disp_flush_ready(disp_drv);
}
优化建议:如果转换是必须的,尽量用查表法代替移位运算。比如把 8 位红色值到 5 位红色值的映射做成一个 256 字节的表,能省掉不少 CPU 周期。
4.4 优化刷新性能:让屏幕「飞」起来
性能优化是显示驱动移植的进阶话题。我总结了几个实战中验证过的技巧:
- 使用 DMA 传输:SPI 或并行接口的数据传输,交给 DMA 去做,CPU 可以继续处理 LVGL 的渲染任务。我在一个 STM32F429 项目里,用 DMA 后刷新率从 15fps 提到了 45fps。
- 开启 LVGL 的「直接模式」:配置
lv_disp_drv_t.direct_mode = 1,让 LVGL 直接往帧缓冲区里画,省去一次内存拷贝。 - 减少刷新区域:LVGL 默认只刷新变化的部分。如果你的 flush 回调每次都刷全屏,那就白费了这个优化。检查一下
area参数,只刷必要的区域。 - 调整缓冲区大小:部分缓冲区模式下,缓冲区越大,刷新次数越少。我一般取屏幕高度的 1/10 到 1/4 作为缓冲区行数,效果不错。
下面这张图展示了显示驱动移植的完整流程:
4.5 实战经验:一个踩过的坑
我曾经在一个项目里,屏幕是 800x480 的 RGB 接口屏,LVGL 配了双缓冲,但刷新率始终上不去。排查了半天,发现是 DMA 传输完成中断的优先级设得太低,被其他外设中断频繁打断。每次传输都要等好几个毫秒才能拿到「传输完成」的信号。
解决办法很简单:把 DMA 中断优先级提到最高,同时开启 FIFO 模式减少中断次数。改完之后,刷新率从 20fps 直接飙到 55fps。
嗯,这里要注意:中断优先级不是越高越好。如果 DMA 中断抢占了系统滴答定时器,会导致 LVGL 的定时器任务跑偏。我建议 DMA 中断优先级设为次高,留一级给系统时钟。
总结一下:显示驱动移植的核心就四步——配缓冲区、写 flush、转颜色、调性能。每一步都有坑,但只要你按流程来,先让画面亮起来,再逐步优化,就不会走弯路。