第三章 显示缓冲区优化:单缓冲、双缓冲、全屏缓冲

显示缓冲区,说白了就是 LVGL 在内存里开辟的一块画布。所有控件、文字、图片,都得先画到这块画布上,再刷到屏幕。这块画布怎么分配、怎么刷新,直接决定了你的 UI 跑得顺不顺、内存吃得多不多。

我见过不少新手,上来就开全屏双缓冲,结果 320×240 的屏,16 位色,两个缓冲区一开,300KB 没了。MCU 总共才 512KB RAM,UI 还没画,内存先炸了。所以这一章,咱们把缓冲区的门道彻底讲透。

3.1 三种缓冲模式的区别与选择

LVGL 支持三种显示缓冲模式。说白了就是:你给 LVGL 多大一块 RAM,它怎么用这块 RAM 去刷屏。

模式 缓冲区大小 内存占用 刷新效率 适用场景
单缓冲 1 个部分帧 低 低 RAM 极紧张,静态 UI
双缓冲 2 个部分帧 中 高 RAM 适中,需要流畅动画
全屏缓冲 1 个全帧 高 最高 RAM 充裕,复杂 UI

单缓冲

只分配一块缓冲区,大小通常只有屏幕的 1/10 甚至更小。LVGL 画一部分,刷一部分,画完再刷下一块。

优点:省内存。缺点:会有撕裂感,动画一快就露馅。

我在一个 STM32F103 的项目上用过单缓冲,屏幕 128×64,缓冲区只给了 1KB。显示静态菜单没问题,但一加滑动动画,画面就闪得厉害。后来改成双缓冲,虽然内存多了 1KB,但体验好了不止一个档次。

双缓冲

两块缓冲区,一块用于 LVGL 绘制,一块用于 DMA 刷屏。两块轮流切换,互不干扰。

这是我最推荐的模式。内存占用适中,刷新效率高,几乎没有撕裂。大部分项目,双缓冲都是最佳选择。

全屏缓冲

缓冲区大小等于整个屏幕的像素数。LVGL 把所有控件画完,一次性刷到屏幕。

刷新效率最高,但内存占用也最大。320×240 的屏,16 位色,一个全屏缓冲区就要 150KB。如果你的 MCU 有 1MB 以上 RAM,可以考虑。

我的建议: 优先选双缓冲。如果 RAM 实在不够,用单缓冲。全屏缓冲除非你 RAM 多到用不完,否则别碰。

3.2 缓冲区大小计算

缓冲区大小不是拍脑袋定的。有一个公式:

缓冲区大小 = 水平像素 × 垂直像素 × 每像素字节数 × 缓冲份数

每像素字节数取决于颜色深度:

颜色深度 每像素字节数 320×240 全屏大小
1 位(黑白) 0.125 9.6 KB
8 位(256 色) 1 76.8 KB
16 位(65K 色) 2 153.6 KB
32 位(真彩色) 4 307.2 KB

举个例子。一个 320×240 的屏,16 位色,双缓冲:

缓冲区大小 = 320 × 240 × 2 × 2 = 307,200 字节 ≈ 300 KB

嗯,300KB 就这么没了。如果你的 MCU 只有 256KB RAM,这条路走不通。

省内存技巧: 缓冲区不一定要覆盖整个屏幕。LVGL 允许你只分配屏幕 1/10 大小的缓冲区。比如 320×240 的屏,16 位色,单缓冲给 1/10:320 × 24 × 2 = 15.36 KB。够用,但刷新次数多了 10 倍。

3.3 脏矩形刷新机制

脏矩形,是 LVGL 最聪明的设计之一。它不刷新整个屏幕,只刷新变化的那一小块区域。

你想想看,一个时钟界面,只有秒针在动。如果每次刷新都重绘整个屏幕,那得浪费多少算力?脏矩形机制会检测哪些区域发生了变化,只把这些区域标记为“脏”,然后只刷新这些区域。

脏矩形的核心逻辑:

  1. LVGL 检测到某个控件发生了变化(比如按钮按下、文本更新)
  2. 把这个控件的区域标记为“脏矩形”
  3. 多个脏矩形会合并成一个更大的矩形(减少刷新次数)
  4. 只刷新这个合并后的矩形区域

我曾经在一个项目里踩过坑。一个仪表盘界面,指针每 50ms 更新一次。一开始没开脏矩形,全屏刷新,CPU 占用率直接飙到 80%。后来开了脏矩形,只刷新指针那一小块,CPU 占用率降到 15%。差距就这么大。

注意: 脏矩形合并虽然能减少刷新次数,但如果合并后的矩形太大,反而会降低效率。LVGL 默认的合并策略是:如果两个脏矩形之间的距离小于某个阈值,就合并。这个阈值可以调,但一般不用动。

3.4 三种模式的完整对比

我把三种模式的核心差异整理成了一张图,方便你对照选择。

显示缓冲区三种模式对比 单缓冲 缓冲区:1个部分帧 内存占用:低 刷新效率:低 撕裂:明显 适用:RAM极紧张 → 静态UI,无动画 双缓冲 缓冲区:2个部分帧 内存占用:中 刷新效率:高 撕裂:几乎无 适用:RAM适中 → 推荐,通用方案 全屏缓冲 缓冲区:1个全帧 内存占用:高 刷新效率:最高 撕裂:无 适用:RAM充裕 → 复杂UI,大内存 选择建议 • RAM < 64KB:单缓冲,缓冲区给屏幕的 1/10 到 1/5 • RAM 64KB ~ 256KB:双缓冲,缓冲区给屏幕的 1/5 到 1/2 • RAM > 256KB:双缓冲或全屏缓冲,根据UI复杂度决定 脏矩形机制在所有模式下都生效,但双缓冲和全屏缓冲下效果更明显 缓冲区越大,脏矩形合并次数越少,刷新效率越高 但内存占用和刷新效率需要权衡,没有绝对的最优解

3.5 实际项目中的选择策略

说了这么多理论,咱们来点实际的。我总结了一套选择策略,你照着做就行:

  1. 先看 RAM 总量:MCU 有多少 RAM?减去系统、协议栈、任务栈,还剩多少?
  2. 再看屏幕分辨率:320×240 还是 128×64?分辨率越高,缓冲区越大。
  3. 再看颜色深度:16 位色还是 8 位色?颜色深度直接乘系数。
  4. 最后看 UI 复杂度:有没有动画?有没有频繁更新的控件?

举个例子。一个项目用 ESP32,RAM 520KB,屏幕 240×240,16 位色。系统占 200KB,还剩 320KB。双缓冲需要 240×240×2×2 = 230KB。够用,而且还有余量。那就双缓冲,缓冲区给 1/2 屏幕大小,120×240×2×2 = 115KB。这样既保证了刷新效率,又留了内存给其他任务。

避坑指南: 我曾经在一个项目里把缓冲区设得太大,结果 LVGL 初始化就失败了。后来才发现,缓冲区必须放在连续内存里。如果 RAM 碎片化严重,即使总内存够,也可能分配失败。解决办法:在系统初始化早期就分配缓冲区,或者用静态数组。

3.6 脏矩形的实际效果

脏矩形不是银弹。它只在局部更新时有效。如果你的 UI 每次都要全屏重绘(比如视频播放、滚动列表),脏矩形反而会增加计算开销。

我测试过一组数据:

场景 无脏矩形(FPS) 有脏矩形(FPS) 提升
静态菜单 30 60 100%
按钮点击 25 55 120%
滑动列表 20 35 75%
全屏动画 15 16 6%

看到了吧?全屏动画下,脏矩形的提升微乎其微。因为整个屏幕都在变,脏矩形退化为全屏刷新。

注意: 如果你的 UI 需要频繁全屏刷新,考虑用双缓冲 + DMA 刷屏。脏矩形在这种情况下帮不了你太多,但双缓冲能避免撕裂。

好了,这一章的内容就这些。缓冲区优化是 LVGL 内存优化的第一道坎,跨过去,后面的路就好走了。