目录
概述
本篇文章介绍如何使用STM32HAL库,移植CmBacktrace:ARM Cortex-M 系列 MCU 错误追踪库示例。
GitHub:https://github.com/armink/CmBacktrace
硬件:STM32F103CBT6最小系统板
软件:Keil 5.29 + STM32CubeMX6.20
一、使用方法
0、CmBacktrace 是什么
CmBacktrace (Cortex Microcontroller Backtrace)是一款针对 ARM Cortex-M 系列 MCU 的错误代码自动追踪、定位,错误原因自动分析的开源库。主要特性如下:
- 支持的错误包括:
- 断言(assert)
- 故障(Hard Fault, Memory Management Fault, Bus Fault, Usage Fault, Debug Fault)
- 故障原因 自动诊断 :可在故障发生时,自动分析出故障的原因,定位发生故障的代码位置,而无需再手动分析繁杂的故障寄存器;
- 输出错误现场的 函数调用栈(需配合 addr2line 工具进行精确定位),还原发生错误时的现场信息,定位问题代码位置、逻辑更加快捷、精准。也可以在正常状态下使用该库,获取当前的函数调用栈;
- 支持 裸机 及以下操作系统平台:
- RT-Thread
- UCOS
- FreeRTOS(需修改源码)
- 根据错误现场状态,输出对应的 线程栈 或 C 主栈;
- 故障诊断信息支持多国语言(目前:简体中文、英文);
- 适配 Cortex-M0/M3/M4/M7 MCU;
- 支持 IAR、KEIL、GCC 编译器;
1、为什么选择 CmBacktrace
入门新人 :对于从 C51 、MSP430 等简单单片机转而使用更加复杂的 ARM 新人来说,时不时出现的 "hard falut" 死机会让新人瞬间懵掉。定位错误的方法也往往是连接上仿真器,一步步 F10/F11 单步,定位到具体的错误代码,再去猜测、排除、推敲错误原因,这种过程十分痛苦。
熟练老手 :慢慢的大家知道可以通过故障寄存器信息来定位故障原因及故障代码地址,虽然这样能解决一小部分问题,但是重复的、繁琐的分析过程也会耽误很多时间。而且对于一些复杂问题,只依靠代码地址是无法解决的,必须得还原错误现场的函数调用逻辑关系。虽然连接仿真器可以查看到的函数调用栈,但故障状态下是无法显示的,所以还是得一步步 F10/F11 单步去定位错误代码的位置。另外,还有两种场景,
- 1、很多产品真机调试时必须断开仿真器
- 2、问题确实存在,但是极难被重现
所以定位这类问题就显得难上加难。
使用本库 :上述所有问题都迎刃而解,可以将错误信息输出到控制台上,还可以将错误信息使用 EasyFlash 的 Log 功能保存至 Flash 中,设备死机后重启依然能够读取上次的错误信息。CmBacktrace 输出的信息包括函数调用栈、故障诊断结果、堆栈、故障寄存器及产品固件信息,极大的提升了错误定位的效率及准确性。
俗话说,工欲善其事,必先利其器。所以有时候做事效率低的原因也许是,你会用的工具种类太少。
合作、贡献 :开源软件的发展离不开大家的支持,欢迎大家多提建议,也希望更多的人一起参与进来,共同提高 。如果觉得这个开源项目很赞,可以点击 项目主页 (Github|OSChina|Coding) 右上角的 Star ,同时把它推荐给更多有需要的朋友。
2、CmBacktrace 如何使用
2.1 演示
该演示分如下几个步骤:
- 1、制造除零异常(IAR 工程,点击查看源码)
- 2、查看错误诊断信息
- 3、查看函数调用栈基本信息
- 4、通过命令行工具进入项目工程存放可执行文件的路径
- 5、使用 addr2line 命令,查看函数调用栈详细信息,并定位错误代码
2.2 Demo
目录 | 平台 | 链接 |
---|---|---|
\demos\non_os\stm32f10x | 裸机 STM32 Cortex-M3 | 点击查看 |
\demos\os\rtthread\stm32f4xx | RT-Thread STM32 Cortex-M4 | 点击查看 |
\demos\os\ucosii\stm32f10x | UCOSII STM32 Cortex-M3 | 点击查看 |
\demos\os\freertos\stm32f10x | FreeRTOS STM32 Cortex-M3 | 点击查看 |
2.3 移植说明
2.3.1 准备工作
- 1、查看
\demos
目录下有没有合适自己的 Demo ,如有类似,则建议在其基础上修改 - 2、明确操作系统/裸机平台及 CPU 平台
- 3、将
\src
下的全部源文件添加至产品工程中,并保证源码目录被添加至头文件路径 - 4、cmb_fault.s 汇编文件(点击查看)可以选择性添加至工程,添加后需要把项目原有的
HardFault_Handler
注释掉 - 5、把
cm_backtrace_init
函数放在项目初始化地方执行 - 6、将
cm_backtrace_assert
放在项目的断言函数中执行,具体使用方法参照下面的 API 说明 - 7、如果第 4 步骤没有将 cmb_fault.s 汇编文件启用,则需要将
cm_backtrace_fault
放到故障处理函数(例如:HardFault_Handler
)中执行,具体使用方法参照下面的 API 说明
2.3.2 配置说明
配置文件名: cmb_cfg.h
,针对不同的平台和场景,用户需要自自行手动配置,常用配置如下:
配置名称 | 功能 | 备注 |
---|---|---|
cmb_println(...) | 错误及诊断信息输出 | 必须配置 |
CMB_USING_BARE_METAL_PLATFORM | 是否使用在裸机平台 | 使用则定义该宏 |
CMB_USING_OS_PLATFORM | 是否使用在操作系统平台 | 操作系统与裸机必须二选一 |
CMB_OS_PLATFORM_TYPE | 操作系统平台 | RTT/UCOSII/UCOSIII/FREERTOS |
CMB_CPU_PLATFORM_TYPE | CPU平台 | M0/M3/M4/M7 |
CMB_USING_DUMP_STACK_INFO | 是否使用 Dump 堆栈的功能 | 使用则定义该宏 |
CMB_PRINT_LANGUAGE | 输出信息时的语言 | CHINESE/ENGLISH |
注意:以上部分配置的内容可以在
cmb_def.h
中选择,更多灵活的配置请阅读源码
2.4 API 说明
2.4.1 库初始化
void cm_backtrace_init(const char *firmware_name, const char *hardware_ver, const char *software_ver)
参数 | 描述 |
---|---|
firmware_name | 固件名称,需与编译器生成的固件名称对应 |
hardware_ver | 固件对应的硬件版本号 |
software_ver | 固件的软件版本号 |
注意 :以上入参将会在断言或故障时输出,主要起了追溯的作用
2.4.2 获取函数调用栈
size_t cm_backtrace_call_stack(uint32_t *buffer, size_t size, uint32_t sp)
参数 | 描述 |
---|---|
buffer | 存储函数调用栈的缓冲区 |
size | 缓冲区大小 |
sp | 待获取的堆栈指针 |
示例:
/* 建立深度为 16 的函数调用栈缓冲区,深度大小不应该超过 CMB_CALL_STACK_MAX_DEPTH(默认16) */
uint32_t call_stack[16] = {0};
size_t i, depth = 0;
/* 获取当前环境下的函数调用栈,每个元素将会以 32 位地址形式存储, depth 为函数调用栈实际深度 */
depth = cm_backtrace_call_stack(call_stack, sizeof(call_stack), cmb_get_sp());
/* 输出当前函数调用栈信息
* 注意:查看函数名称及具体行号时,需要使用 addr2line 工具转换
*/
for (i = 0; i < depth; i++) {
printf("%08x ", call_stack[i]);
}
2.4.3 追踪断言错误信息
void cm_backtrace_assert(uint32_t sp)
参数 | 描述 |
---|---|
sp | 断言环境时的堆栈指针 |
注意 :入参 SP 尽量在断言函数内部获取,而且尽可能靠近断言函数开始的位置。当在断言函数的子函数中(例如:在 RT-Thread 的断言钩子方法中)使用时,由于函数嵌套会存在寄存器入栈的操作,此时再获取 SP 将发生变化,就需要人为调整(加减固定的偏差值)入参值,所以作为新手 不建议在断言的子函数 中使用该函数。
2.4.4 追踪故障错误信息
void cm_backtrace_fault(uint32_t fault_handler_lr, uint32_t fault_handler_sp)
参数 | 描述 |
---|---|
fault_handler_lr | 故障处理函数环境下的 LR 寄存器值 |
fault_handler_sp | 故障处理函数环境下的 SP 寄存器值 |
该函数可以在故障处理函数(例如: HardFault_Handler
)中调用。另外,库本身提供了 HardFault
处理的汇编文件(点击查看,需根据自己编译器进行选择),会在故障时自动调用 cm_backtrace_fault
方法。所以移植时,最简单的方式就是直接使用该汇编文件。
2.5 常见问题
2.5.1 编译出错,提示需要 C99 支持
点击查看教程:一步开启 Keil/IAR/GCC 的 C99 支持
2.5.2 如何查看到函数调用栈中函数的具体名称及代码行号
点击查看教程:如何使用 addr2line 工具获取函数调用栈详细信息
2.5.3 故障处理函数:HardFault_Handler 重复定义
在使用了本库提供的 cmb_fault.s 汇编文件时,因为该汇编文件内部已经定义了 HardFault_Handler ,所以如果项目中还有其他地方定义了该函数,则会提示 HardFault_Handler 被重复定义的错误。此时有两种解决方法:
- 1、注释/删除其他文件中定义的
HardFault_Handler
函数,仅保留 cmb_fault.s 中的; - 2、将 cmb_fault.s 移除工程,手动添加
cm_backtrace_fault
函数至现有的故障处理函数,但需要注意的是,务必 保证该函数数入参的准备性 ,否则可能会导致故障诊断功能及堆栈打印功能无法正常运行。所以如果是新手,不推荐第二种解决方法。
2.5.4 初始化时提示无法获取主栈(main stack)信息
在 cmd_def.h
中有定义默认的主栈配置,大致如下:
#if defined(__CC_ARM)
/* C stack block name, default is STACK */
#ifndef CMB_CSTACK_BLOCK_NAME
#define CMB_CSTACK_BLOCK_NAME STACK
#endif
...
#elif defined(__ICCARM__)
/* C stack block name, default is 'CSTACK' */
#ifndef CMB_CSTACK_BLOCK_NAME
#define CMB_CSTACK_BLOCK_NAME "CSTACK"
#endif
...
#elif defined(__GNUC__)
/* C stack block start address, defined on linker script file, default is _sstack */
#ifndef CMB_CSTACK_BLOCK_START
#define CMB_CSTACK_BLOCK_START _sstack
#endif
/* C stack block end address, defined on linker script file, default is _estack */
#ifndef CMB_CSTACK_BLOCK_END
#define CMB_CSTACK_BLOCK_END _estack
#endif
...
#else
比如在 Keil-MDK 编译器下会默认选择 STACK
作为主栈 block 的名称,但在一些特殊平台下,项目的主栈 block 名称可能不叫 STACK
,导致 CmBacktrace 无法获取到正确的主栈信息,所以在初始化时会有如上的错误提示信息。
解决这个问题一般有两个思路
- 1、在
cmb_cfg.h
中重新定义主栈的信息,此时 CmBacktrace 会优先使用cmb_cfg.h
中的配置信息; - 2、修改项目配置,如果是 Keil-MDK ,则在启动文件的开头位置,将主栈的名称修改为默认的
STACK
,其他编译器一般很少出现该问题。
2.6 许可
采用 MIT 开源协议,细节请阅读项目中的 LICENSE 文件内容。
二、STM32CubeMx配置
三、Examples
1、打开STM32CubeMx生成的keil工程,把下来的源码中的cm_backtrace文件夹下所有.c和.h文件,复制到工程目录中来,并添加keil工程中来即可。
1.1、把cm_backtrace中的cm_backtrace.c和fault_handler->keil文件夹中的cmb_fault.S汇编文件添加进来即可。
2、添加头文件路径
3、编译工程
3.1、按照提示信息,修改cmb_cfg.h文件
#ifndef _CMB_CFG_H_
#define _CMB_CFG_H_
/* print line, must config by user */
#define cmb_println(...) printf(__VA_ARGS__); printf("\r\n") /* e.g., printf(__VA_ARGS__);printf("\r\n") */
/* enable bare metal(no OS) platform */
#define CMB_USING_BARE_METAL_PLATFORM
/* enable OS platform */
/* #define CMB_USING_OS_PLATFORM */
/* OS platform type, must config when CMB_USING_OS_PLATFORM is enable */
/* #define CMB_OS_PLATFORM_TYPE CMB_OS_PLATFORM_RTT or CMB_OS_PLATFORM_UCOSII or CMB_OS_PLATFORM_UCOSIII or CMB_OS_PLATFORM_FREERTOS */
/* cpu platform type, must config by user */
#define CMB_CPU_PLATFORM_TYPE CMB_CPU_ARM_CORTEX_M3 /* CMB_CPU_ARM_CORTEX_M0 or CMB_CPU_ARM_CORTEX_M3 or CMB_CPU_ARM_CORTEX_M4 or CMB_CPU_ARM_CORTEX_M7 */
/* enable dump stack information */
#define CMB_USING_DUMP_STACK_INFO
/* language of print information */
/* #define CMB_PRINT_LANGUAGE CMB_PRINT_LANGUAGE_ENGLISH(default) or CMB_PRINT_LANGUAGE_CHINESE */
#endif /* _CMB_CFG_H_ */
3.2、再次编译,错误提示信息:cmb_fault.c与stm32f10x_it.c中的HardFault_Handler函数重定义问题。
3.3、修改stm32f10x_it.c,把HardFault_Handler这个函数注释掉即可。
6、把源码中测试代码复制到项目中来,并添加keil。
6.1、再次编译
7、main.c文件
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* <h2><center>© Copyright (c) 2021 STMicroelectronics.
* All rights reserved.</center></h2>
*
* This software component is licensed by ST under BSD 3-Clause license,
* the "License"; You may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
* opensource.org/licenses/BSD-3-Clause
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "cm_backtrace.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
#define HARDWARE_VERSION "V1.0.0"
#define SOFTWARE_VERSION "V0.1.0"
extern void fault_test_by_unalign(void);
extern void fault_test_by_div0(void);
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
#ifdef __GNUC__
/* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
/**
* @brief Retargets the C library printf function to the USART.
* @param None
* @retval None
*/
PUTCHAR_PROTOTYPE
{
/* Place your implementation of fputc here */
/* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
int fgetc(FILE * f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
/* CmBacktrace initialize */
cm_backtrace_init("STM32F103CB_CmBacktrace_Demo", HARDWARE_VERSION, SOFTWARE_VERSION);
// fault_test_by_unalign();
fault_test_by_div0();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_Delay(1000);
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
四、运行结果
1、默认是英文打印
2、改为中文打印,只需修改cmb_cfg.h文件,添加如下语句即可。
#define CMB_PRINT_LANGUAGE CMB_PRINT_LANGUAGE_CHINESE
运行结果:
3、追踪错误问题
通过运行结果得知,出错原因。按照它的提示,我们运行命令:
addr2line -e STM32F103CB_CmBacktrace_Demo.axf -a -f 080019f6 08001a42
运行这个命令需要用到addr2line.exe工具,这个工具在CmBacktrace源码目录下的tools文件夹中:
4、分别有两个版本32bit和64bit,根据系统环境选择,并拷贝到keil工程目录下中可执行文件.axf所在的文件夹中:
5、如我的工程目录路径为:“C:\Users\LYLens-02\Desktop\STM32F103CB_CmBacktrace_Demo\MDK-ARM\STM32F103CB_CmBacktrace_Demo”,在次目录下
找到.axf 可执行文件(STM32F103CB_CmBacktrace_Demo.axf),此时,按下Shift键的同时点击鼠标右键:进入命令行窗口,如下所示:
运行上面那条命令:addr2line -e STM32F103CB_CmBacktrace_Demo.axf -a -f 080019f6 08001a42
传送门->代码
参考文章:
1、https://blog.csdn.net/zhengnianli/article/details/103468369
2、https://blog.csdn.net/jiejiemcu/article/details/87615397
五、总结
好了,就介绍到此。