Cortex-M的异常处理

对于大型的嵌入式系统,调试工作通常都会比较痛苦,尤其多线程的系统,通常只能通过打印来定位问题,但如果程序发成崩溃或者重启,并且来不及打印任何信息,问题就会很难定位。
今天个大家介绍一个ARM Cortex-M系列MCU定位崩溃重启问题的方法。
注:在开始之前,先安利一本书《THE DEFINITIVE GUIDE TO ARM CORTEX-M3 AND CORTEX-M4 PROCESSORS》,深入学习Cortex MCU的不二之选。
1. 堆栈指针
Cortex-M系列对寄存器结构做了一些调整,与经典的ARM7TDMI相比去掉了各种MCU运行模式,引入了MSP与PSP的机制:
cortexm_01
R13是栈指针,当MCU运行在处理模式的时候指向MSP,运行在线程模式的时候指向PSP。
对于这两种模式,一般来讲裸奔的系统通常只用MSP,OS中通常使用PSP。
在产生中断或者异常之后进入服务例程之前,MCU会对当前处理器状态和寄存器数值进行压栈操作:
cortexm_02
从服务例程返回时再进行出栈操作:
cortexm_03
换句话说,当嵌入式系统发生崩溃异常的时候,我们只需要重写异常例程就可以获得发生异常时的处理器现场,从而准确定位问题。
2. 异常事件处理程序
Cortex-M系列MCU通过NVIC管理中断和异常,在启动代码中,我们初始化堆栈之后,就需要初始化中断向量表:
cortexm_04
我们看到,可处理的异常被分为四类:硬件错误、内存管理、总线错误和使用错误。
其中这四个异常中,HardFault异常无论任何异常都会被出发,所以通常为了简便,通常只需要编写HardFault处理例程。
cortexm_05
异常服务例程与中断例程没有什么差别,具体处理代码在我开篇推荐的那本书中12.8小节中有样例以及详细的描述,现摘录如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
HardFault_Handler:
	tst lr, #4
	ite eq
	mrseq r0, msp /* stacking was using MSP */
	mrseq r0, psp /* stacking was using PSP */
	mov r1, lr /* second parameter */
	ldr r2,=HardFault_Handler_c
	bx r2
	.end
 
// Second part of the HardFault handler in C
void HardFault_Handler_C(unsigned long * hardfault_args, unsigned int lr_value)
{
	unsigned long stacked_r0;
	unsigned long stacked_r1;
	unsigned long stacked_r2;
	unsigned long stacked_r3;
	unsigned long stacked_r12;
	unsigned long stacked_lr;
	unsigned long stacked_pc;
	unsigned long stacked_psr;
	unsigned long cfsr;
	unsigned long bus_fault_address;
	unsigned long memmanage_fault_address;
 
	bus_fault_address = SCB->BFAR;
	memmanage_fault_address = SCB->MMFAR;
	cfsr = SCB->CFSR;
 
	stacked_r0 = ((unsigned long) hardfault_args[0]);
	stacked_r1 = ((unsigned long) hardfault_args[1]);
	stacked_r2 = ((unsigned long) hardfault_args[2]);
	stacked_r3 = ((unsigned long) hardfault_args[3]);
	stacked_r12 = ((unsigned long) hardfault_args[4]);
	stacked_lr = ((unsigned long) hardfault_args[5]);
	stacked_pc = ((unsigned long) hardfault_args[6]);
	stacked_psr = ((unsigned long) hardfault_args[7]);
 
	printf ("[HardFault]\n");
	printf ("- Stack frame:\n");
	printf (" R0 = %x\n", stacked_r0);
	printf (" R1 = %x\n", stacked_r1);
	printf (" R2 = %x\n", stacked_r2);
	printf (" R3 = %x\n", stacked_r3);
	printf (" R12 = %x\n", stacked_r12);
	printf (" LR = %x\n", stacked_lr);
	printf (" PC = %x\n", stacked_pc);
	printf (" PSR = %x\n", stacked_psr);
	printf ("- FSR/FAR:\n");
	printf (" CFSR = %x\n", cfsr);
	printf (" HFSR = %x\n", SCB->HFSR);
	printf (" DFSR = %x\n", SCB->DFSR);
	printf (" AFSR = %x\n", SCB->AFSR);
	if (cfsr & 0x0080) printf (" MMFAR = %x\n",
		memmanage_fault_address);
	if (cfsr & 0x8000) printf (" BFAR = %x\n", bus_fault_address);
	printf ("- Misc\n");
	printf (" LR/EXC_RETURN= %x\n", lr_value);
 
	while(1); // endless loop
}
这样,在发生异常时,我们就可以从打印中得到当时的处理器状态,可以根据BAFAR、CFAR等寄存器中的状态初步判断可能发生的问题种类。
但,如果想进一步定位问题发生的位置,那就需要用到PC和LR两个寄存器了。
3. PC、LR与MAP文件
之前我们提到在进入异常例程之前会压栈处理器状态,其中也包括两个重要的处理器PC和LR,其中存放了两个个32位的地址,PC指向异常发生之前程序指令的执行地址,LR存放了异常发生之前函数调用的返回地址,在正常的函数调用结束后,处理器会通过将LR装载到PC中完成返回跳转。
有了LR与PC就可以确定异常发生时的函数调用关系,甚至有些时候可以直接定位到某一具体的代码。
当然,这就需要用到编译时一个重要的文件,MAP文件。
对于gcc工具链,在连接中增加一组参数
LFLAGS+= -Wl,-Map,test.map
就可以在编译时生成MAP文件,MAP文件描述了bin文件中数据与代码在运行时映射的地址关系。
举个例子说明:
假设在系统启动过程中发生崩溃,打印信息中
PC=0x080002b8
查看MAP文件得到:
cortexm_06
由于PC值>0x08000294,<0x080003e0,所以在发生异常之前程序运行到了led_init函数中。
如果想要进一步确定发生异常的具体指令,还可以反汇编led.o进行定位。
offset = PC – 0x08000294
cortexm_07
同时辅助,异常例程中的寄存器打印信息,确定出错原因。
4 常见问题
结合平时的工作经验,我在这里给出几种常见的异常时状态及其可能的Bug原因。
(1) 栈被冲
我们都知道,堆是向上增长的,栈是向下增长的,而嵌入式系统中通常堆和栈处于同一片连续的内存中,所以如果程序运行时临时使用了大内存,很容易使得堆和栈发生重叠。
在发生函数调用时,函数的返回地址存放在栈中,如果被意外修改,那么在函数返回时就会产生异常。
所以当异常例程打印中LR与PC数值异常,超出存储器的划分或者映射范围,CFSR显示取指令,那么很可能是栈被冲了。
此时,需要检查地址空间是否需要重新划分,是否有大块的内存申请,或者内存泄露等情况。
(2) 野指针
野指针可以说是最常见的Crush Bug产生的原因,也是最容易定位的问题。
当使用野指针或者指针指向非法地址是通常都会触发总线错误,同时PC和LR均正常,可以通过这辆个地址定位到问题发生的汇编指令,再进一步通过异常例程中打印的寄存器上下文判断发生问题指针即可。
(3) 死循环
通常死循环并不会导致异常,但整个嵌入式系统会宕机,如果配置了看门狗,那么在狗超时后系统便会重启。
如果需要定位此类问题,可以同样配置看门狗中断例程,在例程中同样打印MSP、PSP寄存器,与异常处理过程完全相同。
最常见的死循环是ASSERT语句,一般在嵌入式系统中assert通常都会被定义为一个死循环,这种情况只要通过PC和LR定位到发生问题的assert,然后根据触发条件判断原因即可。

发表评论