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 一张图看懂卡顿链路
下面这张图,是我自己总结的“卡顿链路”。每次排查问题,我都会对着它捋一遍。
从这张图可以看出来,卡顿可能发生在任何一个环节。应用层画得太慢,缓冲区写入太慢,数据传输太慢,或者VSync同步没做好。
排查的时候,我习惯先看“数据传输”这一步。因为大部分嵌入式GUI的瓶颈,都在这里。尤其是用SPI接口的LCD,时钟频率跑不高,数据量一大就卡。
警告:不要盲目优化绘制算法。先确认你的硬件传输带宽够不够。比如320x240的屏幕,16位色,60帧,需要的带宽是:320 * 240 * 2 * 60 = 9.216 MB/s。如果你的SPI只能跑到18MHz,理论带宽才2.25 MB/s,那无论如何都会卡。这是物理瓶颈,软件救不了。
1.6 小结:卡顿的根源就两个
讲到这里,你应该明白了。卡顿的本质,归根结底就两个原因:
- 绘制超时:一帧画太久,超过了VSync周期。
- 同步失败:双缓冲交换时机不对,或者根本没有双缓冲。
后面的章节,我会带着你一个一个解决这些问题。从硬件选型、驱动优化,到绘制算法、调度策略,咱们一步步把卡顿干掉。
嗯,今天就先到这里。记住这两个根源,后面就好办了。