STM32 中断
1. 基础知识
- Cortex-M3支持 256 个中断,其中包含了 16 个内核中断,240个外部中断。
- STM32只有 84 个中断,包括 16 个内核中断和 68 个可屏蔽中断。
- STM32F103上只有 60 个可屏蔽中断,F107上才有 68 个中断。
- 先占优先级(PreemptionPriority)也就是抢占优先级 。假设有两中断先后触发,已经在执行的中断 先占优先级如果没有后触发的中断 先占优先级更高,就会先处理先占优先级高的中断。也就是说又有较高的先占优先级的中断可以打断先占优先级较低的中断。这是实现中断嵌套的基础。 次占优先级(SubPriority) ,也就是 响应优先级只在同一先占优先级的中断同时触发时起作用,先占优先级 相同,则优先执行次占优先级较高的中断。 次占优先级不会造成中断嵌套 。如果中断的两个优先级 都一致,则优先执行位于中断向量表中位置较高的中断。
2. 中断的概念
中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。形象点比喻就是:你在做一件事情,然后电话打进来,你去做电话里要求你做的事情,然后做完了又回来做刚才没做完的事情。
在Cortex-M3(CM3)内核中,每个中断的优先级都是用寄存器中的 8 位来设置的,这样就有
2
8
2^8
28=256级中断,意味
着可以支持 256 个中断,这其中包含了 16 个内核中断和 240 个外部中断,并且具有 256 级的可编程中断设置。但许多芯片厂商并没有使用CM3内核的全部东⻄,而是只用了它的一部分,而多余的部分应该是设计者考虑到后续应用发展而冗余设计的。
实际情况中,芯片厂商根据自己生产的芯片做出了调整。比如ST(意法半导体)公司的STM32F1xx和F4xx系列只使用了这个(寄存器NVIC->IPR,如图所示)8位中的高四位[7:4],低四位取零,这样 2 4 2^4 24=16,只能表示 16 级中断嵌套。
bit7 | bit6 | bit5 | bit4 | bit3 | bit2 | bit1 | bit0 |
---|---|---|---|---|---|---|---|
用于表达优先级 | 用于表达优先级 | 用于表达优先级 | 用于表达优先级 | 未使用,读回为 0 | 未使用,读回为 0 | 未使用,读回为 0 | 未使用,读回为 0 |
STM32有 84 个中断,包括 16 个内核中断和 68 个可屏蔽中断,具有 16 级可编程的中断优先级。我们使用的是STM32F103系列,只有 60 个可屏蔽中断,而在 107 系列有 68 个。
3. NVIC 嵌套向量中断控制器
全称 Nested Vectored Interrupt Controller ,属于内核外设,管理着包括内核和片上所有外设的中断相关
的功能。用于为中断分组,从而分配抢占优先级和响应优先级。
STM32通过中断控制器NVIC(Nested Vectored Interrupt Controller)进行中断的管理 。NVIC是属于Cortex内核的
器件,不可屏蔽中断(NMI)和外部中断都由它来处理,但是SYSTICK不是由NVIC控制的。
STM32的中断向量具有两个属性:抢占属性和响应属性。其属性编号越小,优先级越高。抢占,是指打断其他中断
的属性,即会出现中断的嵌套。响应,是指在抢占相同的情况下,优先处理响应优先级高的中断。
例如
情况一:
外设B工作时遇到中断请求,外设A需要工作,因为A的抢占优先级高,这时外设B会立刻停止,外设A抢占B开始工作。如果遇到多个中断请求,还会进入中断嵌套,形成“套娃”。所以,抢占优先级是可以嵌套的。
外设 | 抢占优先级 | 响应优先级 | 中断向量号 |
---|---|---|---|
A | 1 | 2 | 3 |
B | 2 | 1 | 4 |
如果是A和B同时到来,因为A的抢占优先级高,会先执行A,后执行B。
情况二:
外设B工作时遇到中断请求,外设A需要工作,因为两者抢占优先级相同,此时只能实行先到先得,B弄完后A再来了。所以,响应优先级是不能嵌套的。
外设 | 抢占优先级 | 响应优先级 | 中断向量号 |
---|---|---|---|
A | 1 | 2 | 3 |
B | 1 | 1 | 4 |
如果是A和B同时到来,因为两者抢占优先级相同,此时继续比较响应优先级,B的响应优先级高,会先执行B,后执行A。
情况三:
外设B工作时遇到中断请求,外设A需要工作,因为两者抢占优先级和响应优先级相同,所以实行的是先到先得的办法,谁先执行就让谁了。
外设 | 抢占优先级 | 响应优先级 | 中断向量号 |
---|---|---|---|
A | 1 | 1 | 3 |
B | 1 | 1 | 4 |
如果是A和B同时到来,因为A的中断向量号小,会先执行A,后执行B。
抢占优先级和响应优先级共同通过一个 4 位数字来决定,有 5 组分配方式。
优先级分组 | 抢占优先级 | ⼦优先级 | ⾼四位使⽤情况描述 |
---|---|---|---|
NVIC_PriorityGroup_0 | 0级抢占优先级 | 0-15级⼦优先级 | 0bit⽤于抢占优先级 4bit全⽤于⼦优先级 |
NVIC_PriorityGroup_1 | 0-1级抢占优先级 | 0-7级⼦优先级 | 1bit⽤于抢占优先级 3bit全⽤于⼦优先级 |
NVIC_PriorityGroup_2 | 0-3级抢占优先级 | 0-3级⼦优先级 | 2bit⽤于抢占优先级 2bit全⽤于⼦优先级 |
NVIC_PriorityGroup_3 | 0-7级抢占优先级 | 0-1级⼦优先级 | 3bit⽤于抢占优先级 1bit全⽤于⼦优先级 |
NVIC_PriorityGroup_4 | 0-15级抢占优先级 | 0级⼦优先级 | 4bit⽤于抢占优先级 0bit全⽤于⼦优先级 |
对应:
-
第 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);
4. EXTI 外部中断
全称 External Interrupt
Cortex内核把能够打断当前代码执行流程的事件分为 异常(Exception)和中断(Interrupt) ,并把它们用一个表进行管理,即 中断向量表 。STM32对这个表进行编排,将编号 -3至 6 的中断向量定义为 系统异常 ,从编号 7 开始的为 外部中断 。
STM32的所有GPIO都引入到EXTI外部中断线上了,PA0-PG0连接到EXTI0上,PA1-PG1连接到EXTI1上,PA15-PG15连接到EXTI15上。
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 ; //!< EXTI Line4 Interrupt
EXTI9_5_IRQn = 23 ; //!< External Line[9:5] Interrupts
EXTI5_10_IRQn = 40 ; //!< External Line[15:10] Interrupts
EXTI由 19 个(互联型为 20 个)产生事件/中断请求的边沿检测器组成,每个输入线可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。
EXTI功能框图
- 信号线 图中箭头为信号线,可以看到箭头所指方向为信号传输方向,双箭头表示信号可双向传导。上面的“/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):
-
输入线 EXTI有 19 个中断/事件输入线,中断输入可以来自GPIO,也可以来自外设。
-
边沿检测电路 可以通过寄存器设置电路是上升沿触发(EXTI_RTSR)或下降沿触发(EXTI_FTSR)或两者皆触发。若检测到有效信号则输出高电平(1),否则低电平(0)。
-
或⻔ 输入分别是来自边沿检测电路和软件中断寄存器(EXTI_SWIER),只要有一个输入为高电平,输出即为高电平。
-
与⻔ 输入分别来自中断屏蔽寄存器(EXTI_IMR)和请求挂起寄存器(EXTI_PR),需要两个输入为高电平,输出才为高电平。
-
输出至NVIC 将EXTI_PR寄存器内容输出到NVIC内,从而实现系统中断事件控制。以上均为NVIC路线。
-
与⻔ 输入分别来自事件屏蔽寄存器(EXTI_EMR)和或⻔输出,需要两个输入为高电平,输出才为高电平。
-
脉冲发生器 当输入端,即与⻔(6)的输出端,是一个高电平就会产生一个脉冲;如果输入端是低电平就不会输出脉冲。
-
产生事件 脉冲发生器产生的脉冲信号,是事件线路最终的产物,这个脉冲信号可以给其他外设电路使用。
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;
同时,也定义了库函数;
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();