Bootstrap

STM32程序发生异常崩溃时,怎样从串口输出当时的程序调用栈等信息

当STM32程序发生异常崩溃时,为了从串口输出当时的程序调用栈信息,并使用Keil等工具确定具体的函数信息,你可以按照以下步骤操作:

  1. 启用调试信息输出

    • 在STM32程序中,你需要先确保启用了调试信息的输出。这通常涉及到在编译器设置中开启调试信息(如DWARF格式),以便在程序崩溃时能够输出有用的调试数据。
  2. 捕获异常并输出调用栈

    • 在程序中加入异常处理机制,例如使用C++的异常处理或者嵌入式系统中的硬件异常处理(如使用setjmplongjmp,或者定义硬件异常处理函数)。
    • 当异常发生时,捕获异常并在异常处理函数中获取当前的调用栈信息。你可以使用backtrace库或者自定义的函数来获取调用栈。
    • 将调用栈信息通过串口输出。这可能需要你将调用栈地址转换为具体的函数名或行号,这通常需要在编译时包含调试信息,并在程序运行时解析这些信息。
  3. 串口输出

    • 配置STM32的UART(通用异步收发传输器)以输出调试信息。确保串口初始化正确,波特率等参数与接收设备匹配。
    • 在异常处理函数中,将捕获的调用栈信息通过串口发送出去。
  4. 使用Keil等工具分析

    • 在Keil等IDE中,你可以使用调试器来加载崩溃时的程序镜像。
    • 通过查看串口输出的调用栈信息,你可以在Keil中找到对应的函数地址。
    • 利用Keil的符号表(Symbol Table)或者地图文件(Map File),将地址解析为具体的函数名和行号。
    • 通过分析调用栈,你可以确定是哪个函数调用导致了崩溃,并进一步调试以找到问题的根源。
  5. 注意事项

    • 确保在编译时开启了调试信息的生成,这样你才能将地址映射到具体的函数和行号。
    • 串口输出的调用栈信息可能需要进行后处理才能方便查看,例如转换为可读的函数名和行号。
    • 如果程序崩溃时无法直接输出完整的调用栈,可以考虑在关键位置插入日志输出,以便在崩溃前获取尽可能多的信息。

综上所述,通过捕获异常、输出调用栈信息,并结合Keil等工具的调试功能,你可以有效地定位和解决STM32程序中的崩溃问题。

在STM32微控制器上,捕获异常并输出调用栈信息是一个相对复杂的任务,因为这涉及到操作系统的异常处理机制和调试信息的使用。由于STM32通常运行在裸机环境中,没有操作系统的支持,因此需要手动实现异常处理和调用栈跟踪。

以下是一个简单的示例,展示如何在STM32上捕获异常并尝试输出调用栈信息。请注意,这个示例假设你使用的是STM32F4系列,并且使用HAL库进行开发。这个示例可能需要根据具体的硬件和软件环境进行调整。

1. 配置异常向量表

首先,你需要配置异常向量表,将有一个专门的函数来处理未定义指令或系统错误等异常情况。

在你的启动代码或者向量表文件中,确保有定义一个默认的异常处理函数。

例如,在startup_stm32f4xx.s文件中,找到默认的异常向量,如Default_Handler,并确保它被定义。

2. 实现异常处理函数

接下来,实现一个异常处理函数,当发生异常时,该函数会被调用。

void Default_Handler(void) {
    // 进入异常处理模式
    __disable_irq();

    // 输出错误信息
    printf("Exception occurred!\r\n");

    // 尝试获取调用栈信息
    uint32_t* stack_pointer = (uint32_t*)__get_PSP();
    for (int i = 0; i < 10; i++) {
        printf("Stack %d: 0x%08X\r\n", i, stack_pointer[i]);
    }

    // 无限循环或重启
    while (1);
}

在这个函数中,我们禁用了中断,输出错误信息,并尝试读取当前的程序堆栈指针(PSP),然后输出堆栈中的内容。这里假设堆栈是满递减堆栈,并且每个栈帧是32位。

3. 配置串口输出

确保串口已经正确配置并初始化,以便能够输出调试信息。

#include "stm32f4xx_hal.h"

UART_HandleTypeDef huart2;

void MX_USART2_UART_Init(void) {
    huart2.Instance = USART2;
    huart2.Init.BaudRate = 115200;
    huart2.Init.WordLength = UART_WORDLENGTH_8B;
    huart2.Init.StopBits = UART_STOPBITS_1;
    huart2.Init.Parity = UART_PARITY_NONE;
    huart2.Init.Mode = UART_MODE_TX_RX;
    huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart2.Init.OverSampling = UART_OVERSAMPLING_16;
    if (HAL_UART_Init(&huart2) != HAL_OK) {
        // 初始化错误处理
        Error_Handler();
    }
}

void Error_Handler(void) {
    while(1);
}

int __io_putchar(int ch) {
    HAL_UART_Transmit(&huart2, (uint8_t*)&ch, 1, 0xFFFF);
    return ch;
}

4. 在主函数中调用串口初始化

确保在主函数中调用了串口初始化函数。

int main(void) {
    HAL_Init();
    MX_USART2_UART_Init();

    // 主程序代码

    while (1) {
        // 主循环
    }
}

5. 生成调试信息

在Keil中,确保项目设置中启用了调试信息,例如DWARF格式。这样,你可以将地址映射到函数名和源代码行号。

6. 分析调用栈信息

当程序崩溃并输出调用栈信息时,你可以记录下这些地址,然后在Keil中使用“Address to line”功能,将这些地址转换为具体的函数和行号。

例如,在Keil的命令行中,输入:

addr2line -e <your_project.axf> 0xAddress

这将输出对应的文件名和行号。

注意事项

  • 这个方法仅能输出堆栈中的地址,需要手动映射到函数和行号。
  • 堆栈帧的解析取决于编译器的设置和调用约定,可能需要根据实际情况调整。
  • 在裸机环境中,没有标准的调用栈跟踪机制,因此这只是一个基本的实现,可能不适用于所有情况。

通过以上步骤,你可以在STM32程序崩溃时,通过串口输出调用栈信息,并结合Keil工具进行分析,从而定位问题所在。

在Keil中,当你已经获得了调用栈上各个函数的地址后,可以通过以下步骤利用**符号表(Symbol Table)地图文件(Map File)**来将地址解析为具体的函数名和行号。以下是详细的操作步骤:


1. 生成地图文件(Map File)

在Keil中生成地图文件是第一步。地图文件包含了程序中所有符号(函数、变量、地址等)的详细信息,包括它们的地址、大小、所属模块等。

步骤:
  1. 打开Keil项目。
  2. 点击菜单栏的 Project -> Options for Target
  3. 在弹出的窗口中,选择 Linker 标签页。
  4. 勾选 Create Map File,并选择生成地图文件的格式(通常选择 Plain 或 Extended)。
  5. 点击 OK 保存设置。
  6. 重新编译项目(Build)。

编译完成后,地图文件会生成在项目的输出目录下,通常命名为 项目名.map


2. 使用地图文件解析地址

步骤:
  1. 打开生成的 .map 文件。

  2. 在地图文件中,查找 Image Symbol Table 部分。这里列出了程序中所有符号的地址和名称。

    • 例如:
      Image Symbol Table
      Address        Name
      0x08000340     main
      0x08000500     HAL_Init
      0x08000600     SystemClock_Config
      
  3. 根据你从调用栈中获取的地址,在 Image Symbol Table 中查找对应的函数名。

    • 例如,调用栈地址是 0x08000340,在地图文件中找到对应的名称为 main
  4. 如果需要进一步定位到具体的源代码行号,可以结合调试信息。


3. 使用调试信息解析行号

如果你在编译时启用了调试信息(如DWARF格式),Keil可以直接解析地址到具体的函数和行号。

步骤:
  1. 在Keil中打开调试模式(Debug)。

  2. 在调试窗口中,点击 View -> Command Window

  3. 在命令窗口中输入以下命令,将地址解析为函数名和行号:

    ADR2LINE <地址>
    
    • 例如,如果地址是 0x08000340,输入命令:
      ADR2LINE 0x08000340
      
    • Keil会返回类似以下的结果:
      main (main.c: 10)
      
      这表示地址 0x08000340 对应的是 main 函数,位于 main.c 文件的第 10 行。
  4. 如果你没有在调试模式下,可以使用 addr2line 工具。


4. 使用 addr2line 工具解析地址

如果你在命令行环境下,可以使用 addr2line 工具来解析地址。

步骤:
  1. 确保你有编译生成的 .axf 文件(包含调试信息)。
  2. 在命令行中输入以下命令:
    addr2line -e <你的.axf文件> <地址>
    
    • 例如:
      addr2line -e project.axf 0x08000340
      
    • 输出结果类似:
      main.c:10
      
      这表示地址 0x08000340 对应的是 main.c 文件的第 10 行。

5. 结合符号表(Symbol Table)解析地址

符号表是地图文件的一部分,通常在 .map 文件的 Image Symbol Table 部分。它列出了程序中所有符号的地址、名称和大小。

步骤:
  1. 在 .map 文件中,查找 Image Symbol Table 部分。
  2. 根据调用栈中的地址,在符号表中查找对应的函数名。
  3. 如果需要进一步定位行号,可以结合调试信息和 ADR2LINE 命令或 addr2line 工具。

总结

  • 获取地图文件:确保生成 .map 文件,用于查找符号地址和名称。
  • 解析函数名:通过地图文件的 Image Symbol Table 部分,查找调用栈地址对应的函数名。
  • 解析行号
    • 在Keil调试模式下,使用 ADR2LINE 命令。
    • 在命令行环境下,使用 addr2line 工具。
  • 符号表:符号表是 .map 文件的一部分,用于快速定位函数名。

通过以上方法,你可以将调用栈中的地址解析为具体的函数名和行号,从而快速定位程序崩溃的原因。

;