4、页交换的时机:垂直同步(VSync)的作用,为什么要在消隐期交换缓冲区,否则依然会撕裂

好,咱们接着聊。上一章我们把双缓冲的机制讲清楚了,你知道了前缓冲和后缓冲怎么配合。但有个关键问题我一直悬着没说——什么时候交换?

你想想看,如果你在屏幕正在从上往下扫描像素的时候,突然把后缓冲切到前缓冲,会发生什么?

屏幕上半截还是老画面,下半截已经变成新画面了。这就是撕裂(Tearing)。我早年做一款工业平板的时候,就被这个撕裂坑过一次。画面一滚动,屏幕中间一条明显的错位线,客户当场就拍了照片发过来……嗯,从那以后我再也不敢忽略交换时机了。

4.1 屏幕是怎么画出一帧的?

要理解交换时机,得先知道屏幕的刷新过程。说白了,屏幕不是一瞬间把整张图全亮出来的。它是逐行扫描的。

  • 从左到右:每行像素从左扫到右,这叫水平扫描。
  • 从上到下:扫完一行,换下一行,这叫垂直扫描。
  • 扫完一帧:所有行都扫完了,这一帧就结束了。

扫完最后一行的右下角之后,电子枪(或者 LCD 的驱动电路)需要回到左上角,准备下一帧。这个回扫的时间段,就叫消隐期(Blanking Interval)。

我个人习惯把屏幕刷新想象成一台老式打印机:喷头从左到右喷一行,然后纸往上走一点,再喷下一行。喷完一整页,纸退回去,准备下一页。那个“纸退回去”的时间,就是消隐期。

4.2 垂直同步(VSync)到底是什么?

VSync 是 Vertical Synchronization 的缩写。它其实就是一个硬件信号,由显示控制器在特定时刻拉高或拉低。

这个信号什么时候触发?就在消隐期开始的那一刻。

具体来说,VSync 信号在每帧的消隐期开始时产生。它告诉 GPU 或 MCU 的显示控制器:“上一帧已经完整送出去了,现在屏幕不干活了,你可以安全地交换缓冲区了。”

核心结论:VSync 信号 = 安全的缓冲区交换窗口。只有在 VSync 到来时交换,才能保证画面完整。

4.3 为什么不在消隐期交换就会撕裂?

咱们用一张图把这个问题彻底讲透。下面是我画的一个时序图,展示了三种情况:

VSync 与缓冲区交换时序对比 VSync 与缓冲区交换时序对比 情况A:正确交换(无撕裂) 帧1 完整显示 VSync 帧2 完整显示 VSync 帧3 完整显示 交换 交换 情况B:非消隐期交换(撕裂) 帧1 上半部分 撕裂 帧2 下半部分 错误交换 情况C:无 VSync 同步(随机撕裂) 帧1 撕裂 帧2 撕裂 帧3 说明: • 情况A:在 VSync 信号(消隐期)交换缓冲区,每帧完整显示,无撕裂。 • 情况B:在屏幕扫描过程中交换,上半帧是旧画面,下半帧是新画面,产生撕裂。 • 情况C:完全不等待 VSync,撕裂位置随机出现,画面质量最差。

看到了吗?情况 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 的等待方式。这是双缓冲机制能正常工作的前提。