3、帧缓冲策略优化:单缓冲 vs 双缓冲 vs 三缓冲、Partial Buffer机制、Buffer大小与位置选择

帧缓冲,说白了就是显存里的一块区域,专门用来存放你要显示的画面数据。TouchGFX 的渲染引擎每画一帧,都得往这块区域里写数据。怎么写、写几份、写多大,直接决定了你的界面是丝滑还是卡顿。

我刚开始做嵌入式 GUI 的时候,觉得帧缓冲不就是一块内存嘛,随便分配一下就行了。结果项目跑到一半,屏幕撕裂、闪屏、内存爆掉,各种问题全来了。嗯,后来我才明白,帧缓冲策略是整个性能优化的基石。

3.1 单缓冲 vs 双缓冲 vs 三缓冲

这三种模式,本质上是「写画面」和「读画面」之间的协调方式。我习惯用一个比喻来理解:

  • 单缓冲:只有一个画板。你一边画,观众一边看。你画到一半,观众看到的就是半成品——这就是撕裂。
  • 双缓冲:两个画板。你在后台画,前台展示已经画好的。画完了,直接交换。观众永远看到完整画面。
  • 三缓冲:三个画板。除了前台展示和后台绘制,多一个备用。当绘制速度跟不上刷新率时,备用缓冲能平滑过渡。

下面这张图可以帮你快速理解三者的区别:

单缓冲 vs 双缓冲 vs 三缓冲 工作流程 单缓冲 帧缓冲(唯一) LCD 控制器直接读取 ⚠ 绘制中可能被读取 → 撕裂 双缓冲 前台缓冲 后台缓冲 LCD 读取 GPU 绘制 ✓ 绘制完成 → 交换指针 → 无撕裂 三缓冲 前台 后台 备用 LCD GPU GPU ✓ 绘制不等待 → 更高帧率

核心结论:

  • 单缓冲:内存占用最小,但画面撕裂几乎不可避免。只适合静态画面或极低刷新率场景。
  • 双缓冲:内存翻倍,但彻底解决撕裂。这是 TouchGFX 项目中最常用的模式。
  • 三缓冲:内存再翻倍,但能平滑掉帧。适合动画复杂、刷新率要求高的场景。

我的经验:在 STM32F429 上做 480x272 的界面时,我一开始用了双缓冲,帧率稳定在 30fps。后来加了几个动画,帧率掉到 20fps,画面开始卡。换成三缓冲后,虽然内存多了 300KB,但帧率回到了 28fps,而且卡顿感消失了。说白了,三缓冲是用内存换流畅度。

3.2 Partial Buffer 机制

你想想看,如果你的屏幕分辨率是 1024x600,颜色深度 16bit,一个全帧缓冲就要 1024×600×2 = 1.2MB。双缓冲就是 2.4MB,三缓冲就是 3.6MB。很多 MCU 的 SRAM 总共才 1MB 或 2MB,根本扛不住。

Partial Buffer 就是用来解决这个问题的。它不一次性渲染整帧,而是把屏幕分成多个小块(称为「脏矩形」或「分块」),每次只渲染一小块,然后立即刷到屏幕上。

TouchGFX 的 Partial Buffer 机制,说白了就是「化整为零」:

  1. 引擎计算出当前帧需要更新的区域(脏矩形)。
  2. 把这个区域切成若干个小块,每个小块的大小由你配置的 Partial Buffer 大小决定。
  3. 依次渲染每个小块,渲染完一块就通过 DMA 或直接写屏刷到 LCD。
  4. 所有小块渲染完毕,这一帧就完成了。

关键参数:Partial Buffer 的大小在 TouchGFXConfiguration.cpp 中配置,通常建议设置为屏幕一行像素的 1~4 倍。

// TouchGFXConfiguration.cpp 中的典型配置
static const uint16_t PARTIAL_BUFFER_SIZE = 480 * 2; // 480像素宽,2行
// 或者
static const uint16_t PARTIAL_BUFFER_SIZE = 480 * 4; // 4行

为什么是这个值?我解释一下:

  • 如果 Partial Buffer 太小(比如只有半行),渲染次数会暴增,CPU 频繁切换上下文,性能反而下降。
  • 如果太大(比如整帧),那就退化成了全帧缓冲,失去了 Partial Buffer 的意义。
  • 我个人习惯从 2 行开始试,然后看性能分析工具的帧率数据,逐步调整到 4 行或 8 行。

我曾经踩过的坑:在一个 800x480 的项目里,我把 Partial Buffer 设成了 800×1(一行),结果帧率只有 12fps。后来改成 800×4,帧率直接跳到 28fps。原因是每次渲染小块的开销太大了,小块越多,总开销越大。记住:Partial Buffer 不是越小越好,要找到一个平衡点。

3.3 Buffer 大小与位置选择

Buffer 放在哪里,直接影响访问速度。我见过不少开发者直接把帧缓冲放在 SRAM 里,结果发现刷新率上不去。为什么?因为 SRAM 的带宽有限,CPU 和 DMA 抢着用,互相拖后腿。

下面这张表是我在实际项目中总结的 Buffer 位置选择指南:

存储位置 访问速度 典型容量 适用场景 注意事项
内部 SRAM 最快(~200MHz) 64KB ~ 512KB 小分辨率、Partial Buffer 容量有限,容易被其他模块占用
外部 SDRAM 较快(~100MHz) 8MB ~ 64MB 大分辨率、全帧缓冲 需要初始化时序,延迟略高
内部 DTCM 极快(~400MHz) 64KB ~ 128KB 关键帧缓冲、频繁读写 仅部分 MCU 支持(如 STM32H7)
FMC/FSMC 外设 中等(~50MHz) 取决于外接设备 低成本方案 布线复杂,信号完整性需注意

我的建议:如果你的 MCU 有 DTCM(比如 STM32H743),把 Partial Buffer 放在 DTCM 里,帧率能提升 10%~20%。如果没有 DTCM,优先用内部 SRAM 放 Partial Buffer,用外部 SDRAM 放全帧缓冲。这样既能保证渲染速度,又能节省宝贵的内部 SRAM。

Buffer 大小的计算公式其实很简单:

// 全帧缓冲大小(字节)
full_buffer_size = width * height * bytes_per_pixel;

// 双缓冲总大小
dual_buffer_size = full_buffer_size * 2;

// Partial Buffer 大小(推荐)
partial_buffer_size = width * 2 * bytes_per_pixel;  // 2行
// 或
partial_buffer_size = width * 4 * bytes_per_pixel;  // 4行

举个例子:480×272 分辨率,16bit 颜色(2字节/像素)

  • 全帧缓冲:480 × 272 × 2 = 261,120 字节 ≈ 255KB
  • 双缓冲:255KB × 2 = 510KB
  • Partial Buffer(2行):480 × 2 × 2 = 1,920 字节 ≈ 1.9KB

你看,用 Partial Buffer 的话,只需要 1.9KB 的 SRAM 就能跑起来。这就是为什么很多低端 MCU 也能跑 TouchGFX 的原因。

注意内存对齐:帧缓冲的起始地址最好 4 字节对齐,有些 DMA 控制器甚至要求 8 字节或 16 字节对齐。不对齐的话,DMA 传输可能会出错,或者性能下降。我习惯在链接脚本里用 __attribute__((aligned(32))) 来保证对齐。

3.4 实际项目中的选择策略

说了这么多,到底怎么选?我总结了一个简单的决策流程:

  1. 先看内存够不够:如果内部 SRAM 能放下双缓冲,优先用双缓冲 + 全帧。这是最简单、最稳定的方案。
  2. 内存不够?上 Partial Buffer:从 2 行开始试,逐步增加,直到帧率满足要求。
  3. 帧率还不够?考虑三缓冲:但前提是外部 SDRAM 或大容量 SRAM 能支持。
  4. 最后调位置:把最频繁读写的 Buffer 放到最快的存储里。

一句话总结:

  • 内存充足 → 双缓冲 + 全帧 → 最简单
  • 内存紧张 → 双缓冲 + Partial Buffer → 最常用
  • 追求极致流畅 → 三缓冲 + Partial Buffer → 最烧内存

嗯,帧缓冲策略这块,说白了就是「用空间换时间,还是用时间换空间」的权衡。没有绝对的最优解,只有最适合你项目需求的方案。我每次做新项目,都会先跑一遍性能分析工具,看看帧率和内存占用,再决定用哪种策略。你也试试看?