4、动画与特效优化:帧率控制、动画曲线选择、减少重绘区域、硬件加速利用
动画做得好,用户觉得流畅。动画做得不好,CPU 直接拉满,界面卡成幻灯片。
我在 TouchGFX 项目里见过太多「为了动画而动画」的代码。一个简单的按钮缩放,非要每帧都重绘整个屏幕。结果呢?60fps 的项目硬生生掉到 20fps。用户一滑动,画面就撕裂。
这一章,咱们就聊聊怎么让动画跑得又快又顺。说白了,就是四个字:省着点用。
核心原则:动画优化的本质不是「让 MCU 跑得更快」,而是「让 MCU 少干没用的活」。每减少一次不必要的重绘,帧率就多一分保障。
4.1 帧率控制:别让 MCU 做无用功
帧率控制,是动画优化的第一道防线。很多人一上来就设 60fps,觉得越高越好。其实不是。
人眼对 30fps 以上的动画就已经觉得流畅了。60fps 和 30fps 的差别,在大多数嵌入式场景下根本看不出来。但 CPU 的负载差别,那可是实打实的——差了一倍。
我的经验:在 STM32F429 上做过一个仪表盘项目。指针旋转动画用 60fps 跑,CPU 占用率 78%。降到 30fps 后,CPU 占用率只有 32%,用户完全没察觉区别。省下来的算力,全给了后台数据采集。
TouchGFX 里控制帧率,主要有两种方式:
| 方式 | 实现方法 | 适用场景 |
|---|---|---|
| 固定帧率 | 在 TouchGFXConfiguration.cpp 中设置 setFrameRateCompensation(true/false),配合 OSWrappers::waitForVSync() 实现固定刷新 |
动画节奏固定、对时序要求严格的场景(如视频播放、波形显示) |
| 自适应帧率 | 根据上一帧的实际渲染耗时,动态调整下一帧的等待时间。用 Application::getInstance()->setFrameRate(30) 动态切换 |
UI 交互为主、动画不密集的场景(如菜单切换、弹窗动画) |
我个人习惯用自适应帧率。为什么?因为嵌入式系统的负载不是恒定的。数据采集来了,CPU 忙一下;用户没操作,CPU 闲得很。自适应帧率能自动平衡。
// 自适应帧率示例:根据 CPU 负载动态调整
void updateAnimationFrameRate() {
static uint32_t lastFrameTime = 0;
uint32_t currentTime = HAL::getInstance()->getCPUTicks();
uint32_t elapsed = currentTime - lastFrameTime;
if (elapsed > 50) { // 上一帧耗时超过 50ms
// 降低帧率到 20fps
Application::getInstance()->setFrameRate(20);
} else if (elapsed < 16) { // 上一帧耗时小于 16ms
// 可以提升到 30fps
Application::getInstance()->setFrameRate(30);
}
lastFrameTime = currentTime;
}
注意:不要频繁切换帧率。我曾经在一个项目里每帧都判断是否切换帧率,结果帧率在 20 和 30 之间来回跳,动画反而更卡了。建议至少保持 5 帧的稳定期再切换。
4.2 动画曲线选择:不只是好看
动画曲线,很多人只关心「好不好看」。其实它直接影响性能。
你想想看,一个 EaseInOut 曲线,在动画开始和结束时速度慢,中间速度快。这意味着什么?意味着中间那几帧,物体的位移变化很大,重绘区域也很大。如果 MCU 性能不够,中间帧就会掉帧。
TouchGFX 内置了多种动画曲线,我按性能开销排了个序:
| 曲线类型 | 性能开销 | 视觉效果 | 推荐场景 |
|---|---|---|---|
EasingEquations::linear |
极低 | 匀速,略显生硬 | 进度条、滚动列表 |
EasingEquations::cubicEaseInOut |
低 | 平滑,自然 | 按钮缩放、页面切换 |
EasingEquations::backEaseOut |
中 | 带轻微回弹,生动 | 弹窗出现、通知提示 |
EasingEquations::bounceEaseOut |
高 | 弹跳效果,活泼 | 游戏化交互、特殊动效 |
EasingEquations::elasticEaseOut |
极高 | 橡皮筋式弹性 | 慎用,仅限高性能平台 |
我在项目中遇到过一个问题:用 bounceEaseOut 做了一个按钮点击动画,在 STM32F746 上跑得好好的,换到 STM32F429 上就卡。后来发现是 bounceEaseOut 在每帧都要做多次三角函数计算,F429 的 FPU 扛不住。
建议:如果 MCU 不带硬件 FPU,或者主频低于 200MHz,尽量用 cubicEaseInOut 或 linear。它们只用加减乘除,不需要三角函数。效果也不差。
4.3 减少重绘区域:脏矩形机制的精髓
这是 TouchGFX 性能优化的核心。说白了,就是「只画变了的部分」。
TouchGFX 内部有一个脏矩形(Dirty Rectangle)机制。当一个控件发生变化时,它不会重绘整个屏幕,而是只重绘那个控件所在的矩形区域。但问题是——很多开发者不小心破坏了这种机制。
常见的「破坏行为」有哪些?
- 全屏 invalidate: 调用
invalidate()而不是invalidateRect()。前者重绘整个控件,后者只重绘指定区域。 - 透明背景的动画: 如果一个透明控件在移动,它背后的所有内容都需要重绘。重绘区域会叠加,最终可能覆盖半个屏幕。
- 重叠控件过多: 多个控件叠在一起,每个都做动画。脏矩形会合并成一个巨大的矩形,等于全屏重绘。
// 错误做法:全屏 invalidate
void moveButton(int newX, int newY) {
myButton.invalidate(); // 重绘整个按钮区域,但可能触发父容器全屏重绘
myButton.setX(newX);
myButton.setY(newY);
myButton.invalidate(); // 再次全屏重绘
}
// 正确做法:只 invalidate 变化区域
void moveButton(int newX, int newY) {
// 先 invalidate 旧位置
myButton.invalidateRect(myButton.getRect());
myButton.setX(newX);
myButton.setY(newY);
// 再 invalidate 新位置
myButton.invalidateRect(myButton.getRect());
}
我曾经踩过的坑:在一个多语言切换的项目里,每次切换语言都调用 invalidate() 刷新所有文本控件。结果切换一次要 200ms。后来改成只 invalidate 文本内容变化的区域,时间降到了 30ms。记住:invalidateRect() 是你的朋友。
还有一个技巧:合并脏矩形。如果多个控件在同一帧内发生变化,TouchGFX 会自动合并它们的脏矩形。但合并后的矩形可能比实际变化区域大很多。我建议把位置相近的控件放在同一个容器里,这样它们的脏矩形更容易合并成紧凑的区域。
4.4 硬件加速利用:让硬件干活,CPU 歇着
现代 MCU 都带硬件图形加速器。STM32 的 DMA2D、Chrom-ART、LTDC,NXP 的 PXP,都是好东西。但很多人不会用,或者不知道怎么在 TouchGFX 里用。
TouchGFX 对硬件加速的支持,主要体现在这几个方面:
| 硬件模块 | TouchGFX 中的用途 | 性能提升 |
|---|---|---|
| DMA2D | 图像拷贝、颜色格式转换、Alpha 混合、图像填充 | 2-5 倍 |
| Chrom-ART (DMA2D 增强版) | 硬件加速的图像缩放、旋转、L8/L4 调色板解码 | 3-8 倍 |
| LTDC | 图层混合、显示时序控制、双缓冲切换 | 释放 CPU 显示负载 |
| GPU2D (部分高端 MCU) | 矢量图形渲染、复杂路径填充 | 10 倍以上 |
怎么在 TouchGFX 里开启硬件加速?其实很简单:
- 在
TouchGFXHAL.cpp中,确保setDMAType()设置为对应的硬件类型。 - 在
STM32DMA.cpp中,实现setupDataCopy()、setupDataFill()等函数,调用硬件 API。 - 在
TouchGFXConfiguration.cpp中,启用setFrameBufferStrategy()为双缓冲或三缓冲。
// TouchGFXHAL.cpp 中启用 DMA2D 加速的典型配置
void TouchGFXHAL::initialize() {
// 调用父类初始化
TouchGFXGeneratedHAL::initialize();
// 启用 DMA2D 加速
setDMAType(DMA_TYPE_DMA2D);
// 设置 DMA2D 中断优先级
HAL::getInstance()->setDMAPriority(5);
// 启用双缓冲,配合 LTDC 实现无撕裂切换
setFrameBufferStrategy(FRAME_BUFFER_STRATEGY_DOUBLE_BUFFERING);
}
我的建议:如果你用的是 STM32F7 或 H7 系列,一定要把 Chrom-ART 用起来。它的 L8 调色板解码功能,能让 8 位色深的图片显示效果接近 16 位,但内存占用只有一半。我在一个智能家居面板项目里,用这个功能把图片内存从 4MB 降到了 1.2MB,动画帧率还从 25fps 提到了 45fps。
最后说一个容易被忽略的点:硬件加速不是万能的。DMA2D 在拷贝大块数据时很快,但启动 DMA 本身有开销。如果每次只拷贝几十个字节,那还不如 CPU 直接 memcpy。我一般以 256 字节为阈值:小于 256 字节用 CPU,大于 256 字节用 DMA2D。
本章小结:动画优化的四个维度——帧率控制让你知道「跑多快合适」,动画曲线选择让你知道「怎么跑更省力」,减少重绘区域让你知道「别画没用的」,硬件加速利用让你知道「让专业的人干专业的事」。把这四点吃透了,你的 TouchGFX 项目想卡都难。