Cortex-M 异常处理的 C 实现、栈帧以及 EXC_RETURN
在之前的文章中介绍了 Cortex-M 处理器异常和中断的多种信息,其中包括可用的异常类型、异常处理流程概述以及如何设置 NVIC。
在接下来的文章中,我们会一起来探究异常处理流程中更细节的部分,其中包括压栈和出栈操作、用于异常返回的 EXC_RETURN 的详细介绍以及系统控制块(SCB)中和这些操作相关的可编程寄存器。
当然,从今天这篇文章开始,许多话题都会较之前更为深入,大多数嵌入式软件开发人员并不需要完全了解。然而,它们对异常和中断方面的调试会很有作用,而且这些内容能够给实时操作系统(RTOS)的编程人员提供非常大的帮助。
C 实现的异常处理
对于 Cortex-M 处理器,可以将异常处理或中断服务程序(ISR)用普通的 C 程序/函数进行实现。要详细理解这种机制,我们首先需要看一下 C 函数在 ARM 架构上到底是如何工作的。
用于 ARM 架构的 C 编译器遵循 ARM 的一个名为 AAPCS(ARM 架构过程调用标准)的规范。根据这份标准,C 函数可以修改 R0~R3、R14(LR)以及 PSR。若 C 函数需要使用 R4~R11,就应该将这些寄存器保存到栈空间,并且在函数结束前将它们恢复。
R0~R3、R12、LR 以及 PSR 被称作 “调用者保存寄存器”,若在函数调用后还需使用这些寄存器的数值,在调用前,调用子程序的程序代码需要将这些寄存器的内容保存到内存中(如栈)。函数调用后不需要使用的寄存器数值则不用保存。
R4~R11 为 “被调用者保存寄存器”,被调用的子程序或函数需要确保这些寄存器在函数结束时不会发生变化(与进入函数时的数值一样)。这些寄存器的数值可能会在函数执行过程中变化,不过需要在函数退出前将它们恢复为初始值。
若 Cortex-M 处理器具有浮点单元,则浮点单元的寄存器也有类似的需求:
-
S0~S15 为 “调用者保存寄存器”
-
S16~S31 为 “被调用者保存寄存器”
一般来说,函数调用将 R0~R3 作为输入参数,R0 则用作返回结果。若返回值为 64 位,则 R1 也会用于返回结果,如下图所示:
要使 C 语言可以用作异常处理,则异常处理机制需要在异常入口处自动保存 R0~R3、R12、LR 以及 PSR,并且在异常退出时将它们恢复,这些都要由处理器硬件控制。这样,当返回到被中断的程序时,所有寄存器的数值都会与进入中断时相同。另外,与普通 C 函数的调用不同,返回地址(PC)的数值并没有存储在 LR 中(异常机制在进入异常时将 EXC_RETURN 代码放入了 LR 中,该数值会在异常返回时用到),因此,异常流程也需要将返回地址保存。这样,对于 Cortex-M3 或不具有浮点单元的 Cortex-M4 处理器,需要在异常处理期间保存的寄存器就有 8 个。
对于具有浮点单元的 Cortex-M4 处理器,若用到了浮点单元,则异常机制还需要保存 S0~S15 以及 FPSCR。CONTROL 寄存器中的 FPCA (浮点上下文活跃)位表示这一操作的执行情况。
栈帧
在异常入口处被压入栈空间的数据块为栈帧。对于 Cortex-M3 或不具有浮点单元的 Cortex-M4 处理器,栈帧都是 8 个字大小的,如下图所示:
对于具有浮点单元的 Cortex-M4, 栈帧则可能是 8 或 26 个字。
AAPCS 的另外一个要求为,栈指针的数值在函数入口和出口处应该是双字对齐的。因此,若在中断产生时栈帧未对齐到双字地址上,Cortex-M3 和 Cortex-M4 处理器会自动插入一个字。这样,可以保证栈指针位于异常处理的开始处。“双字栈对齐” 特性是可编程的,若异常处理不需要完全符合 AAPCS,则可以将该特性关闭。
压栈的 xPSR 中的第九位表示栈指针的数值是否调整过。在上图中,栈指针为双字对齐的,因此就不需要额外插入一个字,并且 xPSR 的第九位也为 0。在双字对齐特性被关闭后,栈帧还会保持这种形式,只是栈指针的数值有可能未对齐到双字地址。
若使能了双字栈对齐特性,同时栈指针的数值并没有对齐到双字边界上,则栈中会被插入一段空位,从而使栈指针强制对齐到双字地址,此时压栈的 xPSR 的第 9 位被置为 1,表明栈被调整,如下图所示:
压栈的 xPSR 的第九位在异常退出流程中用于确定是否需要调整 SP 的数值。
对于具有浮点单元的 Cortex-M4,若浮点单元已使能且使用,栈帧还会包括浮点单元寄存器组中的 S0~S15,如下图所示:
通用目的寄存器 R0~R3 的数值位于栈帧底部,可以很方便地由 SP 相关寻址访问。有些情况下,这些压栈的寄存器可用于向软件触发的中断处理函数或 SVC 服务中传递有用信息。
我们可以通过系统控制块(SCB)中,配置控制寄存器(CCR,地址 0xE000ED14)的一个控制位来使能双字栈对齐特性。并且可以在初始化期间利用下面的 C 代码来使能该特性:
SCB->CCR |= SCB_CCR_STKALIGN_Msk; //设置 CCR 中的 STKALIGN 位(第 9 位)
双字栈对齐的有效性如下:
-
Cortex-M4 处理器中默认使能
-
Cortex-M3 r2p0 及以后的版本默认使能
-
Cortex-M3 r1p0 和 r1p1 默认禁止
-
Cortex-M3 r0p0 中不可用
尽管该特性会略微增大栈空间的使用,但仍然强烈建议使用本特性。并且在异常处理内部不应该修改该位。
EXC_RETURN
处理器进入异常处理或中断服务程序(ISR)时,链接寄存器(LR)的数值会被更新为 EXC_RETURN 数值。当利用 BX、POP 或存储器加载指令(LDR 或 LDM)将其加载到程序寄存器中时,该数值用于触发异常返回机制。
EXC_RETURN 中的一些位用于提供异常流程的其他信息。EXC_RETURN 数值定义如下表所示:
EXC_RETURN 的合法值如下表所示:
由于 EXC_RETURN 的编码格式(最高 4 位固定为 0xF),所以中断是不能返回到0xF0000000~0xFFFFFFFF 这个地址区域的(否则会与 EXC_RETURN 冲突)。不过,由于这个地址范围在当前架构中本身就处于不可执行的系统区域,并不会存在需要返回到这个地址区域的情况,自然也就不会存在什么问题。