如何用 Keil5 性能分析器“揪出”S32K144 上的性能元凶?
你有没有遇到过这样的场景:代码逻辑没问题,外设配置也正确,但系统就是偶尔卡顿、响应变慢,甚至中断丢失?调试串口打了一堆
printf
,结果发现打印本身就成了瓶颈——越查越慢,越慢越查。
这时候,别再靠“猜”了。我们需要一个真正懂 Cortex-M 内核心跳的工具,来告诉我们: 到底是谁在偷偷吃掉 CPU 时间?
今天我们就来聊聊,在基于 NXP S32K144(文中提到的 SF32LB52 应为该平台或开发板代号)这类汽车级 MCU 的项目中,如何利用 Keil MDK 自带的性能分析器(Performance Analyzer) ,精准定位那些藏得极深的“热点函数”,把优化做到刀刃上 🎯。
别让“看不见”的问题拖垮实时性
S32K144 这颗芯片不简单。ARM Cortex-M4F 核心,带 FPU,主频能跑到 112MHz,还有丰富的定时器和通信接口,广泛用于车身控制、电机驱动、BMS 等对实时性要求极高的场合。
但在这些应用里,我们常常面临一个矛盾:
💬 “功能我都实现了,为啥一到现场就抽风?”
答案往往是: 某些函数执行时间太长,挤占了关键任务的资源。
比如:
- 一个看似简单的滤波算法,在高频采样下成了定时炸弹;
- 某个状态机判断写了太多分支,编译后生成一堆跳转指令;
- 中断服务程序(ISR)里不小心调用了标准库函数,导致嵌套延迟累积……
这些问题,在
-O0
编译或者小规模测试时可能完全暴露不出来。可一旦进入真实工况,CPU 负载悄然飙升,系统就开始“喘不过气”。
传统的调试手段在这里几乎失效。加
printf
?UART 波特率才 115200,发几个字节就得等几十微秒,严重干扰实时行为。用逻辑分析仪打 IO 口?需要额外引脚,还得改代码埋点,治标不治本。
那怎么办?
好消息是: 你的芯片早就内置了一个“黑匣子”——DWT 和 ITM 模块。而 Keil5 的 Performance Analyzer,正是打开这个黑匣子的钥匙 🔑。
DWT + ITM:Cortex-M 的“隐形探针”
ARM 在设计 Cortex-M 系列内核时,就考虑到了深度调试的需求。于是有了 CoreSight 架构下的两个关键组件:
- DWT(Data Watchpoint and Trace)
- ITM(Instrumentation Trace Macrocell)
它们不像传统调试那样需要暂停 CPU,而是像“潜伏特工”一样,在程序运行的同时悄悄收集信息。
DWT 是谁?它能干啥?
你可以把 DWT 当作一个高精度计时器+地址监控器的组合体。它有几个核心寄存器:
| 寄存器 | 功能 |
|---|---|
CYCCNT
| 每个 CPU 周期自动加 1,32 位宽,最高可达主频精度 |
COMPx / MASKx
| 设置数据观察点或地址匹配触发 |
CTRL
| 控制使能与事件选择 |
其中最常用的,就是
CYCCNT
—— 它是我们测量函数执行时间的“原子钟”。
举个例子,假设 S32K144 主频为 80MHz:
enable_cycle_counter(); // 启动 CYCCNT
uint32_t start = DWT->CYCCNT;
some_function();
uint32_t end = DWT->CYCCNT;
uint32_t cycles = end - start;
float us = (float)cycles / 80.0f; // 得到微秒级耗时
是不是比
HAL_GetTick()
精准多了?而且完全不依赖任何外设定时器资源。
但问题是:手动插桩太麻烦,还容易影响代码结构。更致命的是,对于被频繁调用的小函数(比如 PID 计算中的限幅操作),你不可能每个都去测一遍。
所以,我们要让硬件自己“上报”这些数据。
ITM:让函数主动“报到”
ITM 就像是一个轻量级的日志通道,可以通过 SWO(Serial Wire Output)引脚将信息实时传输出去,而无需占用 UART 或其他外设。
当启用插桩模式后,编译器会在指定函数的入口和出口插入类似这样的指令:
MOV r0, #enter_tag
STR r0, [ITM_PORT(0)] ; 发送到 ITM Port 0
Keil 调试器接收到这些消息后,就能精确知道某个函数何时开始、何时结束,调用了多少次,总共花了多久。
这就解决了 PC 采样法的一个痛点: 短函数容易漏检 。
想象一下,如果一个函数只执行了 200 个周期(约 2.5μs @80MHz),而我们的采样间隔是 1ms,那它很可能刚好落在两次采样之间,直接被忽略。但通过 ITM 插桩,哪怕只有几条指令,也能被捕获。
当然,插桩是有代价的:每进出一次函数都要多几条指令开销。因此通常建议:
- 对疑似热点函数开启插桩;
- 其余函数使用 PC 采样法辅助观察;
- 实际上线前关闭所有跟踪功能。
打开 Keil5 性能分析器:四步锁定瓶颈
好了,理论讲完,现在上手实战。
下面我带你一步步在 Keil µVision5 中启动 Performance Analyzer,并在一个典型的 S32K144 工程中找出隐藏的性能杀手。
第一步:准备环境
确保以下几点已配置妥当:
-
调试器支持 SWO 输出
推荐使用 J-Link Pro 或 ULINKplus,普通版 J-Link 也可以,但需确认固件版本支持 SWO。 -
目标板连接 SWO 引脚
S32K144 的 SWO 功能通常复用在某个 GPIO 上(如 PTB16)。检查原理图是否引出并连接至调试器。 -
工程开启调试信息
在 Options for Target → Output 中勾选:
- ✅ Debug Information
- ✅ Browse Information
并在 C/C++ 选项卡中启用:
-
-g
(生成调试符号)
-
-O2
或
-Os
(避免
-O0
下的误导性结果)
- 初始化 Trace 模块
void trace_init(void) {
// 使能调试模块时钟
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
// 配置 SWO 波特率(假设系统时钟 80MHz,目标 2Mbps)
uint32_t swo_div = (80000000 / 2000000) - 1;
TPI->ACPR = swo_div;
// 选择 NRZ 编码格式
TPI->SPPR = 2; // Async Mode: UART-NRZ
// 使能 ITM 和 SWO 输出
ITM->TCR = ITM_TCR_SWOENA_Msk | ITM_TCR_TraceBusID_Msk;
ITM->TER = 0x01; // 使能 Port 0 输出
}
然后在
main()
开头调用
trace_init()
。
⚠️ 注意:波特率必须与 Keil 设置一致,否则会丢包或解析失败!
第二步:启动性能记录
进入调试模式(Debug → Start/Stop Debug Session),点击菜单栏:
👉 View → Performance Analyzer
你会看到一个新的窗口弹出。点击左上角的 🟢 Start Recording 。
此时系统开始运行,Keil 会通过 SWO 接收来自 ITM 的函数进入/退出事件,同时结合 DWT 的周期计数进行时间统计。
建议让系统运行 至少 10~30 秒 ,覆盖典型工作流程。例如:
- 完成一次完整的传感器采集 → 数据处理 → 控制输出循环;
- 触发几次外部中断;
- 模拟负载突变情况。
结束后点击 🔴 Stop Recording。
第三步:查看热点函数榜单
停止记录后,Keil 会自动解析
.axf
文件中的符号表,将原始采样数据映射回函数名。
切换到 Function 标签页,按 “%Total Time” 降序排列,你会看到一张清晰的“罪犯排行榜”:
| Function Name | Count | Time (%) | Avg Time (μs) |
|---|---|---|---|
calculate_pid_output
| 1200 | 38.7% | 124.3 |
filter_sensor_data
| 980 | 21.5% | 87.1 |
update_pwm_duty
| 1200 | 6.2% | 15.8 |
memcpy
| 45 | 5.1% | 210.6 |
uart_send_response
| 30 | 3.8% | 320.0 |
一眼就能看出问题所在:
calculate_pid_output
占了快 40% 的 CPU 时间!这在一个实时控制系统中是非常危险的信号。
双击这个函数,还可以展开它的调用栈(Call Tree),看看是谁在频繁调它:
main_loop()
└── control_cycle_tick()
└── calculate_pid_output() ← 被调用者
├── sqrt_approximate()
├── float_addition_heavy()
└── limit_output_range()
哦豁,里面竟然藏着一个
sqrt_approximate()
?赶紧点进去看汇编。
第四步:深入汇编层找真相
右键函数 → Show Assembly Code,你会发现一段令人皱眉的代码:
LDR r0, =0x3F800000
VMLA s0, s1, s2 ; 浮点运算……
BL __aeabi_f2d ; 啊?软浮点转换?
什么?明明有 FPU,怎么还在用
__aeabi_*
这种软件模拟函数?
回头一看编译选项:❌ “Use FPU” 没有勾选!
难怪!CMSIS-DSP 库里的
arm_sqrt_f32()
根本就没走硬件加速,全靠软件模拟,速度自然慢如蜗牛。
一次真实案例:从 124μs 到 28μs 的逆袭
上面这个场景不是虚构的。我在做一个永磁同步电机(PMSM)矢量控制项目时就踩过这个坑。
当时系统在低速运行时一切正常,但一提速就出现电流震荡。示波器抓 PWM 发现占空比更新不及时,怀疑是控制周期被拉长。
用 Keil 性能分析器一跑,果然
calculate_pid_output
占了 38%,平均耗时 124μs,远超预期的 50μs 以内。
进一步分析发现:
- 使用的是自写的近似平方根函数,精度尚可但效率低下;
- 编译器未启用 FPU,导致所有 float 运算都被拆解成整数操作;
-
某些中间变量没有声明为
register float,增加了内存访问次数。
解决步骤如下:
✅ 第一步:启用 FPU 支持
在 Options for Target → Target 选项卡中:
- Set “Floating Point Hardware” to Single Precision
-
添加编译宏:
__FPU_USED=1
✅ 第二步:替换为 CMSIS-DSP 高效函数
原代码:
float sqrt_approximate(float x) {
// 牛顿迭代法,写得不够紧凑
}
改为:
#include "arm_math.h"
float result;
arm_sqrt_f32(input, &result); // 硬件加速路径
✅ 第三步:优化数据流与局部变量
- 将频繁使用的中间变量改为局部静态或 register 声明;
- 合并重复计算项;
-
使用
const提示编译器常量折叠。
✅ 第四步:重新测试验证
再次运行性能分析器:
| 函数 | 优化前 Avg Time | 优化后 Avg Time | 下降幅度 |
|---|---|---|---|
calculate_pid_output
| 124.3 μs | 28.1 μs | ↓77.4% |
sqrt_approximate
| 96.5 μs | — (已移除) | — |
arm_sqrt_f32
| — | 3.2 μs | — |
总 CPU 占比从 38.7% 降到 12.3%,PWM 更新恢复准时,电机运行平稳如初 ✅。
🎯 关键洞察:有时候最大的性能提升,不是来自算法重构,而是来自 编译器配置的一次正确设置 。
实战技巧:让你的分析更准、更快、更有说服力
光会用工具还不够,要想成为真正的性能猎人,还得掌握一些高级玩法。
技巧一:混合使用 PC 采样与 ITM 插桩
Keil 默认采用 PC Sampling 方式,即每隔一段时间读取一次程序计数器(PC),然后反向查找它属于哪个函数。
优点是零侵入,缺点是可能漏掉短函数。
而 ITM 插桩虽然精确,但会改变函数执行时间。
最佳实践是: 主流程用 PC 采样全局扫描,疑似热点用 ITM 插桩重点突破 。
如何指定哪些函数插桩?
方法一:手动添加注解
__attribute__((annotate("profile")))
void critical_function(void) {
// ...
}
方法二:在 Scatter File(.sct)中排除无关模块
LR_IROM1 0x00000000 0x00080000 { ; load region size_region
ER_IROM1 0x00000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
; 排除调试/日志模块减少干扰
my_debug_lib.o (+RO)
}
RW_IRAM1 0x1FFF0000 0x00010000 {
.ANY (+RW +ZI)
}
}
这样可以避免非关键代码污染分析结果。
技巧二:警惕 CYCCNT 溢出陷阱
DWT->CYCCNT
是 32 位寄存器,主频 80MHz 下大约
53 秒就会溢出一次
(0xFFFFFFFF / 80e6 ≈ 53.7s)。
如果你做的是长时间稳定性测试(比如跑一个小时的数据采集),可能会遇到计数回绕问题。
解决方案有两个:
- 定期重置 CYCCNT
if (DWT->CYCCNT > 0xFFFF0000) {
DWT->CYCCNT = 0;
overflow_count++; // 软件计数补偿
}
- 使用 64 位扩展计数器(推荐)
static uint64_t get_full_cycle_count(void) {
static uint32_t last = 0;
static uint64_t high = 0;
uint32_t curr = DWT->CYCCNT;
if (curr < last) {
high += 0x100000000ULL; // 溢出进位
}
last = curr;
return high | curr;
}
这样可以获得长达数小时的连续计时能力。
技巧三:用“黄金标准”验证分析器准确性
总有工程师问我:“Keil 分析出来的数据靠谱吗?会不会因为采样误差导致误判?”
我的做法是: 拿 DWT 快照做“裁判员” 。
在怀疑的函数前后手动记录 cycle 数:
uint32_t start = DWT->CYCCNT;
hot_function();
uint32_t end = DWT->CYCCNT;
LOG_CYCLES("hot_function", end - start);
然后对比 Performance Analyzer 给出的平均时间。如果两者偏差超过 10%,就要检查是否:
- ITM 传输丢包?
- 编译优化导致函数内联?
- 多核竞争或缓存影响?
这种交叉验证能极大增强你对工具的信任度。
设计层面的思考:什么时候该优化?什么时候不该?
最后想聊点更深层的东西。
性能分析不是为了追求“最低耗时”,而是为了达成系统的整体目标。
在汽车电子领域,我们经常面临多重约束:
- 实时性:控制周期必须稳定;
- 可靠性:不能因优化引入边界错误;
- 可维护性:代码要易于理解和修改;
- 功耗:尽量缩短活跃时间以降低能耗。
因此,我总结了一套 “三不原则” 来指导优化决策:
🟢 值得优化的情况:
- 某函数占用 CPU > 20%,且无不可替代性;
- ISR 执行时间接近中断周期的 1/3;
-
存在明显冗余计算(如重复调用
sin()查表); -
内存拷贝过大(如频繁使用
memcpy处理大结构体);
🔴 不必强求优化的情况:
- 已满足实时性要求,系统余量充足;
- 优化会导致代码复杂度显著上升;
- 涉及安全相关的校验逻辑(宁可慢一点,也不能错);
- 即将被硬件模块取代(如未来改用 DMA+HTU);
记住一句话:
📣 “最好的优化,是不让问题发生。”
这意味着:
- 在架构设计阶段就考虑模块划分与调度策略;
- 使用 RTOS 合理分配优先级,避免优先级反转;
- 对关键路径预留足够的性能裕量(建议 ≤60% 负载);
- 建立自动化性能基线测试流程,每次提交都能看到变化趋势。
写在最后:让系统不仅“跑通”,更要“跑好”
回到最初的问题:为什么我们的嵌入式系统总是差那么一点点?
很多时候,不是缺技术,也不是缺经验,而是缺少一种 量化思维 。
我们习惯于问:“功能实现了吗?”
却很少追问:“它是怎么实现的?代价是什么?”
Keil5 性能分析器的价值,不只是帮你找到那个耗时最长的函数,更是推动你建立一种 以数据驱动优化 的工作方式。
当你能说出:
“这次重构让主控函数从 124μs 降到 28μs,CPU 负载下降 26个百分点。”
你就不再是一个“修 bug 的人”,而是一个 系统性能的塑造者 。
下次你在调试 S32K144 或其他 Cortex-M 平台时,不妨试试打开 Performance Analyzer 看一眼。
也许你会发现,那个你以为“很轻”的函数,正在后台默默吞噬着宝贵的 CPU 时间。
而你要做的,就是把它揪出来,优雅地解决掉 💥。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
1万+

被折叠的 条评论
为什么被折叠?



