2. 帧率与刷新率:理解FPS与Hz的关系,如何测量当前帧率,目标帧率设定(30fps vs 60fps),帧率不稳定的根因分析。

做嵌入式UI,尤其是TouchGFX这类带硬件加速的框架,你迟早会碰到两个词:FPS 和 Hz。

很多人把它们混为一谈。我刚开始做项目时也犯过这个错——以为屏幕刷新率60Hz,那UI跑60fps就是理所当然。结果呢?动画看着卡,但帧率计数显示60。后来才发现,帧率和刷新率根本不是一回事。

2.1 FPS vs Hz:一个讲内容,一个讲载体

咱们先把这个最基础的概念掰扯清楚。

概念 全称 描述 类比
FPS Frames Per Second GPU/CPU每秒能渲染出多少帧画面 厨师每秒能炒出几盘菜
Hz Hertz 屏幕硬件每秒能刷新多少次画面 餐桌每秒能上几盘菜

说白了,FPS是软件能力,Hz是硬件能力。

你想想看,如果厨师(GPU)每秒能炒60盘菜,但餐桌(屏幕)每秒只能上30盘,那有30盘菜就浪费了,用户看到的还是30盘。反过来,厨师只能炒30盘,餐桌能上60盘,那餐桌有一半时间是空转的,用户看到的还是30盘。

所以,最终用户看到的流畅度,取决于FPS和Hz中较小的那个值。这个值,我们叫它“有效帧率”。

核心结论: 有效帧率 = min(FPS, Hz)。你调优的目标,是让FPS尽量接近或等于Hz,而不是盲目追求FPS无限高。

2.2 如何测量当前帧率?

知道了概念,下一步就是动手测。我见过不少工程师,凭肉眼感觉说“嗯,挺流畅的”。千万别这样。人眼在30fps以上就很难分辨具体帧率了,你觉得流畅,可能实际只有25fps。

在TouchGFX里,测量帧率有几种方法,我按推荐度排个序:

方法一:使用TouchGFX Designer内置的FPS计数器(推荐)

这是最省事的方法。在TouchGFX Designer里,你可以在模拟器或真机调试时打开一个隐藏开关。

// 在 main.cpp 中,初始化 TouchGFX 之前加入:
hal->setFrameRateMeasurement(true);

// 之后,屏幕左上角会显示实时 FPS
// 格式类似:FPS: 58.2

我个人习惯在开发阶段一直开着这个计数器。它不会占用太多资源,但能让你随时感知性能变化。

方法二:使用HAL的帧率回调

如果你想记录帧率数据做分析,可以用回调函数。

// 在 TouchGFXHAL.cpp 中重写:
void TouchGFXHAL::swapFrameBuffer()
{
    // 记录帧开始时间
    static uint32_t lastTick = 0;
    uint32_t currentTick = HAL::getInstance()->getCPUTicks();
    
    if (lastTick != 0)
    {
        uint32_t delta = currentTick - lastTick;
        float fps = 1000.0f / delta;  // 假设 tick 单位是 ms
        // 这里你可以把 fps 存到全局变量,或者通过串口打印
    }
    
    lastTick = currentTick;
    
    // 调用父类方法
    TouchGFXGeneratedHAL::swapFrameBuffer();
}

小技巧: 我建议在真机上连续记录1000帧的FPS数据,然后算平均值和最低值。平均值代表整体性能,最低值代表最差情况——也就是卡顿的瞬间。

方法三:使用逻辑分析仪或示波器

这是最硬核的方法。把屏幕的VSYNC信号引出来,用示波器看。两个VSYNC脉冲之间的时间间隔,就是实际帧周期。这个方法最准,但需要硬件支持。

2.3 目标帧率设定:30fps vs 60fps

很多新手一上来就追求60fps,觉得越高越好。其实不是这样。

我做过一个工业控制面板的项目,屏幕是60Hz的,但MCU主频只有200MHz。硬要跑60fps,结果CPU占用率飙到90%,UI操作响应变慢,动画反而感觉卡。后来我降到30fps,CPU占用率降到40%,操作响应快了,用户反馈说“流畅多了”。

所以,目标帧率怎么选?我列个对照表:

场景 推荐帧率 原因
视频播放、游戏 60fps 运动画面需要高帧率避免拖影
菜单切换、列表滑动 30-60fps 30fps以上人眼基本感觉不到卡顿
静态页面、数据显示 15-30fps 没有动画,低帧率省电省CPU
低功耗设备(电池供电) 15-30fps 帧率每降一半,功耗大约降30-40%

注意: 不要设置一个“不上不下”的帧率。比如屏幕是60Hz,你跑45fps,那每帧的显示时间就不固定,会出现“帧抖动”,看起来比30fps还难受。要么30fps(每2个VSYNC更新1帧),要么60fps(每1个VSYNC更新1帧)。

2.4 帧率不稳定的根因分析

好,现在你会测帧率了,也设了目标。但跑起来发现,帧率像过山车一样,忽高忽低。怎么办?

我总结了一个排查框架,用一张SVG图来表示:

帧率不稳定根因分析框架 帧率不稳定 CPU 过载 渲染瓶颈 内存带宽 逻辑计算太复杂 中断频繁触发 任务调度不合理 图片解码耗时 Alpha混合过多 纹理过大/过多 帧缓冲切换慢 DMA传输冲突 外部存储器慢 排查建议 先看CPU占用率 → 再看渲染耗时 → 最后检查内存带宽

下面我展开讲讲每个根因,以及我在项目中踩过的坑。

2.4.1 CPU过载

这是最常见的原因。TouchGFX的UI线程和你的业务逻辑线程如果跑在同一个核上,业务逻辑一忙,UI就掉帧。

我曾经在一个项目中,把数据采集和UI渲染放在同一个任务里。数据采集用了DMA,但每次DMA完成中断都去处理数据,处理时间长达5ms。而60fps下,每帧只有16.7ms。5ms的中断处理,直接吃掉30%的帧时间。帧率从60掉到40多。

解决方案:把数据采集放到另一个任务,或者用消息队列延迟处理。

2.4.2 渲染瓶颈

TouchGFX的渲染管线里,最耗时的操作是:

  • 图片解码: 尤其是大尺寸的PNG或JPEG。每帧都解码,CPU直接拉满。
  • Alpha混合: 半透明图层叠加,每个像素都要做乘法运算。
  • 纹理过大: 超过硬件加速器支持的最大尺寸,会回退到软件渲染。

我记得有个客户,UI上放了一张800x480的全屏背景图,还是PNG格式。每次页面切换,都要重新解码这张图,耗时30ms。帧率直接掉到30以下。后来我把背景图转成BMP格式(无压缩),并且只解码一次,缓存到帧缓冲里。帧率立刻回到60。

2.4.3 内存带宽

这个比较隐蔽。你CPU和GPU都不忙,但帧率就是上不去。为什么?因为内存带宽不够。

嵌入式设备常用SDRAM,带宽有限。如果你的帧缓冲在SDRAM里,而屏幕分辨率又高(比如1024x600,32位色),那每帧的数据量是:1024 x 600 x 4 = 2.4MB。60fps下,每秒需要传输144MB数据。如果SDRAM带宽只有100MB/s,那你就只能跑到40fps左右。

避坑指南: 我曾经在一个项目里用了双帧缓冲(double buffering),结果帧率反而比单缓冲低。排查后发现,双缓冲需要同时维护两个2.4MB的缓冲区,内存带宽翻倍,SDRAM扛不住了。后来改成单缓冲+撕裂检测,帧率反而上去了。

2.5 实战:快速定位帧率问题

最后,我分享一个我常用的排查流程,三步走:

  1. 看CPU占用率: 如果CPU占用率超过80%,大概率是CPU过载。优化业务逻辑,或者把UI放到独立核上。
  2. 看渲染耗时: 在swapFrameBuffer里加计时,看渲染一帧花了多久。如果超过16.7ms(60fps)或33.3ms(30fps),那就是渲染瓶颈。优化图片和图层。
  3. 看内存带宽: 如果前两步都没问题,用DMA测试工具测一下SDRAM的实际带宽。如果接近理论极限,考虑降低分辨率、减少帧缓冲数量、或者用更快的存储器(比如内部SRAM做部分缓冲)。

嗯,这套流程我用了好几年,基本能解决90%的帧率问题。剩下的10%,往往是硬件本身的问题——比如屏幕的VSYNC信号不稳定,或者LTDC时钟配置有误。那种情况,就得拿示波器去量了。