2. 帧率(FPS)分析与监控:如何测量当前帧率
帧率这玩意儿,说白了就是你的UI画面每秒刷新多少次。单位是FPS(Frames Per Second)。
我见过不少团队,UI做出来看着挺漂亮,一上真机就卡成PPT。为什么?因为没人盯着帧率看。你想想看,一个60FPS的项目,突然掉到20FPS,用户手指滑动的时候那个撕裂感,嗯,懂的都懂。
这一章我们就来聊聊,怎么把帧率这个指标抓在手里。我会从最基础的测量方法讲起,再到TouchGFX自带的工具,最后说说怎么用STM32CubeMonitor做远程监控。
核心观点:帧率不是跑分用的,它是你调优的指南针。没有帧率数据,你就是在黑灯瞎火里调UI。
2.1 帧率的基本概念与测量原理
先搞清楚一个概念:帧率不等于刷新率。刷新率是屏幕硬件每秒能刷多少次,帧率是你的UI软件每秒能渲染多少帧。两者不匹配的时候,就会出现画面撕裂或者卡顿。
TouchGFX的渲染流程大致是这样的:
- 应用层更新UI状态
- 框架计算需要重绘的区域
- 硬件加速器(如果有的话)开始渲染
- 帧缓冲交换,送到屏幕
每一帧的耗时,就是从第一步到第四步的总时间。帧率就是1秒除以这个时间。
举个例子:如果一帧耗时16.67ms,那帧率就是60FPS。如果一帧耗时33.33ms,帧率就是30FPS。看起来很简单对吧?但实际测量的时候,坑不少。
注意:不要用示波器去量帧同步信号来算帧率。虽然理论上可行,但实际项目中干扰太多,而且你没法把帧率和具体的UI操作对应起来。我见过有人这么干,最后发现测出来的数据和实际用户体验完全对不上。
2.2 在代码中手动测量帧率
最直接的方法,就是在代码里打时间戳。TouchGFX的Screen类里有个tick()函数,每帧都会调用。我们可以利用它来统计帧率。
我个人习惯的做法是这样的:
// 在Screen的子类中
#include <touchgfx/hal/Types.hpp>
#include <touchgfx/hal/HAL.hpp>
class MyScreen : public Screen {
private:
uint32_t frameCount = 0;
uint32_t lastTime = 0;
float currentFPS = 0.0f;
public:
virtual void tick() {
frameCount++;
uint32_t now = HAL::getInstance()->getCPUTimestamp();
uint32_t elapsed = now - lastTime;
// 每秒更新一次帧率
if (elapsed >= 1000000) { // 1秒 = 1,000,000微秒
currentFPS = (float)frameCount * 1000000.0f / (float)elapsed;
frameCount = 0;
lastTime = now;
// 可以在这里把currentFPS打印出来或者显示在UI上
// printf("Current FPS: %.2f\n", currentFPS);
}
}
};
这段代码的原理很简单:每帧计数,每秒算一次平均值。为什么用微秒?因为TouchGFX的CPU时间戳精度通常就是微秒级的。
小技巧:如果你不想在代码里加打印,可以把帧率数值显示在UI的一个隐藏文本控件里。调试的时候打开,发布的时候隐藏。我在一个车载项目里就是这么干的,客户验收的时候直接看数字,比什么都说服力。
不过,手动测量有个问题:它本身会占用CPU时间。尤其是你用了printf,那玩意儿慢得要命,测出来的帧率会比实际低。所以我建议,只计数,不打印,等需要看的时候再通过某种方式把数据取出来。
2.3 使用TouchGFX Designer的Performance Analyzer
手动打点太原始了?那就用官方工具。TouchGFX Designer里自带了一个Performance Analyzer,位置在菜单栏的「Tools」-「Performance Analyzer」。
这个工具能干什么?
- 帧率实时显示:在模拟器里跑的时候,右上角会显示当前帧率
- 渲染耗时分解:告诉你每一帧里,绘制花了多少时间,刷新花了多少时间
- 内存使用情况:显示帧缓冲和动态内存的占用
我建议你在开发阶段就开着它。别等到最后集成的时候才想起来看性能。有一次我帮一个客户排查问题,他们的UI在模拟器上跑60FPS,一上真机就掉到20FPS。打开Performance Analyzer一看,发现是某个自定义控件的绘制函数里有个死循环——在模拟器上因为CPU快所以不明显,在真机上就暴露了。
使用步骤很简单:
- 在TouchGFX Designer中打开你的项目
- 点击「Tools」-「Performance Analyzer」
- 点击「Start」开始分析
- 在模拟器中操作你的UI
- 观察帧率变化和耗时分布
这里有个表格,是Performance Analyzer里几个关键指标的含义:
| 指标名称 | 含义 | 正常范围 |
|---|---|---|
| FPS | 当前帧率 | ≥30FPS(流畅),≥60FPS(极佳) |
| Frame Time | 每帧总耗时 | ≤33ms(30FPS),≤16.7ms(60FPS) |
| Draw Time | 绘制耗时 | 通常占Frame Time的60%-80% |
| Flush Time | 帧缓冲刷新耗时 | 通常占Frame Time的10%-20% |
| MCU Load | MCU负载率 | ≤70%(留有余量给其他任务) |
你看这个表格,重点关注Draw Time和Flush Time的比例。如果Flush Time占比过高,说明你的帧缓冲交换策略有问题,或者屏幕接口太慢。如果Draw Time占比过高,那就是你的UI元素太多或者绘制逻辑太复杂。
2.4 使用STM32CubeMonitor进行帧率监控
模拟器上跑得再欢,最终还是要上真机。STM32CubeMonitor就是用来干这个的——它可以通过SWO、UART或者USB,实时读取MCU内部的数据。
我曾经在一个工业HMI项目里,设备部署在产线上,UI偶尔会卡一下。用CubeMonitor一抓,发现是某个传感器中断频繁触发,导致UI线程被抢占。这种问题,你在模拟器上永远复现不了。
配置步骤大致如下:
- 在STM32CubeIDE中启用SWO或UART的调试输出
- 在TouchGFX代码中,通过
HAL::getInstance()->setFrameRateMeasurement()开启帧率测量 - 在CubeMonitor中创建仪表盘,添加帧率显示控件
- 连接目标板,开始监控
代码里需要加这么一段:
// 在main.cpp或者HAL初始化之后
#include <touchgfx/hal/HAL.hpp>
// 开启帧率测量
HAL::getInstance()->setFrameRateMeasurement(true);
// 设置帧率数据输出回调(通过UART或者SWO)
HAL::getInstance()->setFrameRateCallback([](uint32_t fps) {
// 这里把fps通过UART发送出去
// 例如:printf("FPS:%lu\n", fps);
});
然后在CubeMonitor里,配置一个串口接收解析器,把FPS:60这样的字符串解析成数值,显示在仪表盘上。
经验之谈:CubeMonitor的图表功能很好用。你可以把帧率、CPU负载、内存占用画在同一个时间轴上。这样当帧率下降的时候,一眼就能看出是CPU忙不过来了,还是内存不够了。我在一个医疗设备项目里,就是靠这个组合图表,定位到了一个内存泄漏问题——帧率每隔几分钟就掉一次,和内存占用曲线完全吻合。
2.5 三种方法的对比与选择
说了三种方法,到底用哪个?我整理了一个对比表:
| 方法 | 适用阶段 | 精度 | 对性能的影响 | 是否支持真机 |
|---|---|---|---|---|
| 代码手动测量 | 开发初期、快速验证 | 中等 | 低(仅计数) | 是 |
| Performance Analyzer | 开发中期、模拟器调试 | 高 | 中(模拟器环境) | 否(仅模拟器) |
| STM32CubeMonitor | 集成测试、现场排查 | 高 | 低(仅输出数据) | 是 |
我的建议是:
- 刚开始写UI的时候,用代码手动测量,快速验证你的核心交互是否流畅
- 开发到一定阶段,打开Performance Analyzer,仔细分析每一帧的耗时分布
- 上真机之后,用CubeMonitor做长时间监控,捕捉偶发性的卡顿
三种方法不是互斥的,而是互补的。你想想看,如果只靠模拟器,你永远不知道真机上的中断和DMA会怎么影响你的UI。如果只靠真机,你又很难像在模拟器里那样方便地分析每一帧的细节。
避坑指南:我曾经在一个项目里,只依赖Performance Analyzer做调优,模拟器上跑到了65FPS,觉得稳了。结果上真机之后,发现帧率只有40FPS。查了半天,发现是LTDC的时钟配置不对,导致屏幕实际刷新率只有50Hz。所以记住:模拟器上的帧率只能作为参考,真机上的数据才是最终标准。
2.6 本章知识体系
下面这张图,把本章的核心逻辑串起来了:
这张图把三种方法的关系和适用场景说清楚了。你从上往下看,先确定自己处在哪个开发阶段,然后选择对应的方法。别想着一步到位,也别只用一种方法走到底。
帧率监控这件事,说白了就是给你的UI装一个仪表盘。没有它,你就是在盲开。有了它,你才能知道哪里该踩油门,哪里该松刹车。