3、帧缓冲区优化:单缓冲 vs 双缓冲 vs 三缓冲
帧缓冲区,说白了就是显存里用来存一帧画面的那块区域。很多刚接触 TouchGFX 的朋友觉得这玩意儿不就是个存像素的地方嘛,有啥好纠结的?嗯,我当年也这么想,直到在一个 480×272 的屏上做动画,发现画面撕裂得像撕报纸一样……从那以后,我再也不敢小看缓冲区的配置了。
这一节,咱们把单缓冲、双缓冲、三缓冲掰开揉碎讲清楚。顺便聊聊怎么选策略、怎么算带宽。你想想看,缓冲区选对了,性能可能翻倍;选错了,CPU 和 DMA 互相打架,画面卡成 PPT。
3.1 三种缓冲模式的核心差异
先看一张对比表,把三种模式的核心差异说清楚:
| 特性 | 单缓冲 | 双缓冲 | 三缓冲 |
|---|---|---|---|
| 内存占用 | 1 帧 | 2 帧 | 3 帧 |
| 画面撕裂 | 严重 | 无 | 无 |
| CPU 等待时间 | 长(等 VSync) | 短(交换指针) | 极短(始终有可用缓冲区) |
| 帧率稳定性 | 差 | 好 | 最好 |
| 适用场景 | 静态画面、极低内存 | 大多数 UI 应用 | 高帧率动画、视频播放 |
核心结论:双缓冲是 TouchGFX 项目的「甜点配置」。三缓冲只在帧率要求极高(>60fps)且内存充裕时推荐。单缓冲……说实话,除了验证硬件或者做极简界面,我基本不用。
3.2 单缓冲:简单但危险
单缓冲模式下,CPU 直接在显示驱动正在读取的帧缓冲区上画图。你画一笔,屏幕就刷新一笔——听起来很实时对吧?但问题来了:
- 画面撕裂:显示控制器从顶部扫描到底部,如果 CPU 在扫描过程中修改了缓冲区,上半屏是旧帧,下半屏是新帧,中间一条撕裂线。
- CPU 空等:为了避免撕裂,你只能在 VSync 间隙画图。如果一帧画不完,就得等下一个 VSync,帧率直接腰斩。
我记得有个项目,客户非要省 200KB 内存用单缓冲。结果动画一跑起来,用户反馈「屏幕在抖」。我拿逻辑分析仪一看,撕裂率超过 30%。后来还是老老实实加了双缓冲。
避坑:我曾经在 STM32F429 上试过单缓冲 + 局部刷新,以为能绕过撕裂问题。结果发现 TouchGFX 的局部刷新算法在单缓冲下依然有概率触发撕裂——因为 DMA2D 和 LTDC 的访问冲突不可控。所以,除非你的屏完全不更新(比如只显示固定文本),否则别用单缓冲。
3.3 双缓冲:最稳妥的选择
双缓冲的原理很简单:两个缓冲区,一个叫 Front Buffer(前缓冲),一个叫 Back Buffer(后缓冲)。显示控制器读前缓冲,CPU/DMA 往后缓冲写。画完了,交换指针——注意,不是拷贝数据,只是改个地址。
这样做的好处:
- CPU 永远不用等 VSync,画完就交换,交换完继续画下一帧。
- 显示控制器始终读完整的一帧,不会看到半成品。
在 TouchGFX 里启用双缓冲,只需要在 TouchGFXConfiguration.cpp 里设置:
// 双缓冲配置示例
static uint16_t frontBuffer[480 * 272];
static uint16_t backBuffer[480 * 272];
void TouchGFXHAL::initialize()
{
// 设置两个缓冲区地址
setFrameBufferStartAddresses(
(uint16_t*)frontBuffer,
(uint16_t*)backBuffer,
0 // 第三个参数是三缓冲用的,双缓冲传0
);
// 启用双缓冲
setFrameBufferStrategy(FRAME_BUFFER_STRATEGY_DOUBLE);
}
小技巧:双缓冲的内存开销是 2 帧。以 480×272、16位色为例:480 × 272 × 2 = 261,120 字节,约 255KB。两帧就是 510KB。如果你的 MCU 内部 SRAM 不够,可以考虑用外部 SDRAM——但要注意 SDRAM 的访问延迟,我遇到过因为 SDRAM 时序没调好导致帧率掉到 30fps 的情况。
3.4 三缓冲:为极致性能而生
三缓冲在双缓冲的基础上再加一个中间缓冲区。三个缓冲区轮流使用:一个被显示控制器读取,一个被 CPU 写入,一个空闲等待。这样 CPU 几乎永远不会被阻塞——即使显示控制器还没读完上一帧,CPU 也能立刻拿到空闲缓冲区开始画下一帧。
但代价也很明显:
- 内存占用增加 50%(相比双缓冲)。
- 帧延迟增加 1 帧(因为多了一级缓冲)。
- 对内存带宽的要求更高。
我个人习惯只在两种情况下用三缓冲:一是跑 60fps 以上的复杂动画(比如地图缩放),二是视频播放场景。普通按钮点击、页面切换,双缓冲完全够用。
3.5 缓冲策略选择:一张流程图帮你决策
下面这张 SVG 流程图,是我在实际项目中总结的决策路径。你可以对照自己的项目情况走一遍:
3.6 内存带宽计算:别让带宽成为瓶颈
选好了缓冲策略,还得算算内存带宽够不够。很多开发者只关注内存大小,忽略了带宽——结果帧率上不去,查了半天发现是总线瓶颈。
带宽计算公式很简单:
带宽需求 = 分辨率 × 色深 × 帧率 × 缓冲倍数
其中:
- 分辨率:水平像素 × 垂直像素
- 色深:16位色 = 2字节,24位色 = 3字节,32位色 = 4字节
- 帧率:目标帧率(如 60fps)
- 缓冲倍数:单缓冲=1,双缓冲=2,三缓冲=3
举个例子,480×272、16位色、60fps、双缓冲:
带宽 = 480 × 272 × 2 × 60 × 2
= 31,334,400 字节/秒
≈ 29.9 MB/s
这个数字意味着什么?我列一下常见总线的理论带宽:
| 总线类型 | 理论带宽 | 实际可用(约) | 能否支撑上述场景? |
|---|---|---|---|
| 内部 SRAM(32位 @ 168MHz) | 672 MB/s | ~500 MB/s | ✅ 绰绰有余 |
| 外部 SDRAM(16位 @ 100MHz) | 200 MB/s | ~80 MB/s | ✅ 可以 |
| 外部 QSPI PSRAM | 80 MB/s | ~30 MB/s | ⚠️ 刚好够 |
| 外部 SPI Flash(读取) | 20 MB/s | ~10 MB/s | ❌ 不够 |
注意:实际可用带宽通常只有理论值的 40%~60%。因为总线还要服务其他外设(DMA、中断、外设寄存器访问)。我曾经在一个项目里用 QSPI PSRAM 做帧缓冲区,算下来理论带宽 80MB/s,觉得够了。结果一跑,帧率只有 35fps——因为 QSPI 的读命令开销和 DMA 的握手延迟吃掉了大量带宽。后来换成 SDRAM,问题才解决。
3.7 实战建议:我的配置清单
最后,给出一份我多年总结的配置清单,你可以直接照着检查:
- 先算内存够不够:分辨率 × 色深 × 缓冲数 ≤ 可用内存的 70%(留 30% 给堆栈和中间变量)。
- 再算带宽够不够:用上面的公式算带宽需求,对比实际可用带宽,留 50% 余量。
- 优先选双缓冲:除非有明确的高帧率需求,否则双缓冲是最省心的选择。
- 三缓冲慎用:确认内存和带宽都够,且帧率确实需要 >60fps,再上三缓冲。
- 单缓冲只做备选:只在内存实在不够时用,且要做好画面撕裂的心理准备。
最后一个小建议:如果你用的是 STM32F7/H7 系列,可以利用 Chrom-ART(DMA2D)来做缓冲区交换,它支持硬件自动地址翻转,能省掉 CPU 的指针交换开销。我在 F746 上试过,帧率提升了约 15%。