目录
Keil调试技巧:
一.不破坏现场连接仿真器与进入debug
二.栈回溯
死机调试示例
J-Link调试方法
示例:空指针异常
不能连接烧录器或者读取内存怎么办?
在日常开发中,经常会遇到单片机卡死机等问题,经常很难定位到问题代码在哪里。
常见的会导致单片机跑飞卡死的原因比如说死循环、数组越界、野指针等,尤其野指针最难调试,一般调试只能看见程序掉进 HardFault_Handler 死循环,无从得知是什么导致程序掉进了HardFault_Handler,连什么问题卡死都不知道,更别提上哪找了。
(一些硬件问题也会导致HardFault_Handler,了解就好)
Keil调试技巧:
一.不破坏现场连接仿真器与进入debug
假如死机触发比较难,好不容易触发一次想连接仿真器,连完debug自动把单片机reset了,又要等它再触发。
Keil只需按如下设置即可:
1.不要进入debug自动回到startup
2.Jlink连接仿真器自动reset
STlink也一样
3.连接仿真器,点击debug,但发现C语言代码未发生关联,无从得知现在运行到哪了
解决办法:在debug的command窗口输入 LOAD %L INCREMENTAL
然后就会发现C语言关联上了,可以定位到当前代码。
二.栈回溯
首先看调试页面左侧,会有一列寄存器在Core中,它们是ARM的内核寄存器
在ARM中,R0到R15是通用寄存器,除此之外还有特殊寄存器。
R0到R11也叫通用目的寄存器,可以存数值进行运算,
其中R0到R3可以存函数参数, 所以设计函数传参最好不要超过4个,超过4个的话,CPU会有额外负担。
另外函数结束返回时,R0和R1也可以存函数返回值。
我们的关注点在于R13 R14 R15,也叫SP LR PC, 用来存储地址(内存或flash)
SP保存内存中的栈指针地址;
LR保存函数的返回地址;
PC保存当前程序指令的地址。
所以根据上图可知,发生异常中断如 HardFault_Handler 时,通过SP寄存器的栈指针即可找到保存现场的栈结构,
再通过其中的PC和LR上的地址即可对应到当前程序发生问题的指令代码。
死机调试示例
int32_t Caculate(int32_t val1, int32_t val2)
{
return val1 / val2;
}
int32_t Process(void)
{
volatile int32_t aVal = 10;
volatile int32_t bVal = 0;
return Caculate(aVal, bVal);
}
//... ... .... ...
int main() {
//... ...
volatile int *SCB_CCR = (volatile int *)0xE000ED14;
*SCB_CCR |= (1<<4);
volatile int32_t mVal = Process();
//... ...
}
以上代码是个除数为0的问题,正常来说除数为0可能会导致死机,但单片机有时并不会
例如STM32中出现除以零的操作时,会进入异常处理,而导致程序出现异常,但是进入Usage Fault 是有前提条件的,即 只有在 DIV_0_TRP 置位时才会发生。
所以在以上代码中我们加入对CCR寄存器的操作,让除数为0进入异常中断。
现在单片机已经死机,我们按上述不破坏现场方法进入调试。
程序进入异常中断,PC也指向异常中断所在地址。
如何找到问题代码呢?程序是执行问题代码之后进入中断,所以我们要向前回溯。
根据上文说的栈回溯,观察此时SP指向 0x20000448 即栈空间中保存的问题现场。
栈空间在内存中,要查看内存数据,需要点击:
View → Memory windows → Memory 1
打开一个内存视窗,默认为1字节显示,但是我们是32位单片机
所以在视窗中右键→ unsigned → long , 选择4字节显示,方便我们查看
在其中输入SP的地址
找到了对应的栈,根据上文栈结构的介绍,要找到这个栈内的PC LR地址
R0 R1 R2 R3 R12 LR PC PRS , 其中LR为08001415 , PC为08001046。
找到08001046所在指令:
return出现了问题?肯定不是,所以看汇编代码。
MOVS是往寄存器里装数值
SDIV是除法指令
71: volatile int32_t aVal = 10;
0x08001040 210A MOVS r1,#0x0A
72: volatile int32_t bVal = 0;
0x08001042 2000 MOVS r0,#0x00
73: return Caculate(aVal, bVal);
0x08001044 9000 STR r0,[sp,#0x00]
0x08001046 FB91F0F0 SDIV r0,r1,r0
可知向 r1 存入 0x0A(10),然后向 r0 存入 0x00 ,然后SDIV r0,r1,r0 ,让 r1 除以 r0 的结果存在 r0 里,
这就能看出问题是除数为0了。
如果汇编看不懂,可以直接丢给GPT让它解释,或者查一下ARM指令手册。
J-Link调试方法
如果不用keil的debug,也可以用Jlink。
但是需要keil中页面的debug的那种地址和汇编指令对应文件。
文件格式为dis,在魔术棒中添加即可
如 fromelf --text -a -c --output .\Arm_test\Arm_test.dis .\Arm_test\Arm_test.axf
再次编译可以得到dis文件,拉到keil里打开就是地址汇编指令的对应
示例:空指针异常
这里有一个函数指针的任务调度框架,如果某个任务的执行函数指针设为空,则主循环执行这个任务时会触发空指针异常
打开Jlink 或者 jlink commande,在你jflash安装目录里面有
打开后是个命令行,连接好仿真器之后
输入usb连接设备,输入halt,再选择对应单片机和调试接口以及速度。
得到PC 和SP 的值,在dis文件中搜索PC的地址可以得知又落入了异常中断
下面要根据SP的值在内存中找到栈。
在jlink命令行输入:
如:savebin d:/ram.bin 0x20000000 0x10000
表示将0x20000000开始0x10000大小的内存数据读出来存到D盘,叫ram.bin
然后打开J-Flash,把bin文件拉进来,选择起始地址
选择4字节,按32位显示,搜索SP地址,
顺着往下
找到LR地址,08001417
找到PC地址,发现是00000000
为什么PC地址会为0??
很显然是空指针,这里调用指令的地址指向了空
怎么看在哪里出现的空指针呢?
我们知道LR保存函数的返回地址,查一下LR就知道在哪里调用的空指针
查LR地址要减去1,也就是在dis文件中查08001416
发现在main函数中
(这里main函数中只有一个TaskHandler,汇编直接把这个函数展开了,不同编译器和单片机有所区别,GD32中TaskHandler没有被展开在main里)
这里一大段汇编看起来很难理解,GPT来解释一下
多元素数据结构的便利,刚好就是我们TaskHandler的内容,
BCC 0x8001400; B 0x80013fc;
都是转到前面的指令,也就是两个循环嵌套,对应了主循环while1和TaskHandler中的for循环
再看问题指令:
0x08001414: 4780 .G BLX r0
0x08001416: 1c64 d. ADDS r4,r4,#1
BLX是加载对应地址的指令,然后再下一步就会出错,
说明这里加载指令有问题,对应到上文就是PC指向了00000000,即一个空指针。
不能连接烧录器或者读取内存怎么办?
对于一些企业项目,会有读保护不允许读取内存,或者烧录口被复用无法连接调试器的。
理论上可以用串口把错误信息打印出来,如(公司里大神写的):
void hard_fault_handler_c(unsigned int * hardfault_args)
{
uint32_t stacked_r0;
uint32_t stacked_r1;
uint32_t stacked_r2;
uint32_t stacked_r3;
uint32_t stacked_r12;
uint32_t stacked_lr;
uint32_t stacked_pc;
uint32_t stacked_psr;
stacked_r0 = ((uint32_t) hardfault_args[0]);
stacked_r1 = ((uint32_t) hardfault_args[1]);
stacked_r2 = ((uint32_t) hardfault_args[2]);
stacked_r3 = ((uint32_t) hardfault_args[3]);
stacked_r12 = ((uint32_t) hardfault_args[4]);
stacked_lr = ((uint32_t) hardfault_args[5]);
stacked_pc = ((uint32_t) hardfault_args[6]);
stacked_psr = ((uint32_t) hardfault_args[7]);
RTT_Printf("\n\n[Hard fault handler - all numbers in hex]\n");
RTT_Printf("R0 = 0x%08x\n", stacked_r0);
RTT_Printf("R1 = 0x%08x\n", stacked_r1);
RTT_Printf("R2 = 0x%08x\n", stacked_r2);
RTT_Printf("R3 = 0x%08x\n", stacked_r3);
RTT_Printf("R12 = 0x%08x\n", stacked_r12);
RTT_Printf("LR [R14] = 0x%08x subroutine call return address\n", stacked_lr);
RTT_Printf("PC [R15] = 0x%08x program counter\n", stacked_pc);
RTT_Printf("PSR = 0x%08x\n", stacked_psr);
RTT_Printf("BFAR = 0x%08x\n", (*((volatile uint32_t *)(0xE000ED38))));
RTT_Printf("CFSR = 0x%08x\n", (*((volatile uint32_t *)(0xE000ED28))));
RTT_Printf("HFSR = 0x%08x\n", (*((volatile uint32_t *)(0xE000ED2C))));
RTT_Printf("DFSR = 0x%08x\n", (*((volatile uint32_t *)(0xE000ED30))));
RTT_Printf("AFSR = 0x%08x\n", (*((volatile uint32_t *)(0xE000ED3C))));
RTT_Printf("SCB_SHCSR = 0x%08x\n", SCB->SHCSR);
while (1) __NOP();
}
/* Cortex M0/1 */
__attribute__((naked)) void HardFault_Handler()
{
asm volatile (
"MOVS R0, #4\n\t"
"MOV R1, LR\n\t"
"TST R0, R1\n\t"
"BEQ WDT_IRQHandler_call_real\n\t"
"MRS R0, PSP\n\t"
"LDR R1, =hard_fault_handler_c\n\t"
"BX R1\n"
"WDT_IRQHandler_call_real:\n\t"
"MRS R0, MSP\n\t"
"LDR R1, =hard_fault_handler_c\n\t"
"BX R1"
);
}