TRACE原理及实现
引入
在嵌入式开发中我们面临最大的问题就是对于程序的调试。比如通过jtag仿真、通过串口打印信息、通过硬件的led指示等等都是我们常用的调试手段,但是当我们的产品在正常运行中出现bug应该如何调试呢?或者说我们的产品出现了hardfault中断又应该如何进行分析呢?我们可以通过“栈”来进行分析。
原理
栈:栈是一种特别的数据结构,遵循先进后出的原则。STM32的栈是向下增长型(满递减),如图:
栈生长方向
栈的作用就是保存现场。
函数调用分析
函数调用也会涉及到对于栈的操作。
举例:
void led_flicker(void) { led_on(); led_wait(5); led_off(); led_wait(8); } |
当我们仿真进行单步执行的过程中发现在进入led_wait函数之前会执行push操作,然后退出led_wait函数之前会执行pop操作。
PUSH操作
POP操作
表示当我们进行函数调用的时候会将当前的现场进行入栈,然后函数执行完成再进行现场恢复(出栈)。
中断分析
当我们的程序出现问题,进入不可屏蔽中断的时候,同样也会进行入栈操作,入栈的顺序是xPSR,PC,LR,R12,R3,R2,R1,R0。那么在进入不可屏蔽中断前我们判断到sp的地址,然后将栈信息打印出来,辅助我们进行程序的Debug。
异常入栈图
trace实现
我们以hardfault为例:
1.首先判断使用的是MSP还是PSP
2.将MSP/PSP的地址传递到C函数
3.根据当前的地址打印出堆栈信息
源码:
HardFault_Handler\ PROC EXPORT HardFault_Handler [WEAK] IMPORT hardfault_c TST LR,#4 ;判断使用的是msp还是psp ITE EQ ; MRSEQ R0,MSP;使用msp MRSNE R0,PSP;使用psp ; B . B hardfault_c ENDP |
当产生hardfault后,单片机会跳转到中断向量HardFault_Handler处执行程序.
TST指令:
逻辑处理指令,用于把一个寄存器的内容和另一个寄存器的内容或立即数进行按位的与运算,并根据运算结果更新CPSR中条件标志位的值。当前运算结果为1,则Z=0;当前运算结果为0,则Z=1。
LR寄存器:
在进入异常服务程序的时候,LR寄存器会更新为EXC_RETURN的值,EXC_RETURN的值位域如下图(摘自Arm Cortex-M3与Cortex-M4权威指南)。
所以我们通过判断bit[2]来确定使用的是MSP还是PSP。根据不同的结果来将对应的栈地址传递给R0。
ITE EQ
MRSEQ R0,MSP
MRSNE R0,PSP
如果运算结果为1,也就是PSP,Z==0,那么则将PSP读取后传递到R0
如果运算结果为0,使用的MSP,那么Z == 1,那么将MSP读取后传递到R0
汇编调用C语言,参数的传递通过R0---R3来进行。
所以C函数hardfault_c的参数就是R0的值。
hardfault_c源码
/** * @brief:中断函数 * @param:sp_addr 堆栈的地址 * @retval:无 * @note: */ void hardfault_c(unsigned long * sp_addr) { trace_info("hardfault",(struct TRACE_REG_T *)sp_addr); while(1); } |
将堆栈的地址也就是R0传递到C函数,调用打印函数:
/** * @brief:trace实现 * @param:it_type 中断类型 * @param:addr 堆栈地址 * @retval:无 * @note: */ void trace_info(unsigned char * it_type,struct TRACE_REG_T * reg) { trace_print("\r\nVersion:"); trace_print(g_pver); trace_print("\nType:"); trace_print(it_type); trace_print("\r\n"); trace_print("R0 = 0x%08X\r\n",reg->_R0); trace_print("R1 = 0x%08X\r\n",reg->_R1); trace_print("R2 = 0x%08X\r\n",reg->_R2); trace_print("R3 = 0x%08X\r\n",reg->_R3); trace_print("R12 = 0x%08X\r\n",reg->_R12); trace_print("LR = 0x%08X\r\n",reg->_LR); trace_print("PC = 0x%08X\r\n",reg->_PC); trace_print("xPSR = 0x%08X\r\n",reg->_xPSR); // trace_sp_mem_data((unsigned char *)reg);//此处存在问题 待完善 } |
结构体TRACE_REG_T定义:
struct TRACE_REG_T{ unsigned long * _R0; unsigned long * _R1; unsigned long * _R2; unsigned long * _R3; unsigned long * _R12; unsigned long * _LR; unsigned long * _PC; unsigned long * _xPSR; }; |
验证
此trace信息需要结合反汇编文件使用,反汇编文件可以在keil中通过命令设置:
C:\Keil_v5\ARM\ARMCLANG\bin\fromelf.exe –c -s –t ./Objects/traceDemo.axf -o ./debug/traceDemo.dis
/** * @brief:LED状态 * @param:sta true->on false->off * @retval:无 * @note: */ void led_sta(uint8_t sta) { volatile uint32_t * p = (uint32_t *)0xFFFFFFFF; uint32_t n = 0; if(sta) { led_on(); n = *p;//测试trace 人为设置死机 } else { led_off(); } } |
查看串口助手打印信息:
结合反汇编文件所有PC值。如下:
证明程序的出错在led_sta函数中led_on后面的地方出现错误。也就是我们对地址0xFFFFFFFF进行读取操作的地方。
当然,你可以将更多的栈内存的内容进行打印,从而进行栈回溯来还原当时出现错误的现场以及函数的调用结构。