2. 帧率监控与调优:FPS计数器实现、帧率波动分析、垂直同步(VSync)策略
帧率这东西,说白了就是UI的“心跳”。
我刚入行那会儿,总觉得画面能跑就行,帧率多少无所谓。直到有一次做车载仪表项目,客户在评审会上拿手机对着屏幕拍慢动作,说“你看,这里跳帧了”。嗯,从那以后,我再也不敢忽视帧率监控了。
这一章,我们就来聊聊怎么把帧率“看”清楚,怎么分析波动,以及垂直同步到底该不该开。
2.1 为什么帧率是UI性能的“体温计”
TouchGFX的渲染管线是固定帧率的。你设了60fps,它就每16.6ms刷一帧。如果一帧画不完,下一帧就得等,表现出来就是卡顿。
我个人习惯把帧率比作体温——正常范围是60fps,偶尔掉到55fps可以接受,但要是经常跌破30fps,那UI基本就“发烧”了。
核心指标:
- 平均帧率:整体流畅度的参考值,但容易被“平均”骗了。
- 最低帧率:真正的用户体验瓶颈,掉到30fps以下用户一定能感知。
- 帧率方差:波动越大,感觉越“顿”,哪怕平均帧率很高。
2.2 手写一个FPS计数器
TouchGFX没有内置FPS显示?没关系,我们自己写一个。代码量很少,但非常实用。
思路很简单:每帧计数,每秒算一次平均值。
// FpsCounter.hpp
#include <touchgfx/hal/HAL.hpp>
#include <touchgfx/widgets/TextArea.hpp>
class FpsCounter : public touchgfx::Widget {
public:
FpsCounter() : frameCount(0), lastTime(0), fps(0) {
fpsText.setTypedText(touchgfx::TypedText(T_FPS));
fpsText.setColor(touchgfx::Color::getColorFrom24BitRGB(0xFF, 0xFF, 0x00));
}
virtual void draw(const touchgfx::Rect& invalidatedArea) const {
// 每帧调用一次
uint32_t now = touchgfx::HAL::getInstance()->getCPUTicks();
frameCount++;
if (now - lastTime >= 1000) { // 1秒到了
fps = frameCount;
frameCount = 0;
lastTime = now;
// 更新显示
Unicode::snprintf(fpsBuffer, 6, "%d", fps);
fpsText.setWildcard(fpsBuffer);
fpsText.invalidate();
}
}
virtual touchgfx::Rect getSolidRect() const {
return touchgfx::Rect(0, 0, getWidth(), getHeight());
}
private:
uint32_t frameCount;
uint32_t lastTime;
uint16_t fps;
touchgfx::TextArea fpsText;
Unicode::UnicodeChar fpsBuffer[6];
};
小技巧: 我习惯把FPS计数器做成一个透明浮层,放在屏幕右上角。调试时打开,发布时关掉。用条件编译控制,省心。
2.3 帧率波动分析:找到“罪魁祸首”
光看数字不够,你得知道为什么掉帧。我总结了一套排查流程,你可以照着来。
2.3.1 先看渲染时间
TouchGFX的HAL层提供了几个关键时间戳:
| 时间点 | 说明 | 正常范围(60fps) |
|---|---|---|
| 帧开始 | VSync中断触发 | 0ms |
| 绘制结束 | 所有Widget绘制完成 | < 12ms |
| 传输完成 | 数据送到显存 | < 4ms |
| 总耗时 | 帧开始到传输完成 | < 16.6ms |
如果总耗时超过16.6ms,那这帧就“超时”了。下一帧会被跳过,帧率直接减半。
我曾经踩过的坑: 有一次发现帧率总在45fps左右波动,查了半天发现是某个自定义控件里用了malloc分配临时缓冲区。每次分配耗时2-3ms,累积起来就超了。换成预分配静态缓冲区后,帧率直接回到60fps。
2.3.2 再看绘制次数
TouchGFX的绘制是增量式的。只有脏区域才会重绘。如果全屏都在动,那绘制面积就大,耗时自然长。
我建议你打开HAL的调试输出:
// 在main.cpp中启用
touchgfx::HAL::getInstance()->setFrameRateCompensation(true);
touchgfx::HAL::getInstance()->enableMCULCDGate(true);
然后看日志里的 draws 和 blits 次数。如果 blits 次数特别多,说明图片拷贝太频繁,可以考虑用缓存或减少透明叠加。
2.4 垂直同步(VSync)策略:开还是不开?
这是个老生常谈的问题。我的答案是:默认开,特殊情况关。
2.4.1 开启VSync的好处
- 画面不撕裂:帧率和屏幕刷新率同步,视觉上最平滑。
- 省电:MCU不用拼命画,画完就歇着。
- 帧率稳定:强制锁在60fps(或屏幕刷新率),不会忽高忽低。
2.4.2 关闭VSync的场景
- 性能不足时:如果一帧画不完,VSync会让帧率直接掉到30fps。关掉后,虽然画面可能撕裂,但帧率能维持在40-50fps,用户反而觉得“更流畅”。
- 低刷新率屏幕:比如30Hz的屏幕,开VSync最高也就30fps,不如关掉让MCU全力跑。
我的经验: 在STM32F429上做智能家居面板时,我试过两种方案。开VSync,帧率稳定60fps,但动画稍微复杂一点就掉到30fps。关掉VSync,帧率在45-55fps之间波动,但用户反馈“感觉更跟手”。最后我选择了关VSync + 帧率补偿,效果最好。
2.4.3 如何配置VSync
在TouchGFX的 TouchGFXConfiguration.cpp 里:
// 开启VSync
hal.setFrameRateCompensation(false);
hal.setVSyncInterval(1); // 每1个VSync中断画一帧
// 关闭VSync
hal.setFrameRateCompensation(true);
hal.setVSyncInterval(0); // 不等待VSync,画完就送显
注意:setFrameRateCompensation(true) 是帧率补偿,它会尽量让帧率稳定在目标值。关掉VSync后,这个补偿机制就很重要了。
2.5 知识体系总览
下面这张图,是我自己总结的帧率监控与调优的完整流程。你可以把它贴在工位旁边。
2.6 实战建议
最后,给你几个我实际项目里用过的建议:
- 先跑一遍基准测试:在空工程里测一下裸帧率,知道硬件上限在哪。
- FPS计数器常驻:开发阶段一直开着,养成看帧率的习惯。
- 波动比低帧率更可怕:稳定45fps比忽高忽低60fps体验好得多。
- VSync不是银弹:根据实际性能曲线决定开还是关,别迷信默认配置。
一个小工具: 我写了个Python脚本,通过串口实时读取TouchGFX的帧率数据,然后画成折线图。这样一眼就能看出哪些操作导致掉帧。如果你感兴趣,可以在课程配套资源里找到它。