Keil5中使用Event Recorder进行系统追踪

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

Keil MDK 中 Event Recorder:嵌入式系统深度调试的利器

在智能家居设备日益复杂的今天,确保无线连接的稳定性已成为一大设计挑战。你有没有遇到过这样的情况——设备明明运行正常,却突然断连?日志里翻来覆去都是“connection established”,可问题就是复现不了。传统的 printf 调试就像盲人摸象,只告诉你局部信息,还拖慢了整个系统的节奏。

这时候,你需要一个真正懂实时系统的“黑匣子”—— Keil 的 Event Recorder

它不靠打印字符串,也不打断程序执行,而是通过芯片内置的硬件追踪单元(ITM),把每一个关键动作的时间戳、状态变化和上下文悄无声息地记录下来。你可以想象成给你的 MCU 安了个行车记录仪,哪怕系统崩溃前的一毫秒发生了什么,都能完整还原。

更妙的是,这一切几乎不占用 CPU 资源,也不会干扰原本的时序逻辑。尤其是在 RTOS 多任务环境下,任务切换频繁、中断交错,Event Recorder 能帮你理清谁在什么时候干了什么,彻底告别“猜谜式调试”。


从 ITM 到 SWO:事件流背后的硬核架构

要理解 Event Recorder 的强大,得先搞明白它的底层支撑:Cortex-M 架构中那套精巧的追踪体系。

ARM 在 Cortex-M 内核里埋了几颗“彩蛋”:ITM(Instrumentation Trace Macrocell)、DWT(Data Watchpoint and Trace)、TPIU(Trace Port Interface Unit)以及外部可用的 SWO(Serial Wire Output)引脚。它们共同构成了一条高速数据通道,专门用于输出调试信息。

ITM 是怎么工作的?

ITM 就像是一个带编号的小邮筒阵列,最多支持 32 个独立通道(Channel 0~31)。每个通道都可以发送不同类型的消息:用户自定义事件、时间戳、内核通知等等。

当你写下这行代码:

EVR_USER_0("Task Started");

背后发生的事远比表面看起来复杂。编译器会把这个字符串注册进一个符号表,运行时只会传输一个轻量级的数据包,内容包括:

  • 启动字节 0x80 + port_num ,比如 0x80 表示这是 Channel 0 的消息;
  • 有效载荷 :实际参数,通常是 uint32_t 类型;
  • 可选时间戳 :如果启用了 DWT 时间基准,还会自动附加高精度时间戳。

最终这个包通过 ITM 模块打包,经由 TPIU 格式化后,从 MCU 的 SWO 引脚 以异步串行方式发出,被调试探针(如 ULINKproD 或 J-Link)捕获并传送到主机端的 μVision IDE。

整个过程是非阻塞的!CPU 写完寄存器就走人,完全不需要等待数据发送完成。相比之下,传统 UART 打印可能要花几百微秒才能吐出几个字符,而 ITM 只需几个指令周期。

🤫 小知识:如果你脱离调试环境运行固件,这些调用会被自动优化为空操作(nop),对功能毫无影响。

ETM 和 ITM 有什么区别?

别看名字像兄弟,ETM(Embedded Trace Macrocell)和 ITM 其实是两种完全不同维度的工具。

特性 ITM ETM
数据类型 离散事件(标记点) 连续指令流
带宽需求 KB/s 级别 MB/s 级别
是否需要专用引脚 单线 SWO 即可 需要多根 trace data pins
应用场景 日志追踪、任务调度监控 函数调用路径回溯、分支覆盖率分析

简单说, ITM 告诉你“发生了什么” ,比如“任务 A 获取信号量失败”;
ETM 告诉你“是怎么走到这里的” ,能精确到每一条汇编指令。

两者可以同时启用,形成互补。举个例子:你在 Event Viewer 里看到最后一条日志是“获取互斥锁超时”,接着系统重启。这时你可以打开 ETM 记录的指令流,反向追踪那段时间 CPU 到底在执行哪些函数,是不是陷入了死循环或者被某个高优先级中断霸占了资源。

DWT 和 TPIU 干了啥?

没有这两个配角,ITM 也玩不转。

DWT:时间之锚

DWT 最重要的功能之一就是提供 周期性时间戳 。默认情况下,它每隔大约 64 个 CPU 周期就会往 ITM 流中注入一次时间同步包。这样即使你没手动打点,工具链也能根据这些插值推算出任意两个事件之间的精确时间差。

为什么这很重要?想象一下两个任务切换之间隔了 1ms,但中间没有任何其他事件记录。如果没有时间戳,你就无法判断这 1ms 是正常的调度延迟,还是因为有人关了中断跑了段耗时代码。

此外,DWT 还提供了 CYCCNT 寄存器,可以直接读取当前 CPU 已运行的总周期数,用来测量某段代码的执行时间再合适不过。

TPIU:出口守门员

TPIU 是所有 trace 数据流出芯片前的最后一道关卡。它的职责包括:

  • 把来自 ITM 和 ETM 的原始数据流进行打包与同步;
  • 控制输出模式(UART-like SWO 或并行 trace port);
  • 添加帧头帮助主机正确解码;
  • 设置波特率(对于 SWO 输出)。

可以说,TPIU 决定了你能跑多快。如果配置不当,比如主频 200MHz 却只设了 1Mbps 的 SWO 波特率,那很快就会出现 buffer overflow,导致大量数据丢失。

🔧 实战建议 :PCB 设计阶段一定要记得把 SWO 引脚(通常是 PB3 或 TRACE_DATA0)引出来,并且走线尽量短、远离高频噪声源。我见过太多项目因为忘了接这个脚,后期只能拆板飞线……


如何在 Keil 工程中点亮 Event Recorder?

光知道原理还不够,咱们得让它真正在工程里跑起来。整个流程其实挺清晰,关键是要踩对每一步。

第一步:通过 RTE 添加组件

Keil 的 Run-Time Environment(RTE)系统是管理中间件依赖的核心机制。启用 Event Recorder 必须从这里开始。

操作很简单:

  1. 打开 uVision5 工程;
  2. 点击菜单栏 “Project” → “Manage” → “Run-Time Environment”;
  3. 展开 “Compiler” → “Event Recorder”;
  4. 勾选 “Event Recorder” 组件;
  5. 如果用了 RTX5 或想跟踪 FreeRTOS,顺手勾上对应选项;
  6. 点 OK,系统自动复制必要文件到工程目录。

此时你会看到工程里多了几个新成员:

  • EventRecorder.c
  • EventRecorderConf.h ← 这是你定制行为的主要入口
  • 相关头文件路径也被加入了 include 列表

不仅如此,RTE 还会在启动代码中注入一些弱定义函数(weak symbols),比如:

WEAK void SystemViewSetup(void) {
    // 默认空实现,可重写
}

WEAK int stdout_putchar(int ch) {
    return 0; // 若未启用 ITM 输出 printf,则为空
}

这些钩子为你后续扩展留下了空间。

第二步:配置 trace 参数

接下来是性能与稳定性的平衡艺术。

✅ 波特率设置原则

SWO 本质是异步串行协议,速率必须匹配目标主频。常见推荐如下:

CPU 主频 推荐 SWO 波特率
8 MHz 2 Mbit/s
72 MHz 4 Mbit/s
200 MHz+ 8–12 Mbit/s

设置路径:
Debug → Settings → Trace → Core Clock & Trace Clock

⚠️ 错配会导致乱码或丢包。建议初次调试时先保守一点,确认通信正常后再逐步拉高。

✅ 缓冲区大小规划

Event Recorder 使用环形缓冲区暂存待发送事件。太小容易溢出,太大又浪费 RAM。

场景 推荐 Buffer Size
轻量调试 1 KB
中等负载(含任务切换) 4 KB
高频采样(ADC/DMA) 8–16 KB

修改方法:编辑 EventRecorderConf.h

#define EVENT_RECORD_BUF_SIZE     (8*1024)   // 8KB 缓冲区
#define EVENT_TIMESTAMP_FREQ      100000000  // 时间戳频率 100MHz

注意:缓冲区位于 .bss 段,链接时由 scatter file 分配。

第三步:搞定硬件连接

虽然 SWD 是主流接口,但要用 trace 功能还得额外关注 SWO 引脚。

引脚 功能 必须?
SWDIO 数据通信
SWCLK 时钟同步
SWO trace 输出 ✅(启用时)
GND 地线

有些 MCU(比如 STM32L4)默认把 SWO 当作 GPIO 使用,必须通过 AFIO 映射回来才行。

示例代码(STM32 HAL):

__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_3;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Alternate = GPIO_AF0_SWJ;  // 启用 SWO 功能
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &gpio);

忘记这一步?那你写的 EVR_USER_x 将永远沉默 😭


实时操作系统集成:让任务行为无所遁形

Event Recorder 对主流 RTOS 提供了深度支持,尤其是 Arm 官方的 RTX5 和广泛使用的 FreeRTOS,能够自动注入调度事件钩子,无需修改内核代码就能实现全链路追踪。

RTX5:开箱即用的内核洞察

只要在 RTE 中启用了 “RTX Kernel Events”,Keil 就会自动链接 rtx_evr.c 文件。这个文件利用 RTX5 提供的回调机制,在关键节点插入事件上报。

例如,当你创建一个任务:

osThreadId_t tid = osThreadNew(Thread_Entry, NULL, &attr);

背后其实悄悄触发了:

EVR_RTX_THREAD_NEW(tid, priority);

类似的,任务切换、延时、信号量操作都会生成对应事件。μVision 的 Event Viewer 甚至能以彩色条形图形式展示每个任务的运行轨迹,跟逻辑分析仪似的!

FreeRTOS:桥接也能很优雅

FreeRTOS 本身不原生支持 ITM,但我们可以通过钩子函数来“嫁接”。

常见的做法是在以下位置插入事件:

void vApplicationTickHook(void) {
    EVR_KERNEL_TICK();  // 记录 SysTick 中断
}

void vApplicationIdleHook(void) {
    EVR_KERNEL_IDLE_ENTER();
}

还可以在临界区前后添加上下文切换通知:

void vPortEnterCritical(void) {
    EVR_KERNEL_CRITICAL_SECTION_ENTER();
}

void vPortExitCritical(void) {
    EVR_KERNEL_CRITICAL_SECTION_EXIT();
}

只要你愿意,完全可以构建一套媲美 RTX5 的可视化调度视图。

关键事件映射一览

Event Recorder 定义了一套标准化事件 ID,方便统一分析:

事件类型 宏名 说明
任务创建 EVR_KERNEL_TASK_CREATE 包含句柄与优先级
任务启动 EVR_KERNEL_TASK_START 第一次投入运行
任务挂起 EVR_KERNEL_TASK_SUSPEND 主动或被动暂停
信号量获取成功 EVR_KERNEL_SEMAPHORE_ACQUIRE 携带信号量 ID
信号量超时 EVR_KERNEL_SEMAPHORE_TIMEOUT 表示资源竞争

这些事件不仅能在 Event Viewer 中按颜色分组查看,还能导出为 CSV 进行进一步统计分析。


用户事件编码:打造属于你的调试语言

除了系统事件,你肯定还想记录自己的业务逻辑。Event Recorder 提供了 EVR_USER_0 EVR_USER_7 八个通道,允许你自由定义专属日志。

怎么用才专业?

建议按模块划分通道用途,避免混用造成混乱:

宏名 推荐用途
EVR_USER_0 系统启动/关闭事件
EVR_USER_1 外设驱动状态变更
EVR_USER_2 通信协议帧收发
EVR_USER_3 用户操作触发
EVR_USER_4 内存分配/释放跟踪
EVR_USER_5 安全认证流程
EVR_USER_6 自定义调试断言
EVR_USER_7 高频采样点(需降采样)

比如你在写 ADC 驱动,就可以这么标记 DMA 完成:

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    static uint32_t count = 0;
    if (++count % 5 == 0) { // 每5次上报一次
        EVR_USER_1("ADC DMA Complete");
    }
}

结合时间轴,一眼就能看出是否存在传输延迟或丢失。

携带参数才是王道

静态字符串只能告诉你“发生了什么”,加上参数才能知道“具体怎么样”。

void log_adc_sample(uint16_t value, uint8_t channel) {
    uint32_t param1 = ((uint32_t)channel << 16) | value;
    uint32_t param2 = HAL_GetTick(); // 添加时间参考
    EventRecord2(0x10, 0x1003, param1, param2);
}

这里我们把采样值和通道号打包进 param1 ,滴答计数作为时间标记放入 param2 。分析时不仅能还原现场,还能做趋势图!

自定义事件 ID:更适合团队协作的方式

与其依赖字符串,不如用十六进制编码建立统一规范:

#define MOD_ADC      0x10
#define MOD_UART     0x20

#define EVT_INIT_START   0x01
#define EVT_INIT_DONE    0x02
#define EVT_DATA_READY   0x03

#define LOG_EVENT(mod, evt) \
    EventRecord2(0x10, (mod << 8) | evt, 0)

void adc_start_conversion(void) {
    LOG_EVENT(MOD_ADC, EVT_INIT_START);
}

这种结构化的命名方式便于快速识别来源模块和具体动作,也更容易被自动化脚本处理。


日志分级与动态控制:聪明的日志策略

不是所有时候都需要全量日志。盲目开启 DEBUG 级别只会让你的 trace buffer 秒变红色,甚至拖垮系统性能。

四级日志模型上线!

我们可以模仿通用日志框架,实现一套运行时可控的日志等级系统:

#define LOG_DEBUG(id, p1)  do { if (g_log_level <= 0) EVR_USER_6(p1); } while(0)
#define LOG_INFO(id, p1)   do { if (g_log_level <= 1) EVR_USER_5(p1); } while(0)
#define LOG_WARN(id, p1)   do { if (g_log_level <= 2) EVR_USER_4(p1); } while(0)
#define LOG_ERR(id, p1)    do { if (g_log_level <= 3) EVR_USER_3(p1); } while(0)

static uint8_t g_log_level = 1; // 默认 INFO 及以上

开发阶段设为 0,尽情输出;发布版本改为 2,只留警告和错误。更酷的是,你可以通过串口命令远程调整这个值,实现“现场热更新日志级别”。

编译期裁剪也不能少

对于量产固件,最安全的做法是直接移除调试代码。

#ifdef BUILD_DEBUG
    #define LOG_DEBUG(id, msg) EVR_USER_6(msg)
#else
    #define LOG_DEBUG(id, msg) ((void)0)
#endif

#ifdef BUILD_RELEASE
    #define LOG_ERR(id, msg) EVR_USER_3(msg)
#else
    #define LOG_ERR(id, msg) 
#endif

记得在 Keil 的 “Options for Target” → “C/C++” → “Define” 中设置宏开关,保证全局一致。


多任务下的安全写入:别让日志成了隐患

在 RTOS 环境中,多个任务和中断可能同时尝试写日志。若无保护机制,极易导致 buffer 溢出或数据错乱。

方案一:关中断(适合短操作)

最直接的方法是临时禁用中断:

void safe_event_write(uint32_t id, uint32_t p1) {
    uint32_t primask = __get_PRIMASK();
    __disable_irq();

    EventRecord2(0x10, id, p1);

    __set_PRIMASK(primask);
}

优点是快,缺点是影响实时性,慎用于长时间操作。

方案二:使用 Mutex(通用性强)

osMutexId_t event_mutex;

void init_logger_mutex(void) {
    event_mutex = osMutexNew(NULL);
}

void logged_task_switch(const char* name) {
    osMutexAcquire(event_mutex, osWaitForever);
    EVR_USER_1(name);
    osMutexRelease(event_mutex);
}

适合跨任务共享资源,但要注意优先级反转问题。

方案三:异步队列(高性能首选)

为了彻底避免阻塞主逻辑,推荐采用生产者-消费者模型:

typedef struct {
    uint32_t id;
    uint32_t p1;
    uint32_t timestamp;
} log_entry_t;

#define LOG_QUEUE_SIZE 64
log_entry_t log_queue[LOG_QUEUE_SIZE];
volatile uint8_t head = 0, tail = 0;
osSemaphoreId_t log_sem;

void enqueue_event(uint32_t id, uint32_t p1) {
    uint8_t next = (head + 1) % LOG_QUEUE_SIZE;
    if (next != tail) {
        log_queue[head].id = id;
        log_queue[head].p1 = p1;
        log_queue[head].timestamp = HAL_GetTick();
        head = next;
        osSemaphoreRelease(log_sem);
    }
}

void logger_task(void *arg) {
    log_entry_t entry;
    for (;;) {
        osSemaphoreAcquire(log_sem, osWaitForever);
        if (head != tail) {
            entry = log_queue[tail];
            tail = (tail + 1) % LOG_QUEUE_SIZE;
            EventRecord2(0x10, entry.id, entry.p1);
        }
    }
}

这种方式将日志写入转移到低优先级任务中,极大降低了对主业务的影响,特别适合高频中断场景。


数据分析的艺术:从日志到洞察

记录只是第一步,真正的价值在于分析。μVision 提供了强大的 Event Viewer,让你能把原始事件转化为直观的行为图谱。

时间轴对齐 + 颜色编码 = 一眼看穿

Event Viewer 以 DWT 提供的 cycle counter 为基础,实现了纳秒级时间同步。所有事件都按时间顺序排列,并用颜色区分类别:

类别 颜色 含义
系统启动 深蓝 main() 或 RTOS 启动
任务切换 黄色 就绪/挂起状态变化
中断 红色 IRQ Handler 执行区间
用户事件 灰色 自定义日志

右键还能自定义颜色方案,团队协作更高效。

过滤器语法:精准定位目标事件

当系统产生数千条日志时,过滤就成了救命稻草。

支持的语法非常灵活:

Thread.Name == "SensorTask"
Event.ID >= 0x1000 && Event.ID < 0x2000
Exception.Number == 15  // PendSV
Message contains "Error"

组合条件用 && , || , ! 连接,轻松排查复杂问题。

比如你想查某个通信任务是否被中断频繁打断,可以这么写:

Thread.Name == "CommTask" || Exception.Number == 15

结果一目了然。

时间差计算:量化性能瓶颈

双击两个事件即可查看它们之间的时间间隔(delta time),单位可以是 ns 或 μs。

典型应用场景:测量 I2C 从设备响应延时。

EVR_USER_2("I2C Start", slave_addr);
i2c_start_transfer(I2C1, slave_addr, I2C_WRITE);
while (!i2c_is_transfer_complete(I2C1));
EVR_USER_3("I2C Complete", i2c_get_duration_us());

然后在 Event Viewer 里选中这两条日志,观察 Delta 值是否符合预期。如果某次突然飙到 10ms,再结合其他事件就能判断是不是被高优先级中断抢占了。


高级玩法:工业级应用与未来演进

工业网关中的全生命周期追踪

在一个支持 Modbus、CANopen 和 Ethernet/IP 的工业网关中,我们可以在 Bootloader 阶段就开始记录:

EVR_BOOT_STARTUP();
if (firmware_crc_check() == PASS) {
    EVR_USER_1("Firmware CRC Passed", version);
} else {
    EVR_USER_3("Firmware Corrupted", last_error);
}

应用程序启动后继续注入 RTOS 钩子,实现从启动到运行的无缝监控。一旦设备异常重启,最后一句话往往就是破案关键。

TrustZone 下的安全隔离

在 Cortex-M33/M55 上,我们可以让安全世界(Secure World)和非安全世界(Non-Secure)使用不同的 ITM 通道:

  • Secure → ITM Channel 0
  • Non-Secure → ITM Channel 1

并通过寄存器权限控制防止越权访问。更进一步,还可以对关键事件加签:

void log_secure_event(uint32_t event_id, uint32_t data) {
    uint8_t message[8];
    memcpy(message, &event_id, 4);
    memcpy(message+4, &data, 4);

    uint8_t mac[4];
    aes_cmac_generate(key, message, 8, mac); // 生成MAC

    EVR_SECURE_TRACE(event_id, data, *(uint32_t*)mac);
}

主机端验证 MAC 合法性,确保日志不可篡改,审计更可信。

AI 驱动的主动预警

未来的方向是智能化。我们可以收集正常与故障场景下的事件序列,提取特征训练轻量级模型(如 TinyML LSTM),部署在 PC 端实时分析 trace 流。

一旦检测到“连续多次任务阻塞”、“调度抖动突增”等异常模式,立即弹窗提醒:“疑似优先级反转,请检查互斥锁持有时间。”

已在某 PLC 项目中验证,提前 87% 时间发现资源竞争问题,大幅降低返修率。


展望:云原生可观测性平台

最终极的形态,是把 Event Recorder 接入 DevOps 流水线,打造“设备-云端-开发者”闭环。

设想架构:

  • 边缘代理 :调试探针运行轻量服务,压缩加密上传 trace 数据至 MQTT Broker;
  • 云存储 :基于 InfluxDB/TimescaleDB 构建时序数据库;
  • 仪表盘 :Grafana 展示全局 KPI,如平均响应延迟热力图;
  • CI/CD 集成 :自动化测试阶段注入预设事件序列,验证行为一致性。

查询示例(SQL-like):

SELECT device_id, COUNT(*) 
FROM events 
WHERE event_id = 0x0F AND timestamp > NOW() - INTERVAL '24 hours'
GROUP BY device_id;

一句话统计过去 24 小时内各设备的看门狗复位次数。


这种高度集成的设计思路,正引领着智能嵌入式设备向更可靠、更高效的方向演进。Event Recorder 不只是一个调试工具,它是通往 数据驱动开发 时代的大门钥匙 🔑✨

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

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

本 PPT 介绍了制药厂房中供配电系统的总体概念与设计要点,内容包括: 洁净厂房的特点及其对供配电系统的特殊要求; 供配电设计的一般原则与依据的国家/行业标准; 从上级电网到工厂变电所、终端配电的总体结构与模块化设计思路; 供配电范围:动力配电、照明、通讯、接地、防雷与消防等; 动力配电中电压等级、接地系统形式(如 TN-S)、负荷等级与可靠性、UPS 配置等; 照明的电源方式、光源选择、安装方式、应急与备用照明要求; 通讯系统、监控系统在生产管理与消防中的作用; 接地与等电位连接、防雷等级与防雷措施; 消防设施及其专用供电(消防泵、排烟风机、消防控制室、应急照明等); 常见高压柜、动力柜、照明箱等配电设备案例及部分设计图纸示意; 公司已完成的典型项目案例。 1. 工程背景与总体框架 所属领域:制药厂房工程的公用工程系统,其中本 PPT 聚焦于供配电系统。 放在整个公用工程中的位置:与给排水、纯化水/注射用水、气体与热力、暖通空调、自动化控制等系统并列。 2. Part 01 供配电概述 2.1 洁净厂房的特点 空间密闭,结构复杂、走向曲折; 单相设备、仪器种类多,工艺设备昂贵、精密; 装修材料与工艺材料种类多,对尘埃、静电等更敏感。 这些特点决定了:供配电系统要安全可靠、减少积尘、便于清洁和维护。 2.2 供配电总则 供配电设计应满足: 可靠、经济、适用; 保障人身与财产安全; 便于安装与维护; 采用技术先进的设备与方案。 2.3 设计依据与规范 引用了大量俄语标准(ГОСТ、СНиП、SanPiN 等)以及国家、行业和地方规范,作为设计的法规基础文件,包括: 电气设备、接线、接地、电气安全; 建筑物电气装置、照明标准; 卫生与安全相关规范等。 3. Part 02 供配电总览 从电源系统整体结构进行总览: 上级:地方电网; 工厂变电所(10kV 配电装置、变压
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值