2. LVGL帧缓冲区优化:单缓冲、双缓冲与全缓冲模式的原理与选择
帧缓冲区,说白了就是显存。LVGL在刷新屏幕时,需要一块内存来存放像素数据。这块内存怎么分配、怎么用,直接决定了你的UI跑得顺不顺、内存省不省。
我刚开始用LVGL时,觉得帧缓冲嘛,随便给一块就行。结果在STM32F429上跑个带动画的仪表盘,画面撕裂得没法看。后来才明白——缓冲模式选错了,再好的优化技巧也白搭。
2.1 三种缓冲模式的核心差异
LVGL支持三种缓冲模式。它们的区别,说白了就是「画布」有几块、怎么用。
| 模式 | 内存占用 | 画面完整性 | 适用场景 |
|---|---|---|---|
| 单缓冲 | 1个缓冲区 | 可能有撕裂 | 低内存、静态UI |
| 双缓冲 | 2个缓冲区 | 无撕裂 | 动画、触摸交互 |
| 全缓冲 | 整个显存 | 最佳效果 | 高性能平台 |
2.2 单缓冲模式
单缓冲就是只分配一块缓冲区。LVGL在这块缓冲区里画图,画完后直接刷到屏幕。
听起来简单,但有个坑——画图和刷屏是串行的。CPU在画的时候,屏幕不能刷新;屏幕在刷新的时候,CPU不能画。这就导致帧率上不去。
我曾经踩过的坑:在ESP32上跑一个带滑动列表的界面,单缓冲模式下列表滑动时明显看到画面从顶部往下刷新。这就是典型的撕裂现象。后来换成双缓冲,问题立刻消失。
单缓冲的代码配置很简单:
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10]; // 10行缓冲区
lv_disp_draw_buf_init(&draw_buf, buf, NULL, LV_HOR_RES_MAX * 10);
注意第二个参数是缓冲区指针,第三个参数传NULL——这就是单缓冲的标志。
2.3 双缓冲模式
双缓冲分配两块缓冲区。一块用于CPU画图,另一块用于DMA或SPI刷屏。两块轮流切换,互不干扰。
我个人习惯把双缓冲叫做「流水线模式」。CPU画完A缓冲区,通知硬件刷A,同时CPU开始画B缓冲区。等B画完,A也刷完了,再切换过来。
关键点:双缓冲模式下,LVGL的lv_disp_flush_ready()必须在DMA传输完成后调用。如果提前调用,LVGL会认为缓冲区已空闲,开始往里面写数据——这时候DMA还在读,画面就乱了。
配置代码:
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[LV_HOR_RES_MAX * 40];
static lv_color_t buf2[LV_HOR_RES_MAX * 40];
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LV_HOR_RES_MAX * 40);
第三个参数传了buf2,这就是双缓冲。缓冲区大小我习惯设成40行,既能覆盖大部分刷新场景,又不会太占内存。
2.4 全缓冲模式
全缓冲就是分配一整块显存大小的缓冲区。比如320×240的屏幕,每个像素16位,那就需要320×240×2 = 150KB。
全缓冲的好处是:LVGL可以在后台任意重绘整个画面,然后一次性刷到屏幕。不会有撕裂,也不会有局部刷新带来的闪烁。
但代价也很明显——内存占用太大了。在资源紧张的MCU上,150KB可能比整个剩余RAM还多。
我的建议:全缓冲只用在以下场景——①有外部SDRAM;②屏幕分辨率很低(比如128×64);③RTOS下内存充足。否则,双缓冲加局部刷新才是正道。
2.5 三种模式的对比总结
我整理了一张对比表,方便你根据项目选型:
| 对比维度 | 单缓冲 | 双缓冲 | 全缓冲 |
|---|---|---|---|
| 内存需求 | 低(几KB) | 中(几十KB) | 高(几百KB) |
| 画面撕裂 | 有 | 无 | 无 |
| CPU占用 | 高(等待刷屏) | 低(流水线) | 低 |
| 实现复杂度 | 简单 | 中等 | 简单 |
| 推荐场景 | 静态界面、低功耗 | 动画、触摸交互 | 高性能平台 |
2.6 帧缓冲区的内存压缩技巧
内存不够怎么办?别急着上外部RAM。我总结了几个实战中验证过的压缩技巧。
技巧一:缩小缓冲区行数
双缓冲不一定要分配全屏行数。比如320×240的屏幕,你可以只分配20行。LVGL会分多次刷新,每次只刷20行。
代价是刷新次数变多,但内存占用从150KB降到6KB(20×320×2)。
注意:行数太少会导致刷新频率过高,反而降低帧率。我一般建议行数不低于屏幕高度的1/10。240行高度的屏幕,20行是个不错的起点。
技巧二:使用16位色深
LVGL默认支持32位色深(ARGB8888),每个像素占4字节。如果屏幕本身只支持16位色(RGB565),那就改成16位模式:
#define LV_COLOR_DEPTH 16
这一改,缓冲区大小直接减半。320×240的屏幕,从307KB降到150KB。
技巧三:启用LVGL的局部刷新
LVGL有一个隐藏功能——LV_USE_PERF_MONITOR和LV_DISP_DEF_REFR_PERIOD配合使用,可以让LVGL只刷新变化区域。
配置方法:
#define LV_USE_PERF_MONITOR 1
#define LV_DISP_DEF_REFR_PERIOD 30 // 30ms刷新周期
这样LVGL会检测脏区域,只重绘变化的部分。缓冲区可以进一步缩小。
技巧四:使用外部PSRAM做缓冲区
如果MCU内部RAM实在不够,可以考虑把帧缓冲区放到外部PSRAM。但要注意——PSRAM的读写速度比内部RAM慢很多。
我曾在ESP32上试过:内部RAM做双缓冲,帧率能到45fps;换成PSRAM后掉到22fps。所以PSRAM只适合做全缓冲,不适合做双缓冲的频繁切换。
2.7 缓冲模式选择决策流程
下面这张图是我自己总结的选择流程,帮你快速决策:
2.8 实战建议
说了这么多,最后给几条实在的建议:
- 资源紧张时:用单缓冲 + 局部刷新 + 16位色深。牺牲一点画面流畅度,换来大量内存。
- 有动画需求时:至少上双缓冲。别在单缓冲上折腾,撕裂问题很难完美解决。
- 有大内存时:全缓冲 + 32位色深,效果最好,开发也最简单。
- 调试阶段:先用双缓冲,等UI稳定后再考虑压缩。别一开始就追求极致优化,容易把自己绕进去。
我的经验:大多数项目,双缓冲 + 20~40行缓冲区 + 16位色深,是性价比最高的组合。内存占用不大,画面流畅度也够用。我最近三个项目都是这么配的,没出过问题。