3、双缓冲实现机制:前缓冲与后缓冲、交换策略(Page Flip vs BitBlt)、VSync同步

双缓冲这名字听起来挺唬人,其实说白了就是准备两个画板。一个给观众看,一个给画家画。画家画完了,喊一声「换」,观众就看到新画面了。

我刚开始做嵌入式图形时,总觉得单缓冲也能凑合。直到有一次在 4.3 寸屏上做菜单滑动,那个撕裂感……嗯,客户直接拍了视频发过来。从那以后,双缓冲就成了我的标配。

3.1 前缓冲与后缓冲:两个画板的分工

先搞清楚两个缓冲区的角色:

  • 前缓冲(Front Buffer):当前正在被显示控制器扫描输出的缓冲区。说白了,就是观众正在看的那块画板。
  • 后缓冲(Back Buffer):应用程序正在绘制的缓冲区。画家正在涂的那块。

显示控制器会以固定帧率(比如 60Hz)从显存中读取像素数据,逐行刷新到屏幕上。它只认前缓冲。你的绘图操作,永远往后缓冲上写。

核心原则:前缓冲只读,后缓冲只写。两者绝不直接冲突。

我见过不少新手直接在帧缓冲上画图,画到一半被显示控制器扫走了,结果屏幕上半截是旧画面,下半截是新画面。这就是撕裂。双缓冲就是为了根治这个。

3.2 交换策略:Page Flip vs BitBlt

后缓冲画完了,怎么让它变成前缓冲?两种主流做法:

特性 Page Flip(页面翻转) BitBlt(位块传输)
原理 修改显示控制器的帧缓冲地址指针 将后缓冲内容拷贝到前缓冲
速度 微秒级(改个寄存器) 毫秒级(取决于分辨率)
显存占用 2 倍帧缓冲大小 2 倍帧缓冲大小
撕裂风险 配合 VSync 可完全消除 拷贝过程中仍可能撕裂
硬件要求 需要支持双地址的显示控制器 几乎任何硬件都支持
典型场景 高性能嵌入式、游戏机 低端 MCU、资源受限系统

Page Flip(页面翻转)

我个人习惯优先用 Page Flip。它只是把显示控制器的起始地址寄存器从 Buffer A 改成 Buffer B。一个赋值语句的事,快得离谱。

// 伪代码:Page Flip 交换
void swap_buffers() {
    // 等待 VSync(后面会讲)
    wait_for_vsync();
    
    // 翻转:修改显示控制器的帧缓冲地址
    LCD->FB_ADDR = back_buffer;
    
    // 交换指针
    temp = front_buffer;
    front_buffer = back_buffer;
    back_buffer = temp;
}

注意,翻转完成后,原来的前缓冲变成了新的后缓冲。你下次绘图就直接往上面写,不用清空——因为上一帧的内容已经被显示完了。

小技巧:Page Flip 后,旧的前缓冲内容其实还在。如果你下一帧只更新局部区域,可以利用这个「残留」做增量绘制,省掉全屏清空的开销。

BitBlt(位块传输)

有些低端显示控制器不支持改地址,或者你的显存是嵌在 LCD 模块内部的。这时候只能用 BitBlt。

BitBlt 就是把后缓冲的像素数据,一行一行拷贝到前缓冲。我曾在 STM32F4 上做过 480x272 的 BitBlt,用 DMA 加速大概要 3-5ms。如果帧率是 60fps(16.6ms 一帧),这个开销还能接受。

// 伪代码:BitBlt 交换
void swap_buffers() {
    // 等待 VSync
    wait_for_vsync();
    
    // 使用 DMA 将后缓冲拷贝到前缓冲
    dma_copy(front_buffer, back_buffer, BUFFER_SIZE);
    
    // 注意:拷贝期间如果显示控制器正在扫描前缓冲,
    // 仍然可能出现撕裂。需要确保拷贝在 VBlank 期间完成。
}

避坑提醒:我曾经在一个项目里用 BitBlt 做 1024x600 的交换,没算好拷贝时间。结果 VBlank 只有 0.5ms,拷贝却要 8ms。撕裂得一塌糊涂。后来老老实实换了 Page Flip。

3.3 VSync 同步:别让交换发生在错误的时间

VSync(垂直同步)是显示控制器发出的一个硬件信号。它告诉你:「我刚扫完一帧,现在开始进入 VBlank(垂直消隐区),你可以安全交换了。」

为什么需要 VSync?

  • 如果交换发生在显示控制器正在扫描屏幕中间的时候,上半截是旧缓冲,下半截是新缓冲——撕裂。
  • 如果交换发生在 VBlank 期间,显示控制器正在从顶部重新开始扫描,整个屏幕都来自同一个缓冲——完美。

我画了一张流程图,帮你理清整个双缓冲 + VSync 的时序:

双缓冲 + VSync 时序流程图 双缓冲 + VSync 时序流程 绘制后缓冲 应用程序写入像素 等待 VSync 硬件垂直同步信号 交换缓冲 Page Flip / BitBlt 显示控制器扫描新前缓冲 从顶部开始逐行刷新,整个画面来自同一缓冲 → 无撕裂 时序关键点 • VSync 信号在 VBlank 开始时触发,此时显示控制器已完成上一帧扫描 • 交换操作必须在 VBlank 窗口内完成(通常 0.5ms - 2ms) • 如果交换未在 VBlank 内完成,必须等待下一个 VSync,否则撕裂 ❌ 错误做法:不等待 VSync 直接交换 • 交换发生在扫描中间 → 屏幕上半截旧画面 + 下半截新画面 = 撕裂 • 人眼对撕裂非常敏感,尤其在快速滑动或动画场景中

3.4 实际项目中的 VSync 处理

不同硬件平台获取 VSync 的方式不一样:

  • 裸机 / RTOS:通过 GPIO 中断或显示控制器的状态寄存器轮询。
  • Linux Framebuffer:用 FBIOWAITFORVSYNC ioctl 命令。
  • LittlevGL / TouchGFX:框架内部已经封装好了,你只需要在配置里开启。
// Linux Framebuffer 等待 VSync 示例
#include <linux/fb.h>
#include <sys/ioctl.h>

int fb_fd = open("/dev/fb0", O_RDWR);
int arg = 0;

// 阻塞等待下一个 VSync
if (ioctl(fb_fd, FBIOWAITFORVSYNC, &arg) == -1) {
    perror("wait for vsync failed");
}

// 此时可以安全执行 Page Flip 或 BitBlt

经验之谈:我建议你在等待 VSync 时加一个超时机制。有些显示控制器在休眠或分辨率切换时可能不产生 VSync,不加超时会导致线程永久阻塞。设个 100ms 超时,超时后直接交换,最多丢一帧,不会死锁。

3.5 三种策略的选型建议

最后给个实际选型参考:

场景 推荐策略 理由
高性能 HMI(Cortex-A + GPU) Page Flip + VSync 零拷贝、无撕裂、帧率稳定
中端 MCU(Cortex-M7/M4) Page Flip + VSync 只要显示控制器支持双地址,优先用
低端 MCU(Cortex-M0/M3) BitBlt + VSync 硬件不支持地址翻转,只能拷贝
极低端(8 位 MCU) 单缓冲 + 局部刷新 双缓冲显存不够,放弃全屏双缓冲