3、代码性能度量:时间测量、CPU占用率、内存占用分析
做点钞机嵌入式开发,最怕什么?
怕钞票数着数着就卡了。怕一开机内存就爆了。怕CPU跑满100%,机器烫得能煎鸡蛋。
这些问题,光靠看代码是看不出来的。你得会量。拿数据说话。
这一章,我就跟你聊聊性能度量的三把尺子:时间、CPU、内存。这三样搞明白了,你的代码优化才有方向。
3.1 时间测量:别靠感觉,靠示波器和定时器
我见过不少工程师,代码写完了,拍着胸脯说“这个函数肯定很快”。结果一上示波器,好家伙,跑了200毫秒。
为什么会这样?因为人的感觉不靠谱。尤其是中断服务函数里,你以为就几条指令,实际上可能调用了printf,或者做了浮点运算。
3.1.1 用GPIO翻转法测时间
这是我最常用的方法。简单粗暴,但有效。
在函数开始前拉高一个GPIO,结束后拉低。然后用示波器看脉宽。
// 测量关键函数执行时间
GPIO_SetBits(GPIOB, GPIO_Pin_0); // 开始测量
process_banknote_data(); // 被测函数
GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 结束测量
我个人习惯用PB0做测量口。为什么?因为示波器探头夹上去方便,而且这个引脚一般没被复用。
3.1.2 用系统定时器精确计时
有些场合不方便接示波器,比如机器已经在产线上了。这时候可以用芯片内部的定时器。
// 使用SysTick测量微秒级时间
uint32_t start_tick = SysTick->VAL;
process_banknote_data();
uint32_t end_tick = SysTick->VAL;
uint32_t elapsed = (start_tick - end_tick) / (SystemCoreClock / 1000000);
嗯,这里要注意:SysTick是递减计数的,所以要用start_tick减去end_tick。我刚开始用的时候搞反了,测出来的时间全是负数,查了半天才发现。
3.1.3 避坑指南:测量本身也会消耗时间
我曾经在项目里犯过一个低级错误。为了测量一个中断响应时间,我在中断入口和出口各加了一个GPIO翻转。结果测出来的时间比预期多了2微秒。
后来才意识到,GPIO翻转本身就要花时间。尤其是用库函数操作,那开销可不小。
3.2 CPU占用率:别让主循环变成死循环
点钞机的主循环,说白了就是一个大while(1)。里面塞了各种任务:电机控制、传感器读取、通信处理、UI刷新。
如果CPU占用率太高,就会出现一个现象:按了停止键,机器要等半秒才反应。这在点钞机上是绝对不能接受的。
3.2.1 用空闲任务统计法
这个方法我用了很多年。原理很简单:在空闲任务里跑一个计数器,统计单位时间内空闲任务执行了多少次。
// 空闲任务中累加计数器
void idle_task(void)
{
static uint32_t idle_count = 0;
idle_count++;
// 每秒计算一次CPU占用率
if(get_system_tick() % 1000 == 0)
{
uint32_t total_ticks = 1000000; // 假设每秒有100万个空闲机会
uint32_t cpu_usage = 100 - (idle_count * 100 / total_ticks);
printf("CPU占用率: %d%%\n", cpu_usage);
idle_count = 0;
}
}
你想想看,如果空闲任务每秒跑了80万次,说明CPU有80%的时间是空闲的,那占用率就是20%。反过来,如果只跑了10万次,那CPU占用率就是90%,得赶紧优化了。
3.2.2 用RTOS自带工具
如果用了FreeRTOS或uCOS,事情就简单多了。这些系统自带CPU占用率统计功能。
// FreeRTOS中获取任务CPU占用率
TaskStatus_t task_status;
vTaskGetInfo(task_handle, &task_status, pdTRUE, eInvalid);
printf("任务%s占用率: %d%%\n", task_status.pcTaskName,
task_status.ulRunTimeCounter * 100 / total_run_time);
3.3 内存占用分析:堆栈溢出是隐形杀手
做嵌入式最怕什么?不是功能实现不了,而是程序跑着跑着就挂了。而且挂得毫无规律。
我遇到过最头疼的一个bug:点钞机连续工作8小时后,突然死机。查了三天,最后发现是堆栈溢出。
3.3.1 静态内存分析:看map文件
编译完成后,编译器会生成一个.map文件。这里面记录了每个变量、每个函数的地址和大小。
// 在.map文件中查找关键信息
.bss 0x20000000 0x4A0 // 未初始化数据段大小
.data 0x200004A0 0x200 // 初始化数据段大小
.stack 0x200006A0 0x400 // 栈大小
.heap 0x20000AA0 0x200 // 堆大小
我个人习惯在编译脚本里加一个检查:如果RAM使用率超过80%,就报warning。这样在开发阶段就能发现问题,不用等到上机测试。
3.3.2 动态内存分析:监控malloc/free
点钞机里有些数据是动态分配的,比如钞票图像缓存。如果频繁malloc/free,很容易产生内存碎片。
// 自定义内存分配器,记录分配情况
void* my_malloc(size_t size)
{
void* ptr = malloc(size);
total_allocated += size;
if(total_allocated > MAX_HEAP_SIZE * 0.8)
{
printf("警告:堆内存使用超过80%%\n");
}
return ptr;
}
3.3.3 栈溢出检测:填充魔法数
这是我最推荐的检测方法。在系统初始化时,把栈空间全部填充成0xDEADBEEF。然后定期检查栈底的值有没有被改写。
// 栈溢出检测
#define STACK_FILL_PATTERN 0xDEADBEEF
void stack_monitor(void)
{
uint32_t* stack_bottom = (uint32_t*)&_estack;
if(*stack_bottom != STACK_FILL_PATTERN)
{
printf("栈溢出!栈底已被改写\n");
// 触发错误处理
}
}
嗯,这里要注意:这个检测函数要放在主循环里定期调用,或者放在低优先级的中断里。我一般每100ms检查一次,够用了。
3.4 实战案例:一次内存泄漏的排查过程
我记得有一次,客户反馈点钞机连续工作3小时后,速度越来越慢,最后卡死。
我远程连上去一看,内存占用率从开始的30%,慢慢涨到了95%。典型的泄漏症状。
排查过程是这样的:
- 先用内存监控工具,发现每次处理一张钞票,堆内存就增加128字节
- 定位到图像处理模块,每次调用都会malloc一个缓冲区
- 检查代码,发现有一个异常分支里忘了free
- 修复后,连续跑了48小时,内存占用率稳定在35%
说白了,内存泄漏就是“借了不还”。每次malloc都要对应一个free,这个原则一定要守住。
3.5 本章小结
性能度量这件事,说白了就是三个字:量化它。
- 时间不够精确?用GPIO翻转法,示波器一看就知道
- CPU跑满了?用空闲任务统计法,找出哪个任务在偷懒
- 内存泄漏了?用填充魔法数法,让栈溢出无处遁形
下一章,我会讲代码优化的具体技巧。但在此之前,我建议你先把手头的项目跑一遍性能度量。你会发现,很多问题其实早就存在,只是你没去量而已。