STM32学习笔记(4)——NVIC中断优先级管理和外部中断EXTI
一、NVIC中断优先级管理
1. 中断简介
在Cortex-M3(CM3)内核中,每个中断的优先级都是用寄存器中的8位来设置的,这样就有2^8 =256级中断,意味着可以支持256个中断,这其中包含了16个内核中断和240个外部中断,并且具有256级的可编程中断设置。但许多芯片厂商并没有使用CM3内核的全部东西,而是只用了它的一部分,而多余的部分应该是设计者考虑到后续应用发展而冗余设计的。
实际情况中,芯片厂商根据自己生产的芯片做出了调整。比如ST(意法半导体)公司的STM32F1xx和F4xx系列只使用了这个(寄存器NVIC->IPR
,如图所示)8位中的高四位[7:4],低四位取零,这样2^4=16,只能表示16级中断嵌套。
STM32有84个中断,包括16个内核中断和68个可屏蔽中断,具有16级可编程的中断优先级。我们使用的是STM32F103系列,只有60个可屏蔽中断,而在107系列有68个。
2. 中断向量表
中断向量表为每个外设作了硬件编号,可以把它理解为默认顺序。如果有两个外设工作顺序发生冲突(一般在NVIC设置好后就很少发生这种情况)时,就按照这个表来分执行先后。
STM32的中断向量表如下(可对照STM32中文参考手册9.1.2节中断和异常向量中的表):
其中灰色的部分(图片未显示)为异常向量,其余白色部分为中断向量。
在头文件stm32f10x.h的163行开始中定义了各中断向量的顺序编号,现摘录该定义(IRQ = Interrupt Request):
/****** Cortex-M3 Processor Exceptions Numbers 内核处理器异常编号(用户一般不使用,说白就是不用管) ***************************************************/
NonMaskableInt_IRQn = -14, /*!< 2 Non Maskable Interrupt */
······
/****** STM32 specific Interrupt Numbers STM32特定中断号 *********************************************************/
WWDG_IRQn = 0, /*!< Window WatchDog Interrupt */
PVD_IRQn = 1, /*!< PVD through EXTI Line detection Interrupt */
TAMPER_IRQn = 2, /*!< Tamper Interrupt */
······
//省略号表示下面还有很多很多,而且使用的是条件编译,因为不同型号对应不同中断号。
在头文件core_cm3.h中可以看到配置与中断相关的寄存器。实际上ST芯片用不到这么大的寄存器,因此我们在网上借鉴了一段代码,反映了ST芯片真实使用到的寄存器大小。其余未使用到的空间均为保留。
/*
cortex-m3内核分组方式(8组)结构体表达方式:
*/
typedef struct
{
__IO uint32_t ISER[8]; //中断使能设置寄存器,作用:用来使能中断
//32位寄存器,每个位控制一个中断的使能。STM32F10x只有60个可屏蔽中断,所以只使用了其中的ISER[0]和ISER[1]。
//ISER[0]的bit0~bit31分别对应中断0~31。ISER[1]的bit0~27对应中断32~59;
/*!< 偏移量: 0x000 Interrupt Set Enable Register */
uint32_t RESERVED0[24]; //这些保留的不用看,也不要使用
__IO uint32_t ICER[8]; //中断清除使能寄存器,作用:用来失能中断
//32位寄存器,每个位控制一个中断的失能。STM32F10x只有60个可屏蔽中断,所以只使用了其中的ICER[0]和ICER[1]。
//ICER[0]的bit0~bit31分别对应中断0~31。ICER[1]的bit0~27对应中断32~59,下面都差不多
/*!<偏移量: 0x080 Interrupt Clear Enable Register */
uint32_t RSERVED1[24];
__IO uint32_t ISPR[8]; //中断挂起设置寄存器,作用:用来挂起中断,就是我之前关掉这个中断,但现在我想打开它了!
/*!< 偏移量: 0x100 Interrupt Set Pending Register */
uint32_t RESERVED2[24];
__IO uint32_t ICPR[8]; //中断清除挂起寄存器,作用:用来解挂中断,说白了就是我不想用这个中断,暂时关掉!
/*!<偏移量: 0x180 Interrupt Clear Pending Register */
uint32_t RESERVED3[24];
__IO uint32_t IABR[8]; //中断激活状态位寄存器,作用:只读,通过它可以知道当前在执行的中断是哪一个
//问题:既然只读的话为何不声明 __I ?没搞懂
/*!< 偏移量: 0x200 Interrupt Active bit Register */
uint32_t RESERVED4[56];
__IO uint8_t IP[240]; //中断优先级寄存器
//240个8位寄存器,每个中断使用一个寄存器来确定优先级。STM32F10x系列一共60个可屏蔽中断,使用IP[59]~IP[0]。
//之前已经讲过,每个IP寄存器的高4位用来设置抢占和响应优先级(根据分组),低4位没有用到。
/*!< 偏移量: 0x300 Interrupt Priority Register (8Bit wide) */
uint32_t RESERVED5[644]; //软件触发方式寄存器
__O uint32_t STIR; /*!< 偏移量: 0xE00 Software Trigger Interrupt Register */
} NVIC_Type;
/*
实际STM32分组(5组)方式结构体表达方式
*/
typedef struct
{
vu32 ISER[2]; //v表示volatile,这个关键字很重要,以后有时间去研究研究
u32 RESERVED0[30];
vu32 ICER[2];
u32 RSERVED1[30];
vu32 ISPR[2];
u32 RESERVED2[30];
vu32 ICPR[2];
u32 RESERVED3[30];
vu32 IABR[2];
u32 RESERVED4[62];
vu32 IPR[15]; //Interrupt Priority Registers,这里可以与上面的IP对应
//大家可以算算,每个寄存器仅占用4位,15*4=60是不是正好。
} NVIC_TypeDef;
这么多的中断,该怎样管理呢?NVIC这时要出场了。
3. 嵌套向量中断控制器(NVIC)
NVIC的全称是Nested vectoredinterrupt controller,即嵌套向量中断控制器。
需要注意一点:NVIC是Cortex-M3核心的一部分,因此就不要在STM32中文参考手册里面找了(STM32:关我鸟事?),应查阅ARM的Cortex-M3技术参考手册。
NVIC一个很重要的概念是优先级分组。和51单片机不同,NVIC将优先级分为两个:抢占优先级(PreemptionPriority)和响应优先级(SubPriority)。从英文就知道了,抢占优先级比响应优先级要高。同一个优先级上,数字越小,优先级越高。工作原理如下:
【情况一】外设B工作时遇到中断请求,外设A需要工作,因为A的抢占优先级高,这时外设B会立刻停止,外设A抢占B开始工作。如果遇到多个中断请求,还会进入中断嵌套,形成“套娃”。所以,抢占优先级是可以嵌套的。
如果是A和B同时到来,因为A的抢占优先级高,会先执行A,后执行B。
外设 | 抢占优先级 | 响应优先级 | 中断向量号 |
---|---|---|---|
A | 1 | 2 | 3 |
B | 2 | 1 | 4 |
【情况二】外设B工作时遇到中断请求,外设A需要工作,因为两者抢占优先级相同,此时只能实行先到先得,B弄完后A再来了。所以,响应优先级是不能嵌套的。
如果是A和B同时到来,因为两者抢占优先级相同,此时继续比较响应优先级,B的响应优先级高,会先执行B,后执行A。
外设 | 抢占优先级 | 响应优先级 | 中断向量号 |
---|---|---|---|
A | 1 | 2 | 3 |
B | 1 | 1 | 4 |
【情况三】外设B工作时遇到中断请求,外设A需要工作,因为两者抢占优先级和响应优先级相同,所以实行的是先到先得的办法,谁先执行就让谁了。
如果是A和B同时到来,因为A的中断向量号小,会先执行A,后执行B。
外设 | 抢占优先级 | 响应优先级 | 中断向量号 |
---|---|---|---|
A | 1 | 1 | 3 |
B | 1 | 1 | 4 |
4. NVIC的定义以及库函数
在STM32中,优先级编号不是你想多少就多少的,是有规定的。首先,NVIC对STM32中断进行分组,一共5个组,组0~4。同时,对每个中断设置一个抢占优先级和一个响应优先级值。分组配置是在寄存器SCB->AIRCR(可在头文件core_cm3.h找到)中配置的。
如下表:
对应:
- 第0组:所有4位(仅能用0-15设置优先级别,下同)用于指定响应优先级
- 第1组:最高1位(0-1)用于指定抢占式优先级,最低3位(0-7)用于指定响应优先级
- 第2组:最高2位(0-3)用于指定抢占式优先级,最低2位(0-3)用于指定响应优先级
- 第3组:最高3位(0-7)用于指定抢占式优先级,最低1位(0-1)用于指定响应优先级
- 第4组:所有4位(0-15)用于指定抢占式优先级
NVIC的定义(位于头文件misc.h,80行还有一个和上面一样的表格)如下:
typedef struct
{
uint8_t NVIC_IRQChannel; //指定是哪个外设需要中断,各外设向量号在stm32f10x.h可以看到
uint8_t NVIC_IRQChannelPreemptionPriority; //设置抢占优先级编号(至于最大可以多少参照上表)
uint8_t NVIC_IRQChannelSubPriority; //设置响应优先级编号(至于最大可以多少参照上表)
FunctionalState NVIC_IRQChannelCmd; //使能中断通道
} NVIC_InitTypeDef;
NVIC的库函数定义(位于头文件misc.h)如下:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
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);
//与挂起和解挂有关的函数:
static __INLINE void NVIC_SetPendingIRQ(IRQn_Type IRQn);
static __INLINE uint32_t NVIC_GetPendingIRQ(IRQn_Type IRQn);
static __INLINE void NVIC_ClearPendingIRQ(IRQn_Type IRQn);
//与中断标志激活位有关的函数(作用就是看这个中断有没挂起,别想多了):
static __INLINE uint32_t NVIC_GetActive(IRQn_Type IRQn);
使用NVIC的流程如下:
NVIC_InitTypeDef NVIC_InitStructure;
// 1.选择优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
// 2.选择需要产生中断的外设
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;
// 3.设置抢占优先级级别
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
// 4.设置响应优先级级别
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
// 5.使能中断
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
// 6.初始化中断
NVIC_Init(&NVIC_InitStructure);
二、外部中断EXTI
EXTI(External interrupt / event controller)又叫外部中断/事件控制器,EXTI是ST公司在其STM32产品上扩展的外中断控制。它负责管理映射到GPIO引脚上的外中断和片内几个集成外设的中断(PVD,RTC闹钟,USB唤醒,以太网),以及软件中断。其输出最终被映射到NVIC的相应通道。因此,配置EXTI中断的过程必然包含对NVIC的配置。(不严谨理解:NVIC包含EXTI)
在头文件stm32f10x.h中,我们用的是STM32F10X_HD型,所以有关EXTI的中断向量号分别如下(尤其需要注意最后两个):
EXTI0_IRQn = 6, /*!< EXTI Line0 Interrupt */
EXTI1_IRQn = 7, /*!< EXTI Line1 Interrupt */
EXTI2_IRQn = 8, /*!< EXTI Line2 Interrupt */
EXTI3_IRQn = 9, /*!< EXTI Line3 Interrupt */
EXTI4_IRQn = 10,
EXTI9_5_IRQn = 23, /*!< External Line[9:5] Interrupts */
EXTI15_10_IRQn = 40, /*!< External Line[15:10] Interrupts */
EXTI由19个(互联型为20个)产生事件/中断请求的边沿检测器组成,每个输入线可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。
1. EXTI功能框图
(该图来自火哥PPT)
对照图中编号顺序来讲讲各个元素(以下寄存器配置对照STM32中文参考手册9.3EXTI寄存器描述):
(0)信号线
图中箭头为信号线,可以看到箭头所指方向为信号传输方向,双箭头表示信号可双向传导。上面的“/20”表示在控制器内部类似的信号线路有20个,这里是省略了其余19个信号线(这里显示的是互联型)。这里详细说一下哪些可以作为输入:每个IO都可以作为外部中断输入,中断控制器支持19个外部中断/事件请求。具体如下:
- 线(EXTI_Linex)0-15:对应外部IO口的输入中断(PX0、PX1、···、PX15,X = A、B、C、D、E、F、G、H、I)
- 线16:连接到PVD输出
- 线17:连接到RTC闹钟事件
- 线18:连接到USB唤醒事件
- 线19(只适用于互联型):连接到以太网唤醒事件
下图显示的是在AFIO_EXTICR寄存器的EXTIx位(AFIO是复用GPIO,以后会讲到):
(1)输入线
EXTI有19个中断/事件输入线,中断输入可以来自GPIO,也可以来自外设。
(2)边沿检测电路
可以通过寄存器设置电路是上升沿触发(EXTI_RTSR)或下降沿触发(EXTI_FTSR)或两者皆触发。若检测到有效信号则输出高电平(1),否则低电平(0)。
(3)或门
输入分别是来自边沿检测电路和软件中断寄存器(EXTI_SWIER),只要有一个输入为高电平,输出即为高电平。
(4)与门
输入分别来自中断屏蔽寄存器(EXTI_IMR)和请求挂起寄存器(EXTI_PR),需要两个输入为高电平,输出才为高电平。
(5)输出至NVIC
将EXTI_PR寄存器内容输出到NVIC内,从而实现系统中断事件控制。以上均为NVIC路线。
(6)与门
输入分别来自事件屏蔽寄存器(EXTI_EMR)和或门输出,需要两个输入为高电平,输出才为高电平。
(7)脉冲发生器
当输入端,即与门(6)的输出端,是一个高电平就会产生一个脉冲;如果输入端是低电平就不会输出脉冲。
(8)产生事件
脉冲发生器产生的脉冲信号,是事件线路最终的产物,这个脉冲信号可以给其他外设电路使用。
2. EXTI的定义以及库函数
在头文件stm32f10x_exti.h中定义了结构体EXTI:
typedef struct
{
uint32_t EXTI_Line;
// EXTI中断/事件线选择,参数为EXTI_Linex
EXTIMode_TypeDef EXTI_Mode;
// EXTI模式选择:中断(EXTI_Mode_Interrupt)触发或者事件(EXTI_Mode_Event)触发
EXTITrigger_TypeDef EXTI_Trigger;
// EXTI边沿触发类型选择:上升沿触发(EXTI_Trigger_Rising)、下降沿触发(EXTI_Trigger_Falling)
//或者上升沿和下降沿都触发(EXTI_Trigger_Rising_Falling)
FunctionalState EXTI_LineCmd;
// 使能EXTI
}EXTI_InitTypeDef;
同时,也定义了库函数(参考原子PPT):
①void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
//设置IO口与中断线的映射关系
exp: GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource2);
②void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
//初始化中断线:触发方式等
③ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
//判断中断线中断状态,是否发生
④void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
//清除中断线上的中断标志位
在设置好EXTI后,还要指定中断后程序要干什么,这就需要用户自己定义中断服务函数,但是函数名不能乱起,在startup_stm32f10x_hd.s文件中已经写好了中断向量表的顺序(从62行__Vectors开始),这些就是函数名。至于怎么写,待会举个例子就好了。
使用EXTI的流程较繁琐,如下:
// 0.先配置想要产生中断的GPIO,用GPIO配置的方法配置
EXTI_InitTypeDef EXTI_InitStructure;
// 1.开启(使能)GPIO复用时钟(关键一步)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 2.选择IO口与中断线的映射关系
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource3);
// 3.EXTI中断/事件线选择
EXTI_InitStructure.EXTI_Line = EXTI_Line3;
// 4.EXTI边沿触发类型选择
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
// 5.EXTI模式选择
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
// 6.使能EXTI
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
// 7.初始化EXTI
EXTI_Init(&EXTI_InitStructure);
// 8.编写中断服务函数,中断后你想干嘛都写在这里面
EXTIx_IRQHandler(){···}
// 9.别忘了在中断服务函数里面清除中断标志位
EXTI_ClearITPendingBit();
三、一个简单的例程
功能:按键KEY1按下,实现一次LED0反转。初始状态时LED0灭。用NVIC和EXTI中断实现。
部分程序(省略led部分)如下:
// main.c
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"
#include "exti_config.h"
int main(void)
{
LED_Init();
delay_init();
EXTI_Key_Config();
EXTI_NVIC_Config();
while(1){}
}
//exti_config.h
#ifndef __EXTI_CONFIG_H
#define __EXTI_CONFIG_H
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"
#define KEY1 GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_3)
void EXTI_Key_Config(void);
void EXTI_NVIC_Config(void);
#endif /* __EXTI_CONFIG_H */
//exti_config.c
#include "exti_config.h"
void EXTI_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); // NVIC组别为0
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //响应优先级为1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
// KEY1: PE3
void EXTI_Key_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
//初始化GPIOE.3
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOE, &GPIO_InitStructure);
//为GPIOE.3配置EXTI
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource3);
EXTI_InitStructure.EXTI_Line = EXTI_Line3;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
}
//中断服务函数
void EXTI3_IRQHandler(void)
{
delay_ms(10); //按键延时判断
if(KEY1 == 0)
{
LED0 = !LED0;
}
EXTI_ClearITPendingBit(EXTI_Line3); //记得最后清除中断标志位,为下一次中断做准备
}