帧缓冲与显示原理:显存架构、CRTC扫描、VSync与撕裂现象

做显示驱动,绕不开一个核心问题:数据怎么从内存跑到屏幕上?

我刚开始接触这块时,以为就是简单地把像素数据往显存里一丢,屏幕自然就亮了。后来被撕裂、闪屏、花屏轮番教育过,才老老实实把帧缓冲的底层原理啃了一遍。

这一章,我们就来拆解显示链路的核心环节。你搞懂了这些,后面做双缓冲优化才有底气。

1. 显存架构:帧缓冲的物理基础

帧缓冲(Framebuffer),说白了就是一块专门存放像素数据的内存区域。它可以是系统内存里划出来的一块,也可以是独立的显存(VRAM)。

我个人习惯把帧缓冲理解成「屏幕的底片」。你往底片上画什么,屏幕就显示什么。

类型 位置 特点 常见场景
系统内存帧缓冲 DDR 共享带宽,延迟较高 低端嵌入式、早期Linux fbdev
独立显存帧缓冲 GDDR/HBM 高带宽,低延迟 桌面GPU、游戏主机
显存映射帧缓冲 DDR + 显存 通过DMA搬运,折中方案 移动SoC、部分ARM平台

这里有个关键点:帧缓冲的格式。比如 RGB565、ARGB8888、YUV420,每种格式每个像素占用的字节数不同。我曾在项目中遇到过一个问题:配置了 1920x1080 的分辨率,ARGB8888 格式,结果屏幕只显示上半部分。查了半天,发现是帧缓冲大小只分配了 1920x1080x3 字节,忘了 Alpha 通道占的 1 个字节。

注意:帧缓冲大小 = 分辨率宽度 × 分辨率高度 × 每像素字节数。别把 stride(行跨度)和 width 搞混了,硬件通常要求行对齐到 32 字节或 64 字节。

2. CRTC扫描:屏幕是怎么一帧一帧刷新的?

CRTC(Cathode Ray Tube Controller)这个名字虽然老,但概念至今沿用。它负责从帧缓冲里一行一行地读取像素数据,然后送给显示接口。

你想想看,屏幕刷新不是瞬间完成的。CRTC 的工作流程是这样的:

  1. 水平扫描:从左到右扫完一行像素。
  2. 水平消隐:扫完一行后,电子枪(或等效逻辑)回到下一行起点,这段时间不传输有效像素。
  3. 垂直扫描:一行一行往下扫,直到扫完整个画面。
  4. 垂直消隐:扫完最后一帧后,回到左上角起点,准备下一帧。

嗯,这里要注意:消隐期(Blanking Interval) 是驱动做优化的黄金窗口。因为这段时间 CRTC 不读取帧缓冲,你可以安全地切换缓冲区、更新配置,不会产生撕裂。

避坑指南:我曾经在垂直消隐期外更新了帧缓冲地址,结果屏幕中间出现了一条明显的撕裂线。后来我强制在 VSync 中断里做更新,问题就解决了。

3. VSync:垂直同步的真相

VSync(Vertical Synchronization)就是垂直同步信号。它发生在 CRTC 完成一帧扫描、进入垂直消隐期的那个瞬间。

为什么需要 VSync?

  • 硬件层面:VSync 是一个硬件中断信号,告诉驱动「上一帧已经显示完了,你可以准备下一帧了」。
  • 软件层面:应用程序等待 VSync 信号,再提交新帧,避免在屏幕刷新中途修改帧缓冲。

我见过不少开发者把 VSync 和帧率控制混为一谈。其实 VSync 只是同步机制,帧率是应用层决定的。你开了 VSync,但渲染速度跟不上,帧率反而会掉到 30fps 甚至更低——这就是常见的「卡 VSync」现象。

4. 撕裂现象:为什么画面会裂开?

撕裂(Tearing)是显示驱动里最直观的 bug。屏幕上半部分显示的是旧帧,下半部分显示的是新帧,中间有一条错位的分界线。

为什么会这样?

说白了,就是CRTC 在读取帧缓冲的时候,你往同一个缓冲区里写了新数据。CRTC 读到一半,内容变了,前后不一致,画面就撕裂了。

我举个例子:

  • CRTC 开始扫描第 500 行。
  • 此时应用程序往帧缓冲里写入了新帧。
  • CRTC 继续扫描第 501 行时,读到的已经是新帧的数据。
  • 结果:第 1-500 行是旧帧,第 501-1080 行是新帧,中间一条撕裂线。
核心结论:撕裂的本质是「读写冲突」。解决思路只有两个:要么让读写不同步(双缓冲),要么让写操作避开读操作(VSync 同步)。

5. 核心流程:从帧缓冲到屏幕显示

为了让你更直观地理解整个链路,我画了一张流程图。这张图我每次给团队新人讲驱动时都会用,你仔细看看。

帧缓冲 → CRTC → 显示面板 核心流程 应用程序 渲染新帧 帧缓冲 像素数据存储 CRTC 控制器 逐行扫描读取 屏幕 ⏱ 扫描时序说明: • 水平扫描:从左到右,每行约 1.5μs ~ 5μs(取决于分辨率与刷新率) • 水平消隐:行尾回到行首,约 0.5μs ~ 2μs • 垂直扫描:从上到下,一帧约 8.3ms(60Hz)或 16.7ms(30Hz) • 垂直消隐:帧尾回到帧首,约 0.5ms ~ 1.5ms(这是驱动做缓冲切换的黄金窗口) ⚠ 撕裂发生条件: 当 CRTC 正在扫描第 N 行时,应用程序向同一帧缓冲写入新数据。 第 1~N 行显示旧帧,第 N+1~最后一行显示新帧 → 画面出现水平撕裂线。

6. 关键参数:刷新率与像素时钟

做驱动适配时,你免不了要和显示时序参数打交道。我整理了一份常用对照表,你调试时可以直接参考。

分辨率 刷新率 像素时钟 水平总像素 垂直总行数
1920x1080 60Hz 148.5 MHz 2200 1125
1920x1080 30Hz 74.25 MHz 2200 1125
1280x720 60Hz 74.25 MHz 1650 750
1024x768 60Hz 65.0 MHz 1344 806

注意看,水平总像素和垂直总行数都大于实际分辨率。多出来的部分就是消隐区。像素时钟 = 水平总像素 × 垂直总行数 × 刷新率。这个公式你记牢了,调时序时经常要用到。

7. 避坑总结:驱动开发者的第一课

最后,我把自己踩过的坑总结成几条原则,你记下来,能省不少调试时间。

  • 不要在 VSync 中断之外更新帧缓冲地址——我曾经图省事,在普通线程里直接改寄存器,结果撕裂线飘了三天才找到原因。
  • 帧缓冲对齐很重要——很多硬件要求帧缓冲起始地址按 4KB 或 1MB 对齐,不对齐直接黑屏或花屏。
  • 消隐期不是无限长的——垂直消隐期通常只有几百微秒到几毫秒,你的缓冲区切换逻辑必须在这个时间内完成,否则会错过 VSync。
  • 多缓冲区不是越多越好——双缓冲够用就别上三缓冲,多一级缓冲多一级延迟,游戏场景下尤其明显。