系统异常 Cortex-M架构支持多种系统异常和中断,编号0~15的为异常,16以上的为中断。从细节来说中断和异常是区别的,由内部事件发生导致的指令执行顺序被打断称为异常,而由外部事件导致的称为中断,比如外设产生的中断信号。
这16个系统异常除了SVC(系统服务调用)和PendSV(可挂起的系统调用)外,在软件开发过程中一般都会常遇到,比如代码问题无意中触发了各种fault,使用SysTick作为延时定时器等。SVC和PendSV主要是由操作系统来使用,在FreeRTOS、μC/OS等RTOS中都会使用PendSV异常来实现任务上下文的切换。不过除了需要做OS的移植工作,对OS用户来说平时也不会关注它们的用途。 SVC异常 SVC异常由SVC指令触发,在很多系统中SVC机制用于实现应用任务访问系统资源。对于高可靠性系统,应用任务运行在非特权模式,只能通过OS访问受保护的硬件资源,使得嵌入式系统更安全。
例如,操作系统不允许应用任务直接访问硬件,而是通过提供一些系统服务函数,用户任务使用SVC对系统请求服务。当用户想访问某个硬件资源时,便产生一个SVC异常,于是操作系统提供的SVC异常服务函数得到执行,它再调用相关的系统服务函数,完成用户的请求。这种请求——响应请求的方式有诸多好处: 1、 硬件控制由OS负责,使得用户程序无需了解繁杂的硬件控制细节,简化了开发难度,应用移植性更好; 2、 OS的代码一般会经过充分验证,从而使系统更加安全可靠; 3、 用户任务无须运行在特权模式,避免了不当操作导致整个系统出现问题的风险; SVC指令需要一个8立即数来作为系统调用的代号,SVC异常服务例程会提取这个立即数,从而知道此次的调用请求。例如调用编号2的系统系统服务请求的汇编指令: SVC #0x2 一些编译器会提供SVC调用的内建函数,在C/C++代码中调用该内建函数就会触发SVC异常。没有提供SVC内建函数的编译器一般是使用内联汇编的方式,在C/C++中插入SVC异常触发的汇编指令。 当SVC异常服务例程执行时又是如何获得本次请求的编号呢?这就需要了解Cortex-M的堆栈和栈帧结构了。因为SVC指令的第一个字节为SVC的编号,只需找到执行的这条SVC指令的本身就能获取编号的数值。Cortex-M有自动入栈机制,当进入到SVC异常之前,R0~R3、R12、LR、PC、xPSR这8个寄存器会被自动压栈。因此要获取PC,只需要从堆栈中将栈帧提取出来。但是Cortex-M有两个堆栈指针,分别是主堆栈指针MSP和进程堆栈指针PSP,于是再进一步就变成了寻找进入SVC异常前栈帧是压入了哪一个堆栈。
在进入异常服务程序后,LR的值被自动更新为特殊的EXC_RETURN,这是一个高28位全为1的值,只有[3:0]的值有特殊含义。其中的bit2指示了出栈时所使用的堆栈,为0时使用的是MSP,为1时使用的是PSP。下面是一个示例,在C代码中通过内联汇编请求编号2系统服务调用。
在使用汇编代码编写的SVC服务例程中按照上述过程从堆栈中提取SVC指令,并从指令的第一字节提取出编号的值存储到R0寄存器,现在有了编号就可以调用对应的系统服务函数执行硬件资源的访问了。
没有使用OS的嵌入式系统,也可以通过SVC异常结合MPU内存保护机制,将外设等重要资源设定访问权限,普通的应用代码只允许在非特权模式,以此提升系统的可靠性。 PendSV异常 PendSV异常对操作系统来说也很重要,其优先级可编程,通过写入中断控制和状态寄存器(ICSR)设置挂起位触发。可挂起的意思是只有在没有更高优先级的异常或中断需要执行时,才会执行对应的异常服务例程,否则只能等待。 为了保证系统的实时性,当中断发生时是必须先得到响应的,当有中断处在活跃状态时OS就不能切换上下文。倘若没有挂起异常机制,任务切换的时间可能会拖的很长,因为需要等待下一次的OS节拍中断产生时才能再次切换,特别是当中断源与OS节拍中断频率接近时,发生“共振”导致任务无法切换。OS将PendSV的优先级设置为最低,并在高优先级中断都执行完毕之后立即进入PendSV异常服务例程,并完成切换上下文,这样就解决了这个问题。
|