一、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):
1 2 3 4 5 6 7 8 9 10 11
| NonMaskableInt_IRQn = -14, ······
WWDG_IRQn = 0, PVD_IRQn = 1, TAMPER_IRQn = 2, ······
|
在头文件core_cm3.h中可以看到配置与中断相关的寄存器。实际上ST芯片用不到这么大的寄存器,因此我们在网上借鉴了一段代码,反映了ST芯片真实使用到的寄存器大小。其余未使用到的空间均为保留。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
|
typedef struct { __IO uint32_t ISER[8]; uint32_t RESERVED0[24]; __IO uint32_t ICER[8]; uint32_t RSERVED1[24]; __IO uint32_t ISPR[8]; uint32_t RESERVED2[24]; __IO uint32_t ICPR[8]; uint32_t RESERVED3[24]; __IO uint32_t IABR[8]; uint32_t RESERVED4[56]; __IO uint8_t IP[240]; uint32_t RESERVED5[644]; __O uint32_t STIR; } NVIC_Type;
typedef struct { vu32 ISER[2]; 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]; } 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行还有一个和上面一样的表格)如下:
1 2 3 4 5 6 7 8 9 10
| typedef struct { uint8_t NVIC_IRQChannel;
uint8_t NVIC_IRQChannelPreemptionPriority;
uint8_t NVIC_IRQChannelSubPriority;
FunctionalState NVIC_IRQChannelCmd; } NVIC_InitTypeDef;
|
NVIC的库函数定义(位于头文件misc.h)如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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的流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
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的中断向量号分别如下(尤其需要注意最后两个):
1 2 3 4 5 6 7
| EXTI0_IRQn = 6, EXTI1_IRQn = 7, EXTI2_IRQn = 8, EXTI3_IRQn = 9, EXTI4_IRQn = 10, EXTI9_5_IRQn = 23, EXTI15_10_IRQn = 40,
|
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:
1 2 3 4 5 6 7 8 9 10 11 12
| typedef struct { uint32_t EXTI_Line; EXTIMode_TypeDef EXTI_Mode; EXTITrigger_TypeDef EXTI_Trigger; FunctionalState EXTI_LineCmd; }EXTI_InitTypeDef;
|
同时,也定义了库函数(参考原子PPT):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ①void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
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的流程较繁琐,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| EXTI_InitTypeDef EXTI_InitStructure;
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);
EXTIx_IRQHandler(){···}
EXTI_ClearITPendingBit();
|
三、一个简单的例程
功能:按键KEY1按下,实现一次LED0反转。初始状态时LED0灭。用NVIC和EXTI中断实现。
部分程序(省略led部分)如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| #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){} }
#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
#include "exti_config.h"
void EXTI_NVIC_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }
void EXTI_Key_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; 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); 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); }
|