ARMCortex-M(STM32)如何调试HardFault

原创 美男子玩编程 2025-04-17 08:04

点击上方蓝色字体,关注我们


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, #4\n"          // Test bit 2 of LR: 0 = MSP, 1 = PSP        " ITE EQ\n"             // If-Then-Else based on EQ flag (result of TST)        " MRSEQ R0, MSP\n"      // EQ=1 (bit 2 is 0): Use MSP, move MSP to R0        " MRSNE R0, PSP\n"      // NE=0 (bit 2 is 1): Use PSP, move PSP to R0        " MOV %0, R0\n"         // 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%08X\n", stacked_sp);    // printf("R0 = 0x%08X\n", stacked_regs.r0);    // printf("R1 = 0x%08X\n", stacked_regs.r1);    // ... (打印其他寄存器)    // printf("PC = 0x%08X\n", stacked_regs.pc); // 出错指令的下一条地址    // printf("LR = 0x%08X\n", stacked_regs.lr);    // printf("PSR= 0x%08X\n", stacked_regs.psr);    // printf("CFSR=0x%08X\n", cfsr_val);    // printf("HFSR=0x%08X\n", hfsr_val);    // printf("MMFAR=0x%08X\n", mmfar_val);    // printf("BFAR=0x%08X\n", bfar_val);    // 设置一个断点在这里,或者进入无限循环等待调试器连接    __asm volatile("BKPT #0\n")// 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 不再发生。

点击阅读原文,更精彩~

美男子玩编程 多领域、有深度的开发者交流平台
评论 (0)
  • 这款无线入耳式蓝牙耳机是长这个样子的,如下图。侧面特写,如下图。充电接口来个特写,用的是卡座卡在PCB板子上的,上下夹紧PCB的正负极,如下图。撬开耳机喇叭盖子,如下图。精致的喇叭(HY),如下图。喇叭是由电学产生声学的,具体结构如下图。电池包(AFS 451012  21 12),用黄色耐高温胶带进行包裹(安规需求),加强隔离绝缘的,如下图。451012是电池包的型号,聚合物锂电池+3.7V 35mAh,详细如下图。电路板是怎么拿出来的呢,剪断喇叭和电池包的连接线,底部抽出PCB板子
    liweicheng 2025-05-06 22:58 641浏览
  • 后摄像头是长这个样子,如下图。5孔(D-,D+,5V,12V,GND),说的是连接线的个数,如下图。4LED,+12V驱动4颗LED灯珠,给摄像头补光用的,如下图。打开后盖,发现里面有透明白胶(防水)和白色硬胶(固定),用合适的工具,清理其中的胶状物。BOT层,AN3860,Panasonic Semiconductor (松下电器)制造的,Cylinder Motor Driver IC for Video Camera,如下图。TOP层,感光芯片和广角聚焦镜头组合,如下图。感光芯片,看着是玻
    liweicheng 2025-05-07 23:55 468浏览
  • 文/郭楚妤编辑/cc孙聪颖‍相较于一众措辞谨慎、毫无掌舵者个人风格的上市公司财报,利亚德的财报显得尤为另类。利亚德光电集团成立于1995年,是一家以LED显示、液晶显示产品设计、生产、销售及服务为主业的高新技术企业。自2016年年报起,无论业绩优劣,董事长李军每年都会在财报末尾附上一首七言打油诗,抒发其对公司当年业绩的感悟。从“三年翻番顺大势”“智能显示我第一”“披荆斩棘幸从容”等词句中,不难窥见李军的雄心壮志。2012年,利亚德(300296.SZ)在深交所创业板上市。成立以来,该公司在细分领
    华尔街科技眼 2025-05-07 19:25 448浏览
  • UNISOC Miracle Gaming奇迹手游引擎亮点:• 高帧稳帧:支持《王者荣耀》等主流手游90帧高画质模式,连续丢帧率最高降低85%;• 丝滑操控:游戏冷启动速度提升50%,《和平精英》开镜开枪操作延迟降低80%;• 极速网络:专属游戏网络引擎,使《王者荣耀》平均延迟降低80%;• 智感语音:与腾讯GVoice联合,弱网环境仍能保持清晰通话;• 超高画质:游戏画质增强、超级HDR画质、游戏超分技术,优化游戏视效。全球手游市场规模日益壮大,游戏玩家对极致体验的追求愈发苛刻。紫光展锐全新U
    紫光展锐 2025-05-07 17:07 345浏览
  • 硅二极管温度传感器是一种基于硅半导体材料特性的测温装置,其核心原理是利用硅二极管的电学参数(如正向压降或电阻)随温度变化的特性实现温度检测。以下是其工作原理、技术特点及典型应用:一、工作原理1、‌PN结温度特性‌硅二极管由PN结构成,当温度变化时,其正向电压 VF与温度呈线性负相关关系。例如,温度每升高1℃,VF约下降2 mV。2、‌电压—温度关系‌通过jing确测量正向电压的微小变化,可推算出环境温度值。部分型号(如SI410)在宽温域内(如1.4 K至475 K)仍能保持高线性度。
    锦正茂科技 2025-05-09 13:52 261浏览
  • Matter协议是一个由Amazon Alexa、Apple HomeKit、Google Home和Samsung SmartThings等全球科技巨头与CSA联盟共同制定的开放性标准,它就像一份“共生契约”,能让原本相互独立的家居生态在应用层上握手共存,同时它并非另起炉灶,而是以IP(互联网协议)为基础框架,将不同通信协议下的家居设备统一到同一套“语义规则”之下。作为应用层上的互通标准,Matter协议正在重新定义智能家居行业的运行逻辑,它不仅能向下屏蔽家居设备制造商的生态和系统,让设备、平
    华普微HOPERF 2025-05-08 11:40 396浏览
  • 温度传感器的工作原理依据其类型可分为以下几种主要形式:一、热电阻温度传感器利用金属或半导体材料的电阻值随温度变化的特性实现测温:l ‌金属热电阻‌(如铂电阻 Pt100、Pt1000):高温下电阻值呈线性增长,稳定性高,适用于工业精密测温。l ‌热敏电阻‌(NTC/PTC):NTC 热敏电阻阻值随温度升高而下降,PTC 则相反;灵敏度高但线性范围较窄,常用于电子设备温控。二、热电偶传感器基于‌塞贝克效应‌(Seebeck effect):两种不同
    锦正茂科技 2025-05-09 13:31 245浏览
  • 飞凌嵌入式作为龙芯合作伙伴,隆重推出FET-2K0300i-S全国产自主可控工业级核心板!FET-2K0300i-S核心板基于龙芯2K0300i工业级处理器开发设计,集成1个64位LA264处理器,主频1GHz,提供高效的计算能力;支持硬件ECC;2K0300i还具备丰富的连接接口USB、SDIO、UART、SPI、CAN-FD、Ethernet、ADC等一应俱全,龙芯2K0300i支持四路CAN-FD接口,具备良好的可靠性、实时性和灵活性,可满足用户多路CAN需求。除性价比超高的国产处理器外,
    飞凌嵌入式 2025-05-07 11:54 97浏览
  • 随着智能驾驶时代到来,汽车正转变为移动计算平台。车载AI技术对存储器提出新挑战:既要高性能,又需低功耗和车规级可靠性。贞光科技代理的紫光国芯车规级LPDDR4存储器,以其卓越性能成为国产芯片产业链中的关键一环,为智能汽车提供坚实的"记忆力"支持。作为官方授权代理商,贞光科技通过专业技术团队和完善供应链,让这款国产存储器更好地服务国内汽车厂商。本文将探讨车载AI算力需求现状及贞光科技如何通过紫光国芯LPDDR4产品满足市场需求。 车载AI算力需求激增的背景与挑战智能驾驶推动算力需求爆发式
    贞光科技 2025-05-07 16:54 230浏览
  • 二位半 5线数码管的驱动方法这个2位半的7段数码管只用5个管脚驱动。如果用常规的7段+共阳/阴则需要用10个管脚。如果把每个段看成独立的灯。5个管脚来点亮,任选其中一个作为COM端时,另外4条线可以单独各控制一个灯。所以实际上最多能驱动5*4 = 20个段。但是这里会有一个小问题。如果想点亮B1,可以让第3条线(P3)置高,P4 置低,其它阳极连P3的灯对应阴极P2 P1都应置高,此时会发现C1也会点亮。实际操作时,可以把COM端线P3设置为PP输出,其它线为OD输出。就可以单独控制了。实际的驱
    southcreek 2025-05-07 15:06 561浏览
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦