> 文章列表 > CmBacktrace库在工程中的添加和应用

CmBacktrace库在工程中的添加和应用

CmBacktrace库在工程中的添加和应用

CmBacktrace

    • 介绍
    • 在工程中添加CmBacktrace
    • 断言
    • 打印全局变量的值
    • 循环输出错误信息
    • 串口处理
    • 看门狗处理

介绍

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 编译器;

在工程中添加CmBacktrace

①下载源码解压之后,将cm_backtrace文件夹复制到工程中,同时复制CmBacktrace-master\\demos\\non_os\\stm32f10x\\app\\src目录下的fault_test.c文件到自己工程中用于测试。

②在keil中添加分组并添加相应文件如下:
CmBacktrace库在工程中的添加和应用
同时添加相应的路径:
CmBacktrace库在工程中的添加和应用
③在项目中包含头文件,定义软硬件版本,软硬件版本为字符串,可以自己随便写,只要不超过最大长度限制CMB_NAME_MAX,CMB_NAME_MAX宏定义在cmb_def.h文件中。

#include <cm_backtrace.h>#define HARDWARE_VERSION               "V1.0.0"
#define SOFTWARE_VERSION               "V0.1.0"

在main函数中使用cm_backtrace_init()函数初始化CmBacktrace,第一个参数为自己工程生成文件的名称。

cm_backtrace_init("CmBacktrace", HARDWARE_VERSION, SOFTWARE_VERSION);

CmBacktrace库在工程中的添加和应用
④在中断的.c文件中注释掉HardFault_Handler()函数,否则会与cmb_fault.S中的HardFault_Handler重定义。

⑤通过cmb_cfg.h文件配置CmBacktrace。

宏定义 说明 示例
cmb_println(…) 打印函数 printf(VA_ARGS);printf(“\\r\\n”)
CMB_USING_BARE_METAL_PLATFORM 不使用OS
CMB_USING_OS_PLATFORM 使用OS
CMB_OS_PLATFORM_TYPE OS类型 CMB_OS_PLATFORM_RTT或CMB_OS_PLATFORM_UCOSIII或CMB_OS_PLATFORM_FREERTOS等
CMB_CPU_PLATFORM_TYPE MCU平台 CMB_CPU_ARM_CORTEX_M0/3/4/7
CMB_USING_DUMP_STACK_INFO 是否输出栈信息
CMB_PRINT_LANGUAGE 输出语言 CMB_PRINT_LANGUAGE_ENGLISH(默认)或CMB_PRINT_LANGUAGE_CHINESE

⑥配置好CmBacktrace之后,在程序中加入测试代码,以除零错误为例,为了更直观,多添加几层函数调用。

void assert_test6()
{uint8_t buf[12] = {0xFF,0XFF,0xFF,0XFF,0xFF,0XFF,0xFF,0XFF,0xFF,0XFF,0xFF,0XFF};fault_test_by_div0();
}void assert_test5()
{uint8_t buf[12] = {0xEE,0XEE,0xEE,0XEE,0xEE,0XEE,0xEE,0XEE,0xEE,0XEE,0xEE,0XEE};assert_test6();
}void assert_test4()
{uint8_t buf[12] = {0xDD,0XDD,0xDD,0XDD,0xDD,0XDD,0xDD,0XDD,0xDD,0XDD,0xDD,0XDD};assert_test5();
}void assert_test3()
{uint8_t buf[12] = {0xCC,0XCC,0xCC,0XCC,0xCC,0XCC,0xCC,0XCC,0xCC,0XCC,0xCC,0XCC};assert_test4();
}void assert_test2()
{uint8_t buf[12] = {0xBB,0XBB,0xBB,0XBB,0xBB,0XBB,0xBB,0XBB,0xBB,0XBB,0xBB,0XBB};assert_test3();
}void assert_test1()
{uint8_t buf[12] = {0xAA,0XAA,0xAA,0XAA,0xAA,0XAA,0xAA,0XAA,0xAA,0XAA,0xAA,0XAA};assert_test2();
}

运行结果如下,可以在输出的栈信息中看到上边函数中定义的局部数组的值,从中也可以看出来栈是向下生长的:
CmBacktrace库在工程中的添加和应用
在工程的Output路径下打开命令行工具,需要用到的是CmBacktrace.axf文件,使用addr2line命令运行后如下,可以看到函数的层层调用关系。函数的最大调用深度为16层,由cmb_def.h中宏定义CMB_CALL_STACK_MAX_DEPTH决定。
CmBacktrace库在工程中的添加和应用
⑦至此就可以使用CmBacktrace了。

断言

如果使用断言的话可以使用系统的断言函数,打开宏定义USE_FULL_ASSERT,然后在assert_failed()函数中把CmBacktrace的断言函数cm_backtrace_assert()添加进去。

void assert_failed(uint8_t *file, uint32_t line)
{cm_backtrace_assert(cmb_get_sp());printf("assert failed at %s:%d \\n", file, line);while (1) {}
}

这样就可以调用assert_param()函数进行参数检查,如果参数有误就输出错误信息。带来的问题就是比如在写flash时,发现参数有误进入断言错误,这样会影响程序执行。
不使用系统的断言函数,自己写一个:

#define my_assert_param(expr) ((expr) ? (bool)0U : my_assert_failed((uint8_t *)__FILE__, __LINE__))
bool my_assert_failed(uint8_t *file, uint32_t line);bool my_assert_failed(uint8_t *file, uint32_t line)
{if(*(uint8_t *)address == 失能){return true;}cm_backtrace_assert(cmb_get_sp());printf("assert failed at %s:%d \\n", file, line);while (1) {}
}

这样通过判断my_assert_param执行之后的返回值可以进行其他的操作,比如输出断言错误信息或者函数直接return等。

打印全局变量的值

①如果在发生错误的时候需要分析项目中的全局变量的当前值,那么上面的组件就不能满足,这时候可以自己添加一些代码来实现这个功能。
正常来说没有自己定义分散加载文件的话,keil会在编译的时候自动生成一个,一般如下:

; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************LR_IROM1 0x08000000 0x00040000  {    ; load region size_regionER_IROM1 0x08000000 0x00040000  {  ; load address = execution address*.o (RESET, +First)*(InRoot$$Sections).ANY (+RO).ANY (+XO)}RW_IRAM1 0x20000000 0x0000C000  {  ; RW data.ANY (+RW +ZI)}
}

全局变量分为初始化的全局变量和未初始化的全局变量,初始化的全局变量位于RW段(.data),未初始化的全局变量位于ZI段(.bss)。

可以通过ARM的系统变量来获取RW和ZI数据区的信息,比如起始地址,结束地址,长度等。keil下Help→Open Books Window,然后打开ARM Linker前缀的pdf文件,然后就可以找到系统变量的使用方法。
CmBacktrace库在工程中的添加和应用
②在cmb_cfg.h文件中添加自己的宏定义CMB_USING_VAR_INFO,用于控制是否输出变量的值。

③在cm_backtrace.h中添加RAM名称

#ifdef CMB_USING_VAR_INFO		/* ram section name, default is ER_IROM1 */#ifndef CMB_RAM_SECTION_NAME#define CMB_RAM_SECTION_NAME           RW_IRAM1#endif
#endif	

在cm_backtrace.c中添加系统变量

#ifdef CMB_USING_VAR_INFO		#define RAM_ZI_SECTION_START(_name_)          Image$$##_name_##$$ZI$$Base#define RAM_ZI_SECTION_LENTH(_name_)          Image$$##_name_##$$ZI$$Length#define RAM_ZI_SECTION_END(_name_)            Image$$##_name_##$$ZI$$Limit#define CMB_RAM_ZI_SECTION_START(_name_)      RAM_ZI_SECTION_START(_name_)#define CMB_RAM_ZI_SECTION_LENTH(_name_)      RAM_ZI_SECTION_LENTH(_name_) #define CMB_RAM_ZI_SECTION_END(_name_)        RAM_ZI_SECTION_END(_name_) extern const int CMB_RAM_ZI_SECTION_START(CMB_RAM_SECTION_NAME);extern const int CMB_RAM_ZI_SECTION_LENTH(CMB_RAM_SECTION_NAME);extern const int CMB_RAM_ZI_SECTION_END(CMB_RAM_SECTION_NAME);#define RAM_RW_SECTION_START(_name_)          Image$$##_name_##$$RW$$Base#define RAM_RW_SECTION_LENTH(_name_)          Image$$##_name_##$$RW$$Length#define RAM_RW_SECTION_END(_name_)            Image$$##_name_##$$RW$$Limit#define CMB_RAM_RW_SECTION_START(_name_)      RAM_RW_SECTION_START(_name_)#define CMB_RAM_RW_SECTION_LENTH(_name_)      RAM_RW_SECTION_LENTH(_name_) #define CMB_RAM_RW_SECTION_END(_name_)        RAM_RW_SECTION_END(_name_) extern const int CMB_RAM_RW_SECTION_START(CMB_RAM_SECTION_NAME);extern const int CMB_RAM_RW_SECTION_LENTH(CMB_RAM_SECTION_NAME);extern const int CMB_RAM_RW_SECTION_END(CMB_RAM_SECTION_NAME);
#endif

##的作用是链接字符的作用,RAM_RW_SECTION_START(name)实际内容是Image$$RW_IRAM1$$RW$$Base,使用的时候需要再用CMB_RAM_RW_SECTION_START(name)包一层,否则编译器会报错。使用系统变量的时候如果定义全局变量并直接赋值,如下:

uint32_t RamRwStart = (uint32_t)&CMB_RAM_RW_SECTION_START(CMB_RAM_SECTION_NAME);

这样编译的时候会报警告…/Core/Src/main.c(59): warning: #1296-D: extended constant initialiser used,可以先定义全局变量,然后在其他地方赋值。

同时添加变量用来表示RW和ZI段起始地址和长度等

#ifdef CMB_USING_VAR_INFO
static uint32_t ram_zi_start_addr = 0;
static size_t ram_zi_size = 0;
static uint32_t ram_rw_start_addr = 0;
static size_t ram_rw_size = 0;
#endif

④有了以上就可以开始实现具体的变量打印函数了,输出格式按照一般bin文件的格式,16字节一行,同时显示具体的地址。

#ifdef CMB_USING_VAR_INFO
static void print_zi_info(uint32_t ram_zi_addr, size_t ram_zi_size) {cmb_println("============ram_zi_start================");for (uint32_t i = 0; i < ram_zi_size; i++) {if(!(i%16)){printf("\\r\\naddr:[%08X]:",(ram_zi_addr+i));}printf("%02X ",*(uint8_t *)(ram_zi_addr+i));}printf("\\r\\n");cmb_println("============ram_zi_end==================");
}static void print_rw_info(uint32_t ram_rw_addr, size_t ram_rw_size) {cmb_println("============ram_rw_start================");for (uint32_t i = 0; i < ram_rw_size; i++) {if(!(i%16)){printf("\\r\\naddr:[%08X]:",(ram_rw_addr+i));}printf("%02X ",*(uint8_t *)(ram_rw_addr+i));}printf("\\r\\n");cmb_println("============ram_rw_end==================");
}
#endif

⑤变量打印函数调用,在cm_backtrace_fault函数中调用上面的输出函数。

#ifdef CMB_USING_VAR_INFO	print_rw_info(ram_rw_start_addr,ram_rw_size);print_zi_info(ram_zi_start_addr,ram_zi_size);
#endif

⑥测试,定义两个数组,一个定义并初始化,一个不初始化。

uint8_t ALLbuf[12] = {0x66,0X66,0x66,0X66,0x66,0X66,0x66,0X66,0x66,0X66,0x66,0X66};
uint8_t ALLbuf_NoInit[12];

这时候ALLbuf的位置应该在RW段,ALLbuf_NoInit的位置应该在ZI段,在main函数中将两个数组的第一个和最后一个字节都赋值为0x88,方便观察,运行效果如下:
CmBacktrace库在工程中的添加和应用
同时也可以对应的在工程生成的map文件中找到这两个变量的地址。
CmBacktrace库在工程中的添加和应用

循环输出错误信息

如果在研发阶段,可以直接通过串口接收到的错误信息来定位问题,但是到了现场的话,就面临着需要保留错误现场的问题,这时候要么把数据错误信息存储在flash,要么就通过串口循环输出。
通过串口循环输出的话可以改造一下cm_backtrace_fault()函数,具体如下:

void report_delay(void)
{for(int32_t i = 0;i < 10000000;i++){__nop();}
}void fault_report(uint32_t fault_handler_lr, uint32_t fault_handler_sp)
{while(1){cm_backtrace_fault(fault_handler_lr,fault_handler_sp);on_fault = false;report_delay();}
} 

同时cmb_fault.S中也改一下:

    AREA |.text|, CODE, READONLY, ALIGN=2THUMBREQUIRE8PRESERVE8; NOTE: If use this file's HardFault_Handler, please comments the HardFault_Handler code on other file.;IMPORT cm_backtrace_faultIMPORT fault_reportEXPORT HardFault_HandlerHardFault_Handler    PROCMOV     r0, lr                  ; get lrMOV     r1, sp                  ; get stack pointer (current is MSP);BL      cm_backtrace_faultBL      fault_reportFault_LoopBL      Fault_Loop              ;while(1)ENDPEND

这样产生HardFault的时候先调用fault_report()函数,在这里面可以做一些事情,比如初始化串口,判断一下要不要输出错误信息等。添加on_fault = false这行代码是因为在cm_backtrace_fault()函数中通过这个变量来判断是否重入(使用os的情况下可能会出现),但是如果这个变量为真的话就会因为CMB_ASSERT(!on_fault);这行代码,进入CmBacktrace的断言函数。

/* assert for developer. */
#define CMB_ASSERT(EXPR)                                                       \\
if (!(EXPR))                                                                   \\
{                                                                              \\cmb_println("(%s) has assert failed at %s.", #EXPR, __FUNCTION__);         \\while (1);                                                                 \\
}

如果屏蔽掉CMB_ASSERT(!on_fault);的话会导致输出的信息少一层函数调用关系,也就是产生HardFault错误的那个函数。这样在裸机中就可以循环打印错误信息。

串口处理

在输出错误信息之前,需要确保串口已经初始化。STM32L431使用HAL库的话,串口句柄是一个全局变量,初始化函数封装也比较复杂,再次使用句柄也会影响当前的值,所以就直接用寄存器来初始化串口,串口引脚只初始化Tx脚就可以了,也可以避免额外的开销。

void FaultReportUartInit(void)
{__HAL_RCC_USART1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();MODIFY_REG(USART1->CR1, USART_CR1_UE, 0);while (READ_BIT(USART1->CR1, USART_CR1_UE) != 0);//io初始化MODIFY_REG(GPIOA->OSPEEDR, GPIO_OSPEEDR_OSPEED9, GPIO_OSPEEDR_OSPEED9);MODIFY_REG(GPIOA->OTYPER, GPIO_OTYPER_OT9, 0);MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE9, GPIO_MODER_MODE9_1);MODIFY_REG(GPIOA->PUPDR, GPIO_PUPDR_PUPD9, 0);MODIFY_REG(GPIOA->AFR[1],GPIO_AFRH_AFSEL9, (GPIO_AF7_USART1 << GPIO_AFRH_AFSEL9_Pos));uint32_t UartDiv = 40000000/115200;USART1->BRR =  (UartDiv & 0xFFFFFFF0) | (UartDiv & 0x0F);MODIFY_REG(USART1->CR1, ((uint32_t)(USART_CR1_M | USART_CR1_PCE | USART_CR1_PS | USART_CR1_TE |USART_CR1_OVER8)),\\((uint32_t)(UART_WORDLENGTH_8B | UART_PARITY_NONE | UART_MODE_TX | UART_OVERSAMPLING_16)));MODIFY_REG(USART1->CR2, USART_CR2_STOP, UART_STOPBITS_1);MODIFY_REG(USART1->CR3, ((uint32_t)(USART_CR3_RTSE | USART_CR3_CTSE | USART_CR3_ONEBIT)), UART_ONE_BIT_SAMPLE_DISABLE);MODIFY_REG(USART1->CR1, USART_CR1_UE, USART_CR1_UE);
}

看门狗处理

软件看门狗启动之后,除非产生一次系统复位,否则是不会停止的,因此需要处理一下看门狗,防止看门狗复位,这样就需要保证输出自己需要全部信息的时间要小于看门狗的复位周期。在循环打印错误信息的时候也需要喂狗。

WRITE_REG(IWDG->KR, IWDG_KEY_RELOAD);