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快所以不明显,在真机上就暴露了。

使用步骤很简单:

  1. 在TouchGFX Designer中打开你的项目
  2. 点击「Tools」-「Performance Analyzer」
  3. 点击「Start」开始分析
  4. 在模拟器中操作你的UI
  5. 观察帧率变化和耗时分布

这里有个表格,是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线程被抢占。这种问题,你在模拟器上永远复现不了。

配置步骤大致如下:

  1. 在STM32CubeIDE中启用SWO或UART的调试输出
  2. 在TouchGFX代码中,通过HAL::getInstance()->setFrameRateMeasurement()开启帧率测量
  3. 在CubeMonitor中创建仪表盘,添加帧率显示控件
  4. 连接目标板,开始监控

代码里需要加这么一段:

// 在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 本章知识体系

下面这张图,把本章的核心逻辑串起来了:

帧率分析与监控知识体系 帧率(FPS)分析与监控 代码手动测量 Performance Analyzer STM32CubeMonitor tick()函数中计数 每秒计算平均帧率 模拟器内实时显示 渲染耗时分解 SWO/UART数据输出 仪表盘实时监控 选择建议 开发初期 → 代码手动测量 开发中期 → Performance Analyzer 真机测试 → STM32CubeMonitor

这张图把三种方法的关系和适用场景说清楚了。你从上往下看,先确定自己处在哪个开发阶段,然后选择对应的方法。别想着一步到位,也别只用一种方法走到底。

帧率监控这件事,说白了就是给你的UI装一个仪表盘。没有它,你就是在盲开。有了它,你才能知道哪里该踩油门,哪里该松刹车。