这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » STM32 » ARMCortex-M(STM32)如何调试HardFault

共3条 1/1 1 跳转至

ARMCortex-M(STM32)如何调试HardFault

菜鸟
2025-04-21 16:20:57     打赏

HardFault 是 ARM Cortex-M 处理器中的一种异常。当处理器遇到无法处理的错误,或者配置为处理特定类型错误(如总线错误、内存管理错误、用法错误)的异常处理程序被禁用,或者在处理这些特定错误的过程中又发生了其他错误时,就会触发 HardFault。它是一个“兜底”的异常,表明系统遇到了严重问题。

调试 HardFault 需要耐心和系统的方法。关键在于:

实现一个能捕获足够信息的 HardFault_Handler。利用调试器获取故障状态寄存器和异常堆栈帧的值。仔细解读这些值,特别是 CFSR, HFSR, MMFAR, BFAR 以及堆栈中的 PC。结合反汇编和源代码,定位到触发故障的具体指令和代码行。分析常见原因(指针、越界、堆栈、对齐、MPU 等)并修复。

发生 HardFault 时,处理器会自动将一些关键的寄存器压入当前使用的堆栈(MSP 或 PSP),并跳转到 HardFault 处理程序。

我们的首要任务就是编写一个有效的 HardFault 处理程序,从中提取有用的信息。

1、步骤 1: 实现一个有效的 HardFault 处理程序

默认的 HardFault_Handler 通常是一个无限循环 while(1);。我们需要替换它,使其能够捕获并报告故障信息。

在你的项目中(通常在 stm32xxxx_it.c 或类似文件中)找到 HardFault_Handler 函数,并用以下代码替换或修改:

// 定义一个结构体来存储从堆栈中提取的寄存器值
typedefstruct {
    uint32_t r0;
    uint32_t r1;
    uint32_t r2;
    uint32_t r3;
    uint32_t r12;
    uint32_t lr; // Link Register
    uint32_t pc; // Program Counter
    uint32_t psr;// Program Status Register
} HardFaultRegs_t;
// 全局变量,用于在调试器中查看
volatile HardFaultRegs_t stacked_regs;
volatileuint32_t cfsr_val;
volatileuint32_t hfsr_val;
volatileuint32_t dfsr_val;
volatileuint32_t afsr_val;
volatileuint32_t mmfar_val;
volatileuint32_t bfar_val;
volatileuint32_t stacked_sp; // 保存堆栈指针本身的值
// HardFault 处理函数
// 使用 __attribute__((naked)) 避免编译器生成额外的栈操作代码
voidHardFault_Handler(void) __attribute__((naked));
voidHardFault_Handler(void)
{
    // 获取当前使用的堆栈指针 (MSP 或 PSP)
    // TST LR, #4 测试 LR 的 bit 2 (EXC_RETURN 的 bit 2)
    // 如果 bit 2 为 1,表示异常返回时使用 PSP;否则使用 MSP
    __asm volatile(
        " TST LR, #4n"          // Test bit 2 of LR: 0 = MSP, 1 = PSP
        " ITE EQn"             // If-Then-Else based on EQ flag (result of TST)
        " MRSEQ R0, MSPn"      // EQ=1 (bit 2 is 0): Use MSP, move MSP to R0
        " MRSNE R0, PSPn"      // NE=0 (bit 2 is 1): Use PSP, move PSP to R0
        " MOV %0, R0n"         // Move the selected stack pointer to the C variable 'stacked_sp'
        : "=r" (stacked_sp)    // Output operand: stacked_sp C variable
        :                      // Input operands: none
        : "r0"                 // Clobbered registers: R0 is used internally
    );
    // 从获取的堆栈指针处加载寄存器值到结构体
    // stacked_sp 现在指向 R0 的位置
    stacked_regs.r0 = *((volatileuint32_t*)(stacked_sp + 0));
    stacked_regs.r1 = *((volatileuint32_t*)(stacked_sp + 4));
    stacked_regs.r2 = *((volatileuint32_t*)(stacked_sp + 8));
    stacked_regs.r3 = *((volatileuint32_t*)(stacked_sp + 12));
    stacked_regs.r12= *((volatileuint32_t*)(stacked_sp + 16));
    stacked_regs.lr = *((volatileuint32_t*)(stacked_sp + 20));
    stacked_regs.pc = *((volatileuint32_t*)(stacked_sp + 24));
    stacked_regs.psr= *((volatileuint32_t*)(stacked_sp + 28));
    // 读取故障状态寄存器
    cfsr_val = (*((volatileuint32_t*)0xE000ED28));
    hfsr_val = (*((volatileuint32_t*)0xE000ED2C)); // 注意:HFSR 地址是 0xE000ED2C
    dfsr_val = (*((volatileuint32_t*)0xE000ED30));
    afsr_val = (*((volatileuint32_t*)0xE000ED3C));
    // 检查 MMFAR 和 BFAR 是否有效并读取
    if (cfsr_val & (1 << 7)) { // MMARVALID bit in MMFSR
        mmfar_val = (*((volatileuint32_t*)0xE000ED34));
    } else {
        mmfar_val = 0xFFFFFFFF; // 无效
    }
    if (cfsr_val & (1 << 15)) { // BFARVALID bit in BFSR
        bfar_val = (*((volatileuint32_t*)0xE000ED38));
    } else {
        bfar_val = 0xFFFFFFFF; // 无效
    }
    // 在这里可以添加代码将这些变量的值通过串口、SWO 或其他方式打印出来
    // printf("HardFault!n");
    // printf("SP = 0x%08Xn", stacked_sp);
    // printf("R0 = 0x%08Xn", stacked_regs.r0);
    // printf("R1 = 0x%08Xn", stacked_regs.r1);
    // ... (打印其他寄存器)
    // printf("PC = 0x%08Xn", stacked_regs.pc); // 出错指令的下一条地址
    // printf("LR = 0x%08Xn", stacked_regs.lr);
    // printf("PSR= 0x%08Xn", stacked_regs.psr);
    // printf("CFSR=0x%08Xn", cfsr_val);
    // printf("HFSR=0x%08Xn", hfsr_val);
    // printf("MMFAR=0x%08Xn", mmfar_val);
    // printf("BFAR=0x%08Xn", bfar_val);
    // 设置一个断点在这里,或者进入无限循环等待调试器连接
    __asm volatile("BKPT #0n"); // Software breakpoint
    // 或者
    // while(1);
}

注意:

__attribute__((naked)) 告诉编译器不要生成函数入口和出口代码(如压栈、出栈),因为我们需要精确控制堆栈指针。volatile 关键字确保编译器不会优化掉对这些变量的读写。代码中包含了读取 MSP 或 PSP 的汇编指令。你需要根据你的项目配置(如串口初始化)来添加打印信息的代码。最后使用 BKPT #0 可以在 HardFault 发生时触发一个软件断点,让调试器停在 HardFault_Handler 中,方便查看变量值。

2、步骤 2: 复现 HardFault 并使用调试器分析

编译并下载包含上述 HardFault_Handler 的代码到目标板。

连接调试器(如 ST-Link, J-Link)。

运行代码直到 HardFault 发生。如果设置了 BKPT #0,程序会自动停在断点处。如果没有设置断点,并且处理函数最后是 while(1);,则在 HardFault 发生后手动暂停程序,程序计数器应该停在 while(1); 循环内。

检查变量值在调试器的 Watch 窗口或 Memory 窗口中查看 stacked_regs, cfsr_val, hfsr_val, mmfar_val, bfar_val 等变量的值。

3、步骤 3: 解读故障信息

分析 CFSR

MMFSR (位 [7:0]):IACCVIOL (位 0): 指令访问冲突 (如从 XN 区域取指)。DACCVIOL (位 1): 数据访问冲突 (如写入只读区)。MUNSTKERR (位 3): MemManage Fault 在异常返回时出栈错误。MSTKERR (位 4): MemManage Fault 在异常进入时压栈错误。MLSPERR (位 5): MemManage Fault 发生在浮点惰性状态保存期间。MMARVALID (位 7): MMFAR 中的地址有效。BFSR(位 [15:8]):IBUSERR (位 8): 指令预取导致的总线错误。PRECISERR (位 9): 精确的数据总线错误。BFAR 有效。IMPRECISERR (位 10): 不精确的数据总线错误。BFAR 无效。通常由写缓冲区或缓存引起,错误点与报告点有延迟。UNSTKERR (位 11): BusFault 在异常返回时出栈错误。STKERR (位 12): BusFault 在异常进入时压栈错误。LSPERR (位 13): BusFault 发生在浮点惰性状态保存期间。BFARVALID (位 15): BFAR 中的地址有效。UFSR(位):UNDEFINSTR (位 16): 执行了未定义指令。INVSTATE (位 17): 尝试进入无效状态(如执行 ARM 指令)。INVPC (位 18): 无效的 PC 加载(如尝试跳转到 LSB=0 的地址)。NOCP (位 19): 尝试执行协处理器指令。UNALIGNED (位 24): 发生了未对齐访问(需要 CCR.UNALIGN_TRP 位使能)。DIVBYZERO (位 25): 执行了除以零的操作(需要 CCR.DIV_0_TRP 位使能)。

分析 HFSR

VECTTBL (位 1): 读取向量表时发生总线错误(通常发生在异常处理启动阶段)。FORCED (位 30): 表明 HardFault 是由一个可配置的故障(MemManage, BusFault, UsageFault)升级而来的,因为其处理程序被禁用或在处理时发生新故障。此时应重点查看 CFSR。DEBUGEVT (位 31): 表明 HardFault 是由调试事件引起的(例如,在 Halting 调试模式下)。

分析 MMFAR 和 BFAR

如果 MMARVALID 或 BFARVALID 置位,这两个寄存器会告诉你导致内存或总线错误的确切地址。检查这个地址是否在你预期的内存范围内,是否需要特殊访问权限(如 MPU 设置),或者是否指向了一个无效的外设地址。

分析堆栈帧中的 PC 和 LR

stacked_regs.pc: 这是导致故障的指令的下一条指令的地址。在调试器的反汇编 (Disassembly) 窗口中跳转到 PC - 2 或 PC - 4(取决于故障指令是 16 位还是 32 位 Thumb 指令)附近,查看是哪条汇编指令触发了错误。stacked_regs.lr: 链路寄存器。如果是一般函数调用导致的 HardFault,LR 包含返回地址。如果 HardFault 发生在中断/异常处理程序内部,LR 会包含一个特殊的 EXC_RETURN 值(例如 0xFFFFFFF9, 0xFFFFFFFD 等),指示处理器状态和返回后使用的堆栈。这可以帮助判断 HardFault 是否发生在中断上下文中。


图片


4、步骤 4: 定位并修复源代码

根据反汇编窗口中定位到的指令地址,结合 .map 文件或调试器的符号信息,找到对应的 C 源代码行。

分析原因:

空指针/野指针:检查 MMFAR 或 BFAR 指向的地址,或者出错指令访问的指针变量是否为 NULL 或指向了无效/已释放的内存区域。数组越界:检查数组索引是否超出了边界,导致访问了非法内存。堆栈溢出: 如果 stacked_sp 的值非常接近或超出了定义的堆栈区域的边界,或者 PC 指向了堆栈区域,则很可能是堆栈溢出。检查函数调用深度、局部变量大小、中断嵌套。可以尝试增大堆栈空间 (startup_stm32xxxx.s 文件中定义)。未对齐访问:检查代码中是否有对 uint16_t, uint32_t 等多字节类型的指针进行强制类型转换和解引用,而该指针的地址不是 2 或 4 的倍数。例如:uint32_t* p = (uint32_t*)0x20000001; val = *p;。可以修改数据结构或使用 memcpy 来避免。除零错误:检查代码中是否存在除数为零的情况。MPU 配置错误:如果使用了 MPU,检查 MPU 区域的配置是否正确,是否允许了必要的读/写/执行权限。访问无效外设地址:检查 BFAR 是否指向了一个未启用时钟或不存在的外设寄存器地址。中断/RTOS 问题:如果 HardFault 发生在中断处理或 RTOS 任务切换期间,问题可能更复杂,可能涉及中断优先级配置错误、临界区保护不足、任务堆栈太小等。检查 LR 的 EXC_RETURN 值有助于判断上下文。

根据分析出的原因修改代码,重新编译、下载并运行代码,确保HardFault 不再发生。





关键词: Cortex-M     STM32     HardFault    

院士
2025-04-22 23:37:58     打赏
2楼

对于小型的MCU来说,hardfault也就这么几个原因。

但这个检查办法还是有必要的


菜鸟
2025-04-23 11:13:09     打赏
3楼

我遇到过导致hardfault的原因:

1、数组越界/非法指针

2、缺中断函数


大家可以来总结总结


共3条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]