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;
}
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;
}
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 | 大容量存储,不适合实时渲染 |
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(用户会明显感觉卡顿)
1.5 性能基线知识体系
下面这张图把本章的核心脉络串起来了。你可以把它当作性能体检的检查清单:
本章核心要点:
- 帧率测量要用滑动平均,别信单帧数据
- CPU/GPU占用率要同时看,才能定位瓶颈
- 内存带宽必须实测,数据手册只能参考
- 启动时间和响应延迟要分段量化,别只盯着总时间
基线建立好了,你手里就有了客观数据。后面每一章优化,你都能拿这些数据做对比——改之前多少,改之后多少,一目了然。这才是工程师该有的工作方式。