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 缓冲模式选择决策流程

下面这张图是我自己总结的选择流程,帮你快速决策:

帧缓冲模式选择决策流程 开始选择缓冲模式 RAM > 显存大小 × 2? 是 全缓冲模式 否 有动画或触摸? 是 双缓冲模式 否 单缓冲模式 内存估算公式 显存大小 = 宽 × 高 × 色深(字节) 例:320×240×2 = 150KB

2.8 实战建议

说了这么多,最后给几条实在的建议:

  • 资源紧张时:用单缓冲 + 局部刷新 + 16位色深。牺牲一点画面流畅度,换来大量内存。
  • 有动画需求时:至少上双缓冲。别在单缓冲上折腾,撕裂问题很难完美解决。
  • 有大内存时:全缓冲 + 32位色深,效果最好,开发也最简单。
  • 调试阶段:先用双缓冲,等UI稳定后再考虑压缩。别一开始就追求极致优化,容易把自己绕进去。

我的经验:大多数项目,双缓冲 + 20~40行缓冲区 + 16位色深,是性价比最高的组合。内存占用不大,画面流畅度也够用。我最近三个项目都是这么配的,没出过问题。