在Keil5中用Event Recorder做实时跟踪,我踩过的坑和真香时刻 💥
说实话,刚接手一个复杂的电机控制项目时,我完全没意识到调试会这么“折磨人”。
系统里跑着6个RTOS任务:ADC采样、PID计算、CAN通信、故障检测、HMI刷新……每个都号称10ms周期。可实际一测,电机响应忽快忽慢,像抽风一样。用
printf
打日志?加几条输出,整个系统就卡顿了;设断点看执行流?等你停下的那一刻,时序全乱了,问题再也复现不了。
那一刻我才明白——传统调试手段在高实时性系统面前,真的 力不从心 。
直到我遇见了 Event Recorder 。
它不是什么新奇玩意儿,ARM早就把它集成进了Keil MDK,但很多人(包括曾经的我)都忽略了它的威力。今天我就来聊聊,这个“低调”的工具是如何让我从“猜bug”变成“精准打击”的。
为什么我们需要一种新的调试方式?
先问个扎心的问题:你在嵌入式开发中最怕遇到哪种Bug?
是编译不过?还是指针越界?
错。最可怕的,是那种 偶发性、与时间强相关、无法复现 的Bug。
比如:
- 某个任务偶尔延迟十几毫秒;
- 中断被延迟响应;
- 多个任务争抢资源导致死锁或优先级反转。
这些问题,靠
printf
基本无解。因为你每打印一次,CPU就得花几百微秒去发UART,这本身就改变了系统的运行节奏。更别说UART通常是阻塞发送,直接把高优先级任务也拖下水。
而断点呢?它像是按下暂停键来看电影——画面是清楚了,但剧情已经断了。
所以我们需要一种 不打断程序运行、又能看到内部逻辑流动 的方法。
这就是 Event Recorder 存在的意义。
🤔 想象一下:你开着一辆车,仪表盘上没有任何显示,只能靠下车查看轮胎是否转动来判断车有没有走——这就是没有运行时可观测性的系统。
Event Recorder 到底是个啥?
简单说,它是 ARM 提供的一套轻量级 运行时事件追踪框架 ,内建于 Keil 的中间件系统中。
但它不像RTOS或者文件系统那样显眼,很多人压根不知道它的存在。其实只要你的芯片是 Cortex-M3 及以上(M4/M7/M33 都行),并且支持 ITM(Instrumentation Trace Macrocell),那你就有资格使用它。
它是怎么工作的?
别被“ITM”、“DWT”这些术语吓到,我们拆开来看:
- 硬件基础 :Cortex-M 芯片里有个叫 ITM 的模块,可以理解为一个高速“数据出口”。它通过一根叫做 SWO (Serial Wire Output)的引脚,把数据以串行方式高速发出来。
-
事件注入
:你在代码里调用
EventRecordX()函数,就会往 ITM 的某个“通道”写入一条结构化消息。 - 数据捕获 :调试器(比如 J-Link 或 ULINK)监听 SWO 引脚,把数据传回电脑上的 Keil µVision。
- 可视化呈现 :Keil 把这些事件按时间轴排列,形成一张清晰的“行为图谱”。
整个过程几乎是 非侵入式 的。什么意思?就是你记录事件的时候,CPU只花了不到1μs,几乎不影响主逻辑运行。
✅ 对比一下:
printf("start")→ 占用 UART → 阻塞几十到几百μs → 系统时序被打乱EventRecord2(...)→ 写寄存器 → <1μs完成 → 系统照常跑
差别巨大。
怎么在Keil5里把它用起来?手把手带你飞 🛫
别急着改代码,咱们一步步来。很多新手失败的原因,往往出在配置环节。
第一步:接好线 ⚡️
这是最容易翻车的地方!
标准的 SWD 下载接口只有4根线:VCC、GND、SWCLK、SWDIO。
但你要用 Event Recorder,必须加上第五根线 —— SWO !
| 引脚 | 功能 |
|---|---|
| SWCLK | 时钟 |
| SWDIO | 数据 |
| GND | 地 |
| VCC | 供电(可选) |
| SWO | 🔥 事件输出通道 |
📌 常见问题排查:
- SWO 引脚是否被复用成 GPIO?查手册确认!
- 是否所有板子都引出了 SWO?有些开发板为了省事没引出。
- 使用 J-Link 时,请确保固件版本支持 SWO trace。
如果你发现点了“开始跟踪”却收不到任何事件……八成是线没接对 😅
第二步:打开 Runtime Environment 👨💻
Keil5有个超实用的功能叫 Manage Run-Time Environment (简称 RTE),藏在工具栏这个图标里:

点进去后,找到这两个关键选项:
-
✅
Compiler > Event Recorder -
如果用了 RTOS,再勾上:
RTOS2 > RTX5并启用 “Enable Event Generation”
✅ 勾上之后,Keil 会自动帮你加入必要的库文件和头文件路径,连
EventRecorder.h
都不用手动包含!
💡 小贴士:RTE 其实是个“组件管理器”,你可以把它理解为嵌入式的“包管理器”。以后用 CMSIS、文件系统、网络栈都可以在这里一键开启。
第三步:初始化 + 打点 📍
现在可以写代码了。
初始化要放在哪里?
#include "cmsis_os2.h"
#include "EventRecorder.h"
int main(void) {
SystemCoreClockUpdate();
// 🔥 必须在这一步!
EventRecorderInitialize(EventRecordAll, 1U);
osKernelInitialize();
// 创建线程...
my_task_init();
osKernelStart(); // 启动调度器
for (;;) {}
}
⚠️ 注意顺序:
-
EventRecorderInitialize()
一定要在
osKernelStart()
之前
调用。
- 否则 RTOS 内部的一些启动事件可能就漏掉了。
参数说明:
-
EventRecordAll
:启用所有类型的事件(用户事件、内存、RTOS等)
-
1U
:开启时间戳(强烈建议开!)
如何插入自定义事件?
最常用的是这个函数:
EventRecord2(type, id, value, description);
举个真实例子:我在做一个温控系统,想看看传感器读取耗时多久。
void sensor_task(void *arg) {
while (1) {
EventRecord2(0x01U, 0x01U, 0U, "Sensor Read Start");
uint16_t raw = read_adc_channel(SENSOR_CH);
float temp = convert_to_celsius(raw);
EventRecord2(0x01U, 0x02U, raw, "Sensor Read Done");
osDelay(100);
}
}
这里的四个参数怎么理解?
| 参数 | 含义 | 实践建议 |
|---|---|---|
type
| 模块类别 | 比如 0x01=传感器,0x02=CAN,0x03=UI |
id
| 事件编号 | 0x01=开始,0x02=结束,0x03=错误 |
value
| 数值数据 | 可传 ADC 值、队列长度、错误码等 |
description
| 字符串标签 | 仅首次传输,后续只发 ID,节省带宽 |
🧠 经验之谈:字符串描述只会传一次!之后相同 ID 的事件只传 type/id/value。所以即使你每秒打几千个点,也不会撑爆 SWO 带宽。
为了方便,我通常会封装宏:
#define EVENT_SENSOR_START() EventRecord2(0x01U, 0x01U, 0U, "Sensor Start")
#define EVENT_SENSOR_DONE(v) EventRecord2(0x01U, 0x02U, v, "Sensor Done")
// 使用时就像打日志一样自然
EVENT_SENSOR_START();
read_sensor();
EVENT_SENSOR_DONE(last_value);
是不是比
printf
还简洁?
第四步:看!那个神奇的时间轴 🕰️
编译烧录后,点击调试按钮进入仿真模式。
然后注意看工具栏,找一个 波浪形图标 ——那是“Start/Stop Trace Logging”。
🔉 点击它!绿色表示正在接收事件流。
接着打开菜单:
View > Analysis Windows > Event Recorder
Boom 💥!眼前会出现一个彩色的时间线视图:
- 不同颜色代表不同模块(由 type 区分)
- 横向是时间轴(精确到微秒!)
- 你能看到每一个事件的发生时刻、持续时间、上下文切换……
更爽的是,如果你用了 RTOS,你会发现:
✅ 线程创建/删除
✅ 任务切换
✅ 信号量获取/释放
✅ 消息队列操作
全都自动出现了!不需要你写一行额外代码!
比如我能看到:
-
main_thread
在什么时候创建了
can_tx_task
-
sensor_task
等待
adc_sem
花了多久
- 某次中断触发后,哪个高优先级任务被唤醒
这一切,都是 静默发生的 ,你什么都不用做。
我靠它解决的第一个大问题 🔍
还记得前面说的那个电机控制延迟的问题吗?
理论周期10ms,实测平均15ms,波动剧烈。
以前我会怀疑是不是
osDelay()
不准?或者是中断太频繁?
但现在我不猜了,直接上 Event Recorder。
我在电机任务里打了两个点:
void motor_control_task(void *arg) {
while (1) {
EVENT_MOTOR_START(); // 自定义宏
control_motor_step();
EVENT_MOTOR_DONE();
osDelay(10);
}
}
运行后打开 Timeline,一眼就发现问题所在:
👉 在两次
MOTOR_START
之间,夹着好几个长达
5~7ms
的
network_task
执行片段!
而且我发现,
network_task
的优先级竟然设成了
osPriorityHigh
,比电机任务还高!
难怪会被抢占。
解决方案很简单
:
- 把网络任务优先级降到
osPriorityBelowNormal
- 改用 DMA + 缓冲机制处理大批量数据
改完再看 Timeline:电机任务终于稳定在 10.2ms ± 0.3ms 内完成。
🎯 从“怀疑人生”到“精准修复”,全程不到半小时。
一些血泪教训和最佳实践 ❤️🔥
别以为上了 Event Recorder 就万事大吉。我也踩过不少坑,分享给你避雷。
❌ 错误1:SWO 波特率没配对
SWO 是异步串行输出,必须和 MCU 主频匹配。
假设你的系统时钟是 100MHz,那推荐设置 SWO 为 2Mbps 。
怎么设置?
Debug → Settings → Trace → Core Clock & Trace Port
- 设置 Core Clock 为实际频率(如 100MHz)
- 勾选 Autodetect 或手动设为 2MHz
如果波特率太高,会导致数据丢失;太低则浪费带宽。
📊 经验公式:SWO 最大速率 ≈ CPU 主频 / 4
所以 100MHz 下最大支持约 25Mbps,但我们一般用 2~4Mbps 足够了。
❌ 错误2:疯狂打点,结果缓冲区溢出
我曾试图在一个 ADC 中断里每帧打一个点,采样率10kHz……结果 Event Recorder 直接罢工。
原因很简单:ITM 输出速度跟不上输入速度。
🛑 建议阈值:
- 每秒不超过
8,000 ~ 10,000 个事件
- 关键路径打点即可,不要遍地开花
可以用条件触发:
if (error_flag) {
EventRecord2(0x03U, 0x01U, err_code, "Critical Error!");
}
✅ 正确姿势1:建立统一事件编码规范
团队协作时尤其重要。
我现在的做法是建一张表:
| Type | 模块 | 示例 ID | 含义 |
|---|---|---|---|
| 0x01 | 传感器 | 0x01=start, 0x02=end | ADC采集开始/结束 |
| 0x02 | CAN通信 | 0x01=tx, 0x02=rx | 发送/接收帧 |
| 0x03 | 故障管理 | 0x01=temp_high | 温度过高 |
| 0x04 | UI界面 | 0x01=update | 屏幕刷新 |
这样后期过滤分析时,可以直接按 Type 筛选,效率极高。
✅ 正确姿势2:Release 版本关闭 Event Recorder
毕竟每次调用还是要占一点 Flash 和 RAM 的。
我的条件编译方案:
#ifdef DEBUG
#define EVENT_LOG(t, id, val, msg) EventRecord2(t, id, val, msg)
#else
#define EVENT_LOG(t, id, val, msg)
#endif
然后在 Debug 构建中定义
DEBUG
宏,在 Release 中去掉。
既不影响性能,又能保留调试能力。
✅ 正确姿势3:善用 Statistics 表格
除了 Timeline,还有一个宝藏功能: Event Statistics
点击窗口上方的 “Statistics” 标签页,你会看到:
| Event Description | Count | Frequency (Hz) | Min Time | Max Time | Avg Time |
|---|---|---|---|---|---|
| Sensor Read Start | 120 | 10.0 | - | - | - |
| Signal acquired | 120 | 10.0 | 82μs | 98μs | 89μs |
看到了吗?它不仅能统计次数,还能算出 每次事件之间的间隔时间分布 !
这对分析定时精度、抖动情况特别有用。
有一次我发现某个任务的执行频率忽高忽低,通过这个表格一看,原来是看门狗定时器干扰了调度器……这种细节光靠肉眼看代码根本发现不了。
时间戳真的可靠吗?关于 DWT_CYCCNT 的那些事 ⏱️
Event Recorder 能提供微秒级时间戳,靠的是 Cortex-M 内置的 DWT_CYCCNT 寄存器。
这是一个 24 位计数器,每 CPU 周期加一。
听起来很完美,但有个隐患: 它会回滚!
比如主频 100MHz:
- 计数范围:0 ~ 16,777,215
- 回滚时间:≈ 0.168 秒
也就是说,大约每 1/6 秒 就会归零一次。
那时间戳岂不是不准了?
放心,Event Recorder 内部已经做了 溢出补偿机制 ,会自动拼接时间,保证全局时间连续。
但你也别完全依赖它做长时间记录(比如超过几分钟)。如果要做长期行为分析,建议结合系统逻辑时间(如
osKernelGetTickCount()
)做校准。
它不只是调试工具,更是系统的“黑匣子” 📦
说到这里,我想强调一点:
Event Recorder 不应该只是你“出问题时才想起来用”的救火工具。
它完全可以成为你嵌入式系统的 内置观测能力 。
想想飞机有黑匣子,汽车有行车记录仪——为什么我们的设备不能也有“运行日志”?
设想一下这样的场景:
- 设备在现场运行异常,客户打电话过来抱怨。
- 你远程让客户插上调试器,开启 trace,复现问题。
- 回放 Event Recorder 日志,立刻定位是某次 CAN 通信超时引发了连锁反应。
这比让用户拍视频、描述现象高效太多了。
甚至你可以设计一种“飞行模式”:平时关闭 trace,当检测到异常(如看门狗复位)时,自动保存最后几秒的事件日志到 Flash,下次上电上传。
这才是真正的智能调试。
结尾没有总结,只有邀请 🚪
这篇文章写到这里,其实还有很多可以展开的点:
- 如何结合 STLINK-V3 使用?
- 能否导出 .etl 文件用第三方工具分析?
- 在双核 MCM 设备中如何同步 trace?
- 如何用 Python 脚本自动化解析事件流?
但我决定留个白。
因为最好的学习方式,不是听别人讲完所有答案,而是你自己动手试一次。
所以,不妨现在就去做一件事:
打开你手头正在开发的项目,在某个关键函数前后加上两行:
EventRecord2(0xFF, 0x01, 0, "MyFunc Start");
// ... your code
EventRecord2(0xFF, 0x02, 0, "MyFunc End");
然后接上 SWO,跑起来,看看那条时间轴上跳动的光标。
那一刻你会懂:
原来我们的代码,不只是静态的文本,它是一条奔腾的河流。
而 Event Recorder,就是那盏照亮水流方向的灯。💡

350


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



