-
中断一般是由硬件(如外设和外部输入引脚)产生的事件。
-
异常一般指CPU内部产生的打断。但是,也可以把中断称为一种异常
-
我们一般称为系统异常和外部中断。
中断管理
以STM32F10x_StdPeriph_Driver
为例,介绍和中断和异常相关的文件。
-
stm32f10x.h
:由ST提供,定义了各种外设寄存器基地址,各种外设初始化的结构体。 -
system_stm32f10x.c
:由ST提供,提供如SystemInit
的系统时钟初始化函数,由启动文件去调用。 -
system_stm32f10x.h
:ST提供。 -
misc.c
:由ST提供,是标准库的文件,提供了操作NVIC和Systick的函数,如NVIC的初始化。 -
core_cm3.c
:由ARM公司提供,包含内核设置的汇编函数。 -
core_cm3.h
:由ARM公司提供,包含了内核的一些寄存器基地址定义,寄存器位的设置,NVIC的访问函数。
Cortex-M处理器具有多个用于中断和异常管理的可编程寄存器,这些寄存器多数位于NVIC和系统控制块(SCB)中。实际上,SCB是作为NVIC的一部分实现的,不过CMSIS-Core将其寄存器定义在了单独的结构体中。
NVIC和SCB位于系统控制空间(SCS),地址从0xE000E000开始,大小为4KB。SCS中还有SysTick定时器、存储器保护单元(MPU)以及用于调试的寄存器等。该地址区域中基本上所有的寄存器都只能由运行在特权访问等级的代码访问。唯一的例外为软件触发中断寄存器(STIR),它可被设置为非特权模式访问。
中断优先级
对于Cortex-M处理器(包括ARMv6-M和ARMv7-M)异常是否能被处理器接受以及何时被处理器接受并执行异常处理,是由异常的优先级和处理器当前的优先级决定的。更高优先级的异常(优先级编号更小)可以抢占低优先级的异常(优先级编号更大),这就是异常/中断嵌套的情形。有些异常(复位、NMI和HardFault)具有固定的优先级,其优先级由负数表示,
-
Cortex-M3和Cortex-M4处理器在设计上具有3个固定的最高优先级以及256个可编程优先级(具有最多128个抢占等级,可编程优先级的实际数量由芯片设计商决定。
-
中断优先级由优先级寄存器控制,宽度为3~8位。优先级的减少是通过去除优先级配置寄存器的最低位(LSB)实现的。对于未实现的位,读出总是为0。
-
STM32F1的中断优先级寄存器是4位。
优先级分组
利用系统控制块(SCB)中一个名为优先级分组的配置寄存器,每个具有可编程优先级的优先级配置寄存器可被分为两部分。上半部分(左边的位)为抢占优先级,而下半部分则为子优先级(响应优先级)。
- 抢占优先级决定运行一个中断处理时能否产生另外一个中断。子优先级(响应优先级)只会用在具有两个相同分组优先级的异常同时产生的情形,此时,具有更高子优先级(数值更小)的异常会被首先处理。
- 若两个中断同时被确认,且它们具有相同的分组/抢占优先级和子优先级,则异常编号更小的中断的优先级更高(IRQ#0的优先级高于IRQ#1的)。
向量表重定位
向量表:当Cortex-M处理器接受了某异常请求后,处理器需要确定该异常处理(若为中断则是ISR)的起始地址。该信息位于存储器内的向量表中,向量表默认从地址0开始,向量地址则为异常编号乘4,如图所示。向量表一般被定义在微控制器供应商提供的启动代码中。
向量表重定位特性提供了一个名为SCB->向量表偏移寄存器(VTOR)的可编程寄存器。该寄存器将正在使用的存储器的起始地址定义为向量表。
在使用VTOR时,需要将向量表大小扩展为下一个2的整数次方,且新向量表的基地址必须要对齐到这个数值。
应用向量表重定位的情形:
- 使用bootloader时,需要将中断向量表定位到app中。
- 应用程序从外部设备加载到RAM中运行,需要修够重定位到新的向量表
- 动态修改。ROM中可能会有一个中断的多个处理实例,在应用的不同阶段之间进行切换。
中断输入和挂起
每个中断都有多个属性:
- 每个中断都可被禁止(默认)或使能。
- 每个中断都可被挂起(等待服务的请求)或解除挂起。
- 每个中断都可处于活跃(正在处理)或非活跃状态。
-
挂起状态的意思是,中断被置于一种等待处理器处理的状态。
- 中断的挂起状态被存储在NVIC的可编程寄存器中,当NVIC的中断输人被确认后,它就会引发该中断的挂起状态。即便中断请求被取消,挂起状态仍会为高。这样,NVIC可以处理脉冲中断请求。
-
当中断正被处理时,它就会处于活跃状态。注意在中断人口处,多个寄存器会被自动压入栈中,这也被称作压栈。同时,ISR的起始地址会被从向量表中取出。
- 在中断服务完成后,处理器会执行异常返回,之前自动压栈的寄存器会被恢复出来,而且被中断的程序也会继续执行。中断的活跃状态会被自动清除。
异常处理流程
异常进入流程
- 多个寄存器和返回地址被压入当前使用的栈。这样就可以将异常处理用普通C函数
实现。若处理器处于线程模式且正使用进程栈指针(PSP),则PSP指向的栈区域就会用于该压栈过程,否则就会使用主栈指针(MSP)指向的栈区域。- 取出异常向量(异常处理/ISR的起始地址)。为了减少等待时间,这一步可能会和压
栈操作并行执行。- 取出待执行异常处理的指令。在确定了异常处理的起始地址后,指令就会被取出。
- 更新多个NVIC寄存器和内核寄存器,其中包括挂起状态和异常的活跃状态,处理器
内核中的寄存器包括程序状态寄存器(PSR)、链接寄存器(LR)、程序计数器(PC)以及栈指针(SP).
根据压栈时实际使用的栈,在异常处理开始前,MSP或PSP的数值会相应地被自动调整。PC也会被更新为异常处理的起始地址,而链接寄存器(LR)则会被更新为名为EXC_RETURN的特殊值。该数值为32位,且高27位为1。低5位中有些部分用于保存异常流程的状态信息(如压栈时使用的哪个栈)。该数值用于异常返回。
执行异常处理
栈操作使用主栈指针(MSP)
处理器运行在特权访问等级
如更高优先级的异常产生,则会抢占当前的异常处理,这是异常嵌套。当相同或更低优先级的异常产生,则处于挂起状态,等待当前处理完成后才会得到处理。
在异常处理的结尾,程序代码执行的返回会引起EXC_RETURN数值被加载到程序计数器中(PC),并触发异常返回机制。
异常返回
由于使用了EXC_RETURN数值触发异常返回,异常处理(包括中断服务程序)就可以和
普通的C函数/子例程一样实现。在生成代码时,C编译器将LR中的EXC_RETURN数值
作为普通返回地址处理。由于EXC_RETURN机制,函数一般不会返回到地址0xFO000000~0xFFFFFFFF。
EXC_RETURN
处理器进入异常处理或中断服务程序(ISR)时,链接寄存器(LR)的数值会被更新为EXC_RETURN数值。当利用BX、POP或存储器加载指令(LDR或LDM)被加载到程序寄存器中时,该数值用于触发异常返回机制。EXC_RETURN中的一些位用于提高异常流程的其他信息。
思考:为什么异常处理可以用C语言函数实现?
对于ARM Cortex-M处理器,异常返回机制由一个特殊的地址EXC_RETURN触发,该数值在异常入口处产生且被存储在链接寄存器(LR)中。当该数值由某个允许的异常返回指令写入PC时,它就会触发异常返回流程。
EXC_RETURN机制的设计允许程序员像编写普通函数一样编写中断服务程序,这是因为它提供了一种从中断返回的标准化方法,使得中断服务程序(ISR)的编写和调用流程与普通函数的调用和返回流程非常相似。
- 自动化的上下文保存和恢复:当异常发生时,Cortex-M3处理器会自动将必要的寄存器(如R0-R3、R12、LR、PC以及程序状态寄存器)保存到栈中。异常处理完毕后,这些寄存器的值会自动从栈中恢复,这使得程序员可以使用C语言编写异常处理程序而不需要手动管理寄存器的保存和恢复。
- EXC_RETURN机制:在进入异常处理函数之前,链接寄存器(LR)会被赋予一个特殊的值EXC_RETURN。当中断或异常处理程序执行完毕后,处理器会将LR中的EXC_RETURN值加载到程序计数器(PC)中,触发异常返回序列。这个序列会根据EXC_RETURN的值来恢复之前保存在堆栈中的寄存器值,包括PC、LR以及其他必要寄存器,从而允许异常处理程序像普通C函数一样返回。
- 编程模型:Cortex-M3处理器的编程模型允许在C语言中使用一组标准的寄存器和调用约定,这使得C语言编写的函数能够遵循与汇编语言相同的约定,从而在异常处理中使用C语言成为可能。
- 处理器和编译器的支持:Cortex-M3处理器的架构和编译器的支持使得C语言编写的异常处理程序可以得到高效的执行。编译器能够生成符合Cortex-M3架构要求的目标代码,包括自动的寄存器保存和恢复指令。
中断控制用的NVIC寄存器
- 中断设置使能寄存器:ISER,写1设置使能
- 中断清除使能寄存器:ICER,写1清除使能
- 中断设置挂起寄存器:ISPR,写1设置挂起状态
- 中断清除挂起寄存器:ICPR,写1清除挂起状态
- 中断活跃位寄存器:IABR,活跃状态位,只读
- 中断优先级寄存器:IP,每个中断都有一个中断优先级寄存器
- 软件触发中断寄存器:STIP,写中断编号设置相应的挂起状态
除了软件触发中断寄存器(STIR)外,所有这些寄存器都只能在特权等级访问。STIR默认只能在特权等级访问,不过可以配置为非特权等级访问。
用于异常或中断屏蔽的特殊寄存器
PRIMASK
在许多应用中,可能都需要暂时禁止所有中断以执行一些时序关键的任务,此时可以使用
PRIMASK寄存器。PRIMASK寄存器只能在特权状态访问。PRIMASK用于禁止除NMI和HardFault外的所有异常,它实际上是将当前优先级改为0(最高的可编程等级)。
FAULTMASK
从行为来说,FAULTMASK和PRIMASK很类似,只是它实际上会将当前优先级修改为-1,这样甚至是HardFault处理也会被屏蔽。当FAULTMASK置位时,只有NMI异常处理才能执行。
BASEPRI
有些情况下,可能只想禁止优先级低于某特定等级的中断,此时,就可以使用BASEPRI寄存器。要实现这个目的,只需简单地将所需的屏蔽优先级写入BASEPRI寄存器。例如,若要屏敲优先级小于等于0x60的所有异常,则可以将这个数值写入BASEPRI。
BASEPRI无法在非特权状态设置。
设置中断的步骤
- 设置优先级分组。优先级分组默认为0(优先级寄存器中只有第0位用于子优先级),这一步是可选的。
- 设置中断的优先级。中断的优先级默认为0(最高的可编程优先级),这一步也是可选的。
- 在NVIC或外设中使能中断。
若存在大量的嵌套中断,除了使能中断外,还应该确保栈空间足够。由于在处理模式中,中断处理总是使用主栈指针(MSP),主栈应该有足够应对最坏情况的栈空间(最大数量的嵌套中断/异常)。计算栈空间时应该考虑中断处理使用的栈以及每级栈帧使用的栈。
软件中断
可以利用软件代码触发异常或中断,之所以要这么做,最常见的原因为,允许多任务环境中处于非特权状态的应用任务,可以访问一些需要在特权状态下才能执行的系统服务。根据要触发的异常或中断的类型,应该使用不同的方法。
-
若要触发一个中断(异常类型16或之上),最简单的方法为使用CMSIS-Core函数NVIC_SetPendingIRQ:
-
若要触发SVC异常,则需要执行SVC指令。
思考:1.软中断有什么用处?
软中断(Software Interrupt,也称为SWI)通常用于操作系统中的系统调用(system call)。它允许用户态的程序请求内核态的服务。例如,在嵌入式系统中,软中断可以用于请求操作系统的服务,如文件操作、内存分配或其他需要内核权限的操作。此外,软中断也可以用于调试目的,比如在某些系统中,它们可以用来触发看门狗定时器或其他诊断功能。
思考:2.除了调试过程中可以使用软中断,还有什么时候可以使用软中断?
- 系统调用:在操作系统中,软中断常用于处理系统调用,使得用户态程序能够请求内核态的服务。
- 任务调度:在实时操作系统中,软中断可以用于任务调度,允许任务之间进行上下文切换。
- 触发看门狗定时器:在某些系统中,软中断可以用于定期触发看门狗定时器,防止系统因为死锁而停止响应。
- 内存管理:在某些嵌入式系统中,软中断可以用于内存管理操作,如动态内存分配和释放。
- 设备驱动:软中断可以用于设备驱动程序中,以便在特定条件下通知操作系统。
- 异常处理:在某些情况下,软中断可以用于处理特定的异常情况,比如硬件故障或系统错误。
软中断提供了一种灵活的方式来处理各种系统级任务,而不需要直接干预硬件中断,这有助于提高系统的可维护性和可扩展性。
深入了解异常处理
对于Cortex-M处理器,可以将异常处理或中断服务程序(ISR)实现为普通的C程序/函数
用于ARM架构的C编译器遵循ARM的一个名为AAPCS(ARM架构过程调用标准,参考文献13)的规范。根据这份标准,C函数可以修改R0~R3、R12、R14(LR)以及PSR。若C函数需要使用R4~R11,就应该将这些寄存器保存到栈空间中,并且在函数结束前将它们恢复。
R0~R3、R12、LR以及PSR被称作“调用者保存寄存器”,若在函数调用后还需要使用这些寄存器的数值,在进行调用前,调用子程序的程序代码需要将这些寄存器的内容保存到内存中(如栈)。这些由处理器硬件完成。
R4~R11为“被调用者保存寄存器”,被调用的子程序或函数需要确保这些寄存器在函数结束时不会发生变化(与进入函数时的数值一样)。这些寄存器的数值可能会在函数执行过程中变化,不过需要在函数退出前将它们恢复为初始值。这些需要手动去保存。
栈帧
在异常入口处被压入栈空间的数据块为栈帧。对于Cortex-M3或不具有浮点单元的Cortex-M4处理器,栈帧都是8个字大小的,对于具有浮点单元的Cortex-M4,栈帧则可能是8或26个字。
AAPCS的另外一个要求为,栈指针的数值在函数入口和出口处应该是双字对齐的。因此,若在中断产生时栈帧未对齐到双字地址上,Cortex-M3和Cortex-M4处理器会自动插人一个字。这样,可以保证栈指针位于异常处理的开始处。“双字栈对齐”特性是可编程的,若异常未完全符合AAPCS,则可以将该特性关闭。
中断等待
中断等待表示从中断请求开始到中断处理开始执行间的时间。对于Cortex-M3和Cortex-M4处理器,若中断系统为零等待的,而且假定系统设计允许取向量和压栈同时执行,则中断等待为12个周期,其中包括寄存器压栈、取向量以及取中断处理的指令。
除了存储器设备或外设产生的等待状态外,其他情况也可能会加大中断等待时间:
- 处理器正处理另外一个相同或更高优先级的异常。
- 调试器访问存储器系统。
- 处理器正执行非对齐传输。从处理器的角度来看,它可能是单次传输,不过由于总线接口需要将非对齐传输转换为多个对齐传输,从总线等级来看它可能会占用几个周期。
末尾连锁(中断咬尾)
若某个异常产生时处理器正在处理另一个具有相同或更高优先级的异常,该异常就会进入挂起状态。在处理器执行完当前的异常处理后,它可以继续执行挂起的异常/中断请求。处理器不会从栈中恢复寄存器(出栈)然后再将它们存入栈中(压栈),而是跳过出栈和压栈过程并会尽快进入挂起异常的异常处理。这样,两个异常处理间隔的时间就会降低很多。对于无等待状态的存储器系统,末尾连锁的中断等待时间仅为6个时钟周期。
丢中断的情况1:中断请求产生于中断悬起过程
- 有一个中断请求产生,NVIC响应并将中断悬起,此时并未进入中断处理。
- 在悬起期间,又产生相同的中断请求,那么中断一直悬起并进入中断处理,而产生相同的中断请求则会被忽略。
丢中断的情况2:中断请求产生于中断处理过程
- 有一个中断请求产生,NVIC响应并将中断悬起并进入中断处理,悬起位解除。
- 进入中断,还未将中断标志位清除(如外设上某个中断的标志位),这时,再次发生依次中断请求,产生中断标志位,然后中断处理中将这个标志位清除。
- 则两个中断标志位都被清除,第二个中断进入中断处理无法判断是产生何种中断类型。
思考:1.什么是丢中断?为什么会丢中断?
丢中断是指系统面对连续相同的中断请求中只响应了一个,或在中断过程再次产生中断,而清除两个的标志位,导致中断处理无法判断类型而丢。
为什么会丢?主要还是中断请求产生的时机。
思考:2.中断的悬起状态是什么时候被恢复的?
悬起状态直到进入中断处理才被恢复。
思考:3.为什么中断要快进快出?
中断处理程序需要“快进快出”(即快速进入和快速退出)的原因主要有以下几点:
- 最小化中断延迟
- 实时性要求:在许多嵌入式系统和实时操作系统中,对响应时间有严格的要求。中断处理程序必须尽快完成,以确保系统的实时性能。
- 减少主程序的阻塞时间:中断处理程序执行时间越长,主程序被中断的时间就越长,这可能导致主程序的执行受到影响,甚至错过其他重要的事件。
- 保持系统稳定性
- 避免堆栈溢出:长时间运行的中断处理程序可能会消耗大量堆栈空间,特别是在嵌入式系统中堆栈资源有限的情况下,容易导致堆栈溢出。
- 防止数据丢失:如果中断处理程序执行时间过长,可能会错过新的中断请求,导致数据丢失或系统状态不一致。
- 提高系统吞吐量
- 高频率中断:在某些应用中,如高速通信或传感器数据采集,中断可能频繁发生。如果每个中断处理程序都执行很长时间,系统将无法处理所有中断请求,从而降低整体吞吐量。
- 多任务调度:在多任务系统中,中断处理程序需要尽快返回,以便操作系统能够及时调度其他任务,保持系统的高效运行。
- 简化设计
- 明确职责:中断处理程序的主要职责是快速响应中断并保存必要的上下文信息。复杂的处理逻辑可以放在主程序或其他后台任务中进行,这样可以使设计更加清晰和易于维护。
- 减少错误:长时间运行的中断处理程序更容易引入错误,因为它们可能涉及更多的状态管理和复杂逻辑。通过将复杂逻辑移出中断处理程序,可以减少潜在的错误源。
思考:4.要满足中断的快进快出,又想在中断产生后向线程发送通知,怎么才能快速通知?
在中断处理程序中,快速通知线程通常需要使用高效且低延迟的机制。以下是一些常见的方法,可以帮助你在中断产生后快速通知线程:
- 标志位(Flag)
使用一个全局标志位来通知线程有新的中断事件发生。这是最简单的方法,但需要注意标志位的原子操作。
- 信号量(Semaphore)
使用信号量可以更安全地进行线程同步。信号量提供了一种原子操作的方式,确保线程不会错过任何中断事件。
- 事件队列(Event Queue)
使用事件队列可以将多个中断事件存储在一个队列中,并由线程定期处理这些事件。这适用于需要处理多个中断事件的情况。