4. 缓冲区配置:深入理解LVGL的双缓冲与单缓冲机制,配置最优的缓冲区大小
说到动画卡顿,十有八九跟缓冲区配置有关。我这些年排查过的LVGL性能问题里,缓冲区设置不当占了将近一半。说白了,缓冲区就是显示驱动和LVGL图形引擎之间的「中转仓库」——仓库太小,货卸不下来;仓库太大,内存又扛不住。
这一章咱们就把单缓冲、双缓冲、局部刷新这些概念彻底讲透。我会结合自己踩过的坑,告诉你到底该怎么配这个缓冲区大小。
4.1 单缓冲机制:最省内存,但最容易卡
单缓冲模式下,LVGL只分配一块缓冲区。绘制和刷新共用同一块内存。流程是这样的:
- LVGL把UI内容绘制到缓冲区
- 绘制完成后,调用
flush_cb把缓冲区数据发送到显示设备 - 在刷新的这段时间里,LVGL不能做任何绘制操作——它得等刷新完成
你想想看,如果刷新一帧需要16ms,那这16ms里CPU就干等着。动画帧率直接砍半。我在一个智能家居面板项目里就吃过这个亏——界面切换动画总是跳帧,查了半天才发现是单缓冲模式下刷新等待时间太长。
flush_cb执行时间超过一帧的绘制时间,帧率会直接腰斩。这是单缓冲最致命的短板。
4.2 双缓冲机制:流畅动画的基石
双缓冲就好多了。它分配两块缓冲区:一块用于绘制,一块用于刷新。两块轮流切换:
- LVGL往缓冲区A绘制内容
- 绘制完成后,把缓冲区A交给
flush_cb去刷新 - 同时LVGL立刻开始往缓冲区B绘制下一帧
- 刷新完成后,两块缓冲区角色互换
这样一来,绘制和刷新就流水线化了。CPU不用干等,利用率拉满。我做过一个60fps的仪表盘动画,双缓冲模式下CPU占用率才30%出头,换成单缓冲直接飙到70%以上。
4.3 局部刷新与全屏刷新
LVGL支持两种刷新策略:
| 刷新策略 | 缓冲区大小 | 适用场景 | 内存占用 |
|---|---|---|---|
| 全屏刷新 | ≥ 屏幕分辨率 × 像素深度 | 高帧率动画、视频播放 | 极高 |
| 局部刷新 | ≥ 屏幕行数 × 宽度 × 像素深度 | 静态界面、低帧率更新 | 较低 |
局部刷新只更新屏幕上的脏区域。缓冲区可以很小——比如只有10行像素的高度。LVGL会逐行扫描脏区域,每次只刷新一小块。嗯,这里要注意:局部刷新对flush_cb的实现要求更高,你得支持部分区域刷新,不能每次都整屏刷。
我遇到过最典型的坑是:有人把局部刷新缓冲区设成1行像素,结果flush_cb每次只刷1行,刷完整个屏幕要几百次调用,性能反而更差。所以缓冲区大小不是越小越好。
4.4 缓冲区大小的黄金法则
到底配多大?我总结了一套经验公式:
// 缓冲区大小计算公式
// 单缓冲最小:屏幕一行像素
#define MIN_SINGLE_BUF (LV_HOR_RES_MAX * LV_VER_RES_MAX * LV_COLOR_DEPTH / 8)
// 双缓冲推荐:1/10 到 1/4 屏幕大小
#define RECOMMEND_DOUBLE_BUF (LV_HOR_RES_MAX * LV_VER_RES_MAX * LV_COLOR_DEPTH / 8 / 4)
// 局部刷新推荐:10~20行像素
#define PARTIAL_BUF_SIZE (LV_HOR_RES_MAX * 20 * LV_COLOR_DEPTH / 8)
实际项目中我一般这样选:
- 内存充足(≥512KB):双缓冲,每块缓冲区取1/4屏幕大小
- 内存中等(128KB~512KB):双缓冲,每块缓冲区取1/10屏幕大小
- 内存紧张(<128KB):单缓冲 + 局部刷新,缓冲区取10~20行像素
lv_conf.h里把LV_MEM_SIZE和缓冲区大小一起调。先定缓冲区,再算LVGL内存池大小。缓冲区占用的内存要从LVGL内存池里扣掉,不然会溢出。
4.5 缓冲区配置的完整流程
下面这张图是我自己项目里用的配置决策流程,你可以照着走一遍:
4.6 实战:配置代码示例
下面是我在一个320×240分辨率、16位色深项目里的配置:
// lv_conf.h 中的缓冲区配置
#define LV_HOR_RES_MAX 320
#define LV_VER_RES_MAX 240
#define LV_COLOR_DEPTH 16
// 双缓冲,每块缓冲区取1/4屏幕大小
// 320 * 240 * 2 / 4 = 38400 字节 ≈ 37.5KB
#define DISP_BUF_SIZE (LV_HOR_RES_MAX * LV_VER_RES_MAX * LV_COLOR_DEPTH / 8 / 4)
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[DISP_BUF_SIZE];
static lv_color_t buf2[DISP_BUF_SIZE];
void my_display_init(void) {
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, DISP_BUF_SIZE);
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &draw_buf;
disp_drv.flush_cb = my_flush_callback;
// 启用局部刷新
disp_drv.full_refresh = 0;
lv_disp_drv_register(&disp_drv);
}
full_refresh = 0表示启用局部刷新。如果设成1,LVGL会强制全屏刷新,缓冲区必须≥整屏大小。我建议除非你有特殊需求,否则保持full_refresh = 0。
4.7 我曾经踩过的坑
最后分享几个真实教训:
- 缓冲区大小不是2的幂? 有一次我把缓冲区大小设成38401字节,结果DMA传输总是多传一个字节,屏幕出现条纹。后来发现DMA控制器要求缓冲区地址和大小都要4字节对齐。从那以后我都在缓冲区大小后面加个
__ALIGNED(4)。 - 双缓冲但刷新回调里加了等待? 有个同事在
flush_cb里加了while(!spi_done);,双缓冲直接退化成单缓冲。双缓冲的精髓就是非阻塞刷新,刷新回调里绝对不能有忙等。 - 局部刷新时脏区域合并没开? LVGL默认会把相邻的脏区域合并,减少刷新次数。但如果你手动调了
lv_refr_set_rounder_cb,合并逻辑可能被破坏。我建议非必要不动这个回调。
缓冲区配置说难不难,说简单也不简单。核心就一句话:在内存允许的范围内,缓冲区越大越好;但一定要用双缓冲,别省那点内存换来卡顿。