MCU调试方法详解:基于Keil MDK的实践指南
MCU常用调试方法—基于keil MDK
一、 附着仿真
当设备出现异常现象,且不能破坏环境时,可以采用此方法进行仿真调试。
注意:以下操作前提需要保证当前工程代码和设备的代码保持一致,否则不能正常执行仿真
依次取消勾选上述三处位置。此时仿真不会破坏环境。
进入仿真后,会发现无法打断点,如下图所示
打开keil帮助文档
按照如下图所示搜索LOAD %L INCREMENTAL,只能右键复制
意思是加载符号信息adds the debugging information to the existing symbol table. This allows multi-application debugging.
打开命令行窗口,填写刚才搜索到的命令,敲回车执行即可正常仿真。注意暂停操作时注意看门狗复位会破坏环境。所以尽量在关狗的时候进行调试复现。
二、 hardfault异常分析
1、分析call stack
出现卡死等异常现象时可在void HardFault_Handler(void)添加断点,然后打开Call Stack Window窗口,查看调用堆栈。(不仅限于此问题,其他问题也可使用此方法)
右键堆栈中的函数可以跳转到执行到当前代码的上一个函数。同时也可以查看堆栈中的变量值。
2、增加调试信息
将进入HardFault_Handler函数前的所有寄存器值打印出来
需要使用汇编重新实现HardFault_Handler,将所有寄存器R0-R15寄存器值压栈,使用串口将寄存器值打印出来,通过分析PC、LR及其他关键寄存器值并结合map文件可定位是哪个函数出现的异常。 可参考这两个文件实现。
cpu.s
AREA |.text|, CODE, READONLY, ALIGN=2
THUMB
REQUIRE8
PRESERVE8
IMPORT hard_fault
EXPORT HardFault_Handler
HardFault_Handler PROC
; get current context
TST lr, #0x04 ; if(!EXC_RETURN[2])
ITE EQ
MRSEQ r0, msp ; [2]=0 ==> Z=1, get fault context from handler.
MRSNE r0, psp ; [2]=1 ==> Z=0, get fault context from thread.
STMFD r0!, {r4 - r11} ; push r4 - r11 register
; IF {FPU} != "SoftVFP"
; STMFD r0!, {lr} ; push dummy for flag
; ENDIF
; STMFD r0!, {lr} ; push exec_return register
TST lr, #0x04 ; if(!EXC_RETURN[2])
ITE EQ
MSREQ msp, r0 ; [2]=0 ==> Z=1, update stack pointer to MSP.
MSRNE psp, r0 ; [2]=1 ==> Z=0, update stack pointer to PSP.
; PUSH {lr}
BL hard_fault
; POP {lr}
ORR lr, lr, #0x04
BX lr
ENDP
ALIGN 4
END
debug.c
static void hard_fault_track(void);
static void usage_fault_track(void);
static void bus_fault_track(void);
static void mem_manage_fault_track(void);
#define SCB_CFSR (*(volatile const unsigned *)0xE000ED28) /* Configurable Fault Status Register */
#define SCB_HFSR (*(volatile const unsigned *)0xE000ED2C) /* HardFault Status Register */
#define SCB_MMAR (*(volatile const unsigned *)0xE000ED34) /* MemManage Fault Address register */
#define SCB_BFAR (*(volatile const unsigned *)0xE000ED38) /* Bus Fault Address Register */
#define SCB_AIRCR (*(volatile unsigned long *)0xE000ED0C) /* Reset control Address Register */
#define SCB_RESET_VALUE 0x05FA0004 /* Reset value, write to SCB_AIRCR can reset cpu */
#define SCB_CFSR_MFSR (*(volatile const unsigned char *)0xE000ED28) /* Memory-management Fault Status Register */
#define SCB_CFSR_BFSR (*(volatile const unsigned char *)0xE000ED29) /* Bus Fault Status Register */
#define SCB_CFSR_UFSR (*(volatile const unsigned short *)0xE000ED2A) /* Usage Fault Status Register */
static void hard_fault_track(void)
{
if (SCB_HFSR & (1UL << 1))
{
/* [1]:VECTBL, Indicates hard fault is caused by failed vector fetch. */
print("failed vector fetch\r\n");
}
if (SCB_HFSR & (1UL << 30))
{
/* [30]:FORCED, Indicates hard fault is taken because of bus fault,
memory management fault, or usage fault. */
if (SCB_CFSR_BFSR)
{
bus_fault_track();
}
if (SCB_CFSR_MFSR)
{
mem_manage_fault_track();
}
if (SCB_CFSR_UFSR)
{
usage_fault_track();
}
}
if (SCB_HFSR & (1UL << 31))
{
/* [31]:DEBUGEVT, Indicates hard fault is triggered by debug event. */
print("debug event\r\n");
}
}
static void usage_fault_track(void)
{
print("usage fault:SCB_CFSR_UFSR:0x%02X ", SCB_CFSR_UFSR);
if (SCB_CFSR_UFSR & (1 << 0))
{
/* [0]:UNDEFINSTR */
print("UNDEFINSTR ");
}
if (SCB_CFSR_UFSR & (1 << 1))
{
/* [1]:INVSTATE */
print("INVSTATE ");
}
if (SCB_CFSR_UFSR & (1 << 2))
{
/* [2]:INVPC */
print("INVPC ");
}
if (SCB_CFSR_UFSR & (1 << 3))
{
/* [3]:NOCP */
print("NOCP ");
}
if (SCB_CFSR_UFSR & (1 << 8))
{
/* [8]:UNALIGNED */
print("UNALIGNED ");
}
if (SCB_CFSR_UFSR & (1 << 9))
{
/* [9]:DIVBYZERO */
print("DIVBYZERO ");
}
// print("\r\n");
}
static void bus_fault_track(void)
{
print("bus fault: SCB_CFSR_BFSR:0x%02X ", SCB_CFSR_BFSR);
if (SCB_CFSR_BFSR & (1 << 0))
{
/* [0]:IBUSERR */
print("IBUSERR ");
}
if (SCB_CFSR_BFSR & (1 << 1))
{
/* [1]:PRECISERR */
print("PRECISERR ");
}
if (SCB_CFSR_BFSR & (1 << 2))
{
/* [2]:IMPRECISERR */
print("IMPRECISERR ");
}
if (SCB_CFSR_BFSR & (1 << 3))
{
/* [3]:UNSTKERR */
print("UNSTKERR ");
}
if (SCB_CFSR_BFSR & (1 << 4))
{
/* [4]:STKERR */
print("STKERR ");
}
if (SCB_CFSR_BFSR & (1 << 7))
{
print("SCB->BFAR:%08X\r\n", SCB_BFAR);
}
else
{
// print("\r\n");
}
}
static void mem_manage_fault_track(void)
{
print("mem manage fault:SCB_CFSR_MFSR:0x%02X ", SCB_CFSR_MFSR);
if (SCB_CFSR_MFSR & (1 << 0))
{
/* [0]:IACCVIOL */
print("IACCVIOL ");
}
if (SCB_CFSR_MFSR & (1 << 1))
{
/* [1]:DACCVIOL */
print("DACCVIOL ");
}
if (SCB_CFSR_MFSR & (1 << 3))
{
/* [3]:MUNSTKERR */
print("MUNSTKERR ");
}
if (SCB_CFSR_MFSR & (1 << 4))
{
/* [4]:MSTKERR */
print("MSTKERR ");
}
if (SCB_CFSR_MFSR & (1 << 7))
{
/* [7]:MMARVALID */
print("SCB->MMAR:%08X\r\n", SCB_MMAR);
}
else
{
// print("\r\n");
}
}
struct exception_stack_frame
{
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr;
uint32_t pc;
uint32_t psr;
};
struct exception_info
{
uint32_t r4;
uint32_t r5;
uint32_t r6;
uint32_t r7;
uint32_t r8;
uint32_t r9;
uint32_t r10;
uint32_t r11;
struct exception_stack_frame exception_stack_frame;
};
void hard_fault(struct exception_info *context)
{
print(" lr: 0x%08x\r\n", context->exception_stack_frame.lr); // 一般问题出现在此地址所在的代码
print(" pc: 0x%08x\r\n", context->exception_stack_frame.pc);
// print("hard fault on handler\r\n");
hard_fault_track();
while (1)
;
}
三、 map文件分析
双击工程名,即可得到对应的map文件
1、设备启动后的运行的第一个代码
对应启动汇编文件中的Reset_Handler
此汇编代码中调用了SystemInit,初始化时钟等。执行完后跳转到main函数__main。跳转后不再返回。
2、分析代码和变量的分布
3、中断向量表的分布
4、栈指针
5、程序大小及分配
6、静态变量查看
在map文件中搜索需要查看的静态变量,例如shut_state
shut_state 0x20000028 Data 4 4g.o(.data)
在memory窗口中查看0x20000028 的值
数据类型是4字节的,所以查看前4个字节即可。
在watch 窗口中查看变量值:
双击Enter expression 写入如下表达式*(int*)0x20000028,敲回车即可查看。
同理,静态结构体也可这样分析
四、 重启问题分析
1、仿真下查看分析
需要在main函数第一行添加断点,当断点停下时,查看RCU复位寄存器值可分析出是什么原因导致的重启
结合用户手册可分析出重启原因。建议每次启动后将复位寄存器清零,防止影响下次分析。
2、运行老化下添加打印分析
参考此文件中的代码实现
typedef union {
struct {
uint32_t REVERSE:23;
uint8_t V12RSTF:1;
uint8_t RSTFC:1;
uint8_t OBLRSTF:1;
uint8_t EPRSTF:1;
uint8_t PORRSTF:1;
uint8_t SWRSTF:1;
uint8_t FWDGT_RSTF:1;
uint8_t WWDGT_RSTF:1;
uint8_t LPRSTF:1;
}reset_s;
uint32_t reset_reg;
}RCU_RESET;
#define RCU_RESET_STATUS ((RCU_RESET*)(0x40021000+0x24))
void print_reset_status(void)
{
print("reset status reg:0x%02x\r\n",RCU_RESET_STATUS->reset_reg);
print("LPRSTF:%d, low power reset flag\r\n",RCU_RESET_STATUS->reset_s.LPRSTF);
print("WWDGT_RSTF:%d,windows wdgt reset flag\r\n",RCU_RESET_STATUS->reset_s.WWDGT_RSTF);
print("FWDGT_RSTF:%d,iwdgt reset flag\r\n",RCU_RESET_STATUS->reset_s.FWDGT_RSTF);
print("SWRSTF:%d, soft reset flag\r\n",RCU_RESET_STATUS->reset_s.SWRSTF);
print("PORRSTF:%d, power reset flag\r\n",RCU_RESET_STATUS->reset_s.PORRSTF);
print("EPRSTF:%d, externel gpio reset flag\r\n",RCU_RESET_STATUS->reset_s.EPRSTF);
print("OBLRSTF:%d, OBLRSTF reset flag\r\n",RCU_RESET_STATUS->reset_s.OBLRSTF);
print("RSTFC:%d, clear reset flag\r\n",RCU_RESET_STATUS->reset_s.RSTFC);
print("V12RSTF:%d, 1.2v power reset flag\r\n",RCU_RESET_STATUS->reset_s.V12RSTF);
RCU_RESET_STATUS->reset_s.RSTFC=1;//清楚复位标志状态
}
五、 中断相关分析
进入仿真可以通过以下方式查看哪些中断被开启
一般来说只需要关注圈中的部分。
E:使能位
P:挂起位,被置位说明有其他高优先级的中断到来打断了当前中断
A:触发位,说明当前中断被触发了
若出现P、A位一直被置位,则可能是频繁进入中断,可能会影响系统运行。
频繁进入中断可能原因:
1、 中断标志位未及时清除
2、 中断标志位清除不完全,若某些错误中断标志未清除会导致频繁进入错误中断服务函数,不能正常喂狗,导致设备重启或死机
六、 寄存器相关操作
进入仿真,打开如下窗口
这些寄存器bit是可以直接修改控制的(可能会存在延时)。其他外设寄存器同理。
七、变量异常分析
仿真过程中发现某些变量发生异常变化,比如:1、全变0;2、全变FF;3、其他不符合预期的变化等等
常用排查方法如下:
右键想要观察的变量选择Set Access Breakpoint ,弹出下图所示窗口。注意断点数量一般不超过5个,否则会导致仿真异常,可以通过快捷键ctrl+b查看
选择write,然后点击Define,成功就会在Current Breakpoints窗口显示已打上断点。
关闭即可。此操作是当此地址的数据发生变化后,自动将程序停下来。然后可以在下图所示查看堆栈调用关系
最后是在gd32_flash_read函数中将断点地址的数据修改,由此可轻松定位问题所在。
出现这样的情况程序并不会一定进入hardfult_handler、重启。一般来说可以考虑如下情况:
1、 例如memset(),memcpy()等内存操作函数传入的长度过长,导致溢出覆盖了其他变量的值。
2、 栈溢出。可尝试修改启动汇编文件中的定义
八、排查小技巧
1、仿真环境下,可以频繁暂停运行程序,观察程序暂停时所处的位置,若频繁停在某一处位置(重点关注中断函数)或是程序停下来时变量值不符合预期,则需要重点关注
2、程序开发时,对于串口、iic、spi等外设通讯过程,尽量考虑使用收发缓冲区,在仿真时可以直观的观察到数据的收发,或是将数据用串口发出来方便调试
3、在hardfault中断函数中将PC和LR寄存器打印出来和main函数中添加重启原因的打印
4、添加全局的系统滴答定时器变量,可以1ms加1,用于指示系统是否还在正常工作。也可以考虑在whlie(1)中添加定时打印。
作者:yhjhaha