异常通常是指在正常的程序执行流程中发生暂时的停止并执行相应的处理操作,包括ARM内核产生复位、取指令或存储器访问失败、遇到未定义指令、执行软件中断指令或者出现外部中断等。大多数异常都有一个异常处理程序与之对应。在处理异常前,当前处理器的状态必须保留。这样,当异常处理完成后,可以继续执行当前程序。处理器允许多个异常同时发生,处理器会按固定的优先级处理它们。
在本书中,经常混用术语“中断”与“异常”。若不加说明,则强调的是它们对主程序所体现出来的“中断”性质,即指由于收到来自外围硬件(相对于CPU和内存)的异步信号或来自软件的同步信号,而进行相应的硬件/软件处理。但中断与异常的区别在于,中断对CM3核来说,都是“意外突发事件”,即该请求信号来自CM3内核之外,来自各种片上外设或外扩的外设;而异常则是由CM3内核活动产生,即在执行指令或访问存储器时产生。
CM3有15种异常,编号为1~15,如表2-15所示(注意:没有编号为0的异常);CM3有240个中断源。因为芯片设计者可以修改CM3的硬件描述源代码,实际的微处理器支持的中断源数目常常不到240个,并且优先级的位数也由芯片制造商最终决定。
在CM3中,优先级的数值越小,优先级越高。CM3支持中断嵌套,使得高优先级异常会抢占(Preempt)低优先级异常。其中3种系统异常(Reset、NMI和硬件失效)有固定的优先级,并且它们的优先级的数值是负数。所有其他异常的优先级都是可编程的,但其数值不能为负数。
表2-15 CM3的系统异常
在表2-15中,有3种异常是专为操作系统设计的。
【SysTick】 以前,大多操作系统都需要一个硬件定时器来产生操作系统需要的分时复用定时,以此作为整个系统的时基。有了SysTick,操作系统就无须占用芯片的定时器外设,而且所有的CM3芯片都有同样的SysTick,软件移植就更简便。
【系统服务调用(SVC)】 多用在操作系统的软件开发中,用于产生系统函数的调用请求。通过它,用户程序无须在特权级下执行,且与硬件无关。SVC相当于以前ARM中的软件中断指令(SWI)。
【PendSV】 操作系统一般都不允许在中断处理过程中进行上下文切换,当SysTick异常不是最低优先级时,它可能会在中断服务期间触发上下文切换,这是无法完成的。引入PendSV后,可以直到其他重要任务完成后才执行上下文切换。
有了以上3种内核级的异常,使得CM3与实时嵌入式操作系统成了绝佳搭配。
嵌套向量中断控制器(NVIC)的基本功能包括支持向量中断、可屏蔽中断、支持嵌套中断,以及支持动态优先级调整。动态优先级调整指的是软件可以在运行期间更改中断的优先级。
如果已发生的中断不能被立即响应,就称它被挂起(Pending)。如果某中断服务程序修改了自己所对应中断的优先级,则这个中断可被具有更高优先级的中断挂起,详见6.3节。
异常是一类特殊的中断,它与普通中断的相同之处在于都被NVIC管理,但特殊之处是它不能被屏蔽。少数故障异常是不允许被挂起的。异常被挂起的原因可能是系统正在执行一个具有更高优先级中断服务程序,或者因相关屏蔽位被置位而导致该异常被禁止。每个异常源在被挂起的情况下,都会有一个对应的“挂起状态寄存器”保存其异常请求。等到该异常能够被响应时,执行其服务程序,这与传统的ARM处理方式是完全不同的。传统的ARM中断系统是由产生中断的设备保持请求信号的,而CM3则是由NVIC的挂起状态寄存器来解决这个问题的,即使设备在后来已经释放了请求信号,曾经发生的中断请求也不会丢失。
CM3处理器中有一个可以重复定位的向量表,表中包含了将要执行的函数地址,可供具体的中断使用。中断被响应后,处理器通过指令总线接口从向量表中获取地址。
为了提高系统灵活性,当异常发生时,程序计数器、程序状态寄存器、链接寄存器,以及R0~R3、R12等通用寄存器将被压栈。当数据总线对寄存器进行压栈时,指令总线从程序存储器中取出异常向量,并获取异常代码的第1条指令。一旦压栈和取指令完成,中断服务程序或故障处理程序就开始执行,随后寄存器自动恢复,被中断的程序也因此恢复执行。由于采用硬件处理堆栈操作,因此CM3处理器免去了在传统的C语言中断服务程序中完成堆栈处理所要编写的程序,使应用程序的开发变得简单。
另外,NVIC还采用了支持内置睡眠模式的电源管理方案。
当CM3进入相应的中断或异常时,会经历如下步骤。
(1)保存现场。压栈操作,处理器通过硬件自动把相关的寄存器xPSR、PC、LR、R12及R0~R3保存到当前使用的堆栈中。如果当前使用的是P SP,则压入进程堆栈;否则压入主堆栈。进入中断服务程序(Interrupt Service Routines,ISR)后,就一直使用MSP。
(2)取向量。CM3处理器有专用的数据总线和指令总线。在D-Bus(数据总线)保存处理器状态的同时,处理器通过I-Bus(指令总线)从向量表中取出异常向量,并获取ISR函数的地址。也就是说,保护现场与取异常向量是并行处理的。
(3)更新寄存器。执行ISR前的最后一步就是更新通用寄存器。ISR将使用MSP来访问堆栈,因此会将堆栈指针更新。xPSR将会被写入中断编号。PC指向ISR的入口地址,准备执行ISR。此时,LR会更新为一个特殊的值(又称EXC_RETURN),发挥异常返回作用,该寄存器仅低4位有效。
中断返回时,通过向PC中写EXC_RETURN的值来识别返回动作。中断返回后,会依次恢复入栈的各个寄存器,并更新NVIC的值。CM3支持嵌套中断,因此在响应中断时,若有高优先级的中断到来,则需要再次执行入栈操作。这里有一个问题需要考虑,就是每增加一级嵌套,就需要至少8W(字)的堆栈空间。因为响应中断时使用的是主堆栈指针(MSP),所以对主堆栈的堆栈空间有一定的要求。
除了通用寄存器,NVIC中的相关寄存器也会被更新。例如,新响应中断的挂起位将被清除,同时其活动位将被置位。
在ISR中,当一个新的中断比当前中断的优先级更高时,处理器将打断当前流程而响应优先级更高的中断,这就产生了中断嵌套。占先是一种对更高优先级中断的响应机制。CM3中断占先的处理过程如图2-23所示。
图2-23 CM3中断占先的处理过程
当处理器响应某中断时,若又发生其他中断,但其他中断的优先级并不高,则它们会被挂起。在当前中断执行返回后,系统处理挂起中断的传统方法是先出栈,然后又把出栈的内容压栈,如图2-24所示。此时,压栈2与出栈1的内容完全相同,因此CM3不再出栈这些寄存器,而是继续使用上一个中断已经压栈的成果,看上去好像前后两个中断连接起来了,前后只执行了一次压栈/出栈操作,这被称为尾链,如图2-25所示。尾链是处理器用于加速中断响应的一种机制,其时序图如图2-26所示。
图2-24 不用尾链的情况
图2-25 尾链示意图
图2-26 尾链时序图
如果前一个ISR尚未进入执行阶段,并且后来中断的优先级比前一个中断的优先级高,则后来中断能够抢先得到服务,这种现象称为迟来。例如,在响应某低优先级中断1(优先级为3)的早期检测到高优先级中断2(优先级为2),系统就能以迟来的方式处理——在压栈完成后采用末尾连锁技术,执行ISR 2,如图2-27所示。因此可以说,迟来是一种加速占先机制。
图2-27 迟来示意图
CM3中断返回流程如图2-28所示。中断返回时,处理器可能处于以下3种状态之一。
(1)尾链到一个已挂起的中断,该中断比栈中所有中断的优先级都高。
(2)如果没有挂起的中断,或者栈中最高优先级的中断比挂起的最高优先级中断具有更高的优先级,则返回最近一个已压栈的中断服务程序(ISR)。
(3)如果没有中断被挂起或位于栈中,则返回线程模式。
若没有挂起中断或没有比被压栈的中断优先级更高的中断,则处理器执行出栈操作,并返回被压栈的ISR或线程模式,即在响应ISR后,处理器通过出栈操作自动将处理器状态恢复为进入ISR前的状态。如果在状态恢复过程中出现一个新的中断,并且该中断的优先级比正在返回的ISR或线程的优先级更高,则处理器放弃状态恢复操作,并将新的中断作为尾链处理。
图2-28 CM3中断返回流程
寄存器出栈后,对于异常,它的活动位也被硬件清除;对于中断,若中断输入再次被置为有效状态,则挂起位也将再次被置位,新的中断响应序列也随之再次启动。
中断基于优先级而采取的占先、尾链、返回和迟来4种机制的区别如表2-16所示。这4种机制最大的区别在于中断出现的时刻不同。
表2-16 中断基于优先级而采取的4种机制的区别