3、帧缓冲策略优化:单缓冲 vs 双缓冲 vs 三缓冲、Partial Buffer机制、Buffer大小与位置选择
帧缓冲,说白了就是显存里的一块区域,专门用来存放你要显示的画面数据。TouchGFX 的渲染引擎每画一帧,都得往这块区域里写数据。怎么写、写几份、写多大,直接决定了你的界面是丝滑还是卡顿。
我刚开始做嵌入式 GUI 的时候,觉得帧缓冲不就是一块内存嘛,随便分配一下就行了。结果项目跑到一半,屏幕撕裂、闪屏、内存爆掉,各种问题全来了。嗯,后来我才明白,帧缓冲策略是整个性能优化的基石。
3.1 单缓冲 vs 双缓冲 vs 三缓冲
这三种模式,本质上是「写画面」和「读画面」之间的协调方式。我习惯用一个比喻来理解:
- 单缓冲:只有一个画板。你一边画,观众一边看。你画到一半,观众看到的就是半成品——这就是撕裂。
- 双缓冲:两个画板。你在后台画,前台展示已经画好的。画完了,直接交换。观众永远看到完整画面。
- 三缓冲:三个画板。除了前台展示和后台绘制,多一个备用。当绘制速度跟不上刷新率时,备用缓冲能平滑过渡。
下面这张图可以帮你快速理解三者的区别:
核心结论:
- 单缓冲:内存占用最小,但画面撕裂几乎不可避免。只适合静态画面或极低刷新率场景。
- 双缓冲:内存翻倍,但彻底解决撕裂。这是 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 机制,说白了就是「化整为零」:
- 引擎计算出当前帧需要更新的区域(脏矩形)。
- 把这个区域切成若干个小块,每个小块的大小由你配置的 Partial Buffer 大小决定。
- 依次渲染每个小块,渲染完一块就通过 DMA 或直接写屏刷到 LCD。
- 所有小块渲染完毕,这一帧就完成了。
关键参数: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 实际项目中的选择策略
说了这么多,到底怎么选?我总结了一个简单的决策流程:
- 先看内存够不够:如果内部 SRAM 能放下双缓冲,优先用双缓冲 + 全帧。这是最简单、最稳定的方案。
- 内存不够?上 Partial Buffer:从 2 行开始试,逐步增加,直到帧率满足要求。
- 帧率还不够?考虑三缓冲:但前提是外部 SDRAM 或大容量 SRAM 能支持。
- 最后调位置:把最频繁读写的 Buffer 放到最快的存储里。
一句话总结:
- 内存充足 → 双缓冲 + 全帧 → 最简单
- 内存紧张 → 双缓冲 + Partial Buffer → 最常用
- 追求极致流畅 → 三缓冲 + Partial Buffer → 最烧内存
嗯,帧缓冲策略这块,说白了就是「用空间换时间,还是用时间换空间」的权衡。没有绝对的最优解,只有最适合你项目需求的方案。我每次做新项目,都会先跑一遍性能分析工具,看看帧率和内存占用,再决定用哪种策略。你也试试看?