前几篇文章我们一起深入理解 Cortex-M 处理器里异常和中断的一些概念,其中包括:
-
异常和中断的基本框架:深入理解 Cortex-M 的异常和中断
-
异常与中断优先级的理解:深入理解 Cortex-M 处理器中异常与中断的优先级
-
向量与向量表的重定位:深入理解 Cortex-M 向量表和向量表重定位
今天就让我们来研究一下处理器对于异常的执行流程究竟是什么样的。
异常请求接受条件
若满足下面的条件,处理器会接受请求:
-
处理器正在运行(未被暂停或处于复位状态)。
-
异常处于使能状态(NMI 和 HardFault 比较特殊,因为它们总是使能的)。
-
异常的优先级高于当前等级。
-
异常未被异常屏蔽寄存器(如 PRIMASK)屏蔽。
需要注意的是,对于 SVC 异常,若 SVC 指令被意外用于某异常处理中,且该异常处理的优先级不小于 SVC,则会引起 HardFault。
异常进入流程
异常进入流程包括以下操作:
1)将多个寄存器的值以及返回地址压入当前使用的栈。这就能够将异常处理用普通 C 函数来实现。若处理器处于线程模式且正在使用进程栈(PSP),则 PSP 指向的栈区域就会用于该压栈过程,否则就会使用主栈指针(MSP)所指向的栈区域。
2)取出异常向量(异常处理函数 / ISR 的起始地址)。为了减少等待时间,这一步可能会和压栈操作并行执行。
3)取出待执行的异常处理指令。在确定了异常处理的起始地址后,指令就会被取出。
4)更新多个 NVIC 寄存器和内核寄存器,其中包括挂起状态和异常的活跃状态,处理器内核中的寄存器包括程序状态寄存器(PSR)、链接寄存器(LR)、程序计数器(PC)以及栈指针(SP)。
根据压栈时实际使用的栈,在异常处理开始前,MSP 或 PSP 的数值会相应地被自动调整。PC 也会更新为异常处理的起始地址,而链接寄存器(LR)则会被更新名为 EXC_RETURN 的特殊值。该数值为 32 位,且高 27 位为 1。低 5 位中有些部分用于保存异常流程的状态信息(如压栈时使用的哪个栈)。该数值用于异常的返回。
执行异常处理
在异常处理内部,可以执行外设所需的服务。在执行异常处理时,处理器就会处于处理模式。此时:
-
栈操作使用主栈指针(MSP)
-
处理器运行在特权访问等级
若更高优先级的异常在这个阶段产生,处理器会接受新的中断,而当前正在执行的处理会被挂起且被更高优先级的处理抢占,这种情况就是异常嵌套。
此时如果另一个在该阶段产生的异常具有相同或更低优先级,则新到的异常就会处于挂起状态,且直到当前异常处理完后才会得到处理。
在异常处理的结尾,程序代码执行的返回会引起 EXC_RETURN 数值被加载到程序计数器(PC)中,并触发异常返回机制。
异常返回
对于某些处理器架构,异常返回会使用一个特殊的指令。不过,这也意味着异常处理无法像普通 C 代码那样编写和编译(需要借助于汇编指令)。对于 ARM Cortex-M 处理器,异常返回机制由一个特殊的地址 EXC_RETURN 触发,该数值在异常入口处产生并被存储在链接寄存器(LR)中。当该数值由某个允许的异常返回指令写入 PC 时,它就会触发异常返回流程。
异常返回可由下表中的指令产生:
当触发了异常返回机制后,处理器会访问栈空间里在进入异常期间被压入栈的寄存器数值,且将它们恢复到寄存器组中,这个过程被称作出栈。此外,多个 NVIC 寄存器(如活跃状态)和处理器内核中的寄存器(如 PSR、SP 和 CONTROL)都会更新。
在压栈操作的同时,处理器会取出之前被中断的程序的指令,并使得程序尽快继续执行。
由于使用了 EXC_RETURN 数值触发异常返回,异常处理(包括中断服务程序)就可以和普通的 C 函数/子例程一样实现。在生成代码时,C 编译器将LR 中的 EXC_RETURN 数值作为普通返回地址处理。由于 EXC_RETURN 机制,正常函数不会返回到地址 0xF0000000 ~ 0xFFFFFFFF。然而,由于根据架构的定义,这段地址区域也不能用于程序代码 [具有永不执行(XN)存储器属性],因此并不会有什么问题。