STM32的中断系统与外部中断

STM32的中断系统

STM32中断概览

几乎所有单片机都具有中断系统,中断允许单片机能实时处理外界发生的随机事件。Cortex 内核设计了一套完善的中断系统,它将暂停当前代码执行流程的事件分为系统异常(exception)和外部中断(interrupt)。

STM32 的中断源很多,并且每一个类型芯片包含的的中断源都不完全一样。可以在 stm32f10x.h 的以下枚举类型中找到当前芯片的所有中断源:

typedef enum IRQn { /* ... */ } IRQn_Type;

在使用到具体的中断类型时,再介绍对应的中断源。

STM32 的中断源有优先级之分,在遇到中断嵌套,即一个中断执行时又发生一个中断时,会根据优先级处理这种嵌套的情况。

Cortex-M 具有一个完整的中断向量表,而 STM32 对该表重新编排了顺序,除了少数几个系统异常外,大部分中断的优先级都是可以调整的。

优先级又分为抢占优先级(preemption priority)和子优先级(subpriority)。当一个中断执行时,如果有更高抢占优先级的中断发生,那么可以直接转到处理这个抢占优先级更高的中断。即,STM32 支持中断的嵌套。

当两个中断源的抢占式优先级相同时,那么后到达的中断需要等到前一个中断处理完之后才会被处理。如果有多个中断同时响应,抢占优先级高的就会先于较低的得到执行;如果抢占优先级相同,就比较子优先级的高低;如果抢占优先级和子优先级都相同,则比较中断源的硬件中断编号。对于这些优先级编号来说,数值越小意味着优先级越高。

NVIC结构

在使用 STM32 的中断功能前,必须要了解 NVIC 这一结构。NVIC 是 Cortex-M 系列芯片的嵌套向量中断控制器(Nested Vectored Interrupt Controller),它是内核里面的一个结构,负责调度整个芯片中断相关的功能,并发送给内核处理。

STM32 的外设很多,引起中断的中断源也很多,这会导致中断的处理很麻烦,如果什么事情都交给 CPU 处理,那么 CPU 的负担会比较大。这个时候就需要 NVIC 登场了:NVIC 就像 CPU 在中断管理方面的一位“秘书”,负责处理中断优先级、中断向量表以及中断使能和禁用等琐碎的问题,帮 CPU 安排中断程序的处理顺序,减轻了 CPU 的工作量。

Cortex-M3 的 NVIC 功能非常强大,以至于 STM32 只用到了其中的一部分功能,它的 NVIC 是 Cortex-M3 的 NVIC 的一个子集。

接下来先介绍中断优先级管理的问题。Cortex-M 内核的中断优先级又可以分为抢占优先级(pre-emption priority)和子优先级(subpriority):

  • 抢占优先级决定了中断是否可以嵌套:抢占优先级高的中断,可以在低抢占优先级的中断处理过程中被响应,或者说高抢占优先级的中断将被优先被处理;如果两个中断的抢占优先级相同,那么后到达的中断就要等待当前的中断被处理完后才能被处理
  • 子优先级决定了中断的处理顺序:如果两个中断同时到达,则先处理子优先级高的中断。如果两个中断的抢占式优先级和子优先级都相同的话,则根据它们在中断向量表中的顺序决定处理顺序

中断源优先级的编号越小,表明它的优先级别越高。例如,假设有以下三个中断源和对应的优先级:

中断源 抢占优先级 子优先级
A 0 1
B 1 1
C 1 0

若内核正在执行 B 的中断服务函数,则它能被抢占优先级更高的中断 A 打断,由于 B 和 C 的抢占优先级相同,所以 B 不能被 C 打断。但如果 B 和 C 中断是同时到达的,内核就会首先响应响应优先级别更高的 C 中断。

NVIC 上有若干个中断优先级寄存器 IPRx ,用来配置每个中断的优先级。这些寄存器都是以字节为单位起效的,因此原则上每个中断可配置的优先级编号为 0-255 。但是 STM32 并没有用到这么多中断,它只使用了高 4 位表达优先级,因此最多只能设置 16 个中断优先级。STM32 根据对这 4 个比特位的不同划分方式,将优先级分为 5 组,每组抢占优先级和子优先级占用的比特位数都不尽相同。这个分组方式被记录在内核的系统控制块(system control block)中的应用程序中断及复位控制寄存器 AIRCR 中的第 8~10 位 PRIGROUP ,具体每个优先级分组的方式如下:

分组编号PRIGROUP 的实际值IPRx 的分组方式说明
00b0110b yyyy抢占优先级占 0 位共 1 级;子优先级占 4 位共 16 级
10b1000b x.yyy抢占优先级占 1 位共 2 级;子优先级占 3 位共 8 级
20b1010b xx.yy抢占优先级占 2 位共 4 级;子优先级占 2 位共 4 级
30b1100b xxx.y抢占优先级占 3 位共 8 级;子优先级占 1 位共 2 级
40b1110b xxxx抢占优先级占 4 位共 16 级;子优先级占 1 位共 0 级

与 NVIC 中断系统配置有关的函数和定义都在 misc.h 头文件中。设置优先级分组可以通过调用以下函数实现:

  • void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);

该函数参数的可用值已经被定义为了 NVIC_PriorityGroup_* 一系列宏。例如需要 4 级抢占优先级和 4 级子优先级,可以设置优先级分组为分组 2 :

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

除了配置整体的优先级分组外,在使用中断前还需要为每个用到的中断源设置优先级。设置优先级所需要的信息由 NVIC_InitTypeDef 结构提供。该结构的定义为:

typedef struct {
    uint8_t NVIC_IRQChannel;
    uint8_t NVIC_IRQChannelPreemptionPriority;
    uint8_t NVIC_IRQChannelSubPriority;
    FunctionalState NVIC_IRQChannelCmd;
} NVIC_InitTypeDef;

成员 NVIC_IROChannel 用来设置中断源,可用的中断源包含在之前提到的 IRQn_Type 定义的枚举常量里。

成员 NVIC_IRQChannelPreemptionPriorityNVIC_IRQChannelSubPriority 分别指定抢占优先级和子优先级,具体的值要根据优先级分组来确定。

最后一个成员 NVIC_IRQChannelCmd 自然是控制启用还是禁用中断。

例如,如果要让 GPIO 接收到的外界信号产生一个中断,需要使用外部中断,对应的中断源可以是 EXTI0_IRQn ,那么配置 NVIC 的代码为:

NVIC_InitTypeDef NVIC_InitStructure = {
    .NVIC_IRQChannel = EXTI0_IRQn,
    .NVIC_IRQChannelPreemptionPriority = 1,
    .NVIC_IRQChannelSubPriority = 1,
    .NVIC_IRQChannelCmd = ENABLE
};
NVIC_Init(&NVIC_InitStructure);

可以看出,NVIC_Init() 这个函数与其说是初始化 NVIC ,倒不如说是初始化中断源(包括开启中断源和分配优先级)更符合它的作用。

接下来以外部中断为例,介绍中断的应用。

外部中断

外部中断/事件控制器

STM32 有一个外设 EXTI ,全称外部中断/事件控制器(external interrupt/event controller),它管理了控制器的外部中断和外部事件。

所谓事件(event),它与中断的概念基本是一致的。事件和中断的产生都是一致的,但是中断信号需要通过 NVIC 进入内核,执行中断服务;而事件不参与内核提供的中断处理流程,而是直接转交给其它外设处理。这样内核可以不用响应事件,从而减少内核的负担。

在使用外设前,首先需要初始化外设。根据标准库的编写思路,初始化这个外设需要用到以下函数:

  • void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);

标准库函数对每个外设都建立了一个初始化结构,并由初始化结构的参数完成寄存器的配置。对于外部中断来说,这个结构体是 EXTI_InitTypeDef ,对应的初始化函数是 EXTI_Init() 。它们的定义在 stm32f10x_exti.h 中,其中初始化结构的定义如下:

typedef struct {
    uint32_t EXTI_Line;
    EXTIMode_TypeDef EXTI_Mode;
    EXTITrigger_TypeDef EXTI_Trigger;
    FunctionalState EXTI_LineCmd;
} EXTI_InitTypeDef;

要明白这个结构每个成员如何取值,以及使用外部中断需要执行哪些步骤,需要结合 EXTI 的结构分析,并了解 STM32 处理外部中断的基本流程。下图展示了 EXTI 的核心结构:

图线上的“/20”代表这样的交互逻辑共有独立的 20 组,每个组都拥有单独的配置。整个处理过程从最右边的输入线接到信号输入开始,经由一系列处理之后到达最左端引起一个中断或事件。

首先是输入线,EXTI 控制器有 20 个独立的中断/事件输入线,换句话说,STM32 可以拥有 20 个外部中断。STM32 的外部中断可以设置为任意一个 GPIO 引脚,甚至也可以是其它外设的输入。

这 20 个外部输入中,前 16 个输入线和 GPIO 相关,但还是有一定限制的:每一个中断/事件输入线 EXTIn 的中断输入源只能接 PXn 。换句话说,GPIO 相同位号的引脚共用一条输入线,如图所示:

所以虽然 STM32 中每个引脚都可以触发外部中断,但是不同外设相同位号的引脚不应该让它们同时触发中断。剩下四个输入线为 PVD 输出、RTC 时钟、USB 唤醒和以太网唤醒。具体初始化哪个输入线,可以通过成员 .EXTI_Line 指定。

接下来信号到达边沿检测电路,它会根据上升沿触发选择寄存器和下降沿触发选择寄存器控制需要检测哪些类型的电平跳变过程,上升沿触发选择寄存器可以开启边沿检测电路检测上升沿触发的功能,下降沿触发选择寄存器可以开启边沿检测电路检测下降沿触发的功能。边沿检测电路以输入线作为信号输入端,如果检测到有边沿跳变就输出有效信号 1 进入下一个环节,否则输出无效信号 0 。初始化结构体中的成员 .EXTI_Trigger 可以配置需要哪种边沿检测,包括上升沿、下降沿以及都触发。

边沿检测电路后的或门电路的用途是可以任意选择来自硬件的中断还是来自软件的中断。软件中断事件寄存器可以通过程序控制,这样就可以直接通过代码直接启动中断。

以上步骤,基本上已经产生了一个完整的中断信号了。接下来,这个中断信号被用在两条支路上:

通向 NVIC 中断控制器的中断信号首先被请求挂起寄存器截获。该寄存器收到这个中断挂起信号后,将某些位设置为 1 ,代表当前正在处理该中断信号。这样,在处理中断请求时,如果又一个中断信号传来,发现对应的位被置 1 了,就明白当前有中断正在处理,就会将新来的中断搁置或丢弃。因此,在处理完中断后,需要将对应的挂起位清 0 ,以恢复接收中断的功能。

如果当前中断功能可用,那么请求挂起寄存器就会将这个中断信号转发到接下来的一个与门电路中。这个与门电路的另外一个输入来自中断屏蔽寄存器。如果中断屏蔽寄存器的值设置为 0 ,那么不管是否有中断信号,最终都无法产生中断,这也是外部中断中断屏蔽的原理,即可以不响应一些中断信号。在编程时,可以通过控制中断屏蔽寄存器来停用一些中断。注意到初始化结构体有一个成员 .EXTI_LineCmd ,它的命名以 Cmd 结尾,说明它可以设置为 ENABLEDISABLE 两种状态:当设置为 DISABLE 时,标准库就会将其屏蔽。

当产生一个中断信号后,中断信号的另一个去处就是产生事件。它会直接到达接下来的一个与门电路中,同样可以通过事件屏蔽寄存器来允许或屏蔽这个事件信号。初始化结构体的成员 .EXTI_Mode 有两个可用值,分别用于配置中断和配置事件。

如果事件不被屏蔽,那信号会继续传递到之后的脉冲发生器电路,并产生一个脉冲,提供给其他外设电路使用,这个脉冲就是事件的实质。如果需要使用事件,一般会提前配置好能够处理这些脉冲的外设来处理事件。

使用外部中断

根据以上对 EXTI 的基本介绍,可以着手编写外部中断相关的程序。以下只展示了代码的核心部分,完整的代码可以从 GitHub 仓库中获取。

初始化配置

EXTI 是位于 APB2 总线上的外设,在使用外设前,需要开启时钟。不过并没有一个叫“ EXTI ”的时钟,外部中断对应的是 AFIO 时钟。AFIO(alternate-function I/O) 指 GPIO 端口的复用功能,在使用 GPIO 的外部中断功能时,GPIO 需要通过复用输入线将信号输入到外部中断结构部分,AFIO 负责控制这部分的复用功能。除此之外,也不要忘记开启 GPIO 的时钟:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);

既然需要使用 GPIO ,那么代码中肯定有一个步骤不能忽略,那就是初始化对应的 GPIO 。这里将 GPIO 的模式配置为浮空输入模式,由外部电路完全决定输入的值:

GPIO_InitTypeDef GPIO_InitStructure = {
    .GPIO_Mode = GPIO_Mode_IN_FLOATING,
    .GPIO_Pin = GPIO_Pin_0
};
GPIO_Init(GPIOA, &GPIO_InitStructure);

接下来就可以初始化外部中断了,这里对初始化结构体的配置为:

EXTI_InitTypeDef EXTI_InitStructure = {
    .EXTI_Line = EXTI_Line0,
    .EXTI_Mode = EXTI_Mode_Interrupt,
    .EXTI_Trigger = EXTI_Trigger_Rising,
    .EXTI_LineCmd = ENABLE
};
EXTI_Init(&EXTI_InitStructure);

这里为各个成员选用值的含义为:

  1. 配置的是哪一个中断/事件输入线。由于使用的 GPIO 为 PA0 ,它的位号是 0 ,对应 0 号输入线
  2. 选择模式,可选为产生中断或事件,这里选择产生中断
  3. 边沿检测的方式:上升沿、下降沿还是都触发。这里选择仅上升沿触发
  4. 对中断信号,是放行还是屏蔽,这里当然选择放行

但是,以上还缺少了一个关键的步骤:PA0 、PB0 一直到 PG0 都可以作为 0 号中断输入线,为了指定 PA0 作为 0 号的输入线,需要用到以下函数:

  • void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);

这是 GPIO 相关的功能,因此该函数的定义在 GPIO 外设库里。为了指定 PA0 作为 0 号的输入线,可以通过如下方法调用函数:

GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

此时,外设部分的配置已经完毕,不过内核的 NVIC 部分也需要配置。这一部分的代码在上文介绍 NVIC 时已经给出,这里不再重复。

编写中断服务

完成以上所有的配置后,中断就可以产生并被处理了。中断在处理时,会根据中断向量表执行特定的函数(中断服务程序)。

在启动文件中,初始化中断向量表时预先为每个中断都写了一个中断服务函数。这些中断函数都为空,需要重新编写。习惯性将中断服务函数统一写在 stm32f10x_it.c 这个库文件中,方便管理。实际上,标准库提供该文件时,已经编写了部分系统级别异常的中断服务函数,只不过它们的实现中基本没怎么处理异常。

中断服务函数的函数名必须跟启动文件里面预先设置的一样。如果函数名和启动文件里设置的不一致,系统在中断向量表中无法找到中断服务函数的入口,那么就无法实现中断。

检查启动文件的定义可知,启动文件会将外部中断 0 引导到 EXTI0_IRQHandler 代码段中,因此编写的中断函数也需要使用该符号作为函数名。这样,外部中断 0 的函数原型为:

void EXTI0_IRQHandler(void);

如果进一步翻阅启动文件可以发现,外部中断 5~9 共用一个中断源 EXTI9_5_IRQn ,这几个外部中断都会引发同一个中断处理函数 EXTI9_5_IRQHandler() ;而外部中断10~15 也共用一个中断源 EXTI15_10_IRQn 和中断函数 EXTI15_10_IRQHandler 。也就是说,这些中断都会调用同一个中断函数。

为了确保中断发生,以下函数可以检查某个输入线是否确实发生了触发请求,并且没有被屏蔽:

  • ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);

其中对中断状态枚举的定义为:

typedef enum {RESET = 0, SET = !RESET} ITStatus;

SET 代表确实发生了需要处理的中断。

中断结束后,需要清除中断挂起位,使硬件退出中断并恢复中断接收功能。这点可以通过以下函数实现:

  • void EXTI_ClearITPendingBit(uint32_t EXTI_Line);

因此,一个中断处理函数的完整框架为:(以外部中断 0 为例)

void EXTI0_IRQHandler(void) {
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        LED_Toggle(LED_Red);
    }
    EXTI_ClearITPendingBit(EXTI_Line0);
}

由于程序中会判断单个输入的情况,因此外部中断 5~15 也可以使用这种处理框架。

外部中断提供了软件触发的方式,如果确实有这个需求,可以通过以下函数触发:

  • void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);

其它话题

中断屏蔽与系统的软复位

中断的屏蔽在 STM32 中也是一个重要的话题。有些时候为了确保指令能够快速响应,或者保证指令执行的连续性,需要暂时屏蔽中断。Cortex-M3 有以下 3 个寄存器,可以处理和中断屏蔽相关的事项:

寄存器 用途
PRIMASK 该寄存器只用到了一个比特,置 1 表示关闭所有可屏蔽的异常,只剩下不可屏蔽中断和硬件 FAULT 可以响应。默认值 0 表示不关中断
FAULTMASK 该寄存器同样只用到了一个比特,置 1 后会关闭包括硬 FAULT 在内的所有中断,只有不可屏蔽中断才能响应。默认值 0 表示不关中断
BASEPRI 该寄存器定义了被屏蔽优先级的阈值。它的有效位数由表达优先级的位数决定,最多 9 位。当它被设成某个值后,会屏蔽所有优先级大于等于此值的中断。默认值 0 表示则不关闭任何中断

core_cm3.h 文件里直接提供了相关函数设置这几个寄存器的值:

  • void __set_PRIMASK(uint32_t priMask);
  • void __set_FAULTMASK(uint32_t faultMask);
  • void __set_BASEPRI(uint32_t value);

NVIC 除了提供中断调度相关的功能外,还提供了系统软复位的函数:(在 core_cm3.h 文件里)

  • static inline void NVIC_SystemReset(void);

要注意的是,从调用该函数到硬件正式执行复位命令期间会有一个间隔,此时处理器仍然可以响应中断请求而暂停复位操作,所以最好在发出复位请求前调用 __disable_irq() 禁用所有中断,这个函数在编译后会被内联为一条汇编指令,确保操作的原子化。

京ICP备2021034974号
contact me by hello@frozencandles.fun