3、刷新策略基础:全屏刷新原理、局部刷新原理、脏矩形算法
好,咱们今天聊点实在的。TouchGFX 的动画到底是怎么一帧一帧画出来的?说白了,就是「刷新策略」在背后干活。我刚开始接触嵌入式 GUI 时,总觉得屏幕刷新不就是把整张图怼上去吗?后来踩了坑才发现——全刷和局部刷,差别大了去了。
3.1 全屏刷新:简单粗暴,但代价不小
全屏刷新,顾名思义,每一帧都把整个 framebuffer 的内容全部推到显示屏上。这种方式在逻辑上最直接,代码也最好写。
核心流程:
- TouchGFX 渲染引擎绘制完一帧完整画面到 framebuffer
- LTDC(或 DMA2D)将整个 framebuffer 通过 RGB 接口发送给 LCD 驱动 IC
- LCD 驱动 IC 逐行扫描整屏,完成刷新
但问题来了——全屏刷新意味着每次都要传输 width × height × 像素深度 的数据量。举个例子,480×272 的屏幕,16 位色深,一帧就是 480×272×2 = 261,120 字节。如果刷新率是 60fps,每秒要传 15.6MB 的数据。这在低端 MCU 上,带宽压力非常大。
我曾经踩过的坑: 在一个 STM32F429 项目上,我图省事用了全屏刷新做动画,结果帧率死活上不去,CPU 占用率飙到 90%。后来才发现,大部分时间都耗在了刷屏上。嗯,从那以后我再也不敢无脑全刷了。
3.2 局部刷新:只改该改的地方
局部刷新的思路很简单——既然只有一小块区域在变化,为什么要把整个屏幕重画一遍?TouchGFX 的局部刷新机制,说白了就是「哪里变了刷哪里」。
它的工作流程是这样的:
- 引擎检测到某个控件(比如按钮、滑块)的状态发生变化
- 只更新 framebuffer 中对应的矩形区域
- 通过 LTDC 或 SPI 接口,仅将这部分数据发送给 LCD
你想想看,如果屏幕上只有一个进度条在动,局部刷新可能只需要传输几千字节,而不是几十万字节。这对带宽的节省是数量级的。
| 对比项 | 全屏刷新 | 局部刷新 |
|---|---|---|
| 数据传输量 | 整屏(大) | 仅脏区域(小) |
| CPU 占用 | 高 | 低 |
| 实现复杂度 | 简单 | 中等 |
| 适用场景 | 静态界面、全屏切换 | 动画、控件交互 |
我个人习惯: 在项目初期先用全屏刷新把功能跑通,等界面稳定了再切到局部刷新做优化。这样调试阶段少了很多麻烦。
3.3 脏矩形算法:局部刷新的核心
局部刷新听起来很美,但有个关键问题——引擎怎么知道「哪里变了」?答案就是脏矩形算法。
脏矩形(Dirty Rect)本质上是一个或多个矩形区域,记录了当前帧相对于上一帧发生变化的像素范围。TouchGFX 内部维护了一个脏矩形列表,每次渲染循环都会做这几件事:
- 标记脏区域: 当某个控件的位置、大小、内容发生变化时,引擎将其 bounding box 标记为「脏」
- 合并脏矩形: 如果有多个相邻或重叠的脏矩形,引擎会尝试合并,减少刷新次数
- 裁剪渲染: 只对脏矩形内的控件进行重绘,跳过未变化的部分
- 提交刷新: 将最终的脏矩形列表传给底层驱动,执行局部刷屏
这里有个细节值得注意——合并策略。如果两个脏矩形相隔很近,合并成一个大的矩形可能反而更高效,因为减少了多次刷新的开销。但如果它们离得很远,强行合并会导致大量未变化区域也被刷新,得不偿失。
避坑指南: 我曾经在一个仪表盘项目上,发现动画有撕裂感。排查了半天,原来是脏矩形合并策略太激进,把不相干的区域也刷了,导致帧率下降。后来我调整了合并阈值,问题就解决了。
3.4 三种策略的对比与选择
在实际项目中,全屏刷新和局部刷新并不是非此即彼的关系。TouchGFX 允许你根据场景动态切换。我个人建议这样选:
- 静态页面(菜单、设置页): 用全屏刷新,简单稳定
- 动态控件(滑块、进度条、图表): 用局部刷新 + 脏矩形,省带宽
- 全屏过渡动画(页面切换): 用全屏刷新,因为整个画面都在变
下面这张图展示了脏矩形算法在局部刷新中的核心位置:
3.5 实际项目中的权衡
最后说点实在的。你在选刷新策略时,不能只看理论,还得考虑硬件限制。比如:
- MCU 的 DMA 带宽: 如果 DMA 支持双缓冲 + 自动传输,局部刷新的优势会更明显
- LCD 接口类型: RGB 并行接口适合全刷,SPI 串口更适合局部刷
- 帧率要求: 60fps 的动画,局部刷新几乎是必须的;30fps 的静态菜单,全刷也够用
我建议: 在项目初期,先用 TouchGFX 的默认配置跑起来,然后用性能分析工具(比如 STM32CubeMonitor)看看瓶颈在哪。别一上来就搞局部刷新优化——先确保功能正确,再谈性能。