3. 帧缓冲区策略:单缓冲 vs 双缓冲 vs 三缓冲,以及 Partial Buffer 的配置

帧缓冲区,说白了就是显存。你屏幕上每一帧的画面,都得先画到这块内存里,再送给 LCD 驱动。这块内存怎么分配、怎么流转,直接决定了你的界面是丝滑还是卡顿。

我见过不少开发者,上来就开双缓冲,觉得“双缓冲肯定比单缓冲好”。结果内存爆了,或者刷新率反而上不去。嗯,这里面的门道,咱们得掰开揉碎了讲。

3.1 三种缓冲模式的本质区别

先看一张图,把三种模式的核心流程理清楚。

帧缓冲区策略对比 单缓冲 (Single Buffer) CPU/GPU 直接写入 LCD 控制器读取 ⚠ 撕裂:读写同一块内存 双缓冲 (Double Buffer) Front Buffer Back Buffer LCD 读取 CPU 写入 ✓ 无撕裂,但内存翻倍 三缓冲 (Triple Buffer) FB0 FB1 FB2 LCD ✓ 最高吞吐,内存 x3 关键指标:内存占用 × 刷新率 × 撕裂风险,三者不可兼得 我的推荐场景: • 单缓冲:内存极度紧张(< 64KB),且界面几乎无动画 • 双缓冲:通用场景,80% 的项目用这个就够了 • 三缓冲:高帧率动画(60fps+),且内存充裕(> 512KB)

3.2 单缓冲:最省内存,但小心撕裂

单缓冲就是只有一块帧缓冲区。CPU 往里面画图,LCD 控制器同时从里面读数据。你想想看,如果 CPU 正在画下半屏,LCD 已经读到上半屏了,那画面就会错位——这就是撕裂。

我在一个低成本的温控器项目上用过单缓冲。屏幕只有 320x240,16 位色,一帧才 150KB。MCU 只有 64KB RAM,根本跑不动双缓冲。怎么办?只能在 VSync 中断里画图,保证画完一帧再让 LCD 读。说白了,就是用软件同步来弥补硬件的不足。

⚠ 避坑指南: 我曾经在 STM32F103 上试过单缓冲 + DMA2D 加速,结果 DMA2D 写帧缓冲的速度比 LTDC 读的速度快,导致撕裂更严重。后来加了双缓冲才解决。单缓冲不是不能用,但你必须保证“写入完成”和“读取开始”严格同步。

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  // 三个地址都传,启用三缓冲
    );
}
💡 小技巧: 三缓冲的内存开销确实大,但你可以用外部 SDRAM 来放帧缓冲区。比如 STM32F769 的 SDRAM 接口,跑 16 位色 800x480 的三缓冲只需要 800×480×2×3 = 2.3MB,SDRAM 完全扛得住。

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 次 高帧率动画
核心要点: Partial Buffer 的窗口高度越小,内存越省,但刷新次数越多,CPU 开销越大。你需要在这两者之间找到平衡点。我个人习惯从 32 行开始试,如果 CPU 占用率低于 60%,就降到 16 行;如果高于 80%,就升到 64 行。

3.6 如何选择?一张表说清楚

策略 内存需求 撕裂风险 帧率稳定性 推荐场景
单缓冲 1x 高 低 内存 < 64KB,无动画
双缓冲 2x 无 中 通用场景,80% 项目
三缓冲 3x 无 高 60fps 动画,内存充裕
Partial Buffer 窗口大小 低 中 内存受限,大分辨率

最后说一句:没有银弹。我见过有人非要在 64KB 的芯片上跑双缓冲,结果内存不够,系统频繁崩溃。也见过有人用 2MB RAM 的芯片跑单缓冲,白白浪费了硬件能力。选哪种策略,取决于你的硬件约束和界面复杂度。动手之前,先算清楚内存账。嗯,这就是调优的第一步。