0 基本术语
ARM Cortex-M支持几种“level”的debug功能:
Halting Debug——在使用像GDB这样的debugger的时候的典型的配置。此种模式下,在调试的时候核心会处于halted(停止)状态。这种模式需要通过JTAG或者SWD来访问Debug Port。
通过ITM,DWT和ETM实现的Trace功能——这些外设可以让你在系统运行的过程中流式指令运行和数据访问。是一种非侵入式的debug方式。 像Lauterbach TRACE32这样的工具可以decode指令流,并将结果显示在GUI上面。
Debug Monitor mode——可以通过实现DebugMonitor exception handler的方式来调试全速运行的系统。这在停止核心将导致时序敏感子系统失败或连接调试器不可行的用例中非常有用。
值得注意的是,调试监视器支持不适用于 ARMv6-M 架构 (Cortex-M0)。 但是,它适用于所有其他 Cortex-M 架构。
1 相关寄存器和指令
1.1 Debug Halting Control and Status Register (DHCSR), 0xE000EDF0
仅当禁用停止调试(halting debug)时,监视模式调试才有效。 值得注意的是,上面的 C_DEBUGEN 设置必须被清除。 该位只能通过 JTAG/SWD 连接进行设置,并且只有在发生完全上电复位 (POR) 或调试器在退出时清除该位时才会复位。
1.2 Debug Exception and Monitor Control Register (DEMCR), 0xE000EDFC
DebugMonitor异常的核心(Core)配置由DEMCR寄存器的高16位控制:
where:
- MON_EN - Controls whether the DebugMonitor exception is enabled or not. When enabled, debug “events” will cause the exception to fire.
- MON_PEND - 可用于触发 DebugMonitor 异常,无论 MON_EN 是否设置为 1。
- MON_REQ - 不被 MCU 使用。 软件可以使用该位来传达有关监视模式的状态。
- MON_STEP - 可以从 DebugMonitor 异常切换以启用硬件单步执行。 设置后,从异常处理程序返回后,内核将执行一条指令,然后返回到 DebugMonitor 异常。
**注意:低位(VC_HARDERR 等)控制当发生各种类型的异常时是否自动发生调试陷阱。 这些仅在使用停止调试模式时生效。 它们允许您保证系统在遇到异常路径时停止,这在尝试调试时非常有用!
**
1.3 Debug Fault Status Register, DFSR, 0xE000ED30
通过DFSR可以查看发生了哪一种Debug Event。
当 DebugMonitor 异常触发时,可以检查 DFSR 以获取有关发生的调试事件的信息:
Some of the events are only possible when using a halting debug. For the DebugMonitor the states of interest are:
- DWTTRAP Indicates the a debug event was generated due to a configuration in the DWT.
- BKPT Indicates one or more breakpoint event took place (either via the FPB or a BKPT instruction).
- HALTED Indicates the core was halted due to a MON_STEP request.
NOTE: DFSR bits are sticky and you have to write 1 to the value to clear them
1.4 BKPT指令
2 Debug Monitor中断示例
2.1 Enabling the DebugMonitor Exception
基于调试监视器(Debug Monitor)模式的调试通过在发生调试事件(Debug Event)时触发称为“DebugMonitor Exception”的异常来进行。
(1)确保DHCSR.C_DEBUGEN == 0
如果您尝试使用监视模式调试,我建议为此添加启动检查。 否则,当您尝试测试该功能并通过 JTAG/SWD 连接调试器时,您一定会感到困惑:
bool debug_monitor_enable(void) {
volatile uint32_t *dhcsr = (uint32_t*)0xE000EDF0;
if ((*dhcsr & 0x1) != 0) {
EXAMPLE_LOG("Halting Debug Enabled - "
"Can't Enable Monitor Mode Debug!");
return;
}
// code to set up debug monitor mode
}
TIP: Disabling Halting Debug from GDB
如果您使用 GDB,可以通过清除 C_DEBUGEN 并为 DBGKEY (0xA05F) 设置适当的值来手动禁用停止模式调试:
(gdb) set *(uint32_t*)0xE000EDF0=(0xA05F<<16)
这也是保持活动 GDB 会话打开并在系统运行时探测其状态的有用方法!
(2)DEMCR.MON_EN = 1
(3)开中断
NVIC_EnableIRQ(DebugMonitor_IRQn);
(4)优先级的设置
DebugMonitor Exception Configuration Nuances
仅当异常的组优先级大于当前执行优先级时,调试事件才会触发 DebugMonitor 异常。
这是一个有用的功能,可以保证在您单步执行系统其他部分的代码时某些高优先级操作(即蓝牙无线电调度)继续运行。
Configuring the DebugMonitor exception priority will require updating SHPR3. More details can be found in our post about ARM Cortex-M exception handling(https://interrupt.memfault.com/blog/arm-cortex-m-exceptions-and-nvic#system-handler-priority-register-shpr1-shpr3—0xe000ed18—0xe000ed20).
2.1.1 Debug Monitor中断使能的完整C代码
为了简单起见,在我们的示例应用程序中,我们将 DebugMonitor 异常设置为最低的可配置中断优先级。 这意味着所有其他中断将能够在调试未从中断运行的代码时运行。
bool debug_monitor_enable(void) {
volatile uint32_t *dhcsr = (uint32_t*)0xE000EDF0;
if ((*dhcsr & 0x1) != 0) {
EXAMPLE_LOG("Halting Debug Enabled - "
"Can't Enable Monitor Mode Debug!");
return false;
}
volatile uint32_t *demcr = (uint32_t*)0xE000EDFC;
const uint32_t mon_en_bit = 16;
*demcr |= 1 << mon_en_bit;
// Priority for DebugMonitor Exception is bits[7:0].
// We will use the lowest priority so other ISRs can
// fire while in the DebugMonitor Interrupt
volatile uint32_t *shpr3 = (uint32_t *)0xE000ED20;
*shpr3 = 0xff;
EXAMPLE_LOG("Monitor Mode Debug Enabled!");
return true;
}
2.2 中断处理函数
2.2.1 A minimal DebugMonitor Handler
这个是一个套路。
我们可以使用我们在有关故障处理(our post about fault handling, https://interrupt.memfault.com/blog/cortex-m-hardfault-debug#halting–determining-core-register-state)的文章中组合的相同处理程序来转储有关导致调用 DebugMonitor 异常的代码的寄存器状态:
typedef struct __attribute__((packed)) ContextStateFrame {
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr;
uint32_t return_address;
uint32_t xpsr;
} sContextStateFrame;
void debug_monitor_handler_c(sContextStateFrame *frame) {
// DebugMonitor Exception Logic
// ...
}
__attribute__((naked))
void DebugMon_Handler(void) {
__asm volatile(
"tst lr, #4 \n"
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"b debug_monitor_handler_c \n");
}
2.2.2 在中断处理函数中打印interested寄存器的值
void debug_monitor_handler_c(sContextStateFrame *frame) {
volatile uint32_t *demcr = (uint32_t *)0xE000EDFC;
volatile uint32_t *dfsr = (uint32_t *)0xE000ED30;
const uint32_t dfsr_dwt_evt_bitmask = (1 << 2);
const uint32_t dfsr_bkpt_evt_bitmask = (1 << 1);
const uint32_t dfsr_halt_evt_bitmask = (1 << 0);
const bool is_dwt_dbg_evt = (*dfsr & dfsr_dwt_evt_bitmask);
const bool is_bkpt_dbg_evt = (*dfsr & dfsr_bkpt_evt_bitmask);
const bool is_halt_dbg_evt = (*dfsr & dfsr_halt_evt_bitmask);
EXAMPLE_LOG("DebugMonitor Exception");
EXAMPLE_LOG("DEMCR: 0x%08x", *demcr);
EXAMPLE_LOG("DFSR: 0x%08x (bkpt=%d, halt=%d, dwt=%d)", *dfsr,
(int)is_bkpt_dbg_evt, (int)is_halt_dbg_evt,
(int)is_dwt_dbg_evt);
EXAMPLE_LOG("Register Dump");
EXAMPLE_LOG(" r0 =0x%08x", frame->r0);
EXAMPLE_LOG(" r1 =0x%08x", frame->r1);
EXAMPLE_LOG(" r2 =0x%08x", frame->r2);
EXAMPLE_LOG(" r3 =0x%08x", frame->r3);
EXAMPLE_LOG(" r12 =0x%08x", frame->r12);
EXAMPLE_LOG(" lr =0x%08x", frame->lr);
EXAMPLE_LOG(" pc =0x%08x", frame->return_address);
EXAMPLE_LOG(" xpsr=0x%08x", frame->xpsr);
// ...
}
2.2.3 在中断处理函数中认为决定后续行为
要添加用于 DebugMonitor 处理的 CLI,我们需要的是一种通过 UART 从 ISR 读取字节的方法。 这可以通过轮询外设或使用配置为以比 DebugMonitor 异常更高的优先级运行的中断来完成读取。
In our example app, we will add two commands:
- c - To continue after the DebugMonitor exception fires
- s - To step one instruction and then return to the DebugMonitor Exception
typedef enum {
kDebugState_None,
kDebugState_SingleStep,
} eDebugState;
static eDebugState s_user_requested_debug_state = kDebugState_None;
void debug_monitor_handler_c(sContextStateFrame *frame) {
// ... logic to dump info ...
if (is_dwt_dbg_evt || is_bkpt_dbg_evt ||
(s_user_requested_debug_state == kDebugState_SingleStep)) {
EXAMPLE_LOG("Debug Event Detected, Awaiting 'c' or 's'");
while (1) {
char c;
if (!shell_port_getchar(&c)) {
continue;
}
EXAMPLE_LOG("Got char '%c'!\n", c);
if (c == 'c') {
s_user_requested_debug_state = kDebugState_None;
break;
} else if (c == 's') {
s_user_requested_debug_state = kDebugState_SingleStep;
break;
}
}
} else {
EXAMPLE_LOG("Resuming ...");
}
// ... other logic ...
}
2.2.4 不同中断源的handler处理方式
Debug Monitor Continue and Step Support
Stepping over a breakpoint is a several step process:
- If it’s a bkpt instruction, we need to advance the program counter by the size of the instruction (2 bytes).
- If a bkpt event was generated from a breakpoint configured in the FPB, we need to:
- Disable the FPB. Otherwise, any attempt to continue will just hit the breakpoint again.
- Single-Step one instruction.
- Re-enable the FPB and disable single-stepping in order to resume program execution.
In our DebugMon_Handler, we can: - inspect the pc from the frame parameter passed to debug_monitor_handler_c to figure out whether or not the debug event was caused by executing a breakpoint instruction.
- Disable/Enable the FPB by clearing & setting the ENABLE bit in the FPB FP_CTRL register.
- Enable/Disable single-step functionality by setting and clearing the MON_STEP bit in the DEMCR.
const uint32_t demcr_single_step_mask = (1 << 18);
if (is_bkpt_dbg_evt) {
const uint16_t instruction = *(uint16_t*)frame->return_address;
if ((instruction & 0xff00) == 0xbe00) {
// advance past breakpoint instruction
frame->return_address += sizeof(instruction);
} else {
// It's a FPB generated breakpoint
// We need to disable the FPB and single-step
fpb_disable();
EXAMPLE_LOG("Single-Stepping over FPB at 0x%x", frame->return_address);
}
// single-step to the next instruction
// This will cause a DebugMonitor interrupt to fire
// once we return from the exception and a single
// instruction has been executed. The HALTED bit
// will be set in the DFSR when this happens.
*demcr |= (demcr_single_step_mask);
// We have serviced the breakpoint event so clear mask
*dfsr = dfsr_bkpt_evt_bitmask;
} else if (is_halt_dbg_evt) {
// re-enable FPB in case we got here via single-step
// for a BKPT debug event
fpb_enable();
if (s_debug_state != kDebugState_SingleStep) {
*demcr &= ~(demcr_single_step_mask);
}
// We have serviced the single step event so clear mask
*dfsr = dfsr_halt_evt_bitmask;
}
2.3 触发中断
2.3.1 BKPT指令
static int prv_issue_breakpoint(int argc, char *argv[]) {
__asm("bkpt 1");
return 0;
}
2.3.2 通过FPB编程的方式下断点
在 FPB 中,我们可以对 FP_COMP 寄存器之一进行编程来保存我们想要中断的地址。(这个就是硬件断点的实质。 就是比较器比较pc的值和comp寄存器中的地址是否匹配。)
FP_COMP 寄存器的布局有点奇怪,但基本上对于给定的 instr_addr,FP_COMP[31:30] 映射 instr_addr[1:0],FP_COMP[28:2] 映射到 instr_addr[28:2],最低位 FP_COMP[0]控制断点是否启用。
bool fpb_set_breakpoint(size_t comp_id, uint32_t instr_addr) {
if (instr_addr >= 0x20000000) {
// for revision 1 only breakpoints in code can be installed :/
return false;
}
// make sure the FPB is enabled
FPB->FP_CTRL |= 0x3;
const uint32_t replace = (instr_addr & 0x2) == 0 ? 1 : 2;
const uint32_t fp_comp = (instr_addr & ~0x3) | 0x1 | (replace << 30);
FPB->FP_COMP[comp_id] = fp_comp;
return true;
}
For more details on how to configure the Flash Patch And Breakpoint (FPB) unit, check out our “How do breakpoints even work?” post!(https://interrupt.memfault.com/blog/cortex-m-breakpoints#flash-patch–breakpoint-unit)
参考资料
[1] https://interrupt.memfault.com/blog/cortex-m-debug-monitor 【非常好的解释blog】