前言
中断,在单片机开发中再常见不过了。当然对于中断的原理和执行流程都了然于胸,那么对于ARM单片机中断的具体处理行为,你真的搞清楚了吗?
今天来简单聊一聊,ARM单片机中断处理过程中的具体行为是什么样的,搞清楚了这些,让你彻底理解中断是如何执行的。
掌握了这些内容后,以后在开发过程中遇到中断问题,可以做到游刃有余。
本篇文章主要梳理一下 Cortex-M3 内核的单片机在处理中断事件的具体行为,以及不同的中断是如何处理的。
中断响应
Cortex-M3 单片机在开始响应一个中断时,会进行以下三个操作:
- 寄存器入栈,将寄存器的值压入栈
- 取向量:从向量表中找出对应的服务程序入口地址
- 选择堆栈指针 MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC
响应中断的第一个动作,就是自动保存现场的必要部分:依次把 xPSR, PC, LR, R12 以及 R3-R0 由硬件自动压入适当的堆栈中。
当响应异常时,当前的代码正在使用 PSP,则压入 PSP,也就是使用进程堆栈;否则就压入 MSP,使用主堆栈。一旦进入了服务例程,就将一直使用主堆栈。
入栈顺序以及入栈后堆栈中的内容,如下图所示。在自动入栈的过程中,把寄存器写入堆栈内存的时间顺序,并不是与写入的空间顺序相对应的。但是机器会保证:正确的寄存器将被保存到正确的位置 。
先把PC与 xPSR 的值保存,就可以更早地启动服务例程指令的预取——因为这需要修改PC;同时,也做到了在早期就可以更新 xPSR 中 IPSR 位段的值。
取出中断服务例程地址,从中断向量表中找出正确的异常向量,然后在服务程序的入口处预取指。这部分由指令总线(I-Code总线)完成。
在入栈和取向量操作完成之后,执行中断服务例程之前,还要更新一系列的寄存器:
- SP:在入栈后会把堆栈指针(PSP 或 MSP)更新到新的位置。在执行服务例程时,将由 MSP 负责对堆栈的访问。
- PSR:更新 IPSR 位段(PSR的最低部分)的值为新响应的异常编号。
- PC:在取向量完成后, PC将指向服务例程的入口地址。
- LR:在出入 ISR 的时候, LR 的值将重新诠释为 “EXC_RETURN”。在异常进入时由系统计算并赋给 LR,并在异常返回时使用它。(后面会讲解 EXC_RETURN)
以上是在响应异常时通用寄存器的变化。另一方面,在 NVIC 中,也会更新若干个相关有寄存器。
在完成以上工作之后,系统开始执行中断服务程序里的指令。当指令执行完毕,进入中断返回处理阶段。
中断返回
当异常服务例程执行完毕后,需要做一个“异常返回”动作,从而恢复先前的系统状态,才能使被中断的程序得以继续执行 。触发中断返回的指令:
有些处理器会使用特殊的返回指令来标示中断返回,例如 8051 就使用 reti。但是在 CM3 中,是通过向 PC 中写入 EXC_RETURN
来识别返回动作的。
在进行中断返回操作后,会进行下面的处理:
- 出栈:先前压入栈中的寄存器在这里恢复。内部的出栈顺序与入栈时的相对应,堆栈指针的值也改回先前的值。
- 更新 NVIC 寄存器:伴随着异常的返回,它的活动位也被硬件清除。对于外部中断,倘若中断输入再次被置为有效,悬起位也将再次置位,新一次的中断响应序列也可随之再次开始。
中断返回值
前面已经讲到,在进入异常服务程序后,将自动更新 LR 的值为特殊的 EXC_RETURN
。这是一个高 28 位全为1的值,只有[3:0] 的值有特殊含义:
当中断服务例程把这个值送往 PC 时,就会启动处理器的中断返回操作。因为 LR 的值是由 CM3 自动设置的,所以只要没有特殊需求,就不要改动它。
总结一下上表,可以得到,合法的 EXC_RETURN 值共3个:
如果主程序在线程模式下运行,并且在使用 MSP 时被中断,则在服务例程中 LR=0xFFFF_FFF9(主程序被打断前的 LR 已被自动入栈)。
如果主程序在线程模式下运行,并且在使用 PSP 时被中断,则在服务例程中 LR=0xFFFF_FFFD(主程序被打断前的 LR 已被自动入栈) 。
这样描述可能比较抽象,不好理解。那就通过两张图来直观感受一下。
主程序运行在线程模式,且使用主堆栈,进入中断后,以及有中断嵌套情况下,模式切换和 LR 的变化如下图。
如果主程序在 Handler 模式下运行,则在服务例程中 LR = 0xFFFF_FFF1(主程序被打断前的LR已被自动入栈)。这时的所谓“主程序”,其实更可能是被抢占的服务例程。事实上,在嵌套时,更深层 ISR 所看到的 LR 总是 0xFFFF_FFF1。
主程序运行在线程模式,且使用进程堆栈的情况下,LR 值的变化如下
通过这两张图,可以很好地理解异常返回值的变化情况。
资料直通车:Linux内核源码技术学习路线+视频教程内核源码
学习直通车:Linux内核源码内存调优文件系统进程管理设备驱动/网络协议栈
中断嵌套
Cortex-M3 内核单片机支持中断嵌套,即高优先级中断可以抢占低优先级去执行指令。我们要根据实际使用情况,为每个中断建立适当的优先级。
NVIC 和 CM3 处理器会根据优先级的设置来控制抢占与嵌套行为。有了自动入栈和出栈,我们不用担心在中断发生嵌套时,会使寄存器的数据损毁。
我们知道,所有服务例程都只使用主堆栈(MSP)。所以当中断嵌套加深时,对主堆栈的压力会增大:每嵌套一级,就至少再需要8个字,即32字节的堆栈空间(这没算上 ISR 对堆栈的额外需求),并且何时嵌套多少级也是不可预料的。
如果主堆栈的容量本来就已经所剩无几了,中断嵌套又突然加深,则主堆栈有溢出的凶险。堆栈溢出是很致命的,新入栈数据会覆盖掉主堆栈前面的数据,数据遭到了破坏。
若在服务例程返回前混迭区的数据又被更改了,则在执行中断返回后,系统极可能功能紊乱,甚至出现程序跑飞的问题。
要注意的,相同的异常(中断)是不允许重入的。因为每个异常都有自己的优先级,并且在异常处理期间,同级或低优先级的异常是要阻塞的。
因此对于同一个异常,只有在上次实例的服务例程执行完毕后,方可继续响应新的请求。因此,在 SVC 服务例程中,就不得再使用SVC指令,否则将产生 fault 现象。
咬尾中断
Cortex-M3 内核为了缩短中断延迟,新增了 “咬尾中断” 机制。
当处理器在响应某个中断时,如果又发生低优先级或者相同优先级中断,则被阻塞。在当前的中断执行返回后,系统处理悬起的中断时,不再先POP,然后又把 POP 出来的内容PUSH回去;而是继续使用上一个中断已经 PUSH 好的成果。
这么一来,看上去好像后一个中断把前一个中断的尾巴咬掉了,前前后后只执行了一次 入栈/出栈 操作。于是,这两个异常之间的“时间沟”变窄了很多。
晚到中断
CM3 的中断处理还有另一个机制,这就是“晚到的异常处理”。
当 CM3 对某异常的响应序列还处在早期:入栈的阶段,尚未执行其服务例程时,如果此时收到了高优先级异常的请求,则本次入栈就成了为高优先级中断所做的了。
入栈后,将执行高优先级异常的服务例程。可见,它虽然来晚了,却还是因优先级高而优先执行。
比如,若在响应某低优先级 异常#1 的早期,检测到了高优先级 异常#2,则只要 #2 没有太晚,就能以“晚到中断”的方式处理:在入栈完毕后执行ISR #2。
如果 异常#2 来得太晚,以至于已经执行了 ISR #1 的指令,则按普通的抢占处理,这会需要更多的处理器时间和额外的堆栈空间。
在 ISR #2 执行完毕后,则以“咬尾中断”方式,来启动 ISR #1 的执行。
原文作者:【 一起学嵌入式】