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 的时序:
3.4 实际项目中的 VSync 处理
不同硬件平台获取 VSync 的方式不一样:
- 裸机 / RTOS:通过 GPIO 中断或显示控制器的状态寄存器轮询。
- Linux Framebuffer:用
FBIOWAITFORVSYNCioctl 命令。 - 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) | 单缓冲 + 局部刷新 | 双缓冲显存不够,放弃全屏双缓冲 |