ARM学习(5) 异常模式学习(CortexM3/M4)
ARM学习(5) 异常模式学习(CortexM3/M4)
笔者来聊聊对CortexM3/M4的异常模式理解。
之前的了解,都是基于具体的芯片而言的,比如ST/GD/NXP公司的,STM32,很常用,基于Keil或者IAR集成开发环境,一直到工作之后,才更清楚的了解了架构这种概念,这种芯片本质上还是基于CortexM3/M4这种架构的,各种厂商都是基于这种架构进行开发的,然后添加很多外设,满足不同的功能需求。
1、通用寄存器
寄存器首先是架构的不可或缺的一部分,也是了解架构编程模型的重要地方。
-
寄存器 ,r0-r12,r13(sp),r14(lr),r15(pc);
-
状态寄存器PSR:APSR、EPSR、IPSR;
T:thumb指令,总是为1
异常编号:正常处理的异常编号,与接下来介绍的中断向量表编号相关,
NZCV:标志位,
与经典的arm架构相比(比如arm7 ),M系列做了很多改变。
模式:模式减少,只有特权模式和非特权模式(均是在线程下,异常模式自动是特权等级),
堆栈:只有主栈+线程栈,MSP+PSP,不像经典架构,每个模式都有各自独立的栈。
寄存器:减少,模式下除了SP,其他寄存器都是共享,经典架构下,寄存器较多,各个模式下都有几个独立的寄存器。
异常:异常也有变化,新增hard fault等,将svc模式转为svc的函数处理,等等 -
中断相关寄存器 :PRIMASK、FAULTMASK和BASEPRI
a. PRIMASK:中断屏蔽位,1位宽,屏蔽除NMI以及hard_fault之外的中断
b. FAULTMASK:中断屏蔽位,1位宽,屏蔽除NMI之外的所有中断
c . BASEPRI:当前优先级设置,位数取决于厂商有多少个外设中断,
d. CMSIS-Core 提供了多个C函数,可以访问中断相关寄存器,也提供了汇编函数
x = __get_BASEPRI(); // Read BASEPRI register
x = __get_PRIMARK(); // Read PRIMASK register
x = __get_FAULTMASK(); // Read FAULTMASK register
__set_BASEPRI(x); // Set new value for BASEPRI
__set_PRIMASK(x); // Set new value for PRIMASK
__set_FAULTMASK(x); // Set new value for FAULTMASK
__disable_irq(); // Set PRIMASK, disable IRQ
enable_irq(); // Clear PRIMASK, enable IRQ
; 汇编代码
MRS r0, BASEPRI ; Read BASEPRI register into R0
MRS r0, PRIMASK ; Read PRIMASK register into R0
MRS r0, FAULTMASK ; Read FAULTMASK register into R0
MSR BASEPRI, r0 ; Write R0 into BASEPRI register
MSR PRIMASK, r0 ; Write R0 into PRIMASK register
MSR FAULTMASK, r0 ; Write R0 into FAULTMASK register
;primask 以及 faultmask 特殊处理
CPSIE i ; Enable interrupt (clear PRIMASK)
CPSID i ; Disable interrupt (set PRIMASK)
CPSIE f ; Enable interrupt (clear FAULTMASK)
CPSID f ; Disable interrupt (set FAULTMASK)
- Control寄存器:
nPRIV:设置特权等级还是非特权等级
SPSEL:选择主栈还是线程栈
之前介绍的,异常永远是特权模式,且使用主栈。
这里有两种情况:(通常RTOS中栈切换版本会使用进程栈/线程栈 PSP,其他裸机情况下都使用的时候主栈MSP)
a .进入异常模式之前是主栈:压栈使用主栈,然后切到异常函数下执行(下文图1),
b. 进入异常模式之前是进程栈(线程栈): 压栈使用进程栈,然后切到异常函数下执行,这时使用主栈,特权模式(下文图2)
步奏: - 线程模式下:使用主栈(MSP)
- 中断#1来临,首先使用主栈(MSP)压栈,然后切到中断服务程序#1,这时处于特权模式下,使用主栈(MSP)
- 中断#2来临:使用主栈进行压栈,然后切到中断服务程序#2执行,这时处于特权模式下,使用主栈(MSP)
- 中断#2结束:根据LR 选择 MSP主栈,进行出栈,同时还是处于特权模式下,
- 中断#1结束,根据LR选择MSP主栈,进行出栈,同时还是特权模式下执行
- 线程模式下:使用主栈继续执行
步奏: - 线程模式下:使用线程栈
- 中断#1来临,首先使用进程栈(PSP)压栈,然后切到中断服务程序#1,这时处于特权模式下,使用主栈(MSP)
- 中断#2来临:使用主栈进行压栈,然后切到中断服务程序#2执行,这时处于特权模式下,使用主栈(MSP)
- 中断#2结束:根据LR 选择 MSP主栈,进行出栈,同时还是处于特权模式下,
- 中断#1结束,根据LR选择PSP进程栈,进行出栈,同时切到线程非特权模式下执行
- 线程模式下:使用线程栈继续执行
上文说的:特权模式与图中的处理模式一致含义。
2、异常与中断
2.1、系统异常
- 复位:reset_handler,用来做系统初始化以及分散加载,最终跳到main函数,
- 异常函数处理:hard_fault,mem_mange_fault,bus_fault,usg_fault,处理各种异常
- SVC:常用来做系统调用,必须立即响应,否则造成硬件错误
- PendSV:RTOS常用该异常做上下文切换,因为该异常可以软件触发且挂起
- SysTick:定时中断,可用作滴答定时器
CortexM3/M4响应异常时,会有一些列的操作,
- 入栈:硬件自动压入xPSR, PC, LR, R12以及R3‐R0
- 取向量:取出正确的异常向量,这样能找到异常处理函数,
- 选择堆栈指针:更新SP指针,更新PSR,更新PC,更新LR(EXC_RETURN),
中断返回: - 出栈:与入栈的顺序想对应,
- 更新NVIC寄存器:清除相关的中断或者异常标志位,
异常返回值LR (EXC_RETURN),异常处理LR在M3/M4架构下有新的意义,
异常的寄存器如下:主要是4个中断(hardfault、mem/bus/usr fault )关联的寄存器信息。
- hardfault:硬件错误中断,很多场景被叫做硬件上访中断。
- 中断优先级是-1,primask无法屏蔽,但是faultmask可以屏蔽。
- 其他异常中断没有使能,则会进入该中断
- 汇编屏蔽代码如上面介绍
- HFSR状态寄存器是指示出现hardfault的原因的寄存器信息,30、31 bit分别指示是:上访错误和调试事件 1bit是指:获取向量错误,比如中断向量表获取?待测试。
__set_PRIMASK(1); //屏蔽除NMI 和 HardFault以外其他的中断 ;__set_PRIMASK(0);
__set_FAULTMASK(1); //屏蔽除NMI 中断;__set_FAULTMASK(0);
//可嵌套的开关中断函数primask
u32 arm_v7_disable_irq()
{
u32 mask;
asm volatile("mrs %0, primask" : "=r"(mask));
asm volatile("cpsid i");
return mask
}
void arm_v7_enable_irq(u32 mask)
{
asm volatile("msr primask,%0" : : "r"(mask));
}
//可嵌套的开关中断函数faultmask
u32 arm_v7_disable_fiq()
{
u32 mask;
asm volatile("mrs %0, faultmask" : "=r"(mask));
asm volatile("cpsid i");
return mask
}
void arm_v7_enable_fiq(u32 mask)
{
asm volatile("msr faultmask,%0" : : "r"(mask));
}
void save_generic_regs();
save_generic_regs:
stmfd sp!,{r9-r12}
stmfd sp!,{r5-r8}
stmfd sp!,{r1-r4}
stmfd sp!,{r0}
bx lr
/* void restore_generic_regs(); */
restore_generic_regs_from_stack:
ldmfd sp!,{r0}
ldmfd sp!,{r1-r4}
ldmfd sp!,{r5-r8}
ldmfd sp!,{r9-r12}
bx lr
u32 regs_data[15];
void save_regs_to_global_var(u32 save_reg_addr)
{
memcpy((u8*)regs_data,(u8*)save_reg_addr,15*4);
}
/*1:hard 2:mem 3:bus 4:usr*/
void enter_exception_mode(u8 exception_reason)
{
/* get the exception reg info*/
/* print backtrace */
/* save the stack and global reg */
}
void HardFault_Handler();
HardFault_Handler:
/* save the lr reg to the stack */
push lr
/* reserve stack space for sp */
sub sp,sp,#4
/*save r0-r12 to stack (13*4=52) */
bl save_generic_regs
/*save sp to stack ,the sp is the reg before it entered the hardfault,4+4+52=60*/
add r0,sp,#60
str r0,[sp,#52]
/*save regs to the global reg */
mov r0,sp
bl save_regs_to_global_var
/* restore the regs from the stack */
bl restore_generic_regs
mov r0,#1
/* enter the assert mode */
blx enter_exception_mode
/* skip the stack space for sp */
add sp,sp,#4
/* restore the lr from stack */
ldmfd sp!,{lr}
bx lr
- mem fault:存储器访问异常中断,
- 配置来使能MemManage,可设置优先级
- 由cfsr低字节指示错误,如MMA VALID 有效,mmfar指示错误地址
- MPU的配置规则,导致指令或者数据访问冲突,比如MPU配置特权访问,而非特权模式却访问对应区域;或者对MPU只读区域进行写操作;压栈和出栈导致的错误。
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;
NVIC_SetPriority(MemoryManagement_IRQn,0);
-
bus fault:总线访问异常
- 配置来使能BusFault,可设置优先级,由总线接口上的错误响应处罚
- 由fsr第二个字节来表示,如BFAR VALID,则BFAR指示错误地址。
- 1、读(取)指令或者读写数据终止,取指令必须是错误的阶段到了执行阶段才会触发异常,取向量异常会强制进入hardfault,即使使能总线错误。
- 2、压栈或者出栈错误
- 3、访问非法的存储器位置,设备未就绪就开始访问(Dram未初始化),收到从设备错误响应,存储器访问权限问题。
- 4、与mem fault错误由很多类似的错误。
-
usr fault:
- 配置来使能UsgFault,可设置优先级,由总线接口上的错误响应处罚
- 222
- 333
来看一个实际的例子,引发了非对齐访问的血案。
typedef struct wifi_data_rec_struct
{
u16 header;
u16 addr;
u32 arg1;
u32 arg2;
u16 arg3;
u16 checksum;
}wifi_data_rec_t,*wifi_data_rec_ptr_t;
typedef strcut wifi_info_struct
{
u32 wifi_addr;
u8 recv_flag;
u8 test_flag;
u16 wifi_data;
u8 tx_buf[128+2];
u8 rx_buf[16];
}wifi_info_t, wifi_info_ptr_t;
wifi_info_t wifi_info_g;
u8* rx_buf=wifi_info_g.rx_buf;
/*......*/
u16 temp_value = (*(u16*)rx_buf);
PRINTF("temp_value=%x rx_buf_addr=0x%p \r\n",temp_value,rx_buf);
wifi_data_rec_t temp_checksum=(*(wifi_data_rec_ptr_t )rx_buf);
PRINTF("temp_value=%x temp_check_sum=%x\r\n",temp value, temp_checksum.checksum);
u16 cal_check_sum=cal_wifi_data_cmd(temp_checksum);
详细介绍一下错误的原因:
- 由下图串口助手以及调试器数据,可以看到打印完成temp_value之后,就导致进入hardfault,其实是usg_fault,因为笔者并没有使能usg_handler函数。
- 串口助手打印:
- Ozone调试界面:
-
那就是那句强转代码导致的异常,
-
因为后面要计算校验和,所以编译器就把每个数据加载到寄存器,然后去计算。
- Trace32加载axf查看汇编:
- 又因为数据恰好是16个字节,编译器直接使用多字节加载指令:ldm,寄存器寻址;而ldm要求地址必须4Byte对齐(WORD,后面图中的arm手册查询),但是笔者的代码中rx buf地址由于上面tx buf的原因,导致没有按照4Byte对齐,引发了对齐访问错误。
- ARM手册查看:
- 笔者尝试把计算校验和的代码删除,则没有发生错误,从汇编来看,只加载了checksum,其他数据没有加载,而单字加载,arm指令是支持非对齐访问的,所以就没有出现异常。
上图中介绍了CCR寄存器,系统控制器,如果使能了非对齐检查,那么LDR 以及LDRH也不能非对齐访问了,如果产生,能报异常。
SCB->CCR = SCB_CCR_UNALIGN_TRP_MSK;
使能了改bit之后,ldrh 正常执行,因为其满足 half word对齐,但是ldr造成了异常,因为不满足word 对齐。
-
addr2line 查看异常地址信息
320行来看,其实并不是强转的代码,是因为编译器把打印和加载放到一行代码来对应,所以也不能全部按照源码的行数来看,因为源码和汇编指令可能不对应,在开了优化的情况下。
如果关闭优化,321行,就是强转的那行代码,对于调试来说,更加友好。 -
寄存器信息与自动压栈匹配:
笔者打开SP指针对应得内存区域,可以看到已经自动将pc lr等数据压入堆栈,比如: -
0x200098E8:R0-R3
-
0x20009FE8:R12,LR,PC,xPSR
可以见到PC:0x080086A0,可以正确看到程序出错前的地址,帮助我们分析错误。2.2、中断
中断管理使用NVIC,嵌入式向量中断控制器进行管理,
3、参考
更多推荐
所有评论(0)