Keil5中使用Event Recorder进行实时跟踪

AI助手已提取文章相关产品:

在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”这些术语吓到,我们拆开来看:

  1. 硬件基础 :Cortex-M 芯片里有个叫 ITM 的模块,可以理解为一个高速“数据出口”。它通过一根叫做 SWO (Serial Wire Output)的引脚,把数据以串行方式高速发出来。
  2. 事件注入 :你在代码里调用 EventRecordX() 函数,就会往 ITM 的某个“通道”写入一条结构化消息。
  3. 数据捕获 :调试器(比如 J-Link 或 ULINK)监听 SWO 引脚,把数据传回电脑上的 Keil µVision。
  4. 可视化呈现 :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),藏在工具栏这个图标里:

![RTE 图标](想象这里有个小电脑+齿轮的图标)

点进去后,找到这两个关键选项:

  1. Compiler > Event Recorder
  2. 如果用了 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,就是那盏照亮水流方向的灯。💡

您可能感兴趣的与本文相关内容

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值