1.3 中断的嵌套
当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。
1.4 STM32中的中断系统
1.4.1 STM32的中断资源
本课程使用的STM32F103C8T6含有68个(这个数字由具体的芯片型号决定)可屏蔽中断通道(中断源),包含外部中断EXTI、定时器TIM、模数转换器ADC、串口通信USART、SPI通信、I2C通信、实时时钟RTC等多个外设。
STM32(本课程使用的型号)中的中断资源如下表所示(表中灰色的部分是内核的中断资源,白色的部分是外设的中断资源):
STM32中,每一个中断资源都有对应的地址(如上表所示)。在编程实现中,我们将中断要执行的操作放在一个子函数中,但是这个中断函数的地址由编译器动态分配,并不是固定的。但是由于硬件电路的限制,中断跳转时必须跳转到固定的地址执行程序,所以为了使硬件能够跳转到一个不固定的中断函数里,就需要在内存中定义一个固定的地址列表,中断条件满足后,就跳转到对应的固定位置,然后由编译器和一句跳转到中断函数位置的代码,这样中断就能跳转到任意位置了。在上述过程中,我们将中断地址的列表成为中断向量表,相当于中断跳转的一个跳板。
应用C语言编程时,我们无需关心中断向量表的工作,这部分工作由编译器自动完成。
1.4.2 嵌套中断向量控制器 NVIC(Nested Vectored Interrupt Controller)
STM32采用NVIC统一管理中断和分配优先级。每个中断通道都拥有16个可编程的优先等级,还可以对优先级进行分组处理,即进一步设置抢占优先级和响应优先级。
NVIC的基本结构如下图所示:
NVIC是一个内核外设,相当于CPU的一个“秘书”,也相当于医院中为医生和患者服务的叫号系统。STM32的中断种类非常多,如果将这些中断全部连接给CPU,在设计上的复杂度将难以估量;斌且如果很多中断同时申请,或者很多中断产生了拥堵,也会增大CPU的处理难度,变相降低了CPU的运算效率。
NVIC有很多输入口,可以连接不同外设的多条中断线路(通道)。上图中NVIC连接的每个外设线路上的
n
n
n 表示一个外设可能会同时占用多个中断通道。
NVIC只有一个输出口,连接到CPU。NVIC根据每个中断的优先级分配中断的先后顺序,然后通过输出口告知CPU应该处理哪个中断。对于中断先后顺序分配的任务,CPU不需要知道。
每个中断通道都拥有16个可编程的优先等级。 NVIC可以由此对优先级进行分组。
1.4.3 NVIC与优先级分组
为了处理不同形式的优先级,NVIC可以对优先级进行分组。在STM32中,中断优先级分为抢占优先级和响应优先级。以医院中的叫号系统作比喻,抢占优先级高的可以进行中断嵌套,即中断当前的中断程序;响应优先级高的可以优先排队,相当于“插队”;抢占优先级和响应优先级均相同的按中断号排队(向量表中的默认优先级)。
每个中断都拥有16个可编程的优先等级,为了把优先级分为抢占优先级和响应优先级,NVIC通过优先级寄存器的4位决定(4位二进制数,可以表示16个数字,即 0-15,对应16个优先等级)。优先级寄存器代表的数值越小,优先级越高,0就是最高优先级。这4位可以进行切分,分为高
n
n
n 位(对应
2
n
2^n
2n 个抢占优先级)的抢占优先级和低
4
−
n
4-n
4−n 位(对应
2
4
−
n
2^{4-n}
24−n 个响应优先级)的响应优先级 (注意:这里的高低指优先级寄存器的高低位,不是优先级寄存器代表的数)。所以STM32的中断不存在先来后到的排队方式,在任何时候,都是优先级高的中断先响应。
二、EXTI 外部中断
2.1 EXTI(Extern Interrupt)简介
EXTI作为STM32的一个外设, **可以监测指定GPIO口的电平信号。**当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序。
2.1.1 EXTI支持的触发方式
- 上升沿:引脚电平从低电平上升为高电平
- 下降沿:引脚电平从高电平下降为低电平
- 双边沿:引脚电平发生变化
- 软件触发:引脚电平不发生变化,由软件代码执行中断程序
2.1.2 EXTI支持监测的GPIO及其条件
EXTI支持检测所有的GPIO口,但相同的Pin不能同时触发中断。“不能同时”的意思是EXTI不能同时监测例如PA0、PB0,或者PA1、PA2、PA3这样的GPIO_Pin相同的端口。所以如果要使用多个中断引脚,就要选择GPIO_Pin不同的引脚。
2.1.3 EXTI占用的通道
GPIO占用20个中断输入通道。其中包括16个GPIO_Pin,外加PVD(电源电压监测)输出、RTC闹钟、USB唤醒、ETH以太网唤醒。在通道中,16个GPIO_Pin是EXTI的主要功能,其余四个是“蹭网”功能。
EXTI的另一个功能是从低功耗的停止模式下唤醒STM32。 对于电源电压检测PVD,当电源从电压过低恢复时,就需要PVD借助外部中断推出停止模式。对于RTC闹钟来说,为了省电,RTC定一个闹钟之后,STM32会进入停止模式,等到闹钟响的时候再唤醒。USB唤醒和ETH以太网唤醒也是类似的作用。
2.1.4 EXTI触发的响应方式
- 中断响应:申请中断,让CPU执行中断函数
- 事件响应:这是STM32对于EXTI增加的一种额外的功能。当EXTI检测到引脚电平的变化时,正常的流程是选择触发中断,但在STM32中,也可以选择触发一个事件。这时外部中断的信号不会通向CPU,而是通向其他外设,用来触发其他外设的操作。比如触发ADC转换,触发DMA等。事件响应不会触发中断,而是触发别的外设操作,相当于外设之间的联合工作。
2.2 EXTI的工作原理
EXTI的基本结构及工作方式如下图所示:
2.2.1 AFIO(Alternate function I/O)复用功能输入输出进行中断引脚选择
AFIO和GPIO相对,分别称为复用功能输入输出和通用功能输入输出。AFIO主要用于引脚复用功能的选择和重定义。 在STM32中,AFIO主要执行以下两个任务:
- 复用引脚功能重映射:将引脚的默认复用功能替换为重定义功能
- 中断引脚选择
每个GPIO外设有16个引脚,但是EXTI中只有16个GPIO通道,如果每个GPIO的引脚都占用一个EXTI中的GPIO通道,那么EXTI中的GPIO通道明显不够用。所以 需要AFIO实现中断引脚选择的电路模块。 AFIO实际上就是一个 数据选择器,它可以选择所有GPIO的其中的16个引脚接入到EXTI的通道中。它的选择规则是 :对于PA0、PB0、PC0、…、PG0这样的相同的GPIO_Pin引脚,AFIO只能选择一个接入到EXTI的通道0中,类似的,PA1、PB1、PC1、…、PG1中,只有一个能接入EXTI的通道1中。这就是所有的GPIO口都能触发中断,但是相同的GPIO_Pin的端口不能同时触发中断的原因。上述过程如下图所示:
2.2.2 EXTI的边沿检测与控制功能
20个输入信号(16个GPIO_Pin通道、PVD、RTC、USB、ETH)经过EXTI后分成两路输出:通往NVIC触发中断响应和通往其他外设触发事件响应。在通往NVIC的通道中,按理来说应该有20路输出,但ST公司为了节省NVIC的中断资源,将EXTI的5 ~ 9通道合并称一个通道输出(EXTI9_5),将10 ~ 15合并到一个通道输出(EXTI15_10),在编程的时候,在这两个中断函数中,需要再根据标志位来区分到底是哪个端口的中断条件触发。
那么EXTI如何实现边沿的检测、中断的响应以及事件的响应呢?EXTI的内部结构框图如下图所示:
首先,由20条输入信号线输入的信号首先通过边沿检测电路。这个边沿检测电路的检测方式由上升沿触发选择寄存器和下降沿触发选择寄存器控制,由这两个寄存器可以将EXTI的监测的触发方式控制为上升沿触发、下降沿触发或双边沿触发;之后输出信号和软件中断事件寄存器通过一个或门直接输出(只要软件中断有效,或门的输出将为“1”,将立即触发中断,与外部电平无关)。输出信号兵分两路,一路连接到EXTI中断控制器触发中断响应,另一路连接到其他外设触发事件响应。
- 触发中断响应:或门的输出信号通过一个请求挂起寄存器,再和中断屏蔽寄存器的输出信号通过一个与门输出到NVIC中断控制器。读取请求挂起寄存器就相当于读取中断的标志位,可以通过读取判断是哪个通道发生的中断。如果中断屏蔽寄存器允许中断(输出“1”),挂起寄存器输出的“1”将会输出到NVIC中断控制器中。
- 触发事件响应:或门的输出信号与事件屏蔽寄存器的输出信号通过一个与门接入一个脉冲发生器,之后连接到其他外设。
上图中的中断屏蔽寄存器和事件屏蔽寄存器都相当于电子开关的作用。对于它们与图中的寄存器,我们可以通过外设接口和总线访问这些寄存器。
2.3 EXTI的实际应用
在实际应用中,到底什么样的设备需要使用外部中断,使用外部中断又有什么好处?使用外部中断的模块输出的信号有如下特性:
- 外部:由外部模块驱动,STM32只能被动读取
- 突发:STM32不知道信号什么时候来
- 快速:如果STM32没有及时读取,就会错过很多信号(波形)
例如,旋转编码器和红外接收模块输出的信号就具有以上特性。开关信号是其中的一个特例。 虽然按键按下也是外部驱动的突发事件,但是并不推荐使用外部中断来读取按键,原因是外部中断不好处理按键抖动和松手检测的问题;对于按键而言,由于按下按键的物理动作较长和按键抖动的问题,它的输出波形也不一定是转瞬即逝的,所以要求不高的情况下可以在主程序中循环读取,要求较高的情况下可以采用定时器TIM中断读取的方式,以达到后台读取按键值,不阻塞主程序,还可以处理按键抖动和松手检测的问题。
2.3.1 旋转编码器简介
旋转编码器是用来测量旋转的位置、速度、方向的装置。当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向。读取两路输出信号的相位差,或者一路方波信号对应位置和速度,另一路高低电平信号代表方向的输出信号即可得知旋转的方向。旋转编码器主要由以下四种类型:
-
光栅式旋转编码器:通过对射式红外传感器测速。当光栅编码盘转动时,输出口就会输出一个对应的方波,方波的个数对应旋转的角度(位置),方波的频率对应转速(速度)。它的缺点是无法确认旋转的方向。
-
机械触点式旋转编码器(本次课程使用):一般用于调节某一变量,例如调节音量。如下图所示,两侧金属触点的外侧分别连接A、B引脚,内测都连接到C引脚。中间的金属片是一个按键,控制上方的两个引脚的通断(本次实验没有用到)。当编码盘旋转时,依次接通两侧的金属触点,且可以让两侧金属触点的通断产生一个
π
2
\frac {\pi} 2
2π的相位差,通过检测A、B引脚的相位差,就可以在测量旋转角度、速度的基础上测量旋转的方向了(编码盘正传时,B滞后A
π
2
\frac {\pi} 2
2π,编码盘反转时,B超前A
π
2
\frac {\pi} 2
2π)。**这种相位相差
π
2
\frac {\pi} 2
2π的波形称为正交波形。** 带正交波形输出信号的编码器是可以输出旋转方向的。
旋转编码器的硬件电路如下图所示(本次实验中C引脚没有使用 ):
3. 霍尔传感器式旋转编码器:直接连接在电机后,通过圆形磁铁和两个霍尔传感器,同样可以输出正交的方波信号。原理与上一种机械触点式旋转编码器类似。
4. 独立的旋转编码器元件:当输入转动时,输出就会有波形。详细用法请参考手册。
2.3.2 EXTI的配置流程和中断函数的定义
使用EXTI的配置顺序如下:
- 打开GPIO和AFIO的外设时钟。EXTI和NVIC的时钟自动开启,不需要手动开启。按理来说EXTI作为一个独立外设需要开启时钟,但是寄存器中没有EXTI时钟的控制位,原因可能与EXTI的唤醒功能或者电路设计考量有关。NVIC是内核的外设,内核的外设都不需要手动开启时钟(RCC也只能管理内核外的外设,对内核里的外设没有操作权限)。
- 配置GPIO。
像这种外设使用GPIO的情况,如果不清楚GPIO应该配置为什么模式,可以参考手册。EXTI输入线推荐配置为浮空输入、带上拉输入或带下拉输入,所以配置哪一种都可以。
-
配置AFIO。对于AFIO外设,ST公司没有给它分配专门的库函数文件,它的库函数和GPIO在同一个库函数中。打开
stm32f10x_gpio.h文件的最后可以找到以下函数(有些函数在GPIO一节没有讲解到,但是也不常用,这里作统一了解):
// 复位AFIO外设,调用后将清除AFIO的全部配置
void GPIO\_AFIODeInit(void);
// 锁定某个GPIO端口的配置
void GPIO\_PinLockConfig(GPIO_TypeDef\* GPIOx, uint16\_t GPIO_Pin);
// 下面两个函数用来配置AFIO的事件输出功能
void GPIO\_EventOutputConfig(uint8\_t GPIO_PortSource, uint8\_t GPIO_PinSource);
void GPIO\_EventOutputCmd(FunctionalState NewState);
// 进行引脚功能重映射
void GPIO\_PinRemapConfig(uint32\_t GPIO_Remap, FunctionalState NewState);
// 配置AFIO实现EXTI中断引脚选择
void GPIO\_EXTILineConfig(uint8\_t GPIO_PortSource, uint8\_t GPIO_PinSource);
// 配置以太网需要用到的函数(本课程没有涉及)
void GPIO\_ETH\_MediaInterfaceConfig(uint32\_t GPIO_ETH_MediaInterface);
对于本节课使用的GPIO_EXTILineConfig函数,它虽然以GPIO开头,但实际上操作的是AFIO的寄存器:
/\*\*
\* @brief Selects the GPIO pin used as EXTI Line.
\* @param GPIO\_PortSource: selects the GPIO port to be used as source for EXTI lines.
\* This parameter can be GPIO\_PortSourceGPIOx where x can be (A..G).
\* @param GPIO\_PinSource: specifies the EXTI line to be configured.
\* This parameter can be GPIO\_PinSourcex where x can be (0..15).
\* @retval None
\*/
void GPIO\_EXTILineConfig(uint8\_t GPIO_PortSource, uint8\_t GPIO_PinSource)
{
uint32\_t tmp = 0x00;
/\* Check the parameters \*/
assert\_param(IS\_GPIO\_EXTI\_PORT\_SOURCE(GPIO_PortSource));
assert\_param(IS\_GPIO\_PIN\_SOURCE(GPIO_PinSource));
tmp = ((uint32\_t)0x0F) << (0x04 \* (GPIO_PinSource & (uint8\_t)0x03));
AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp;
AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32\_t)GPIO_PortSource) << (0x04 \* (GPIO_PinSource & (uint8\_t)0x03)));
}
-
配置EXTI。首先来学习EXTI的库函数及其作用(有些不常用,仅作了解即可):
// 下面三个函数是一个外设的“模板函数”,即基本所有的外设都拥有以下三个函数,定义和用法都类似
// 清除EXTI的所有配置,清除为上电默认状态
void EXTI\_DeInit(void);
// 用结构体变量配置EXTI
void EXTI\_Init(EXTI_InitTypeDef\* EXTI_InitStruct);
// 给用来参数配置的结构体变量赋一个默认值
void EXTI\_StructInit(EXTI_InitTypeDef\* EXTI_InitStruct);
// 软件触发外部中断,调用之后,可以触发指定中断线上的中断
void EXTI\_GenerateSWInterrupt(uint32\_t EXTI_Line);
// 接下来的四个函数也是一个外设的“模板函数”,用来查看状态寄存器中保存的外设标志位
// 在主程序中查看标志位和清除标志位用下面两个函数,可以查看任意标志位(不触发中断的标志位也可以读取)
FlagStatus EXTI\_GetFlagStatus(uint32\_t EXTI_Line); // 获取指定的标志位是否被置1
void EXTI\_ClearFlag(uint32\_t EXTI_Line); // 对置1的标志位进行清除(如果不清除就会一直触发中断)
// 在中断函数中查看标志位和清除标志位用下面两个函数,它们只能读写与中断有关的标志位,并且对中断是否允许做出判断
ITStatus EXTI\_GetITStatus(uint32\_t EXTI_Line); // 查看中断标志位是否被置1
void EXTI\_ClearITPendingBit(uint32\_t EXTI_Line); // 清除中断挂起标志位
-
配置NVIC。NVIC是内核的外设,其库函数定义在
misc.h中。库函数定义如下所示:
void NVIC\_PriorityGroupConfig(uint32\_t NVIC_PriorityGroup); // 对中断进行分组,配置抢占优先级pre-emption和响应优先级subpriority
void NVIC\_Init(NVIC_InitTypeDef\* NVIC_InitStruct); // NVIC的初始化函数
void NVIC\_SetVectorTable(uint32\_t NVIC_VectTab, uint32\_t Offset); // 设置中断向量表
void NVIC\_SystemLPConfig(uint8\_t LowPowerMode, FunctionalState NewState); // 系统低功耗配置
void SysTick\_CLKSourceConfig(uint32\_t SysTick_CLKSource);
在配置NVIC时,需要按照以下步骤配置(以下面的对射式红外传感器计次实验为例):
1. 对中断优先级进行进行分组,使用`NVIC_PriorityGroupConfig`函数,具体参数详见函数定义注释。
2. 定义`NVIC_Init`需要的结构体变量并给结构体的成员变量赋值。
```
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; // 配置中断通道需要参考stm32f10x.h文件中的中断列表
// 列表中包含所有的f1芯片,不同的芯片中断通道列表不同,故需要根据芯片型号选择STM32F10X\_MD的中断列表
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 指定中断通道是使能还是失能
// 下面两个参数的取值范围需要参考NVIC\_Priority\_Table中不同优先级分组的取值范围
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 将中断定义为NVIC抢占优先级中的取值
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 将中断定义NVIC响应优先级中的取值
```
定义中断函数需要注意:STM32中中断函数的名称都是固定的。在启动文件startup_stm32f10x_md.s中的中断向量表中可以找到以IRQHandler结尾的就是中断函数的名称。在中断函数中,我们还需要对中断标志位进行判断。因为如果选择EXTI15_10_IRQHandler函数,EXTI15到EXTI10都能进入函数,如果连接EXTI14,我们需要确定是我们想要的中断源(EXTI14)触发了这个函数。使用EXTI_GetITStatus函数判断中断标志位,在中断函数中完成相关功能后还需要使用EXTI_ClearITPendingBit函数清除中断挂起标志位。形式如下所示:
void EXTI15\_10\_IRQHandler(void)
{
if (EXTI\_GetITStatus(EXTI_Line14) == SET) // 检查EXTI\_Line14的中断标志位是否为1
{
/\*
中断函数中需要执行的操作...
\*/
EXTI\_ClearITPendingBit(EXTI_Line14); // 清除EXTI\_Line14的中断标志位
}
}
2.3.3 对射式红外传感器计次
建立工程并添加工程文件CountSensor.c/.h后,首先要在CountSensor_Init函数中配置实现该功能需要的全部外设。
硬件连接图和源码如下所示:
CountSensor.c
#include "stm32f10x.h" // Device header
uint16\_t CountSensor_Conut;
void CountSensor\_Init(void)
{
// 开启所需要使用的外设的时钟,EXTI和NVIC的时钟自动开启
RCC\_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC\_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO\_Init(GPIOB, &GPIO_InitStructure);
// 配置AFIO
GPIO\_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
// 配置EXTI(将EXTI的第14条线路配置为中断触发模式)
EXTI_InitTypeDef EXTI_InitStructure; // 定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line14; // EXTI对应的输入线路
EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 是否开启中断
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // EXTI配置为中断触发模式(与事件触发模式相对)
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发(该参数的定义在官方文件中是错误的,应该在枚举变量EXTITrigger\_TypeDef中选择)
EXTI\_Init(&EXTI_InitStructure);
// 配置NVIC(分组方式整个芯片只能用同一种)
NVIC\_PriorityGroupConfig(NVIC_PriorityGroup_2); // NVIC中断优先级分组,整个工程只能使用一个分组,如果在模块中配置需要保证每个模块中的分组方式都相同
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 指定中断通道是使能还是失能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // NVIC抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // NVIC响应优先级
NVIC\_Init(&NVIC_InitStructure);
}
uint16\_t CountSensor\_Get(void)
{
return CountSensor_Conut;
}
void EXTI15\_10\_IRQHandler(void)
{
if (EXTI\_GetITStatus(EXTI_Line14) == SET) // 检查EXTI\_Line14的中断标志位是否为1
{
if (GPIO\_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0) // 如果数字乱跳,可以在这里再次检查端口的电平信号
{
CountSensor_Conut ++;
}
EXTI\_ClearITPendingBit(EXTI_Line14); // 清除EXTI\_Line14的中断标志位
}
}
main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "CountSensor.h"
int main()
{
OLED\_Init();
OLED\_ShowString(1, 1, "Count:"); // 显示一个字符串
CountSensor\_Init();
while(1)
{
OLED\_ShowNum(1, 7, CountSensor\_Get(), 5);
}
}
2.3.3 旋转编码器计次
硬件连接图和源码如下所示:
Encoder.c
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新