1. 性能基线建立:帧率、CPU/GPU、内存与启动时间

做性能调优,第一件事不是动手改代码。你得先知道系统现在到底跑得怎么样。我见过太多人上来就优化渲染管线,结果发现瓶颈根本不在GPU——这种白费功夫的事,我干过一次就长记性了。

这一章,咱们把基线建立的方法论和工具链讲透。说白了,就是给系统做个全面体检,拿到客观数据,后面优化才有方向。

1.1 帧率(FPS)测量方法

帧率是最直观的性能指标。但怎么测,这里头有讲究。

1.1.1 传统方法:getTickCount / clock()

最简单的办法,在主循环里记录每帧耗时:

uint32_t lastTick = getTickCount();
while(1) {
    // 渲染逻辑
    uint32_t currentTick = getTickCount();
    float frameTime = (currentTick - lastTick) / 1000.0f; // 单位ms
    float fps = 1000.0f / frameTime;
    lastTick = currentTick;
    
    // 用滑动窗口平均,避免单帧抖动
    static float avgFps = 0;
    avgFps = avgFps * 0.9f + fps * 0.1f;
}
注意: 别用单帧FPS做决策。我曾经在一个项目里看到FPS在58到62之间跳,以为是性能波动,结果发现是计时器精度不够。用滑动平均或者中位数滤波,数据才靠谱。

1.1.2 硬件级测量:VSync计数器

如果你的GUI跑在带显示控制器的MCU上,可以直接读VSync中断次数。这个方法精度最高,不占用CPU资源。

volatile uint32_t vsyncCount = 0;
void VSync_IRQHandler(void) {
    vsyncCount++;
}

// 每秒读一次差值
uint32_t lastCount = vsyncCount;
while(1) {
    delay(1000); // 等1秒
    uint32_t currentCount = vsyncCount;
    uint32_t realFps = currentCount - lastCount;
    lastCount = currentCount;
}
我的习惯: 同时用软件计时和硬件VSync两种方法。如果两者偏差超过2%,说明你的软件计时器可能被中断延迟了,或者VSync信号本身有问题。

1.2 CPU/GPU占用率监控

帧率低,你得知道是谁拖了后腿。CPU还是GPU?

1.2.1 CPU占用率:任务级监控

在RTOS环境下,每个任务都可以统计自己的运行时间:

typedef struct {
    uint32_t totalRunTime;
    uint32_t lastStartTime;
    uint32_t maxRunTime;
} TaskMonitor;

void TaskMonitor_Start(TaskMonitor *mon) {
    mon->lastStartTime = getTickCount();
}

void TaskMonitor_Stop(TaskMonitor *mon) {
    uint32_t elapsed = getTickCount() - mon->lastStartTime;
    mon->totalRunTime += elapsed;
    if(elapsed > mon->maxRunTime) {
        mon->maxRunTime = elapsed;
    }
}

我建议在GUI任务里埋这种监控点。有一次我发现GUI任务的最大运行时间达到120ms,正常只有15ms。查了半天,是某个动画回调里做了文件I/O操作——这种坑,你不监控根本发现不了。

1.2.2 GPU占用率:硬件计数器

大部分嵌入式GPU(比如STM32的DMA2D、NXP的PXP)都有性能计数器。读这些寄存器就能知道GPU忙了多久:

// 以DMA2D为例
uint32_t dma2d_busy_cycles = DMA2D->ISR & DMA2D_ISR_TCIF;
float gpu_util = (float)dma2d_busy_cycles / total_cycles * 100.0f;

关键判断:

  • CPU高 + GPU低 → 渲染API调用开销大,或者CPU在干别的活
  • CPU低 + GPU高 → 渲染指令太密集,考虑合并绘制操作
  • 两者都高 → 系统整体过载,需要降级渲染效果

1.3 内存带宽基准测试

这个指标很多人忽略。但说实话,嵌入式GUI的瓶颈往往不在计算,而在内存带宽。你想想看,一个800×480的RGB565帧缓冲,每秒刷新60次,光这一项就要吃掉多少带宽?

// 内存带宽测试:memcpy速度
#define TEST_SIZE (1024 * 1024) // 1MB
uint8_t src[TEST_SIZE], dst[TEST_SIZE];

uint32_t start = getTickCount();
for(int i = 0; i < 100; i++) {
    memcpy(dst, src, TEST_SIZE);
}
uint32_t elapsed = getTickCount() - start;

float bandwidth = (float)(TEST_SIZE * 100) / (elapsed / 1000.0f) / (1024 * 1024);
printf("内存带宽: %.2f MB/s\n", bandwidth);
内存类型 典型带宽 适用场景
内部SRAM 200-400 MB/s 帧缓冲、频繁读写的小纹理
外部SDRAM 50-150 MB/s 大纹理、字体缓存
外部QSPI Flash 10-40 MB/s 只读资源、字库
外部NAND Flash 5-20 MB/s 大容量存储,不适合实时渲染
我曾经踩过的坑: 一个项目用了外部SDRAM做帧缓冲,理论带宽80MB/s,实际测出来只有30MB/s。查到最后发现是SDRAM的刷新周期配置不对,导致频繁等待。所以,别信数据手册,自己动手测。

1.4 启动时间与响应延迟量化

用户对启动时间的敏感度远超你的想象。我做过盲测,启动时间从2秒降到1.5秒,用户满意度提升了30%。

1.4.1 启动时间分段测量

把启动过程拆成几个阶段,每个阶段打时间戳:

void boot_timeline(void) {
    uint32_t t0 = getTickCount();  // 上电
    
    // 阶段1: 硬件初始化
    HAL_Init();
    SystemClock_Config();
    uint32_t t1 = getTickCount();
    
    // 阶段2: OS启动
    OS_Init();
    uint32_t t2 = getTickCount();
    
    // 阶段3: GUI框架初始化
    GUI_Init();
    uint32_t t3 = getTickCount();
    
    // 阶段4: 首帧渲染
    GUI_DrawFirstFrame();
    uint32_t t4 = getTickCount();
    
    printf("硬件初始化: %dms\n", t1 - t0);
    printf("OS启动: %dms\n", t2 - t1);
    printf("GUI初始化: %dms\n", t3 - t2);
    printf("首帧渲染: %dms\n", t4 - t3);
    printf("总启动时间: %dms\n", t4 - t0);
}

1.4.2 响应延迟:从触摸到画面更新

响应延迟是端到端的。从手指碰到屏幕,到画面真正变化,中间经过触摸驱动、事件处理、渲染、刷新。我习惯用GPIO来测量:

// 触摸中断里置高GPIO
void Touch_IRQHandler(void) {
    GPIO_SetPin(HIGH);  // 开始计时
    // 处理触摸事件
    GUI_ProcessTouchEvent();
    // 渲染完成后置低
    GPIO_SetPin(LOW);
}

// 用逻辑分析仪看高电平持续时间
// 这就是完整的响应延迟
我的经验值:
  • 优秀:< 50ms(用户几乎感觉不到延迟)
  • 可接受:50-100ms(轻微延迟,但能接受)
  • 差:> 100ms(用户会明显感觉卡顿)
如果响应延迟超过100ms,先检查触摸驱动的中断优先级,我遇到过因为中断被其他任务抢占导致延迟飙升到200ms的情况。

1.5 性能基线知识体系

下面这张图把本章的核心脉络串起来了。你可以把它当作性能体检的检查清单:

性能基线建立 · 知识体系 性能基线建立 帧率 (FPS) 测量 方法 • getTickCount 软件计时 • VSync 硬件计数器 • 滑动平均滤波 CPU/GPU 占用率 监控手段 • 任务级运行时间统计 • 硬件性能计数器 • CPU/GPU 负载对比 内存带宽测试 测试方法 • memcpy 吞吐量 • 不同存储介质对比 • 实际带宽 vs 理论值 启动 & 响应延迟 量化指标 • 启动分段时间戳 • 触摸到画面延迟 • GPIO 逻辑分析仪法 目标:拿到可复现的基线数据

本章核心要点:

  • 帧率测量要用滑动平均,别信单帧数据
  • CPU/GPU占用率要同时看,才能定位瓶颈
  • 内存带宽必须实测,数据手册只能参考
  • 启动时间和响应延迟要分段量化,别只盯着总时间

基线建立好了,你手里就有了客观数据。后面每一章优化,你都能拿这些数据做对比——改之前多少,改之后多少,一目了然。这才是工程师该有的工作方式。