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

物联沃分享整理
物联沃-IOTWORD物联网 » MCU调试方法详解:基于Keil MDK的实践指南

发表回复