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 流程图,是我在实际项目中总结的决策路径。你可以对照自己的项目情况走一遍:

帧缓冲区策略决策流程图 开始选择缓冲策略 内存是否紧张?(< 2帧空间) 单缓冲(有撕裂风险) 内存充足,继续判断 帧率要求 > 60fps? 三缓冲(推荐) 双缓冲(标准选择) 决策说明 • 单缓冲:仅用于内存极低(<200KB)且画面几乎不动的场景 • 双缓冲:90% 项目的首选,平衡内存与性能 • 三缓冲:高帧率动画/视频场景,需额外评估内存带宽

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 实战建议:我的配置清单

最后,给出一份我多年总结的配置清单,你可以直接照着检查:

  1. 先算内存够不够:分辨率 × 色深 × 缓冲数 ≤ 可用内存的 70%(留 30% 给堆栈和中间变量)。
  2. 再算带宽够不够:用上面的公式算带宽需求,对比实际可用带宽,留 50% 余量。
  3. 优先选双缓冲:除非有明确的高帧率需求,否则双缓冲是最省心的选择。
  4. 三缓冲慎用:确认内存和带宽都够,且帧率确实需要 >60fps,再上三缓冲。
  5. 单缓冲只做备选:只在内存实在不够时用,且要做好画面撕裂的心理准备。

最后一个小建议:如果你用的是 STM32F7/H7 系列,可以利用 Chrom-ART(DMA2D)来做缓冲区交换,它支持硬件自动地址翻转,能省掉 CPU 的指针交换开销。我在 F746 上试过,帧率提升了约 15%。