1、中断/异常的响应序列
当CM3开始响应一个中断时,会在它小小的体内奔涌起三股暗流: 入栈:把8个寄存器的值压入栈 取向量:从向量表中找出对应的服务程序入口地址 选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC。
1.1 入栈
响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR, PC,LR, R12以及R3-R0由硬件自动压入适当的堆栈中:如果当响应异常时,当前的代码正在使用PSP,则压入PSP,也就是使用进程堆栈;否则就压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用主堆栈。
假设入栈开始时,SP的值为N,则在入栈后,堆栈内部的变化如表9.1表示。又因为AHB接口上的流水线操作本性,地址和数据都在经过一个流水线周期之后才进入。另外,在自动入栈的过程中,把寄存器写入堆栈内存的时间顺序,并不是与写入的空间顺序相对应的。但是机器会保证:正确的寄存器将被保存到正确的位置。
CM3在看不见的内部“搅浑”了入栈的顺序,这是有深层次的原因的。先把PC与xPSR的值保存,就可以更早地启动服务例程指令的预取——因为这需要修改PC;同时,也做到了在早期就可以更新xPSR中IPSR位段的值。
CM3内核自动保存R12以及R3-R0,不自动保存R4-R11,是因为在ARM上有一套C函数的调用标准《C/C++ Procedure Call Standard for the ARM Architecture》。有了这个标准,它使得中断服务例程能用C语言编写,编译器优先使用入栈了的寄存器来保存中间结果。如果程序过大也可能要用到R4-R11,此时编译器负责生成代码来push它们。但是,ISR应该短小精悍,不要让系统如此操心。R0-R3, R12是最后被压进去的,为的是可以更容易地使用SP基址来索引寻址,这也方便了LDM等多重加载指令。因为LDM必须加载地址连续的一串数据,而现在R0-R3, 这种顺序也舒展了参数的传递过程:使之可以方便地通过读取入栈了的R0-R3取出R12的存储地址连续了(主要为系统软件所利用,多见于SVC与PendSV中的参数传递)。
1.2 取向量
当数据总线(系统总线)正在进行入栈操作时,指令总线(I-Code总线)从向量表中找出正确的异常向量,然后在服务程序的入口处预取指。由此可以看到各自都有专用总线的好处:入栈与取指这两个工作能同时进行。
1.3 更新寄存器
在入栈和取向量操作完成之后,执行服务例程之前,还要更新一系列的寄存器:
SP:在入栈后会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程时,将由MSP负责对堆栈的访问。
PSR:更新IPSR位段(地处PSR的最低部分)的值为新响应的异常编号。
PC:在取向量完成后,PC将指向服务例程的入口地址,
LR:在出入ISR的时候,LR的值将得到重新的诠释,这种特殊的值称为“EXC_RETURN”,在异常进入时由系统计算并赋给LR,并在异常返回时使用它。EXC_RETURN的二进制值除了最低4位外全为1,而其最低4位则有另外的含义。
以上是在响应异常时通用寄存器的变化。另一方面,在NVIC中,也会更新若干个相关有寄存器。例如,新响应异常的悬起位将被清除,同时其活动位将被置位。
2、异常返回
当异常服务例程执行完毕后,需要很正式地做一个“异常返回”动作序列,从而恢复先前的系统状态,才能使被中断的程序得以继续执行。从形式上看,有3种途径可以触发异常返回序列,如图所示。而不管使用哪一种,都需要用到先前储到LR的EXC_RETURN。
有些处理器使用特殊的返回指令来标示中断返回,例如8051就使用reti。但是在CM3中,是通过把EXC_RETURN往PC里写来识别返回动作的。因此,可以使用上述的常规返回指令,从而为使用C语 言编写服务例程扫清了最后的障碍(无需特殊的编译器命令,如__interrupt)。
在启动了中断返回序列后,下述的处理就将进行:
1).出栈:先前压入栈中的寄存器在这里恢复。内部的出栈顺序与入栈时的相对应,堆栈指针的值也改回先前的值。
2).更新NVIC寄存器:伴随着异常的返回,它的活动位也被硬件清除。对于外部中断,倘若中断输入再次被置为有效,悬起位也将再次置位,新一次的中断响应序列也可随之再次开始。
3、 嵌套的中断
在CM3内核以及NVIC的深处,就已经内建了对中断嵌套的全力支持,根本无需使用汇编去写封皮代码(wrapper code)。事实上,我们要做的就只是为每个中断适当地建立优先级,不用再操心别的。表现在:
1)NVIC和CM3处理器会根据优先级的设置来控制抢占与嵌套行为。因此,在某个异常正在响应时,所有优先级不高于它的异常都不能抢占之,而且它自己也不能抢占自己。
2)有了自动入栈和出栈,就不用担心在中断发生嵌套时,会使寄存器的数据损毁,从而可以放心地执行服务例程。
有一件事需要特别注意:主堆栈容量的最小安全值。异常使用的是主堆栈,每嵌套一级,就至少再需要8个字,即32字节的堆栈空间,还不包括ISR对对战的额外需求,且嵌套多少级也很难预测,因此,适当增大主堆栈的栈空间大小有益于系统的正常运行。
另一个要注意的,是相同的异常是不允许重入的。因为每个异常都有自己的优先级,并且在异常处理期间,同级或低优先级的异常是要阻塞的。因此对于同一个异常,只有在上次实例的服务例程执行完毕后,方可继续响应新的请求。
4、 咬尾中断
CM3为缩短中断延迟做了很多努力,第一个要提的,就是新增的“咬尾中断”(Tail-Chaining)机制。当处理器在响应某异常时,如果又发生其它异常,但它们优先级不够高,则被阻塞。那么在当前的异常执行返回后,系统处理悬起的异常时,倘若还是先POP,然后又把POP出来的内容PUSH回去,将十分浪费时间。CM3会继续使用上一个异常已经PUSH好的成果,消灭了这种铺张浪费。这么一来,看上去好像后一个异常把前一个的尾巴咬掉了,前前后后只执行了一次入栈/出栈操作。于是,这两个异常之间的“时间沟”变窄了很多,
5、晚到(的高优先级)异常
CM3的中断处理还有另一个机制,它强调了优先级的作用,这就是“晚到的异常处理”。当CM3对某异常的响应序列还处在早期:入栈的阶段,尚未执行其服务例程时,如果此时收到了高优先级异常的请求,则本次入栈就成了为高优先级中断所做的了——入栈后,将执行高优先级异常的服务例程。
晚到异常的处理模式
6、异常返回值
在进入异常服务程序后,将自动更新LR的值为特殊的EXC_RETURN。这是一个高28位全为1的值,只有[3:0]的值有特殊含义。当异常服务例程把这个值送往PC时,就会启动处理器的中断返回序列。为LR的值是由CM3自动设置的,所以只要没有特殊需求,就不要改动它。EXC_RETURN的合法值共3个,如下:
合法的EXC_RETURN值共3个
如果主程序在线程模式下运行,并且在使用MSP时被中断,则在服务例程中LR=0xFFFF_FFF9(主程序被打断前的LR已被自动入栈)。
如果主程序在线程模式下运行,并且在使用PSP时被中断,则在服务例程中LR=0xFFFF_FFFD(主程序被打断前的LR已被自动入栈)。
LR的值在异常期间被设置为EXC_RETURN(线程模式使用主堆栈)
如果主程序在Handler模式下运行,则在服务例程中LR=0xFFFF_FFF1(主程序被打断前的LR已被自动入栈)。这时的所谓“主程序”,其实更可能是被抢占的服务例程。事实上,在嵌套时,更深层ISR所看到的LR总是0xFFFF_FFF1,如图所示。
LR的值在异常期间被设置为EXC_RETURN(线程模式使用进程堆栈)
由EXC_RETURN的格式可见,我们不能把0xFFFF_FFF0-0xFFFF_FFFF中的地址作为任何返回地址。其实也并不用担心会弄错,因为CM3已经把这个范围标记成“取指不可区”了。
7、中断延迟
在设计实时系统时,必须对中断延迟进行严肃和仔细地估算。在这里,中断延迟的定义是:从检测到某中断请求,到执行了其服务例程的第一条指令时,已经流逝了的时间。在CM3中,若存储器系统够快,且总线系统允许入栈与取指同时进行,同时该中断可以立即响应,则中断延迟是雷打不动的12周期(满足硬实时所要求的确定性)。在与时间赛跑的这12个周期里,处理器内部一直开足马力,进行了入栈、取向量、更新寄存器以及服务例程取指的一系列操作。但若存储器太慢以至于引入等待周期,或者还有其它因素,则会引入额外的延时
当处理咬尾中断时,省去了堆栈操作,因此切入新异常服务例程的耗时可以短至6周期。 有些指令需要较多的周期才能完成。它们是除法指令,双字传送指令LDRD/STRD以及多重数据传送指令(LDM/STM)。对于前两者,CM3将为了保证中断及时响应而取消它们的执行,待返回后重新开始,对于LDM/STM,则有另外的处理方式。因为它们不照前两者那么浑然一体——它们其实是一串LDR/STR的速度优化版。于是,为了加速中断的响应,CM3支持LDM/STM指令的中止和继续,就好像它们只是普通的一串LDR/STR一样。为了实现“指令撕裂与粘合”的目的,需要记录中断时数据传送的进程。为此,CM3在xPSR中开出若干个“ICI位”,记录下一个即将传送的寄存器是哪一个(LDM/STM在汇编时,都把寄存器号升序排序)。在服务例程返回后,xPSR被弹出,CM3再从ICI bits中获取当时LDM/STM执行的进度,从而可以继续传送。
在个别情况下还有一点限制:IF-THEN(IT)指令的执行也需要在xPSR中使用几个位,可它需要的位刚好与ICI位重合(类似C中的union)——ICI bits和IT条件都记录在EPSR中。所以,如果在IF-THEN中使用了LDM/STM,则不再记录LDM/STM的执行进度。但尽管如此,及时响应中断依然是首要任务。此时只好把LDM/STM取消,待中断返回后继续执行。
另外,如果在总线接口上还有未完成的(outstanding)数据传送,例如有一个带缓冲的写操作未完成,处理器也只能等待此传送完成。这是迫不得以的——只有这样,才能保证在发生了总线fault时,其服务例程能够安全地抢占其它程序。
当多个中断同时请求时,也会发生中断延迟,这表现在只有优先级最高的得到立即响应,所有其它的中断将被延迟。另外,在中断嵌套时,每个中断都会阻塞同级和低优先级的中断。最后,如果中断被掩蔽(也就是俗称的关中,在多任务系统下满地都有),则在掩蔽期间也会附加中断延迟。
8、异常响应期间的faults
Faults是运行时发生各种故障的表现,在中断响应期间的故障也不例外。中断响应的每一步骤都可以触发faults。
8.1 入栈期间
如果在入栈期间引起了总线fault,则本次入栈操作将被强行中止,并且把总线异常悬起或者在允许时立即响应。若除能了总线fault,则此次故障将成为“硬伤”——上访至硬fault。在总线fault被使能的情况下,如果它的优先级比正在响应的异常高,则抢占之,否则将悬起直到引起fault的异常执行完毕。这种情况被称为“入栈错误”(stacking error),由总线fault状态寄存器(BFSR,地址:0xE000_ED29)的STKERR位指示(位偏移:4)。 如果入栈操作引起MPU访问违例,则产生存储管理fault,并且必须能立即执行MemFault服务例程,否则将无条件上访成硬fault。在发生入栈时访问违例时,存储管理fault寄存器(MFSR,地址:0xE000_ED28)中的MSTKERR位(位偏移:4)被置位,用于指示该fault。入栈是自动完成的,因此不可能产生用法fault。
8.2 出栈期间
如果在中断返回时的出栈期间引起了总线fault,则本次出栈操作将被强行中止,并且把总线异常悬起或立即响应。若除能了总线fault,则此次故障将成为“硬伤”——上访至硬fault。其它情况下,只要总线fault的优先级比当前的高(也包括比当前最深嵌套的优先级高),则可以立即响应。这种情况称为“出栈错误”(unstacking error),由BFSR.3指示(UNSTKERR位)。 类似地,如果是因MPU访问违例造成的MemManage fault,由MFSR.3(MUNSTKERR)指示。且MemManage fault的服务例程必须能立即执行,否则无条件硬fault。
8.3 取向量期间
在取向量期间发生总线fault,这是非常罕见的一种情况,这也是最严重的,因此直接上硬fault(MPU的限制则管不着取向量操作)。这种情况,由硬fault状态寄存器(HFSR,地址:0xE000_ED2C)中的VECTTBL位(位偏移:1)来指示。
8.4 无效返回时
如果LR中的EXC_RETURN不是合法的值(合法值见表9.4,包括企图返回ARM状态),则引起用法fault。如果用法fault被除能,也上访成硬fault。此时,用法Fault状态寄存器(UFSR,地址:0xE000_ED2A)中的INVPC位(位偏移:2),或者是INVSTATE位(位偏移:1)置位。