3. 帧缓冲区策略:单缓冲 vs 双缓冲 vs 三缓冲,以及 Partial Buffer 的配置
帧缓冲区,说白了就是显存。你屏幕上每一帧的画面,都得先画到这块内存里,再送给 LCD 驱动。这块内存怎么分配、怎么流转,直接决定了你的界面是丝滑还是卡顿。
我见过不少开发者,上来就开双缓冲,觉得“双缓冲肯定比单缓冲好”。结果内存爆了,或者刷新率反而上不去。嗯,这里面的门道,咱们得掰开揉碎了讲。
3.1 三种缓冲模式的本质区别
先看一张图,把三种模式的核心流程理清楚。
3.2 单缓冲:最省内存,但小心撕裂
单缓冲就是只有一块帧缓冲区。CPU 往里面画图,LCD 控制器同时从里面读数据。你想想看,如果 CPU 正在画下半屏,LCD 已经读到上半屏了,那画面就会错位——这就是撕裂。
我在一个低成本的温控器项目上用过单缓冲。屏幕只有 320x240,16 位色,一帧才 150KB。MCU 只有 64KB RAM,根本跑不动双缓冲。怎么办?只能在 VSync 中断里画图,保证画完一帧再让 LCD 读。说白了,就是用软件同步来弥补硬件的不足。
3.3 双缓冲:最稳妥的选择
双缓冲有两块缓冲区:Front Buffer 和 Back Buffer。LCD 读 Front Buffer,CPU 画 Back Buffer。画完了,交换指针。这样读写完全隔离,撕裂自然消失。
TouchGFX 默认就是双缓冲。我个人习惯在项目初期先用双缓冲跑起来,等性能调优阶段再考虑要不要换成单缓冲或三缓冲。为什么?因为双缓冲的时序模型最简单,不容易踩坑。
配置方式很简单,在 TouchGFXConfiguration.cpp 里:
// 双缓冲配置示例
static uint16_t frontBuffer[480 * 272]; // 480x272, 16位色
static uint16_t backBuffer[480 * 272];
void TouchGFXHAL::initialize()
{
setFrameBufferStartAddresses(
(uint16_t*)frontBuffer,
(uint16_t*)backBuffer,
nullptr // 第三个参数为 nullptr 表示双缓冲
);
}
这里有个细节:两个缓冲区的地址必须对齐到 32 字节,否则 DMA 传输会出问题。我踩过这个坑,调试了一整天才发现是地址没对齐。
3.4 三缓冲:为高帧率而生
三缓冲在双缓冲的基础上再加一块缓冲区。CPU 可以提前画好下一帧,LCD 读完当前帧后立刻切换到下一帧,中间没有等待时间。说白了,就是用内存换时间。
什么时候用三缓冲?我举个例子:一个带 60fps 动画的智能手表界面。双缓冲下,如果某一帧画得太慢,LCD 读完 Front Buffer 后 Back Buffer 还没准备好,就只能重复上一帧,造成掉帧。三缓冲可以缓冲一帧的延迟,让帧率更稳定。
配置代码:
// 三缓冲配置示例
static uint16_t frameBuffer0[480 * 272];
static uint16_t frameBuffer1[480 * 272];
static uint16_t frameBuffer2[480 * 272];
void TouchGFXHAL::initialize()
{
setFrameBufferStartAddresses(
(uint16_t*)frameBuffer0,
(uint16_t*)frameBuffer1,
(uint16_t*)frameBuffer2 // 三个地址都传,启用三缓冲
);
}
3.5 Partial Buffer:局部刷新的艺术
Partial Buffer 是 TouchGFX 的一个杀手锏。它不要求你为整个屏幕分配帧缓冲区,而是只分配一个“滑动窗口”。每次只刷新屏幕的一小部分,窗口在屏幕上移动,最终拼出完整画面。
我做过一个项目,屏幕是 800x480,16 位色,整帧需要 768KB。MCU 内部 RAM 只有 256KB。用双缓冲?想都别想。用 Partial Buffer,我把窗口设成 800x32,一帧只需要 800×32×2 = 51.2KB。内存瞬间就够用了。
配置方式:
// 在 TouchGFXHAL.cpp 中配置 Partial Buffer
#define PARTIAL_BUFFER_WIDTH 800
#define PARTIAL_BUFFER_HEIGHT 32 // 窗口高度,可调
static uint16_t partialBuffer[PARTIAL_BUFFER_WIDTH * PARTIAL_BUFFER_HEIGHT];
void TouchGFXHAL::initialize()
{
setFrameBufferStartAddresses(
(uint16_t*)partialBuffer,
nullptr,
nullptr
);
setPartialFrameBufferStartAddresses(
(uint16_t*)partialBuffer
);
}
窗口高度怎么选?我一般遵循这个原则:
| 窗口高度 | 内存占用 | 刷新次数/帧 | 适用场景 |
|---|---|---|---|
| 16 行 | 25.6KB | 30 次 | 内存极紧,静态界面 |
| 32 行 | 51.2KB | 15 次 | 通用推荐 |
| 64 行 | 102.4KB | 8 次 | 动画较多,内存尚可 |
| 128 行 | 204.8KB | 4 次 | 高帧率动画 |
3.6 如何选择?一张表说清楚
| 策略 | 内存需求 | 撕裂风险 | 帧率稳定性 | 推荐场景 |
|---|---|---|---|---|
| 单缓冲 | 1x | 高 | 低 | 内存 < 64KB,无动画 |
| 双缓冲 | 2x | 无 | 中 | 通用场景,80% 项目 |
| 三缓冲 | 3x | 无 | 高 | 60fps 动画,内存充裕 |
| Partial Buffer | 窗口大小 | 低 | 中 | 内存受限,大分辨率 |
最后说一句:没有银弹。我见过有人非要在 64KB 的芯片上跑双缓冲,结果内存不够,系统频繁崩溃。也见过有人用 2MB RAM 的芯片跑单缓冲,白白浪费了硬件能力。选哪种策略,取决于你的硬件约束和界面复杂度。动手之前,先算清楚内存账。嗯,这就是调优的第一步。