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 知识体系总览

下面这张图,是我自己总结的帧率监控与调优的完整流程。你可以把它贴在工位旁边。

帧率监控与调优知识体系 帧率监控与调优 FPS计数器实现 每帧计数 + 每秒统计 浮层显示 / 条件编译 帧率波动分析 渲染时间分解 绘制次数 / 脏区域 垂直同步策略 开启:平滑 / 省电 关闭:性能不足时 核心原则:先监控,再分析,最后调优 不要凭感觉改代码,让数据说话

2.6 实战建议

最后,给你几个我实际项目里用过的建议:

  1. 先跑一遍基准测试:在空工程里测一下裸帧率,知道硬件上限在哪。
  2. FPS计数器常驻:开发阶段一直开着,养成看帧率的习惯。
  3. 波动比低帧率更可怕:稳定45fps比忽高忽低60fps体验好得多。
  4. VSync不是银弹:根据实际性能曲线决定开还是关,别迷信默认配置。

一个小工具: 我写了个Python脚本,通过串口实时读取TouchGFX的帧率数据,然后画成折线图。这样一眼就能看出哪些操作导致掉帧。如果你感兴趣,可以在课程配套资源里找到它。