1. 课程导入

做嵌入式GUI优化这么多年,我见过最头疼的问题就是——界面卡顿。你辛辛苦苦画好了界面,一跑起来却像幻灯片。用户点个按钮,半秒后才响应。说实话,这种问题往往不是代码写得不好,而是你根本没搞懂卡顿到底是怎么发生的。

今天我们就从显示链路的最底层,把卡顿的根因扒个干净。

1.1 显示链路:一帧画面是怎么走到屏幕上的?

先看一张图,这是我个人习惯用来跟团队讲链路的结构。

嵌入式GUI显示链路(一帧的旅程) 应用层 GUI框架/业务逻辑 渲染引擎 合成/光栅化 帧缓冲 显存/双缓冲 显示控制器 DMA/时序生成 物理屏幕 LCD/OLED 💡 关键点: 每一帧画面必须依次经过这4个阶段,任何一个环节延迟都会导致帧丢失或刷新不同步。

这条链路,说白了就是:应用层产生画面 → 渲染引擎合成 → 写入帧缓冲 → 显示控制器读取 → 屏幕刷新。每一步都有坑。

1.2 帧丢失:为什么你的60fps变成了15fps?

帧丢失,就是本该在1秒内显示的60帧画面,实际只显示了30帧甚至更少。我在项目中遇到过最典型的场景:一个智能手表表盘,秒针每秒钟移动一次,结果秒针经常「跳秒」——从1秒直接跳到3秒,中间那帧丢了。

为什么会丢帧?三个核心原因:

丢帧原因 底层原理 典型场景
渲染超时 CPU/GPU在16.6ms内没完成一帧的渲染 复杂矢量图形、大量文字重排
帧缓冲竞争 渲染线程和显示控制器同时访问同一块缓冲 单缓冲模式下频繁刷新
垂直同步等待 渲染完成但错过了当前VSync窗口 双缓冲交换时机不当

你想想看,如果你的MCU主频只有200MHz,渲染一帧需要20ms,而屏幕刷新周期是16.6ms(60Hz),那每4帧里就有1帧来不及渲染。这就是最直接的丢帧。

💡 我的经验:排查丢帧时,先看渲染耗时。在渲染入口和出口打时间戳,如果单帧渲染超过16ms,先优化渲染,别急着动缓冲策略。

1.3 刷新不同步:撕裂和闪烁是怎么来的?

刷新不同步,比丢帧更恶心。画面不卡,但屏幕中间有一条撕裂线,上半部分是上一帧,下半部分是下一帧。或者整个画面忽明忽暗地闪烁。

我记得有一次做车载仪表盘,转速表指针在快速摆动时,指针边缘出现明显的锯齿撕裂。排查了半天,发现是显示控制器在读取帧缓冲时,渲染引擎正在往同一块缓冲里写数据——读和写打架了。

刷新不同步的两种典型表现:

  • 撕裂(Tearing):显示控制器读取帧缓冲时,缓冲内容被渲染引擎更新,导致一帧画面包含两个不同时刻的内容。
  • 闪烁(Flicker):缓冲切换时,显示控制器读到了空缓冲或未完成的缓冲,导致画面短暂空白或颜色异常。

为什么会这样?因为显示控制器是独立于CPU运行的硬件模块。它按照固定的时钟频率(比如60Hz)从帧缓冲中读取像素数据,一行一行地扫描到屏幕上。如果渲染引擎在扫描过程中修改了缓冲内容,那扫描到一半时,后面的像素就变成了新数据——撕裂就出现了。

1.4 双缓冲与VSync:解决不同步的经典方案

解决刷新不同步,业界最成熟的做法就是双缓冲 + VSync。我建议所有嵌入式GUI项目都默认采用这个方案,除非你的硬件资源实在紧张到连两个缓冲都开不出来。

双缓冲的工作原理:

  1. 前台缓冲(Front Buffer):显示控制器正在读取的缓冲,内容稳定输出到屏幕。
  2. 后台缓冲(Back Buffer):渲染引擎正在绘制的缓冲,内容对外不可见。
  3. 交换时机:在VSync信号到来时,交换前后台缓冲的指针。
双缓冲 + VSync 工作流程 VSync VSync VSync VSync 前台缓冲(显示中) 前台缓冲(显示中) 前台缓冲(显示中) 后台缓冲(渲染中) 后台缓冲(等待交换) 后台缓冲(渲染中)

嗯,这里要注意:双缓冲不是万能的。如果渲染一帧的时间超过了VSync周期,即使有双缓冲,也会出现丢帧。因为后台缓冲还没渲染完,前台缓冲又不能被覆盖,系统只能重复显示上一帧。

⚠️ 我曾经踩过的坑:在STM32F429上做双缓冲,以为开了LTDC的VSync中断就万事大吉。结果发现中断处理函数里做了太多事情,导致VSync信号来了之后,缓冲交换延迟了2ms。这2ms的延迟,在60Hz下就是12%的帧时间浪费。后来我把交换操作放在中断的最前面,其他处理延后执行,问题解决。

1.5 帧率与刷新率:两个容易混淆的概念

很多工程师把帧率和刷新率混为一谈。我刚开始做GUI时也犯过这个错。其实它们是两码事:

概念 定义 典型值 谁控制
帧率(FPS) 应用层每秒生成的画面数量 30fps / 60fps CPU/GPU渲染能力
刷新率(Hz) 显示控制器每秒从缓冲读取并显示画面的次数 60Hz / 120Hz 硬件时序(晶振/PLL)

帧率超过刷新率时,多余的帧会被丢弃,造成资源浪费。帧率低于刷新率时,屏幕会重复显示同一帧,看起来就是卡顿。最理想的状态是帧率等于刷新率,并且每一帧都在VSync到来前准备好。

1.6 实战排查:如何定位卡顿的根因?

说了这么多理论,来点实际的。如果你现在遇到卡顿问题,按这个步骤排查:

  1. 先确认是丢帧还是不同步:肉眼观察,撕裂是不同步,跳帧是丢帧。
  2. 测量渲染耗时:在渲染开始和结束处加GPIO翻转,用示波器看高电平持续时间。
  3. 检查缓冲模式:单缓冲还是双缓冲?如果是单缓冲,先改成双缓冲。
  4. 验证VSync:用逻辑分析仪抓VSync信号,看渲染完成时间是否在VSync窗口内。
  5. 分析帧缓冲写入:检查DMA传输是否被中断打断,或者CPU是否在写缓冲时被高优先级任务抢占。

💡 我的小技巧:在帧缓冲的角落画一个不断递增的计数器,每渲染一帧加1。如果屏幕上的计数器跳过了某个数字,说明那一帧丢了。这个方法比任何调试工具都直观。