深入了解异常处理

一、简介

1.1 C实现的异常处理

对于cortex-m处理器,可以将异常处理或ISR实现为普通的C程序/函数,为了深入了解这种机制,我们先来看一下C函数在ARM架构上是如何工作的。

用ARM架构的C编译器遵循ARM的一个名为AAPCS的规范。根据这份标准,C函数可以修改R0R3、R12、R14(LR)以及PSR。若C函数需要使用R4R11,就应该将这些寄存器保存到栈空间中,并且在函数结束前将它们恢复,如下图所示:
深入了解异常处理_第1张图片
R0~R3、R12、LR以及PSR被称作调用者保存寄存器,若在函数调用后还需要使用这些寄存器的数值,在进行调用前,调用子程序的程序代码需要将这些寄存器的内容保存到内存中(如栈)。函数调用后不需要使用的寄存器数值则不用保存。

R4~R11为调用者保存寄存器,被调用的子程序或函数需要确保这些寄存器在函数结束时不会发生变化(与进入函数时的数值一样)。这些寄存器的数值可能会在函数执行过程中变化,不过需要在函数退出前将它们恢复为初始值。

若cortex-m处理器具有浮点单元,则浮点单元中的寄存器也有类似的需求:

S0~S15为调用者保存寄存器。

S16~S31为被调用者保存寄存器。

一般来说,函数调用将R0R3作为输入参数,R0则用作返回结果。若返回值为64位,则R1也会用于返回结果。要使C函数可以用作异常处理,异常机制需要在异常入口处自动保存R0R3、R12、LR以及PSR,并在异常退出时将它们恢复,这些都要由处理器硬件控制。这样,当返回到被中断的程序时,所有寄存器的数值都会和进入中断时相同。另外,与普通的C函数调用不同,返回地址PC的数值并没有存在在LR中(异常机制在进入异常时将EXC_RETURN代码放入了LR中,该数值将会在异常返回时用到),因此,异常流程也需要将返回地址保存。这样对于CORTEX-M3或不具有浮点单元的CORTEX-M4处理器,需要在异常处理期间保存的寄存器共有8个。对于具有浮点单元的cortex-m4处理器,若用到了浮点单元,则异常机制还需要保存S0 ~ S15 及FPSCR。CONTROL寄存器中FPCA位表示这一操作的执行情况。

1.2 栈帧

​ 在异常入口处被压入栈空间的数据块为栈帧。对于cortex-M3或不具有浮点单元的cortex-m4处理器,栈帧都是8个字大小,对于具有浮点单元的cortex-m4,栈帧则可能是8或26个字。

AAPCS的另外一个要求为,栈指针的数值在函数入口和出口处应该是双字对齐的。因此,若在中断产生时栈帧未对齐到双字地址上,Cortex-m3和Cortex-m4处理器会自动插入一个字。这样可以保证栈指针位于异常处理的开始处。双字栈对齐特性是可编程的,若异常未完全符合AAPCS,则可以关闭该特性。

压栈的xPSR中的第9位表示栈指针的数值是否调整过。在下图中,栈指针为双字对齐的,因此就不会额外插入一个字,而且xPSR的第9位也为0。在双字栈对齐特性被关闭后,栈帧还是会保持这种方式的,只是栈指针的数值有可能未对齐到双字地址。
深入了解异常处理_第2张图片
若使能了双字栈对齐特性,而且栈指针的数值未对齐到双字边界上,栈中会被插入一段空间,栈指针也会被强制对齐到双字地址,而且压栈的xPSR的第9位被置1,表明引入了一段区域,如下图所示:
深入了解异常处理_第3张图片
压栈的xPSR的第9位在异常退出流程中用于确认是否需要调整SP的数值。对于具有浮点单元的Cortex-M4,若浮点单元以使能且使用,栈帧还会包括浮点单元寄存器组中的S0~S15,如下图所示:
深入了解异常处理_第4张图片
通用目的寄存器R0~R3的数值位于栈帧底部,可以很方便地由SP相关寻址访问。有些情况下,这些压栈的寄存器可用于往软件触发中断或SVC服务传递信息。可以通过系统控制块中配置控制寄存器的一个位来使能双字栈对齐特性。可以在初始化期间利用下面的C代码使能该特性。

SCB->CCR |= SCB_CCR_STKALIGN_Msk; //设置CCR中的STKALIGN位(第9位)

双字栈对齐为:
①cortex-m4处理器中默认使能。
②cortex-m3 r2p0及之后版本默认使能;
③cortex-m3 r1p0和r1p1默认禁止;
④cortex-m3 r0p0中不可用
强烈建议用户使能本特性,尽管它会稍微加大栈空间的使用。异常处理内部不应修改该位。

1.3 EXC_RETURN

处理器进入异常处理或中断服务程序时,链接寄存器LR的数值会被更新为EXC_RETURN数值。当利用BX、POP或存储器加载指令(LDR或LDM)被加载到程序寄存器中时,该数值用于触发异常返回机制。EXC_RETURN中的一些位用于提高异常流程的其他信息。EXC_RETURN数值的定义如下表所示。

描述 数值
31:28 EXC_RETURN指示 0XF
27:5 保留(全为1) 0xEFFFFF(23位都是1)
4 栈帧类型 1(8字)或0(26字)。当浮点单元不可用时总是为1,在进入异常处理时,其会被置为CONTROL寄存器的FPCA位
3 返回模式 1(返回线程)或0(返回处理)
2 返回栈 1(返回线程栈)或0(返回处理)
1 保留 0
0 保留 1

由于EXC_RETURN的编码格式,在地址区域0xF0000000 ~ 0xFFFFFFFF中是无法执行中断返回的。不过,由于系统空间中的地址区域已经被架构定义为不可执行的,因此,这样不会带来什么问题。EXC_RETURN的合法值如下:

浮点单元在中断前使用(FPCA=1) 浮点单元未在中断前使用(FPCA=0)
返回处理模式(总是使用主栈) 0xFFFFFFE1 0xFFFFFFF1
返回线程模式并在返回后使用主栈 0xFFFFFFE9 0xFFFFFFF9
返回处理模式并在返回后使用进程栈 0xFFFFFFED 0xFFFFFFFD

二、异常流程

2.1异常进入和压栈

当异常产生且被处理器接受时,压栈流程会将寄存器压入栈中并组织栈帧,如下图所示:
深入了解异常处理_第5张图片
cortex-m3/4处理器具有多个总线接口。在压栈操作的同时处理器还可以开始取向量(一般通过I-CODE总线)和取指。这样,由于压栈操作可以和Flash存储器访问同时进行,哈佛总线架构可以降低中断等待时间。若向量表位于SRAM或异常也位于SRAM,中断等待会稍微增加。需要注意的是,压栈期间的栈访问顺序和栈帧中寄存器的顺序不同。例如,cortex-m3会在其他寄存器前首先将PC和xPSR压栈,这样在取向量时会尽快更新PC。由于AHB Lite接口的流水线特性,数据传输会至少落后地址一个时钟周期。如下图所示:
深入了解异常处理_第6张图片
压栈操作中用的栈可以为主栈,也可以为进程栈。若处理器运行在线程模式且使用MSP(CONTROL寄存器的第0位为0,默认配置),则压栈操作在执行时使用主栈MSP,如下图所示:
深入了解异常处理_第7张图片
若处理器运行在线程模式且使用进程栈,则严重操作执行时使用进程栈。在进入处理模式后,处理器必须使用MSP,所有嵌套中断的压栈操作执行时都使用主栈SP,如下图所示:
深入了解异常处理_第8张图片

2.2 异常返回和出栈

在异常处理结束时,异常入口处生成的EXC_RETURN数值的第2位用于确定提取栈帧时所用的栈指针。若第2位为0,则处理器会知道之前压栈时使用的是主栈,如下图所示:
深入了解异常处理_第9张图片
若第2位为1,处理器就会知道压栈时使用的是进程栈,间下图所示:
深入了解异常处理_第10张图片
在每次出栈操作结束时,处理器还会检查出栈xPSR数值的第9位,并且若压栈时插入了额外的空间则会将其去除,如下图所示:
深入了解异常处理_第11张图片
为了降低出栈所需的时间,处理器会首先取出返回地址(压栈的PC),因此取指可以和剩下的出栈操作同时进行。

三、中断等待和异常处理优化

3.1什么是中断等待

中断等待表示从中断请求开始到中断处理开始执行时间的时间。对于cortex-m3和cortex-m4处理器,若中断系统为零等待的,而且假定系统设计允许取向量和压栈同时执行,则中断等待为12个周期,其中包括寄存器压栈、取向量以及取中断处理的指令。不过,许多情况下,由于处理器系统中的等待状态,中断等待时间可能会更大。若处理器在执行一次包括缓冲写操作在内的存储器传输,传输要在异常流程开始前完成。执行流程的持续时间还要取决于存储器访问速度。除了存储器设备或外设产生的等待状态外,其他情况也可能会加大中断等待时间:

①处理器正处理另外一个相同或更高优先级的异常

②调试器访问存储器系统。

③存储器正执行非对齐传输。从处理器的角度来看,它可能是单次传输,不过由于总线接口需要将非对齐传输转换为多个对齐传输,从总线等级来看它可能会占用几个周期

④处理器正在执行对位段别名的写操作。内部总线系统会将其转换为读—修改—写流程,这样会花费两个周期。

⑤CORTEX-M3/4处理器使用多种方式来降低中断处理的等待时间。例如,嵌套中断处理等多种操作都是由处理器硬件自动处理的。另外,还需要利用软件代码来确定要服务的中断或定位ISR的起始地址。

3.2 多周期指令执行时的中断

有些指令需要执行多个时钟周期。若在处理器执行多周期指令(如整数除法)时产生了中断请求,该指令可能会被丢弃且在中断处理结束后重新执行。这种设计还适用于加载双字(LDRD)和存储双字(STRD)指令。

另外,CORTEX-M3/4处理器允许中断在多加载和多存储(LDM/STM)以及压栈和出栈指令执行过程中产生。若在中断产生时LDM/STM/PUSH/POP中的一个指令正在执行,当前的存储器访问会结束,且下一个寄存器编号会被存放在压栈的xPSR中(中断继续指令[ICI]位)。异常处理结束后,多加载/存储/压栈/出栈会从传输停止的位置继续执行。这种方式还适用于具有浮点单元的CORTEX-M4处理器的浮点存储器访问指令(如VLDM、VSTM、VPUSH和VPOP)。另外还有一个边界情况:若被打断的多加载/存储/压栈/出栈指令为IF-THEN指令块的一部分,则该指令会被取消且等中断结束后重新执行,这是因为ICI位和IT执行状态位共用执行程序状态寄存器(EPSR)中相同的空间。对于具有浮点单元的cortex-m4处理器,若处理器执行VSQRT(浮点平方根)或VDIV(浮点除法)时产生了中断请求,浮点单元的执行会和压栈操作同步进行。

3.3 末尾连锁

若某个异常产生时处理器正在处理另一个具有相同或更高优先级的异常,该异常就会进入挂起状态。在处理器执行完当前的异常处理后,它可以继续执行挂起的异常/中断请求。处理器不会从栈中恢复寄存器(出栈)然后再将它们存入栈中(压栈),而是跳过出栈和压栈过程并会尽快进入挂起异常的异常处理。这样,两个异常处理间隔的时间就会降低很多。对于无等待状态的存储器系统,末尾连锁的中断等待时间仅为6个时钟周期。末尾连锁优化还给处理器带来了更佳的能耗效率,这是因为栈存储器访问的总数少了,而每次存储器传输都会消耗能量。
深入了解异常处理_第12张图片

3.4 延迟到达

当异常产生时,处理器会接受异常请求并开始压栈操作。若在压栈操作期间产生了另外一个更高优先级的异常,则更高优先级的后到异常会首先得到服务。例如,若异常#1(低优先级)在异常#2(高优先级)几个周期前产生,处理器的执行情况如下图所示,处理#2会在压栈结束时尽快执行。
深入了解异常处理_第13张图片

3.5 出栈抢占

若某个异常请求在另一个刚完成的异常处理出栈期间产生,处理器会舍弃出栈操作且开始取向量以及下一个异常服务的指令。该优化被称作出栈抢占,如下图所示:
深入了解异常处理_第14张图片

3.6惰性压栈

惰性压栈是和浮点单元寄存器压栈相关的一种特性,因此它只同具有浮点单元的cortex-m4设备有关。cortex-m3和不具备浮点单元的cortex-m4则不需要该特性。若浮点单元存在且已使能,在其被使用时,浮点单元的寄存器组中的寄存器可能会包含需要保存的数据。若要将每个异常所需的浮点单元寄存器压栈,则每次都需要额外执行17次存储器压栈操作,这样会将中断等待时间增加12~29个周期。为了减少中断等待时间,cortex-m4处理器实现了一种名为惰性压栈的特性,该特性默认使能。若浮点单元使能且使用(CONTROL寄存器中的FPCA位)的情况下产生了异常,则栈帧的长度会增加。不过,这些浮点寄存器的数值实际上是不会写入栈帧中的,惰性压栈机制只会为这些寄存器保留一定的栈空间,不过只有R0 ~ R3 、 R12、LR、返回地址和xPSR被压栈。这样,中断等待时间还会保持12个时钟周期。当出现惰性压栈时,一个名为LSPACT(惰性压栈保持活跃)的寄存器被置位且浮点单元上下文地址寄存器则存放浮点寄存器预留栈空间的地址。若异常处理不需要任何浮点运算,浮点单元寄存器在异常处理期间会保持不变,而且不会在异常退出时恢复。若异常处理需要浮点运算,处理器检测到冲突后会停止处理器,将浮点寄存器存到保留栈空间并清除LSPACT,接下来异常处理会继续执行。这样浮点单元寄存器只会在必要时压栈。

​ 惰性压栈操作可能会被打断,当惰性压栈期间产生了中断请求,则惰性压栈操作会停止,取而代之的是普通压栈开始执行。由于触发了惰性压栈的浮点指令还没有执行,压入栈中的PC值会指向那条浮点指令。当中断服务结束时,异常会返回到浮点指令,而且重新执行这条指令会再次触发惰性压栈操作。若当前执行上下文未使用浮点单元,则FPCA为0,且栈帧会使用较短的形式。

备注:参考ARM Cortex-M3与Cortex-M4权威指南

你可能感兴趣的:(cortex-m处理器,单片机,stm32,嵌入式硬件)