4、页交换的时机:垂直同步(VSync)的作用,为什么要在消隐期交换缓冲区,否则依然会撕裂
好,咱们接着聊。上一章我们把双缓冲的机制讲清楚了,你知道了前缓冲和后缓冲怎么配合。但有个关键问题我一直悬着没说——什么时候交换?
你想想看,如果你在屏幕正在从上往下扫描像素的时候,突然把后缓冲切到前缓冲,会发生什么?
屏幕上半截还是老画面,下半截已经变成新画面了。这就是撕裂(Tearing)。我早年做一款工业平板的时候,就被这个撕裂坑过一次。画面一滚动,屏幕中间一条明显的错位线,客户当场就拍了照片发过来……嗯,从那以后我再也不敢忽略交换时机了。
4.1 屏幕是怎么画出一帧的?
要理解交换时机,得先知道屏幕的刷新过程。说白了,屏幕不是一瞬间把整张图全亮出来的。它是逐行扫描的。
- 从左到右:每行像素从左扫到右,这叫水平扫描。
- 从上到下:扫完一行,换下一行,这叫垂直扫描。
- 扫完一帧:所有行都扫完了,这一帧就结束了。
扫完最后一行的右下角之后,电子枪(或者 LCD 的驱动电路)需要回到左上角,准备下一帧。这个回扫的时间段,就叫消隐期(Blanking Interval)。
我个人习惯把屏幕刷新想象成一台老式打印机:喷头从左到右喷一行,然后纸往上走一点,再喷下一行。喷完一整页,纸退回去,准备下一页。那个“纸退回去”的时间,就是消隐期。
4.2 垂直同步(VSync)到底是什么?
VSync 是 Vertical Synchronization 的缩写。它其实就是一个硬件信号,由显示控制器在特定时刻拉高或拉低。
这个信号什么时候触发?就在消隐期开始的那一刻。
具体来说,VSync 信号在每帧的消隐期开始时产生。它告诉 GPU 或 MCU 的显示控制器:“上一帧已经完整送出去了,现在屏幕不干活了,你可以安全地交换缓冲区了。”
核心结论:VSync 信号 = 安全的缓冲区交换窗口。只有在 VSync 到来时交换,才能保证画面完整。
4.3 为什么不在消隐期交换就会撕裂?
咱们用一张图把这个问题彻底讲透。下面是我画的一个时序图,展示了三种情况:
看到了吗?情况 A 里,每次交换都发生在 VSync 信号处,也就是消隐期。屏幕在消隐期不干活,你随便换,下一帧从头开始就是新画面。
情况 B 就惨了。交换发生在扫描过程中,屏幕读到一半,缓冲区指针突然变了。结果就是:上半截是帧1,下半截是帧2。中间那条错位线,就是撕裂。
注意:撕裂不是偶尔出现一次。只要你不等 VSync,每次刷新都可能撕裂。而且撕裂位置会随着交换时间的变化而上下移动,看起来就像画面在“抖动”。
4.4 嵌入式系统里怎么等 VSync?
不同硬件平台等 VSync 的方式不一样。我列几个常见的:
| 平台 | 等待 VSync 的方式 | 典型 API / 寄存器 |
|---|---|---|
| STM32 LTDC | 读取状态寄存器或使能中断 | LTDC->SRCR 或 LTDC_LIPCR |
| ESP32 | 使用 LVGL 的 lv_disp_drv_t 回调 |
disp_drv.user_data + TE 引脚 |
| Linux DRM | 调用 drmModePageFlip() 阻塞等待 |
DRM_IOCTL_MODE_PAGE_FLIP |
| 裸机 + DMA2D | 轮询 TE(Tearing Effect)引脚 | GPIO 电平检测 |
以 STM32 为例,我常用的做法是:
// 等待垂直消隐期(VSync)
while (!(LTDC->CDSR & LTDC_CDSR_VSYNCS));
// 此时可以安全交换缓冲区
LTDC->SRCR = LTDC_SRCR_IMR;
这段代码的意思是:不停检查 CDSR 寄存器的 VSYNCS 位,直到它被置位。一旦置位,说明消隐期到了,立刻触发缓冲区交换。
我的经验:轮询方式虽然简单,但会占用 CPU。如果系统里还有别的任务,建议用中断方式。在 VSync 中断里设一个标志位,主循环检测到标志位再交换。这样 CPU 利用率更高。
4.5 如果刷新率比帧率快怎么办?
这个问题很实际。假设你的屏幕是 60Hz,但你的应用只能跑到 30fps。那每两帧 VSync 才有一帧新数据。这时候怎么办?
我的建议是:不要重复交换同一帧。如果后缓冲没有新数据,就保持当前缓冲区不变,等下一帧 VSync 再检查。
具体做法:
- 维护一个
frame_ready标志位。 - 应用层渲染完成后,置位
frame_ready。 - VSync 中断里检查这个标志:如果为真,交换缓冲区并清标志;如果为假,什么都不做。
这样就不会出现“空交换”导致画面闪烁的问题。
4.6 避坑指南:我曾经踩过的 VSync 坑
讲几个我实际遇到过的坑,你遇到了可以少走弯路。
坑一:TE 引脚极性搞反
有一次我用一个 SSD1963 驱动的屏幕,TE 引脚默认是高电平表示消隐期。我代码里写成低电平等待,结果画面一直撕裂。查了两天才发现是极性反了。嗯,后来我学乖了,每次拿到新屏先看数据手册的 TE 时序图。
坑二:VSync 中断里做太多事
VSync 中断应该只做最轻量的事——设标志位、交换缓冲区。如果你在中断里做渲染、计算、甚至打印日志,中断时间过长,下一帧的 VSync 就错过了。我见过有人把整个 GUI 渲染放在 VSync 中断里,结果帧率直接掉到 15fps。
坑三:多缓冲时忘记同步
如果你用了三缓冲(前、中、后),VSync 只控制前缓冲和中缓冲的交换。后缓冲到中缓冲的传输由 DMA 或 CPU 控制。这两个过程必须协调好,否则中缓冲可能被覆盖。
4.7 小结
这一章的核心就一句话:缓冲区交换必须在消隐期进行,VSync 信号就是消隐期的“闹钟”。
没有 VSync 同步,双缓冲就白搭。撕裂该出现还是会出现。所以,不管你用哪个平台、哪款屏幕,第一件事就是找到 VSync 的等待方式。这是双缓冲机制能正常工作的前提。