Cortex-M 调试技术详解:DWT、ETM、ITM 与断点机制
本文基于《ARM Cortex-M 权威指南》第 14 章和实际工程经验,深入讲解 Cortex-M 处理器的调试基础设施(DWT、ETM、ITM)、断点机制(软件断点与硬件断点)及其在实际开发中的使用技巧。
一、Cortex-M 调试架构概览
1.1 CoreSight 调试系统
Cortex-M 处理器的调试功能基于 ARM 的 CoreSight 架构,主要包含以下组件:
┌────────────────────────────────────────────────────┐
│ 调试主机(PC) │
│ (GDB, Keil, IAR, SEGGER Ozone) │
└──────────────────┬─────────────────────────────────┘
│ (USB/JTAG/SWD)
┌──────────────────▼─────────────────────────────────┐
│ 调试接口(Debug Interface) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ JTAG │ │ SWD │ │ SWO │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
└──────────────────┬─────────────────────────────────┘
│
┌──────────────────▼─────────────────────────────────┐
│ TPIU(Trace Port Interface Unit) │
│ 跟踪端口接口单元 │
└──────────────────┬─────────────────────────────────┘
│
┌─────────┼─────────┬─────────────┐
│ │ │ │
┌────▼────┐ ┌─▼──┐ ┌────▼─────┐ ┌─────▼─────┐
│ ETM │ │DWT │ │ ITM │ │ FPB │
│ 嵌入式 │ │数据│ │ 指令 │ │Flash补丁 │
│ 跟踪宏 │ │监视│ │ 跟踪 │ │与断点单元 │
└─────────┘ └────┘ └──────────┘ └───────────┘
关键组件:
- DWT(Data Watchpoint and Trace):数据监视点与跟踪单元
- ETM(Embedded Trace Macrocell):嵌入式跟踪宏单元(可选,需硬件支持)
- ITM(Instrumentation Trace Macrocell):指令跟踪宏单元
- FPB(Flash Patch and Breakpoint):Flash 补丁与断点单元
- TPIU(Trace Port Interface Unit):跟踪端口接口单元
1.2 调试接口类型
| 接口类型 | 引脚数 | 传输方向 | 典型用途 |
|---|---|---|---|
| JTAG | 4~5(TCK, TMS, TDI, TDO, [TRST]) | 双向 | 传统调试接口,支持边界扫描 |
| SWD | 2(SWDIO, SWCLK) | 双向 | 现代主流,引脚少,速度快 |
| SWO | 1(SWO 输出) | 单向输出 | 配合 SWD 使用,输出 ITM/DWT 跟踪数据 |
推荐组合:SWD + SWO(STM32 等现代 MCU 的标准配置)
二、DWT(Data Watchpoint and Trace)详解
2.1 DWT 是什么?
DWT 是 Cortex-M 处理器的核心调试组件,提供以下功能:
-
性能计数器(Performance Counters)
- 周期计数器(CYCCNT):记录 CPU 时钟周期数
- CPI 计数器:记录每条指令的平均周期
- 异常开销计数器:记录中断/异常处理的时间
- 睡眠周期计数器:记录低功耗模式的时间
-
数据监视点(Data Watchpoints)
- 监视特定内存地址的读/写操作
- 触发断点或生成跟踪数据
-
PC 采样(PC Sampling)
- 周期性记录程序计数器(PC)的值
- 用于性能分析(Profiling)
2.2 使用 DWT 测量代码执行时间
2.2.1 启用 DWT 周期计数器
步骤:
- 启用 DWT 和跟踪功能(通过 CoreDebug 寄存器)
- 启用 CYCCNT 计数器
- 复位计数器
- 读取计数器值
代码示例(基于 CMSIS):
#include "stm32f4xx.h" // 或其他 MCU 的 CMSIS 头文件
// 初始化 DWT
void DWT_Init(void) {
// 启用 DWT 和 ITM(需要 CoreDebug->DEMCR)
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
// 复位 CYCCNT
DWT->CYCCNT = 0;
// 启用 CYCCNT
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
// 测量函数执行时间
uint32_t measure_execution_time(void (*func)(void)) {
uint32_t start, end;
start = DWT->CYCCNT;
func(); // 执行待测函数
end = DWT->CYCCNT;
return end - start; // 返回时钟周期数
}
使用示例:
void test_function(void) {
volatile uint32_t sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
}
int main(void) {
DWT_Init();
uint32_t cycles = measure_execution_time(test_function);
// 假设 CPU 频率为 168 MHz
float time_us = cycles / 168.0f; // 转换为微秒
printf("Execution time: %.2f us (%lu cycles)\n", time_us, cycles);
}
输出示例:
Execution time: 5.95 us (1000 cycles)
2.2.2 注意事项
-
CYCCNT 溢出:
- CYCCNT 是 32 位寄存器,在高频 CPU 上(如 400 MHz)约 10.7 秒后溢出。
- 解决方案:使用差值计算(如上例),或监听溢出中断(DWT_CTRL.CYCCNTENA)。
-
中断影响:
- 如果在测量期间发生中断,会导致计数值偏大。
- 解决方案:测量前禁用中断,或使用 DWT 的异常开销计数器过滤。
-
优化级别影响:
- 在
-O0(无优化)和-O3(激进优化)下,时间差异可能达到 10 倍。 - 建议在 Release 模式下测量。
- 在
2.3 使用 DWT 数据监视点
2.3.1 功能说明
数据监视点 可以在特定内存地址被访问时触发断点或生成跟踪数据。
支持的监视类型:
- 读访问(Load)
- 写访问(Store)
- 读写访问(Read/Write)
硬件限制:
- Cortex-M3/M4:最多 4 个 数据监视点
- Cortex-M7:最多 8 个 数据监视点
2.3.2 配置示例(手动设置寄存器)
监视变量 g_counter 的写操作:
volatile uint32_t g_counter = 0;
void setup_watchpoint(void) {
uint32_t addr = (uint32_t)&g_counter;
// 设置 DWT 比较器 0(Comparator 0)
DWT->COMP0 = addr; // 监视地址
// 设置功能寄存器:监视写操作,生成 DebugMonitor 异常
DWT->FUNCTION0 = (1 << 0) // FUNCTION: 数据监视点
| (1 << 5) // DATAVMATCH: 地址匹配
| (1 << 1); // EMITRANGE: 写操作
}
// DebugMonitor 异常处理函数
void DebugMon_Handler(void) {
printf("g_counter was written at PC = 0x%08lX\n", __get_PC());
// 清除 DebugMonitor 异常标志
SCB->DFSR = SCB_DFSR_DWTTRAP_Msk;
}
注意:
- 上述代码需要在
NVIC中启用 DebugMonitor 异常。 - 大多数调试器(如 GDB、Keil)提供更友好的 UI 来设置监视点,无需手动操作寄存器。
2.3.3 在 GDB 中使用监视点
命令示例:
# 监视变量 g_counter 的写操作
(gdb) watch g_counter
Hardware watchpoint 1: g_counter
# 运行程序,当 g_counter 被修改时自动暂停
(gdb) continue
Continuing.
Hardware watchpoint 1: g_counter
Old value = 0
New value = 42
main () at main.c:123
优点:
- 无需修改代码,直接在调试器中设置。
- 自动处理硬件寄存器配置。
三、ITM(Instrumentation Trace Macrocell)详解
3.1 ITM 是什么?
ITM 是一个轻量级的跟踪输出单元,允许程序在运行时通过 SWO 引脚输出调试信息,而不需要停止 CPU。
核心特性:
- 非侵入式:输出不影响程序的时序行为(极低开销)
- 多通道:支持 32 个通道(0~31),可分配给不同模块
- 实时输出:通过 SWO 引脚串行输出到调试主机
- 格式灵活:支持字符串、整数、浮点数等
3.2 ITM 的硬件连接
典型连接(SWD + SWO):
┌─────────────────┐ ┌─────────────────┐
│ 调试器 │ │ MCU (STM32) │
│ (ST-LINK V3) │ │ │
│ │ SWDIO │ PA13 (SWDIO) │
│ ├──────────┤ │
│ │ SWCLK │ PA14 (SWCLK) │
│ ├──────────┤ │
│ │ SWO │ PB3 (SWO) │
│ │◄─────────┤ (TRACESWO) │
│ │ GND │ GND │
│ ├──────────┤ │
└─────────────────┘ └─────────────────┘
注意:
- SWO 引脚在不同 MCU 上可能不同(如 STM32F4 是 PB3,STM32H7 可能是 PB3 或其他)。
- 需在调试器配置中指定 SWO 时钟频率(通常与 CPU 频率相关)。
3.3 使用 ITM 输出调试信息
3.3.1 初始化 ITM
代码示例(基于 CMSIS):
#include "stm32f4xx.h"
void ITM_Init(void) {
// 启用 TRCENA(跟踪使能)
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
// 解锁 ITM(写入 Magic Key)
ITM->LAR = 0xC5ACCE55;
// 启用 ITM
ITM->TCR |= ITM_TCR_ITMENA_Msk;
// 启用通道 0(默认通道,用于 printf)
ITM->TER |= (1 << 0);
}
3.3.2 输出单个字符
底层实现(手动发送字符):
void ITM_SendChar(char c) {
// 等待通道 0 就绪
while ((ITM->PORT[0].u32 & ITM_STIM_FIFOREADY_Msk) == 0);
// 发送字符
ITM->PORT[0].u8 = c;
}
3.3.3 重定向 printf 到 ITM
方法 1:重写 _write 函数(GCC):
#include <stdio.h>
int _write(int file, char *ptr, int len) {
for (int i = 0; i < len; i++) {
ITM_SendChar(*ptr++);
}
return len;
}
方法 2:使用 CMSIS 的 ITM_SendChar:
// 在 main.c 中包含
#include "core_cm4.h"
// 重写 fputc(用于 printf)
int fputc(int ch, FILE *f) {
ITM_SendChar(ch);
return ch;
}
使用示例:
int main(void) {
ITM_Init();
printf("Hello from ITM!\n");
printf("CPU Frequency: %lu Hz\n", SystemCoreClock);
while (1) {
printf("Counter: %d\n", counter++);
HAL_Delay(1000);
}
}
3.3.4 在调试器中查看 ITM 输出
Keil MDK:
- 打开 “View” → “Serial Windows” → “Debug (printf) Viewer”
- 配置 SWO 时钟频率(Project → Options → Debug → Settings → Trace)
SEGGER Ozone:
- 打开 “Terminal” 窗口
- ITM 数据自动显示
OpenOCD + GDB:
# 启动 OpenOCD(配置 SWO 输出)
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
-c "tpiu config internal itm.log uart off 168000000"
# 在另一终端查看输出
tail -f itm.log
3.4 ITM 的高级用法
3.4.1 使用多通道
场景:区分不同模块的日志输出。
代码示例:
#define ITM_CHANNEL_MAIN 0
#define ITM_CHANNEL_NETWORK 1
#define ITM_CHANNEL_SENSOR 2
void ITM_Printf(uint8_t channel, const char *fmt, ...) {
char buffer[128];
va_list args;
va_start(args, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
for (char *p = buffer; *p; p++) {
while ((ITM->PORT[channel].u32 & ITM_STIM_FIFOREADY_Msk) == 0);
ITM->PORT[channel].u8 = *p;
}
}
// 使用
ITM_Printf(ITM_CHANNEL_MAIN, "Main loop iteration: %d\n", i);
ITM_Printf(ITM_CHANNEL_NETWORK, "Received packet: %d bytes\n", len);
3.4.2 输出时间戳
启用硬件时间戳:
ITM->TCR |= ITM_TCR_TSENA_Msk; // 启用时间戳
在调试器中解析:
- 大多数调试器会自动解析时间戳并显示在日志前。
四、ETM(Embedded Trace Macrocell)详解
4.1 ETM 是什么?
ETM 是一个可选的高级跟踪单元(仅在部分 Cortex-M 芯片上实现,如 STM32H7、i.MX RT 系列),用于记录指令执行流。
核心功能:
- 指令跟踪:记录每条执行的指令地址
- 分支跟踪:记录跳转、函数调用、返回等
- 数据跟踪:结合 DWT,记录数据访问
- 异常跟踪:记录中断/异常的进入和退出
与 ITM 的区别:
| 特性 | ITM | ETM |
|---|---|---|
| 输出类型 | 软件主动输出(printf) | 硬件自动记录指令流 |
| 开销 | 极低(仅在调用时) | 较高(持续记录) |
| 硬件需求 | 所有 Cortex-M3/M4/M7 | 仅部分芯片支持 |
| 典型用途 | 实时日志输出 | 性能分析、覆盖率测试 |
4.2 ETM 的使用场景
4.2.1 代码覆盖率分析
工具:SEGGER Ozone + J-Trace
步骤:
- 配置 ETM 捕获所有指令
- 运行测试用例
- 分析哪些代码路径被执行,哪些未被执行
优点:
- 无需插桩(不修改代码)
- 100% 精确(硬件级记录)
4.2.2 性能热点分析(Profiling)
工具:Arm DS (Development Studio)
流程:
运行程序 → ETM 记录指令流 → 导入 DS → 生成火焰图
示例输出:
Function Name | Time (%) | Calls
-----------------------|----------|-------
memcpy() | 35.2% | 1024
sprintf() | 18.7% | 512
process_sensor_data() | 12.3% | 100
4.3 ETM 配置示例
注意:ETM 配置非常复杂,通常通过调试器 UI 完成,不建议手动操作寄存器。
在 SEGGER Ozone 中启用 ETM:
- 打开 “Tools” → “Trace Settings”
- 选择 “Enable ETM”
- 配置跟踪缓冲区大小(通常 16 KB ~ 1 MB)
五、断点机制详解
5.1 软件断点(Software Breakpoint)
5.1.1 原理
软件断点 通过修改目标地址的指令实现:
- 调试器将目标地址的原始指令保存
- 替换为断点指令(
BKPT #imm,编码为0xBExx) - CPU 执行到断点指令时触发异常
- 调试器恢复原始指令
ARM Thumb-2 断点指令:
BKPT #0 ; 编码: 0xBE00(16 位)
5.1.2 特点
优点:
- 数量无限(仅受 Flash/RAM 大小限制)
- 适用于所有代码区域
缺点:
- 需要可写的存储器(不能在只读 Flash 上设置,除非支持 FPB)
- 需要调试器连接(脱离调试器后失效)
5.1.3 在 Flash 上设置断点(FPB)
FPB(Flash Patch and Breakpoint) 单元允许在只读 Flash 上设置断点:
工作原理:
- FPB 比较器监视指令地址
- 当地址匹配时,拦截指令并触发断点
- 无需修改 Flash 内容
硬件限制:
- Cortex-M3/M4:6 个 指令断点比较器
- Cortex-M7:8 个 指令断点比较器
示例(GDB):
# 在 Flash 中的函数设置断点
(gdb) break main
Breakpoint 1 at 0x8000100: file main.c, line 42.
# 查看断点类型
(gdb) info breakpoints
Num Type Disp Enb Address What
1 hw breakpoint keep y 0x08000100 in main at main.c:42
注意:
- GDB 会自动选择软件断点或硬件断点(基于地址是否可写)。
- 如果硬件断点用完,会回退到软件断点(如果可写)。
5.2 硬件断点(Hardware Breakpoint)
5.2.1 原理
硬件断点 使用 FPB 单元的比较器,无需修改代码:
- 配置 FPB 比较器的地址
- CPU 取指时,硬件自动比较
- 匹配时触发 DebugMonitor 异常或停止 CPU
5.2.2 配置示例(手动设置寄存器)
在地址 0x08001234 设置硬件断点:
void setup_hw_breakpoint(uint32_t addr) {
// 启用 FPB
FPB->CTRL = FPB_CTRL_ENABLE_Msk | (2 << FPB_CTRL_NUM_CODE_Pos);
// 设置比较器 0
FPB->COMP[0] = (addr & ~0x3) | FPB_COMP_ENABLE_Msk;
}
注意:
- 地址必须 4 字节对齐(低 2 位忽略)。
- 大多数情况下由调试器自动管理,无需手动操作。
5.3 数据断点(Data Breakpoint)
数据断点 使用 DWT 的比较器(见第 2.3 节),用于监视内存访问。
典型用法:
# 监视变量被修改
(gdb) watch g_buffer[10]
Hardware watchpoint 2: g_buffer[10]
# 监视变量被读取
(gdb) rwatch g_status
Hardware read watchpoint 3: g_status
# 监视读写
(gdb) awatch g_counter
Hardware access watchpoint 4: g_counter
六、调试工具对比与选择
6.1 常用调试器对比
| 工具 | DWT 支持 | ITM 支持 | ETM 支持 | 价格 | 推荐场景 |
|---|---|---|---|---|---|
| ST-LINK V3 | ✅ | ✅ | ❌ | $30 | STM32 开发,日常调试 |
| J-Link | ✅ | ✅ | ⚠️(需 J-Trace) | $500+ | 专业开发,需 ETM |
| CMSIS-DAP | ✅ | ✅ | ❌ | $10-50 | 低成本,开源方案 |
| ULINK Pro | ✅ | ✅ | ✅ | $4000+ | 企业级,完整 ETM |
6.2 软件工具对比
| 软件 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Keil MDK | UI 友好,RTX5 集成 | 商业软件,昂贵 | 企业开发 |
| IAR EWARM | 优化强,稳定性好 | 商业软件 | 企业开发 |
| SEGGER Ozone | 功能强大,支持 ETM | 需 J-Link | 深度调试 |
| GDB + OpenOCD | 开源,灵活 | 配置复杂 | 开源项目 |
| STM32CubeIDE | 免费,STM32 官方 | 仅支持 STM32 | STM32 入门 |
七、实战案例
7.1 案例 1:定位性能瓶颈
问题:程序运行缓慢,怀疑某个函数耗时过长。
解决方案(使用 DWT):
DWT_Init();
uint32_t cycles_total = 0;
for (int i = 0; i < 100; i++) {
uint32_t start = DWT->CYCCNT;
slow_function();
uint32_t end = DWT->CYCCNT;
cycles_total += (end - start);
}
printf("Average cycles: %lu\n", cycles_total / 100);
结果:发现 slow_function() 平均耗时 5000 周期,定位到内部的 sqrt() 调用。
7.2 案例 2:追踪野指针
问题:程序崩溃,怀疑某个变量被意外修改。
解决方案(使用 DWT 监视点):
# 在 GDB 中监视变量
(gdb) watch g_critical_flag
Hardware watchpoint 1: g_critical_flag
(gdb) continue
Hardware watchpoint 1: g_critical_flag
Old value = 0
New value = 12345678
0x080012f4 in buggy_function () at bug.c:89
结果:发现 buggy_function() 中的数组越界导致覆盖了 g_critical_flag。
7.3 案例 3:实时日志输出
问题:需要在高速运行时输出调试信息,但 UART 太慢。
解决方案(使用 ITM):
ITM_Init();
void high_speed_task(void) {
ITM_Printf(0, "Task started\n");
for (int i = 0; i < 10000; i++) {
process_data(i);
if (i % 1000 == 0) {
ITM_Printf(0, "Progress: %d%%\n", i / 100);
}
}
ITM_Printf(0, "Task completed\n");
}
结果:ITM 输出不影响任务时序,UART 会导致任务延迟 10 倍。
八、最佳实践与技巧
8.1 调试建议
-
优先使用硬件断点调试 Flash 代码
- 软件断点可能被编译器优化掉
- 硬件断点不修改代码,更可靠
-
使用 ITM 代替 UART 输出调试信息
- 速度快 100 倍
- 不占用 UART 外设
-
结合 DWT 和 ITM 进行性能分析
uint32_t start = DWT->CYCCNT; critical_section(); uint32_t cycles = DWT->CYCCNT - start; ITM_Printf(0, "Critical section: %lu cycles\n", cycles); -
在 Release 模式下验证性能
- Debug 模式的优化级别通常为
-O0,不代表实际性能
- Debug 模式的优化级别通常为
8.2 常见陷阱
-
忘记启用 TRCENA
// 必须在使用 DWT/ITM 前执行 CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; -
SWO 时钟配置错误
- SWO 时钟必须与 CPU 时钟匹配
- 配置错误会导致 ITM 输出乱码
-
在中断中使用 ITM
- ITM 输出可能阻塞(等待 FIFO),导致中断延迟
- 建议只在低优先级任务中使用
九、总结
9.1 功能对比表
| 工具 | 用途 | 硬件支持 | 典型场景 |
|---|---|---|---|
| DWT | 性能测量、数据监视 | 所有 Cortex-M3/M4/M7 | 定位瓶颈、追踪野指针 |
| ITM | 实时日志输出 | 所有 Cortex-M3/M4/M7 | 替代 UART 调试 |
| ETM | 指令流跟踪 | 部分高端芯片 | 代码覆盖率、深度分析 |
| FPB | 硬件断点 | 所有 Cortex-M3/M4/M7 | Flash 代码调试 |
9.2 学习路线建议
- 入门:掌握 DWT 周期计数器 + ITM 输出
- 进阶:学习硬件断点/数据监视点的使用
- 高级:使用 ETM 进行性能分析(需专业调试器)
9.3 参考资料
-
ARM 官方文档
- ARM CoreSight Architecture Specification
- ARM Cortex-M4 Generic User Guide
-
调试器文档
- SEGGER J-Link User Guide
- ST-LINK V3 User Manual
-
开源工具
- OpenOCD: https://openocd.org
- PyOCD: https://github.com/pyocd/pyOCD
版权声明:本文基于公开技术资料整理,遵循 CC BY-SA 4.0 协议。
2211

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



