3. 刷新卡顿的定义与现象
帧率和刷新率,这两个词你肯定天天见。但说实话,很多嵌入式工程师把它们混为一谈。我见过不少项目,明明硬件性能够,就因为这两个参数没对齐,界面卡得像幻灯片。
先理清概念。刷新率是屏幕硬件的属性,单位 Hz,表示屏幕每秒能刷新多少次。帧率是软件渲染的速度,单位 fps,表示 GPU 或 CPU 每秒能生成多少帧画面。两者不是一回事,但必须协同工作。
3.1 垂直同步:为什么画面会撕裂?
假设你的屏幕刷新率是 60Hz,每 16.6ms 刷新一次。如果 GPU 在屏幕刷新到一半时,突然把新的帧数据写入显存,会发生什么?
屏幕上半部分还是旧帧,下半部分已经变成新帧了。这就是画面撕裂(Tearing)。
核心问题:GPU 的写入和屏幕的读取,没有同步机制。
垂直同步(V-Sync)就是来解决这个问题的。它让 GPU 在屏幕完成一次完整的刷新(垂直消隐期)之后,才允许交换缓冲区。说白了,就是让 GPU 等一等屏幕。
我个人习惯把 V-Sync 分成两种:
- 强制 V-Sync:GPU 必须等到垂直消隐期才能交换缓冲区。帧率会被锁定在刷新率的整数倍,比如 60fps、30fps、20fps。
- 自适应 V-Sync:帧率高于刷新率时开启同步,低于刷新率时自动关闭,避免卡顿。
注意:强制 V-Sync 在帧率低于刷新率时,会导致帧率直接腰斩。比如你只能渲染 55fps,V-Sync 会强制降到 30fps。我曾经在一个车载仪表盘项目里踩过这个坑,用户一踩油门,动画直接掉到 30fps,视觉上明显卡顿。
3.2 双缓冲:最基础的解决方案
双缓冲的思路很简单:准备两个缓冲区,一个叫前缓冲区(Front Buffer),一个叫后缓冲区(Back Buffer)。
屏幕读取前缓冲区的内容进行显示。GPU 在后缓冲区里绘制下一帧。绘制完成后,交换两个缓冲区的指针。
配合 V-Sync,流程是这样的:
- GPU 在后缓冲区绘制完成。
- 等待垂直消隐期。
- 交换前后缓冲区指针。
- 屏幕开始读取新的前缓冲区。
- GPU 开始绘制新的后缓冲区。
你想想看,这个模型有什么问题?
如果 GPU 绘制一帧的时间超过了 16.6ms(60Hz 下),那它就会错过当前的垂直消隐期。只能等下一个。这期间屏幕只能重复显示旧帧。这就是卡顿的来源。
我的经验:在低端 MCU 上做 GUI,双缓冲 + V-Sync 是最稳妥的方案。但前提是你的帧率必须稳定高于刷新率。如果做不到,画面会频繁掉帧。我建议在项目初期就用性能分析工具跑一遍最坏情况下的渲染耗时。
3.3 三缓冲:解决掉帧的利器
三缓冲在双缓冲的基础上,增加了一个中间缓冲区。总共三个:前缓冲区、中间缓冲区、后缓冲区。
流程变成这样:
- 屏幕读取前缓冲区。
- GPU 绘制到后缓冲区。
- 当后缓冲区绘制完成,如果前缓冲区正在被屏幕读取,就把后缓冲区的内容复制到中间缓冲区。
- GPU 立刻开始绘制下一帧到后缓冲区。
- 垂直消隐期到来时,交换前缓冲区和中间缓冲区。
好处很明显:GPU 不用傻等屏幕了。即使绘制一帧需要 20ms,三缓冲也能让 GPU 持续工作,不会出现双缓冲那种「等一帧、丢一帧」的情况。
代价:多一个缓冲区,意味着多占一份显存。对于嵌入式设备,显存通常是共享的 DDR 或 SRAM,非常宝贵。另外,三缓冲会增加 1-2 帧的输入延迟。
我曾经在一个智能家居中控屏项目里做过对比测试。屏幕分辨率 1280x720,刷新率 60Hz。双缓冲下,复杂页面切换偶尔掉到 45fps,肉眼可见的卡。换成三缓冲后,帧率稳定在 60fps,但显存占用从 4.5MB 涨到了 6.75MB。嗯,这个取舍要看你的硬件底子。
3.4 三种机制的对比与取舍
直接上表格,一目了然:
| 机制 | 显存占用 | 输入延迟 | 画面撕裂 | 帧率稳定性 | 适用场景 |
|---|---|---|---|---|---|
| 无同步(单缓冲) | 低 | 低 | 严重 | 差 | 极少使用 |
| 双缓冲 + V-Sync | 中 | 中 | 无 | 中(帧率需稳定高于刷新率) | MCU、低端 MPU |
| 三缓冲 + V-Sync | 高 | 高 | 无 | 高(允许帧率波动) | 中高端 MPU、带 GPU 的 SoC |
怎么选?我个人的经验法则:
- 如果你的 MCU 只有几百 KB 的 SRAM,老老实实用双缓冲。别想三缓冲,显存不够。
- 如果你的 SoC 有独立的 GPU 和 64MB 以上的专用显存,上三缓冲。用户体验提升明显。
- 如果帧率能稳定在刷新率的 1.5 倍以上,双缓冲就够了。三缓冲的收益不大。
避坑指南:我曾经在一个项目里,为了追求极致的帧率,在双缓冲下关闭了 V-Sync。结果画面撕裂严重,用户反馈说「屏幕像被撕开了」。后来我加上了自适应 V-Sync,撕裂消失,帧率波动也在可接受范围内。记住,画面撕裂比偶尔掉帧更让人难受。
3.5 核心流程:一张图看懂
下面这张 SVG 图,把三种机制的核心流程画出来了。你仔细看,箭头代表数据流向,颜色代表不同的缓冲区状态。
3.6 实际项目中的取舍建议
说了这么多理论,落地才是关键。我根据实际项目经验,给你几个具体的建议:
- 先测最坏情况下的渲染耗时。如果最坏情况低于 16.6ms(60Hz),双缓冲就够了。如果偶尔超过,考虑三缓冲。
- 显存不够怎么办?用双缓冲 + 部分更新(Dirty Rect)。只更新变化区域,减少缓冲区大小。我在一个 480x272 的屏上,用这个方法把显存占用从 600KB 降到了 200KB。
- 输入延迟敏感的场景(比如触控笔、游戏):优先用双缓冲。三缓冲多出来的 1-2 帧延迟,在触控场景下能感觉到。
- 动画流畅度优先的场景(比如菜单切换、过渡动画):上三缓冲。掉帧比延迟更影响体验。
一个小技巧:在调试阶段,可以在屏幕上叠加一个帧率显示。我习惯用 GPIO 翻转 + 示波器来测量实际的帧间隔。软件统计的帧率往往不准,因为 V-Sync 会掩盖掉一些掉帧。
嗯,帧率和刷新率的机制就讲到这里。记住一句话:同步是手段,流畅是目的。不要为了同步而同步,要根据你的硬件和场景,选择合适的方案。