nRF5 HardFault与死机等错误调试

arm-none-eabi-addr2line概述:

arm-none-eabi-addr2line 是 GNU Binutils 软件包的一部分,专门用于 ARM Cortex-M 等嵌入式系统的调试工具。它的主要功能是将程序中的地址转换为源代码中的文件名和行号,这对于调试和诊断程序中的错误特别有帮助。 是 ARM 嵌入式开发中重要的调试工具,尤其在程序崩溃或未知问题发生时,能够帮助开发者快速定位问题所在。通过将地址转换为可读的文件和行号信息,它极大地加快了调试过程。 此工具可以将一个内存地址或程序计数器值(PC值)映射回生成该机器指令的源代码文件及行号。它对于从调试信息中恢复源代码位置很有用,特别是在定位内存地址或分析错误时。通常情况下,这种地址信息来自于内存转储(例如硬件故障或程序崩溃时的堆栈回溯)。

arm-none-eabi-addr2line –help:

Usage: arm-none-eabi-addr2line.exe [option(s)] [addr(s)]
 Convert addresses into line number/file name pairs.
 If no addresses are specified on the command line, they will be read from stdin
 The options are:
  @<file>                Read options from <file>
  -a --addresses         Show addresses
  -b --target=<bfdname>  Set the binary file format
  -e --exe=<executable>  Set the input file name (default is a.out)
  -i --inlines           Unwind inlined functions
  -j --section=<name>    Read section-relative offsets instead of addresses
  -p --pretty-print      Make the output easier to read for humans
  -s --basenames         Strip directory names
  -f --functions         Show function names
  -C --demangle[=style]  Demangle function names
  -R --recurse-limit     Enable a limit on recursion whilst demangling.  [Default]
  -r --no-recurse-limit  Disable a limit on recursion whilst demangling
  -h --help              Display this information
  -v --version           Display the program's version

arm-none-eabi-addr2line.exe: supported targets: elf32-littlearm elf32-littlearm-fdpic elf32-bigarm elf32-bigarm-fdpic elf32-little elf32-big srec symbolsrec verilog tekhex binary ihex plugin
Report bugs to <http://www.sourceware.org/bugzilla/>

翻译为中文:
用法: arm-none-eabi-addr2line.exe [选项] [地址]
 将地址转换为行号/文件名对。
 如果命令行中未指定地址,将从标准输入中读取地址。
 可用选项如下:
  @<文件>                从<文件>中读取选项
  -a --addresses         显示地址
  -b --target=<bfdname>  设置二进制文件格式
  -e --exe=<可执行文件>  设置输入文件名(默认为 a.out)
  -i --inlines           展开内联函数
  -j --section=<名称>    读取相对于段的偏移量而不是地址
  -p --pretty-print      使输出更易于人类阅读
  -s --basenames         去掉目录名
  -f --functions         显示函数名称
  -C --demangle[=风格]   解码函数名称
  -R --recurse-limit     启用解码时递归限制(默认)
  -r --no-recurse-limit  禁用解码时的递归限制
  -h --help              显示此信息
  -v --version           显示程序版本

arm-none-eabi-addr2line.exe: 支持的目标格式:elf32-littlearm elf32-littlearm-fdpic elf32-bigarm elf32-bigarm-fdpic elf32-little elf32-big srec symbolsrec verilog tekhex binary ihex plugin
报告错误到 <http://www.sourceware.org/bugzilla/>

应用场景:

  1. 嵌入式系统调试:当嵌入式系统崩溃时,开发者可以从错误日志中获取崩溃时的内存地址(例如硬件异常或断言失败时)。通过 arm-none-eabi-addr2line 工具,可以轻松定位问题出现在源代码的具体位置。
  2. 堆栈回溯分析:在执行堆栈回溯时,可以将所有函数调用的地址转换成相应的代码行,帮助开发者快速找到潜在的问题。

使用技巧:

  • 带符号表的编译:要使用 arm-none-eabi-addr2line 工具,程序需要在编译时保留调试符号(通过编译选项 -g)。
  • 无符号表的处理:如果 ELF 文件没有调试符号,arm-none-eabi-addr2line 将无法提供详细的源代码行号信息,而只能给出模块的偏移地址。
  • 结合 GDB 使用:在调试会话中,arm-none-eabi-addr2line 通常和 GDB 一起使用,用于快速查找源代码中的错误。

分析死机或HardFault异常原因:

当程序死机时,可以通过 PC 和 LR 的结合,分析程序死机的原因。 应该优先读取 PC(程序计数器) 指针来排查错误,但 LR(链接寄存器) 指针同样有重要参考价值。

  • PC(程序计数器):指向当前正在执行的指令地址,是排查错误的主要线索。通过读取 PC,可以确定程序崩溃时正在执行的具体代码位置,有助于分析出问题所在(例如非法指令或访问非法地址)。

  • LR(链接寄存器):通常保存调用函数的返回地址。如果程序因为函数调用错误导致崩溃,LR 可以帮助你确定导致问题的调用链。当程序执行 BL(Branch with Link)指令时,LR 会保存调用函数的返回地址。在某些情况下,LR 比 PC 更有助于追溯崩溃前的执行路径,尤其是当 PC 指向了异常处理程序时。

HardFault和死机等错误查找使用示例:

程序arm-none-eabi-addr2line.exe
编译后的文件:nrf52832_xxaa.axf
使用nrfjprog --readregs获取PC:0x0002C2E2 LR:0x00029147

执行指令:
arm-none-eabi-addr2line -e .\nrf52832_xxaa.axf -a -f 0x0002C2E2 0x00029147

得到结果 可能出现问题地方:
0x0002c2e2
wakeup_button_cfg
../clib/../cmprslib/zerorunl2.c:?

0x00029147
nrfx_coredep_delay_us
nRF5_SDK_17.1.0_ddde560\examples\ble_peripheral\ble_app_uart\pca10040\s132\arm5_no_packs/..\..\..\..\..\..\modules\nrfx\/soc/nrfx_coredep.h:173

使用nrfjprog –readregs获取到寄存器信息示例如下:

获取到的寄存器信息:

  R0: 0x0000C6BE
  R1: 0x0002C2E1
  R2: 0x00000000
  R3: 0x20002E00
  R4: 0x0002C2E0
  R5: 0x0000001A
  R6: 0x0000FA00
  R7: 0x00000000
  R8: 0x00000000
  R9: 0x00000000
  R10: 0x00000000
  R11: 0x00000000
  R12: 0x0000000C
  SP: 0x20005DD0
  LR: 0x00029147
  PC: 0x0002C2E2
  xPSR: 0x21000000
  MSP: 0x20005DD0
  PSP: 0x00000000
  RAZ: 0x00000000
  CFBP: 0x04000000
  APSR: 0x20000000
  EPSR: 0x01000000
  IPSR: 0x00000000

通用寄存器 (R0 - R12)

  • R0 - R12: 这是 ARM Cortex-M 处理器的12个通用寄存器。它们用于存储处理中的临时数据或函数参数。寄存器可以在函数调用中传递参数,也可以用于保存局部变量。
    • R0: 0x0000C6BE:存储了一个中间结果或局部变量。
    • R1: 0x0002C2E1:可能是另一个函数参数或计算结果。
    • R2 - R12: 这些寄存器可能在不同的时刻存储不同的数据,具体内容和意义依赖于当前正在执行的程序。

堆栈指针 (SP) 和 链接寄存器 (LR)

  • SP (Stack Pointer, 堆栈指针): SP 指向当前堆栈顶的位置,用于保存局部变量、返回地址等。
    • SP: 0x20005DD0: 当前堆栈指针指向内存地址 0x20005DD0
  • LR (Link Register, 链接寄存器): LR 保存函数调用返回地址。执行完函数时,处理器会跳回到这个地址。
    • LR: 0x00029147: 当前的函数返回地址为 0x00029147,指向下一个指令。

程序计数器 (PC)

  • PC (Program Counter, 程序计数器): PC 保存了当前执行指令的地址。
    • PC: 0x0002C2E2: 当前正在执行的指令位于地址 0x0002C2E2

程序状态寄存器 (xPSR)

xPSR 是 Cortex-M 处理器中的一个综合状态寄存器,它由以下三个部分组成:

  • APSR (Application Program Status Register): 保存程序的状态信息,如条件码。
    • APSR: 0x20000000: N 位设置,表示上一次运算的结果为负值。
  • EPSR (Execution Program Status Register): 保存指令执行状态。
    • EPSR: 0x01000000: Thumb状态位 T 为1,说明处理器正以Thumb指令集执行程序。
  • IPSR (Interrupt Program Status Register): 保存中断号信息。
    • IPSR: 0x00000000: 当前没有中断正在执行。

其他状态寄存器

  • MSP (Main Stack Pointer): 这是主堆栈指针,用于系统和异常处理模式。
    • MSP: 0x20005DD0: 主堆栈指针和堆栈指针 SP 指向同一个地址,表示当前正处于主模式。
  • PSP (Process Stack Pointer): 这是进程堆栈指针,用于用户模式下的进程。
    • PSP: 0x00000000: 当前未使用进程堆栈指针,可能是在系统模式下执行。
  • CFBP (Control, Faultmask, Basepri, and Primask): 控制寄存器,用于管理异常优先级和屏蔽。
    • CFBP: 0x04000000: 表示 Basepri 设置为 0x04,用于异常优先级控制。

其他值

  • RAZ: 保留值,通常为0。
    • RAZ: 0x00000000: 保持为 0。

重点介绍:

1. 堆栈指针 (SP)

  • SP (Stack Pointer) 是一个指向当前堆栈顶部的寄存器。它在函数调用和返回过程中更新,以便跟踪局部变量和函数参数的存储。
  • 在 Cortex-M 处理器中,SP 的具体值会根据当前执行的上下文变化。通常,SP 会在进入一个函数时向下移动(降低),在退出函数时向上移动(增加)。

2. 主堆栈指针 (MSP)

  • MSP (Main Stack Pointer) 是用于主堆栈的指针,通常在处理器复位后初始化并用于所有的特权级(如处理器进入异常模式时)。
  • MSP 用于系统的默认堆栈,通常由系统调用、ISR(中断服务例程)和上下文切换等情况使用。
  • 当处理器处于 异常模式(如中断、故障等)时,MSP 是默认使用的堆栈指针。

3. 进程堆栈指针 (PSP)

  • PSP (Process Stack Pointer) 是用于用户进程堆栈的指针,通常用于在操作系统环境下的用户任务。
  • 当处理器切换到用户模式(例如,执行用户任务)时,PSP 是被使用的堆栈指针。
  • 这使得用户程序的执行可以与系统级别的处理分开,增强了安全性和稳定性。

4. SP、MSP 和 PSP 的使用场景

  • 系统启动:当 Cortex-M 处理器启动时,MSP 通常被初始化为堆栈的顶部,并且在启动时由系统使用。
  • 异常处理:在处理中断或异常时,系统会使用 MSP。异常处理程序可以直接使用 MSP 堆栈进行局部变量和参数的管理。
  • 任务切换:如果使用实时操作系统(RTOS),在进行任务切换时,系统会保存当前任务的 PSP,并恢复下一个任务的 PSP,从而使每个任务有独立的堆栈空间。
  • 堆栈溢出:当应用程序超出分配给堆栈的内存时,可能导致堆栈溢出,从而影响当前函数或调用链,甚至导致 HardFault。通过检查 SP 的位置(如接近预定义的堆栈边界),可以检测堆栈溢出问题。

5. SP、MSP 和 PSP 寄存器的状态

在你提供的寄存器信息中:

  • SP0x20005DD0,表示当前堆栈指针的位置,可能表示当前正在使用的堆栈。
  • MSP 也为 0x20005DD0,表明在崩溃发生时,MSP 和 SP 可能指向相同的内存地址,这意味着可能使用了主堆栈进行操作。
  • PSP0x00000000,表示当前未在使用进程堆栈。

hardfault_handler_keil.c 函数解释

需要打开宏
#ifndef HARDFAULT_HANDLER_ENABLED
    #define HARDFAULT_HANDLER_ENABLED 1
#endif

需要在keil中此处(Preprocessor Symbols)加入宏
DEBUG 
DEBUG_NRF NRF

需要加入文件:nRF5_SDK_17.1.0_ddde560\components\libraries\hardfault\hardfault_implementation.c
需要加入文件:nRF5_SDK_17.1.0_ddde560\components\libraries\hardfault\nrf52\handler\hardfault_handler_keil.c

主处理逻辑:
   - 使用 `ldr` 指令加载 `HardFault_c_handler` 地址,并根据链接寄存器(LR)的状态判断使用的堆栈(PSP  MSP)。
   - 如果使用 PSP,直接调用 C 语言的处理函数。
   - 如果使用 MSP,检查栈指针是否在有效范围内;如果不在有效范围内,移动栈指针到合适的位置。
   - 最后跳转到处理函数继续执行。

这段代码的核心目的是在发生硬Fault时,通过汇编级别的处理程序将控制权转移到 C 语言的处理函数,确保系统能够正确处理错误。

__ASM void HardFault_Handler(void)  // 定义一个汇编函数,用于处理硬Fault异常
{
    PRESERVE8  // 指示编译器保持 8 字节对齐,确保堆栈的正确性
    EXTERN HardFault_c_handler  // 声明外部的 C 函数,用于处理硬Fault
    EXTERN |STACK$$Base|  // 声明栈的基地址
    EXTERN |STACK$$Limit|  // 声明栈的限制地址

    ldr r3, =HardFault_c_handler  // 将处理函数的地址加载到寄存器 r3 中
    tst lr, #4  // 测试链接寄存器(LR)第 2 位,判断使用的堆栈类型

    /* PSP is quite simple and does not require additional handler */
    itt ne  // 条件执行指令,如果测试结果为不等,则执行后面的指令
    mrsne r0, psp  // 如果使用 PSP,将其值读取到寄存器 r0 中

    /* Jump to the handler, do not store LR - returning from handler just exits exception */
    bxne  r3  // 跳转到 C 处理函数,不保存 LR

    /* Processing MSP requires stack checking */
    mrs r0, msp  // 读取当前的 MSP 值到寄存器 r0

    ldr   r1, =|STACK$$Limit|  // 将栈限制加载到寄存器 r1
    ldr   r2, =|STACK$$Base|  // 将栈基地址加载到寄存器 r2

    /* MSP is in the range of the stack area */
    cmp   r0, r1  // 比较当前 MSP 和栈限制
    bhi   HardFault_MoveSP  // 如果 MSP 大于栈限制,跳转到移动栈指针处理

    cmp   r0, r2  // 比较当前 MSP 和栈基地址
    bhi   HardFault_Handler_Continue  // 如果 MSP 大于栈基地址,继续处理

HardFault_MoveSP  // 标签,表示进入栈指针移动处理
    mov   sp, r1  // 将栈指针(SP)移动到栈限制
    mov   r0, #0  // 将 r0 清零,为后续处理做准备

HardFault_Handler_Continue  // 标签,表示继续处理硬Fault
    bx r3  // 跳转到 C 处理函数

    ALIGN  // 对齐指令,确保后续代码的对齐
}  // 结束 HardFault_Handler 函数定义

新手必知

海量第三方学习资源.

超全常用工具与文档.

本站常用资源下载.

常见问题搜索.

QQ群: 542294007.

文章引用自:元仓库 OLIB.cn.