第三章 代码性能分析:性能瓶颈识别、Profiling工具使用与时间测量方法
做运动控制,说白了就是跟时间赛跑。你写的代码再漂亮,跑起来慢了半拍,电机就该抖了。我见过太多工程师,代码逻辑天衣无缝,一上示波器,周期抖动大得吓人。嗯,今天我们就来聊聊,怎么给代码做个体检,找出那些拖后腿的“坏家伙”。
3.1 性能瓶颈识别:先找“慢”在哪
我个人习惯,拿到一个实时控制项目,不会急着优化。先问自己三个问题:
- 是计算太慢? 比如浮点运算、三角函数、PID迭代。
- 是访问太慢? 比如频繁读Flash、外部RAM、或者DMA没用好。
- 是调度太慢? 比如中断嵌套、任务切换、或者锁竞争。
你想想看,如果连瓶颈在哪都不知道,上来就改代码,大概率是白费功夫。我在项目中遇到过一位同事,花了三天优化一个滤波算法,结果发现是SPI通信的等待时间占了80%。
核心原则: 先测量,后优化。没有数据支撑的优化,都是耍流氓。
3.2 Profiling工具使用:Perf与Tracealyzer
工欲善其事,必先利其器。这里我重点讲两个工具,一个轻量级,一个重量级。
3.2.1 Perf:Linux下的轻骑兵
如果你的运动控制器跑在Linux或RT-Linux上,Perf是你的首选。它不需要改代码,直接采样CPU的PC指针,就能告诉你哪个函数最耗CPU。
# 采样10秒,记录调用栈
perf record -g -F 1000 -- ./your_control_app
# 查看热点函数
perf report
我曾经用Perf抓到一个“幽灵”函数,一个看似无害的printf,在实时循环里每秒打印几千次,直接把CPU吃满了。去掉之后,周期抖动从50us降到了5us。
小技巧: 用perf top可以实时查看CPU占用,适合快速定位“谁在偷懒”。
3.2.2 Tracealyzer:RTOS的显微镜
对于FreeRTOS、ThreadX这类RTOS,Tracealyzer是神器。它能记录每个任务的运行状态、切换时间、中断响应时间。说白了,就是给你一张“时间轴全景图”。
我记得有一次,系统偶尔出现一次“卡顿”,用示波器都抓不到。后来用Tracealyzer一跑,发现是一个低优先级任务在某个条件下,持有了一个信号量不放,导致高优先级任务被阻塞。这种“优先级反转”问题,光看代码很难发现。
注意: Tracealyzer会占用一些资源,生产环境慎用。建议在调试阶段使用,或者只开启关键路径的跟踪。
3.3 时间测量方法:DWT与定时器
工具再好,有时候不如自己动手量一下。尤其是在裸机或者没有Profiling工具的环境下,时间测量是基本功。
3.3.1 DWT:Cortex-M内核的隐藏技能
很多工程师不知道,Cortex-M3/M4/M7内核里有一个Data Watchpoint and Trace (DWT) 模块。它里面有个CYCCNT计数器,每个时钟周期加1。用它来测量时间,精度高,开销小。
// 初始化DWT
#define DWT_CYCCNT *(volatile uint32_t *)0xE0001004
#define DWT_CONTROL *(volatile uint32_t *)0xE0001000
#define SCB_DEMCR *(volatile uint32_t *)0xE000EDFC
void DWT_Init(void) {
SCB_DEMCR |= 0x01000000; // 使能DWT
DWT_CONTROL |= 1; // 使能CYCCNT
DWT_CYCCNT = 0;
}
// 测量函数执行时间
uint32_t start = DWT_CYCCNT;
your_function();
uint32_t end = DWT_CYCCNT;
uint32_t cycles = end - start; // 消耗的时钟周期数
我个人习惯,在关键中断服务函数(ISR)的入口和出口各放一个DWT测量点。如果发现某个ISR执行时间超过了100个时钟周期,我就会警觉起来。
避坑指南: 我曾经在测量时忘记考虑中断嵌套。如果高优先级中断打断了测量,结果会偏大。建议在测量前关闭全局中断,或者使用DWT的溢出中断来处理。
3.3.2 通用定时器:万金油方案
如果芯片不支持DWT,或者你想测量更长时间(比如几秒),用通用定时器最靠谱。配置一个定时器,让它自由运行,然后读取CNT寄存器。
// 假设定时器时钟为100MHz,预分频为1000,则每个tick为10us
TIM_HandleTypeDef htim2;
void TIM_Init(void) {
htim2.Instance = TIM2;
htim2.Init.Prescaler = 1000 - 1; // 100MHz / 1000 = 100kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 0xFFFFFFFF; // 最大计数值
HAL_TIM_Base_Init(&htim2);
HAL_TIM_Base_Start(&htim2);
}
uint32_t GetTimeUs(void) {
return __HAL_TIM_GET_COUNTER(&htim2) * 10; // 单位:微秒
}
你想想看,有了这个GetTimeUs函数,你就可以在代码里到处“埋点”,测量任意一段代码的执行时间。我一般会在控制循环的头部和尾部各放一个,然后通过串口或者共享内存把数据发出去,在上位机画个波形图,一目了然。
注意: 定时器读取本身也有开销,大约几十纳秒。对于微秒级的测量,这个误差可以忽略。但如果测量纳秒级操作,建议用DWT。
3.4 知识体系总览
下面这张图,是我自己总结的“性能分析三板斧”。你可以把它当作一个检查清单,每次遇到性能问题,按这个流程走一遍,基本不会漏。
这张图的核心逻辑很简单:发现问题后,从三个维度同时下手。Profiling工具告诉你“哪个函数最慢”,时间测量告诉你“这段代码到底跑了多久”,代码审查则帮你找出“为什么这么慢”。三者结合,没有找不到的瓶颈。
3.5 实战建议
最后,给你几个我踩过坑之后总结的建议:
- 先做“减法”:优化前,先删掉所有调试打印、冗余日志、不必要的断言。很多时候,删掉比优化更有效。
- 关注“最差情况”:平均执行时间好看没用,要看最差情况下的执行时间。运动控制最怕“偶尔抖一下”。
- 用GPIO翻脚做“穷人的示波器”:在关键代码段前后各放一个GPIO置位和清零,然后用示波器看脉宽。这个方法虽然土,但极其可靠,我至今还在用。
记住: 性能分析不是一次性的工作。每次修改代码后,都应该重新测量一遍。养成习惯,你会发现很多问题在早期就被扼杀在摇篮里了。