1、卡顿现象的本质:为什么你的GUI会掉帧?从显示原理讲起。

做嵌入式GUI开发,最让人头疼的问题是什么?

我个人觉得,排第一的肯定是“卡顿”。

你辛辛苦苦把界面画好了,菜单、按钮、动画都跑起来了。结果一上电,滑动菜单像PPT翻页,进度条一顿一顿的。用户骂街,产品经理拍桌子,你只能对着示波器发呆。

为什么会这样?

说白了,就是你的屏幕刷新速度,跟不上你画图的速度。或者反过来,你画图的速度,跟不上屏幕刷新的节奏。

嗯,这里要注意,这两个“跟不上”是两码事。咱们得从最底层的显示原理讲起。

1.1 屏幕刷新的“节拍器”:VSync

你想想看,一块LCD屏幕,它显示画面是有固定节奏的。这个节奏由硬件决定,比如60Hz的屏幕,每秒刷新60次。每次刷新,屏幕从上到下、从左到右,逐行扫描像素。

这个“开始扫下一帧”的信号,就叫垂直同步信号,也就是VSync。

我刚开始做GUI的时候,根本没在意这个信号。觉得只要把数据写到显存里,屏幕自然就显示了。结果呢?画面撕裂。

什么叫撕裂?就是屏幕上半部分还是上一帧的画面,下半部分已经变成下一帧了。因为你在屏幕正在扫描的时候,修改了显存里的数据。

核心概念:VSync是屏幕刷新的“发令枪”。GUI必须在VSync间隙完成绘制,否则就会出现撕裂或卡顿。

1.2 单缓冲区的“死穴”:一边画一边擦

最简单的方案,就是只有一个显示缓冲区。MCU直接往这个缓冲区里写像素数据,LCD控制器直接从同一个缓冲区读数据去显示。

这就像只有一个黑板。老师(MCU)在上面写板书,学生(LCD)在下面抄笔记。老师还没写完,学生就开始抄了。结果抄到一半,老师把前面的擦了写新的。学生就懵了。

这就是单缓冲区的死穴:读写冲突。

缓冲区模式 优点 缺点
单缓冲区 内存占用小,实现简单 画面撕裂、闪烁、卡顿
双缓冲区 画面平滑,无撕裂 内存占用翻倍,需要同步机制

我在一个STM32的项目上就踩过这个坑。当时做的是一个温控器的界面,数字跳动更新。单缓冲区,每次更新数字,整个数字区域都在闪。用户投诉说“这破玩意儿是不是坏了”。

后来我加了双缓冲,问题才解决。

1.3 掉帧的本质:绘制时间 > 刷新周期

好,现在假设你用了双缓冲。为什么还是会卡顿?

因为掉帧。

掉帧的本质很简单:你画完一帧的时间,超过了屏幕刷新一帧的时间。

举个例子。60Hz的屏幕,一帧的时间是16.67ms。如果你的GUI绘制一帧需要30ms,那会发生什么?

  • 第1个VSync到来:屏幕显示缓冲区A,MCU开始往缓冲区B画。
  • 第2个VSync到来(16.67ms后):屏幕想切换显示缓冲区B,但MCU还没画完!
  • 结果:屏幕只能继续显示缓冲区A的旧画面。缓冲区B的“半成品”不能拿出来用。
  • 第3个VSync到来(33.33ms后):MCU终于画完了。屏幕切换到缓冲区B。

你看,本来应该显示第2帧的时候,屏幕还是第1帧。第3帧的时候才显示第2帧。这就丢了一帧。

丢帧多了,人眼就感觉“卡”了。

避坑指南:我曾经以为只要把绘制函数写得快一点就行。后来发现,真正的瓶颈往往不在绘制本身,而在数据传输。比如从MCU内部SRAM往外部SDRAM刷数据,或者通过SPI往LCD发数据。这些总线传输的时间,才是大头。

1.4 双缓冲的“正确姿势”:交换 vs 拷贝

双缓冲有两种实现方式,很多人搞混。

  • 交换(Swap):两个缓冲区,一个叫“前台”,一个叫“后台”。MCU画后台,LCD读前台。画完后,交换指针。前台变后台,后台变前台。速度快,只需要改个指针。
  • 拷贝(Copy):只有一个显存。MCU在另一个内存区域画好,然后一次性拷贝到显存。这种方式,拷贝本身就很耗时。

我个人习惯用交换。只要硬件支持(比如LTDC控制器可以切换帧缓冲地址),交换几乎是零成本的。

但很多低端MCU不支持硬件交换。那就只能用拷贝。这时候,拷贝的时间必须严格控制,不能超过VSync的间隔。

// 伪代码:双缓冲交换
void VSync_IRQHandler() {
    if (draw_done) {
        // 交换缓冲区指针
        LCD_SetFrameBuffer(back_buffer);
        // 交换前后台索引
        temp = front_buffer;
        front_buffer = back_buffer;
        back_buffer = temp;
        draw_done = 0;
        // 通知绘制线程开始画下一帧
        StartDrawing();
    }
}

1.5 一张图看懂卡顿链路

下面这张图,是我自己总结的“卡顿链路”。每次排查问题,我都会对着它捋一遍。

嵌入式GUI卡顿链路分析 嵌入式GUI卡顿链路分析 应用层绘制 UI组件、图形渲染 缓冲区写入 后台缓冲区填充 数据传输 SPI/8080/LTDC VSync同步 等待垂直消隐 缓冲区交换 指针切换/拷贝 LCD显示 像素扫描输出 触发绘制 ⚠ 卡顿点1 绘制时间 > 16.67ms 导致丢帧 ⚠ 卡顿点2 数据传输带宽不足 总线占用过高

从这张图可以看出来,卡顿可能发生在任何一个环节。应用层画得太慢,缓冲区写入太慢,数据传输太慢,或者VSync同步没做好。

排查的时候,我习惯先看“数据传输”这一步。因为大部分嵌入式GUI的瓶颈,都在这里。尤其是用SPI接口的LCD,时钟频率跑不高,数据量一大就卡。

警告:不要盲目优化绘制算法。先确认你的硬件传输带宽够不够。比如320x240的屏幕,16位色,60帧,需要的带宽是:320 * 240 * 2 * 60 = 9.216 MB/s。如果你的SPI只能跑到18MHz,理论带宽才2.25 MB/s,那无论如何都会卡。这是物理瓶颈,软件救不了。

1.6 小结:卡顿的根源就两个

讲到这里,你应该明白了。卡顿的本质,归根结底就两个原因:

  1. 绘制超时:一帧画太久,超过了VSync周期。
  2. 同步失败:双缓冲交换时机不对,或者根本没有双缓冲。

后面的章节,我会带着你一个一个解决这些问题。从硬件选型、驱动优化,到绘制算法、调度策略,咱们一步步把卡顿干掉。

嗯,今天就先到这里。记住这两个根源,后面就好办了。