STM32单片机学习(16) —— 中断相关概念
文章目录
- 概述
- 中断是什么?
- 中断优先级
- 抢占优先级
- 子优先级
- 中断执行优先级举例
- STM32的中断系统框图
- NVIC的作用
- NVIC不属于片上外设
- NVIC中断优先级的配置
- 中断优先级分组
- 优先级都一样怎么办?
概述
**中断(Interrupt)**作为嵌入式系统一个非常重要的概念,已成为单片机(MCU)中最基础的外设模块/功能之一。
利用中断机制可以有效提高 CPU 效率,绝大多数嵌入式应用开发都会用到中断系统。
熟练编写中断程序是嵌入式开发人员必备的基本技能。
是我们学习STM32外设的第三大部分。
中断是什么?
为了讲清楚中断是什么,我们可以打一个通俗的比喻:
你正在图书馆看书学习,这时快递员通知你的快递到了,你暂停看书去拿快递,那么拿快递就是一个中断。中止你现在正在做的事情(看书)转而去做另一个事情。
如果在去取快递的路上,你突然肚子疼,只好先去趟卫生间,然后再去拿快递。
此时,肚子疼又是一个中断,而且是比拿快递更重要、更紧迫的事件,也就是说,中断中又有一个中断,这就是中断嵌套。
只有更重要、更迫切的(高级别)中断才能中止当前级别低的中断,也就是说中断是有优先级的。
能够引发中断的事件(快递到了、肚子疼等)称为中断源。
而对中断进行处理(拿快递、去卫生间等)称为中断服务程序(Interrupt Service Routine, 简称ISR)
这里有三个重要的概念:
- 中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行。
- **中断优先级:**当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应处理更加紧急的中断源。
- **中断嵌套:**当一个中断服务程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。
下面的两张图就很好的解释了中断和中断嵌套:
在一个实际程序中,中断的处理是自动完成的,请看下图:
简单来说:
程序员只需要编写好中断服务程序,并完成中断触发条件等相关配置即可。
当主程序运行过程中触发中断条件时,嵌入式系统会自动完成以下操作:
- 自动保存主程序的当前执行状态
- 跳转去执行中断处理函数(中断服务程序,ISR)
- 执行完毕中断处理逻辑后,自动恢复之前保存的主程序状态,并继续从中断前的指令处执行。
- 这个过程就是常说的“上下文切换”,或者“中断上下文切换”。
以上这些过程,都由嵌入式系统自动完成,程序员不需要手动处理这些细节。
那么程序员在使用中断这种机制时,需要动手做的事情有哪些呢?
- 设置好中断源,配置好中断相关的参数数据,还需要手动开启这个中断源。
- 编写好相应的中断服务程序,也就是编写对应的中断处理函数。
从这个角度看,中断是一种高效且易用的语法机制。
中断优先级
在上面我们已经理解了中断的概念含义,现在我们进一步来了解一下中断优先级的概念。
在一个STM32嵌入式系统中,几乎所有的外设都可以产生中断源。
如果这些中断同时发生(例如多个外设同时产生中断请求),那么必须决定哪一个中断先被处理。
为此,STM32 提供了中断优先级机制,允许程序员为每个中断源设置不同的优先级。
STM32 的中断优先级被分为两组,这两组优先级分别是:
- 抢占优先级(Preemption Priority)
- 子优先级(Subpriority,有些地方也叫响应优先级)
下面来讲解一下这两个优先级的相关概念。
优先级具体在表示时,都会使用非负整数表示,最小值是0,数值越小表示优先级越高,0就是最高的优先级。
抢占优先级
抢占优先级是最关键且核心的优先级:
- 若主程序正常运行时,同时产生了多个中断,那么抢占优先级更高的中断先执行。
- 若主程序已经在执行中断A了,中断A执行过程中产生了中断B,且中断B的抢占优先级更高,则会产生中断嵌套,优先执行中断B。
总之抢占优先级的核心点在于"抢占"二字,具有更高抢占优先级的中断可以打断正在执行中的低优先级中断。
子优先级
只有当抢占优先级相同时,才需要考虑中断的子优先级:
- 若主程序正常运行时,同时产生了多个抢占优先级相同的中断,那么子优先级更高的中断先执行。
- 若A和B是相同抢占优先级的两个中断:
- 系统当前正在执行中断A,此时产生了中断B
- 无论中断B的子优先级更高还是更低,中断B都不能打断中断A的执行,无法产生中断嵌套。
也就是说,子优先级和中断嵌套没有关系,只要当前已有中断在执行,那么相同抢占优先级的中断就无法打断。
总之结论如下:
- 抢占优先级 决定了多个中断同时触发时,哪个中断优先执行,抢占优先级越高,越先执行。
- 抢占优先级 还决定了哪个中断可以打断另一个中断,抢占优先级高的中断可以打断,当前正在执行的,抢占优先级更低的中断。
- 子优先级 只用来区分同一抢占优先级的中断触发时,执行的先后顺序,子优先级越高,越先执行。子优先级和中断嵌套没有关系!
中断执行优先级举例
假如现在有5个中断:
- A中断,抢占优先级3,子优先级3
- B中断,抢占优先级2,子优先级4
- C中断,抢占优先级2,子优先级2
- D中断,抢占优先级2,子优先级3
假如主程序在正常执行的某点,突然触发了A中断,此时由于只有一个中断源,不用考虑优先级,直接执行A中断的处理函数,如下图所示:
假如在执行A中断处理函数的过程中,突然触发了B中断,会发生什么事情呢?
由于B中断的抢占优先级更高(2 < 3),所以B会打断A中断函数的执行,嵌套执行B中断,即产生了中断嵌套。如下图所示:
再假如在执行B中断处理函数的过程中,突然同时触发了CD两个中断,会发生什么事情呢?
BCD四者的抢占优先级是一致的,哪怕CD的子优先级更高,但子优先级和中断嵌套没有关系,所以CD都必须等待B中断执行完毕后才可能执行。
在B中断执行完毕后,会返回A中断被嵌套中断的断点位置,CD的抢占优先级高于A,所以CD会继续打断A的执行。
而C和D之间,则会根据子优先级来排列它们的执行顺序,C的子优先级更高,所以C先于C执行完毕,都先于A执行完毕。
整个中断执行的流程图,如下图所示:
在大多数嵌入式应用中,程序的复杂度往往比较低,中断触发的情况通常是由单一事件引起的,中断嵌套的情况是比较少见的,更不用说上述这么复杂的情况。
但是在一些实时系统中,还是会存在一些比较复杂的中断系统,所以对于中断优先级的相关概念,大家还是要了解清楚的。
搞清楚了STM32中断优先级的概念,那么如何设置中断的优先级呢?
为了搞清楚这个事情,我们先来看一下STM32的中断系统框图。
STM32的中断系统框图
STM32 当中有很多片上外设都可以产生中断源,例如 USART、EXTI、TIM、SPI、I2C 等。
这些外设在满足中断条件时:
都会向 CPU 提出中断请求,都希望抢占 CPU 的执行权,转而执行各自对应的中断服务程序(ISR)。
这么多中断如果同时存在,而没有一个统一管理的“机构”,那中断系统就乱套了。
在整个中断系统中,处于核心控制地位,负责中断调度、优先级管理和中断嵌套的组件,就是 NVIC。
NVIC的作用
NVIC (Nested Vectored Interrupt Controller) ,直译为嵌套矢量中断控制器。
当然一般没什么人直接叫它的中文译名,直接叫NVIC即可。
NVIC 是单片机中非常重要的、核心的组成部分,几乎所有中断相关的事项都需要NVIC参与处理。
其主要作用有以下几项:
- 中断的开启和关闭。
- 外设可以提交中断请求给NVIC,但只有被NVIC开启的中断才能够抢占CPU执行。
- NVIC如果关闭了某个中断,那么此中断就完全无效。
- 中断的优先级设置,NVIC 负责为各个中断源请求配置和管理优先级。
- 中断调度与执行控制。
- NVIC 会根据已配置的优先级规则,对所有中断请求进行仲裁,决定哪个中断优先进入 CPU 执行
- 中断的挂起等待、中断嵌套等一系列中断行为都由NVIC决定。
总之,用通俗的话来说:
外设只负责产生中断请求,而NVIC来决定“中断是否生效、中断什么时候执行以及什么顺序执行”。
NVIC不属于片上外设
NVIC在使用时可以把它视为一个特殊的片上外设,尤其是在编程的时候。毕竟:
它同样具有寄存器,也需要配置寄存器,SPL库中也有对应函数可以调用。
但严格来说,NVIC并不是片上外设,它和GPIO、USART等外设有着本质上的区别:
- NVIC属于Cortex-M 内核的一部分。
- NVIC也并没有挂载在AHB或者APB总线上,它集成在CPU内部,是CPU的一部分,和CPU“住在一起”。
这样NVIC在使用时,就和一般片上外设具有差异性。最重要的有两条:
- 只要CPU运行,NVIC就处于工作状态。所以NVIC在使用时,不需要像传统外设那样开启时钟!
- 片上外设可能由于芯片型号不同,在数量或功能细节上存在差异。但NVIC属于内核组件,只要核心相同的芯片,NVIC在使用上都是一致的。
当然,在日常交流口头上,说“NVIC外设”通常也没有大问题,大家都能够理解。
NVIC中断优先级的配置
其它外设产生中断请求,然后交给NVIC外设统一配置,其中就包括配置此中断请求的优先级。
那么NVIC是如何实现中断优先级的配置呢?
NVIC的中断优先级由优先级寄存器的4位(上图中间的四个格子)决定,这4位可以进行切分:
- 高n位决定抢占优先级的取值。
- 低m位决定子优先级的取值,其中m = 4 - n。
下面详细讲一下STM32中断优先级分组。
中断优先级分组
很明显,根据n的取值不同,一共可以分为5种情况,也就是5个分组。如下图所示:
分组0,注意从0开始,不是从1开始:
- n = 0,没有抢占优先级,或者可以认为所有中断的抢占优先级都一样,固定是0。
- m = 4,优先级寄存器的 4 位都用于子优先级,子优先级的取值范围是 0 ~ 15
采用分组0时,由于没有抢占优先级,中断系统完全依赖子优先级来管理。
此时中断系统中没有嵌套行为,单纯依赖子优先级排队执行。
分组1:
- n = 1,优先级寄存器的 1 位用于抢占优先级,抢占优先级的取值范围是 0 ~ 1
- m = 3,优先级寄存器的 3 位用于子优先级,子优先级的取值范围是 0 ~ 7
分组2:
- n = 2,优先级寄存器的 2 位用于抢占优先级,抢占优先级的取值范围是 0 ~ 3
- m = 2,优先级寄存器的 2 位用于子优先级,子优先级的取值范围是 0 ~ 3
分组3:
- n = 3,优先级寄存器的 3 位用于抢占优先级,抢占优先级的取值范围是 0 ~ 7
- m = 1,优先级寄存器的 1 位用于子优先级,子优先级的取值范围是 0 ~ 1
分组4:
- n = 4,优先级寄存器的 4 位都用于抢占优先级,抢占优先级的取值范围是 0 ~ 15
- m = 0,没有子优先级,或者可以认为所有中断的子优先级都一样,固定是0。
采用分组4时,中断系统没有子优先级,完全依赖抢占优先级来管理中断的执行顺序,存在中断嵌套。
注意以下几点:
- 在设置具体中断优先级数值之前,应该先设置优先级分组。因为分组决定了优先级数值的具体取值范围!
- 如果不小心忘记明确设置优先级分组,那么单片机本身会采用默认分组0。
- 这个默认的分组0,来自于NVIC上电启动后寄存器的复位值,是一种默认行为。
- 默认分组0就意味着系统内不存在抢占优先级,全部依赖子优先级决定中断的执行。
- 在该分组下,中断之间不会发生嵌套抢占。
- 这显然是一种保守的、安全的中断优先级分组配置。
- 最好不要依赖优先级分组的默认行为。
- 试想一下,如果忘记设置优先级分组,直接配置抢占优先级字段。
- 看似设置成功,实则没有任何作用,中断不会出现任何抢占行为。
- 因为分组0没有抢占优先级。
- 为了避免这种情况出现,请大家一定不要忘记手动设置优先级分组!一定要显式设置分组后再设置优先级数值!
- 多次设置中断优先级分组通常也是无意义的,而且具有风险。所以设置分组的操作最好就放在main函数死循环的上面,只执行一次即可。
- 由于我们采用标准库开发方式,底层寄存器操作被封装了,所以分组和优先级数值的设置,实际都只需要调用函数就足够了。
优先级都一样怎么办?
如果你真正搞懂了上述优先级相关内容,且善于发散思考,你可能会提出这样的问题:
抢占优先级不同优先看抢占优先级,抢占优先级相同就看子优先级。
那么如果两个中断抢占优先级和子优先级都一样,怎么办呢?
谁先谁后执行?
STM32的设计者早已想到了这个问题了:
在多个中断优先级完全一致时,单片机会优先执行中断向量表中向上靠前的中断,也就是优先执行表格中编号小的中断。
那么什么是中断向量表呢?
- 简单来说,中断向量表是一张"函数地址表",每一个中断类型的中断服务程序(ISR)都具有唯一对应的函数入口地址(函数指针)。
- CPU在执行某一中断类型的中断服务程序时,就需要查表来确定函数的入口地址,从而执行该中断服务程序。
在《参考手册》的中断和事件章节,章节开头就给出了中断向量表。
一部分截图如下:
很明显如果两种优先级都一样,系统级中断具有优先执行权。
还有一个叫Reset的中断,也就是复位操作,它的优先级是固定最高的,不管单片机在做什么,触发复位时都会优先执行。
其余外设中断的执行顺序,则可以查表得到,这里就不赘述了。
