中断是嵌入式软件开发中很常见的事情,本文对ARM Cortex-M3/M4的异常/中断处理流程进行梳理

异常/中断的处理流程

首先弄明白什么是异常(exception)/(中断)interrupt,异常是处理器响应内部事件机制,而中断(interrupt)则是特指来自外部硬件的中断请求。

下文中所提到的异常代指异常/中断。

异常的核心流程可以分为以下三个部分:

  • 保护现场
  • 处理异常
  • 恢复现场

那么会有一个问题?现场是什么?如何保护?又如何恢复?

一、保护现场

首先我们需要明确一个概念:在ARM架构中,数据的处理(如加减乘除等操作)均在CPU寄存器中进行。CPU在处理内存数据时,会先通过LOAD机制将数据从内存加载到寄存器,在寄存器之间完成运算后,再将处理结果通过STR机制存回内存。

我们平时编写的程序,正是由无数这样的加载/存储操作构成的。可以设想这样一个场景:当你正在执行某个数据处理操作(比如对两个寄存器的值进行相加)时,异常突然发生,处理器不得不跳转去执行异常服务函数(ISR)。在这个过程中,CPU寄存器将被ISR使用。如果此时不采取任何措施,你原本存放在寄存器中的中间数据就会被覆盖。因此,在执行ISR之前,是不是得先将当前寄存器内容保存一下,这也就是我们说的保护现场。

那么我们怎么知道哪些寄存器需要保存呢?让我们先来看看CM3/4内核的寄存器

  • R0~R12:通用寄存器,前八个(R0~R7)称之为低寄存器,一些16位指令只能访问低寄存器。R8~R12称之为高寄存器,可以用于32位指令和几个16位指令。
  • R13:栈指针(SP),物理上存在两个栈指针,主栈指针(MSP)和线程栈指针(PSP)。一般的裸机程序中不管中断还是应用都只会用到MSP;而带有操作系统的应用中,在中断中使用MSP,在线程中使用PSP。
  • R14:链接寄存器(LR),用于保存函数/子程序调用结束时的返回地址。在函数/子程序调用完成时,可以将LR的值加载到PC寄存器中返回调用函数/子程序的位置继续执行,执行调用后,LR的值会自动更新。简单来说,当函数在调用其它函数/子程序时,会将LR保存到栈中防止丢失,当被调用的函数/子程序执行完成后可以回到原处继续执行。在异常执行时,LR会被更新为EXC_RETURN实现异常返回。
  • R15:程序计时器(PC),可读可写,读PC会返回当前指令地址+4,写PC会引发跳转操作。

图片来自CortexM3/M4权威手册

现在我们来了解一下上面这些寄存器的保存规则,根据ARM架构的C语言调用标准-AAPCS(ARM架构过程调用标准),约定了函数调用时这些寄存器的用途:

  • R0~R3 :用于调用者和被调用者之间进行参数传递(函数参数和返回值),如果被调用函数超过了4个,则使用压栈的方式传参
  • R4~R11:用于保存局部变量
  • R12、LR、xPSR:特殊状态寄存器

根据实际场景,上述寄存器又被分为了两部分:

  • 调用者来保存的寄存器:R0~R3、R12、LR、xPSR
  • 被调用者来保存的寄存器:R4~R11

什么意思呢?

假设函数A调用函数B,那么函数A肯定要知道R0~R3对于函数B而言,函数B可以随意的使用他们(例如传参的场景),那么保存它们的之前的值是函数A的责任,不可以指望函数B去帮助维护,R12、LR、PSR等这些寄存器也是同理。

而对于函数B而言,只需要保证函数A在调用之前之后看到的R4~R11都是一样的就行了。

异常也是一样的,现在异常就是函数B,我们自己的应用代码就是函数A。函数B能够保证R4~R11不会改变,我们只需要保存:

  • R0~R3、R12、LR、xPSR
  • PC

在ARM处理器中,上述的处理器会自动的进行压栈出栈操作,也就是说,异常进入前,这些寄存器会自动被保存,在异常退出时自动恢复。

二、异常处理

异常如何处理的呢?在这之前,我们需要加深一下印象,保存现场就是把关键的寄存器保存起来,那么保存在哪里?保存在栈上。栈又在内存的哪里呢?取决于MSP或者PSP。那么什么时候用PSP什么时候用MSP呢?这取决于操作模式。

  • 使用MSP:处理模式(异常中)或特权级线程模式(裸机场景的应用代码)
  • 使用PSP:线程模式(OS中的任务)

在将寄存器保存完成后,紧接着要做的事情就是取出异常的向量,将PC更换为异常的入口地址(ISR),将LR更换为EXEC_RETURN用于异常返回。

三、恢复现场

前文提到了EXEC_RETURN这个值,这里简单说明一下,当处理器进入到异常处理中时,LR寄存器会被更新为EXEC_RETURN,当将LR中的这个值写入到PC时会触发异常返回。

由上图可以看出,EXEC_RETURN所表示的是在异常进入前,“现场”的状态,处理器可以利用这个状态来恢复现场。

在异常处理中,当我们看到LR寄存器的第二位是0时,我们就知道进入异常前压栈前使用的栈是MSP;为1时压栈前使用的是PSP。

在看到第三位为0时,说明进入异常前处于处理模式(例如中断嵌套);为1时说明进入异常前处于线程模式,也就是我们自己编写的用户代码。

在异常结束时,使用 BX LR执行异常返回,PC在看到这个值是特殊的异常值时不会真的跳转到这个地址,而是会根据EXEC_RETURN的位域进行不同的处理。例如:

  • 进入异常前使用的MSP、线程模式。将MSP的值恢复到CPU寄存器(出栈),将模式配置为线程模式并从异常返回到用户程序。
  • 进入异常前使用的MSP、处理模式。将MSP的值恢复到CPU寄存器(出栈),将模式配置为处理模式并从异常返回到另一个异常。

当然,使用PSP的场景也是一样的,不过一般没有使用OS的场景不会用到PSP。

四、总结

根据前三小节的内容,异常/中断的处理流程大致如下:

  1. 异常产生且被处理器接受后,将当前所使用的R0~R3、R12、LR、xPSR以及PC压入栈中,如果当前处理器处于特权级线程模式/处理模式,使用MSP进行压栈;如果当前处理器处于线程模式,使用PSP压栈。与此同时取异常向量。
  2. 进入异常/中断服务程序后,将链接寄存器(LR)根据处理模式和所用栈(MSP和PSP)不同设置为对于的EXEC_RETURN值。在入口处根据自身服务程序所使用的变量,对R4~R11选择性的进行压栈,紧接着执行中断服务程序。
  3. 在中断服务程序执行完成后,首先根据自身情况恢复R4~R11,通过调用 BX LR 将LR的值写入PC,此时PC检测到EXEC_RETURN触发异常返回,根据EXEC_RETURN的值从对应的栈中恢复R0~R3、R12、LR、xPSR以及PC,并回到异常产生前的状态继续执行代码。