NVIC的全称是Nested vectored interrupt controller,即嵌套向量中断控制器。
高抢占式优先级的中断可以在具有低抢占式优先级的中断服务程序执行过程中被响应,即中断嵌套。
使用NVIC1统一管理中断。
CM3 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断,并且具有 256级的可编程中断设置。
CM3中每个中断通道都具备自己的8位中断优先级控制字节, 但STM32 并没有使用 CM3 内核的全部东西,STM32F103中只使用4位,高4位有效。
STM32 有 84 个中断,包括 16 个内核中断和 68 个可屏蔽中断,具有 16 级可编程的中断优先级。
具体表格见stm32中文参考手册9.1.2。所有中断都在了。
STM32F405xx/07xx 和 STM32F415xx/17xx 具有 82 个可屏蔽中断通道,STM32F42xxx和 STM32F43xxx 具有多达 86 个可屏蔽中断通道(不包括 Cortex™-M4F 的 16 根中断线)。
STM32 将中断分为 5 个组,组 0~4。该分组的设置是由内核外设 SCB 的应用程序中断及复位控制寄存器 AIRCR 的 PRIGROUP[10:8]bit10~8 位决定
例如组设置为 3,那么此时所有的 60 个中断,每个中断的中断优先寄存器的高四位中的最高 3 位是抢占优先级,低 1 位是响应优先级。
优先级原则
中断优先级分组函数 ,在misc.c中可以查看,其函数申明如下:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
这个函数的作用是对中断的优先级进行分组,这个函数在系统中只能被调用一次,一旦分组确定就最好不要更改。
#define NVIC_PriorityGroup_0 ((uint32_t)0x700) /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PriorityGroup_1 ((uint32_t)0x600) /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PriorityGroup_2 ((uint32_t)0x500) /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PriorityGroup_3 ((uint32_t)0x400) /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PriorityGroup_4 ((uint32_t)0x300) /*!< 4 bits for pre-emption priority
0 bits for subpriority */
#define IS_NVIC_PRIORITY_GROUP(GROUP) (((GROUP) == NVIC_PriorityGroup_0) || \
((GROUP) == NVIC_PriorityGroup_1) || \
((GROUP) == NVIC_PriorityGroup_2) || \
((GROUP) == NVIC_PriorityGroup_3) || \
((GROUP) == NVIC_PriorityGroup_4))
比如我们设置整个系统的中断优先级分组值为 2,那么方法是:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
这样就确定了一共为“2 位抢占优先级,2 位响应优先级”。
NVIC为内核外设,程序中不需要开启专门的NVIC时钟。
下面我们讲解一个重要的函数为中断初始化函数 NVIC_Init,其函数申明为:
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
其中 NVIC_InitTypeDef 是一个结构体,我们可以看看结构体的成员变量:
typedef struct
{
uint8_t NVIC_IRQChannel;
uint8_t NVIC_IRQChannelPreemptionPriority;
uint8_t NVIC_IRQChannelSubPriority;
FunctionalState NVIC_IRQChannelCmd;
} NVIC_InitTypeDef;
NVIC_InitTypeDef 结构体中间有三个成员变量,这三个成员变量的作用是:
定义初始化的是哪个中断,这个我们可以在 stm32f10x.h 中找到每个中断对应的名字。例如 USART1_IRQn。
定义这个中断的抢占优先级别。
定义这个中断的子优先级别。
该中断是否使能。
我们要使能串口 1 的中断,同时设置抢占优先级为 1,子优先级位 2,初始化的方法是:
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口 1 中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;// 抢占优先级为 1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;// 子优先级位 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure); //根据上面指定的参数初始化 NVIC 寄存器
书接上文,配置完中断优先级之后,接着就是编写中断服务函数。
单独设置优先级。
__STATIC_INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
if(IRQn < 0) {
SCB->SHP[((uint32_t)(IRQn) & 0xF)-4] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); } /* set Priority for Cortex-M System Interrupts */
else {
NVIC->IP[(uint32_t)(IRQn)] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); } /* set Priority for device specific Interrupts */
}
STM32 的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能方便的使用 STM32 的中断。
typedef struct
{
__IO uint32_t ISER[8]; /*!< Interrupt Set Enable Register */
uint32_t RESERVED0[24];使能
__IO uint32_t ICER[8]; /*!< Interrupt Clear Enable Register */
uint32_t RSERVED1[24];除能
__IO uint32_t ISPR[8]; /*!< Interrupt Set Pending Register */
uint32_t RESERVED2[24];悬起
__IO uint32_t ICPR[8]; /*!< Interrupt Clear Pending Register */
uint32_t RESERVED3[24];清悬
__IO uint32_t IABR[8]; /*!< Interrupt Active bit Register */
uint32_t RESERVED4[56];有效位寄存器
__IO uint8_t IP[240]; /*!< Interrupt Priority Register, 8Bit wide */
uint32_t RESERVED5[644];优先级寄存器
__O uint32_t STIR; /*!< Software Trigger Interrupt Register */
} NVIC_Type;软件触发中断寄存器
NVIC 有一个专门的寄存器,中断优先级寄存器 NVIC_IPRx,全称是:Interrupt Priority Registers,用来配置外部中断的优先级,IPR 宽度为 8bit,原则上每个外部中断可配置的优先级为 0~255,数值越小,优先级越高。
这个寄存器组相当重要!
STM32 的中断分组与这个寄存器组密切相关。
IP 寄存器组由 240 个 8bit 的寄存器组成,每个可屏蔽中断占用8bit,这样总共可以表示 240 个可屏蔽中断。
STM32 只用到了其中的前 60 个。IP[59]~IP[0]分别对应中断 59~0。
每个可屏蔽中断占用的 8bit 并没有全部使用,而是 只用了高 4 位。这 4 位,又分为抢占优先级和子优先级。抢占优先级在前,子优先级在后。而这两个优先级各占几个位又要根据 SCB->AIRCR 中的中断分组设置来决定。
ISER[8] 全称是:Interrupt Set-Enable Registers
CM3 内核支持 256 个中断,这里用 8 个 32 位寄存器来控制,每个位控制一个中断。
CM3 中可以有 240 对使能位/除能位,每个中断拥有一对。
STM32F103 的可屏蔽中断只有 60 个,所以对我们来说,有用的只有两个(ISER[0]和 ISER[1])
STM32F103 只用了其中的前 60 位。ISER[0]的 bit0~bit31 分别对应中断 0~31。ISER[1]的 bit0~27 对应中断 32~59;这样总共 60 个中断就分别对应上了。
欲使能一个中断,你需要写 1 到对应 SETENA 的位中;欲除能一个中断,你需要写 1 到对应的 CLRENA 位中;
这里仅仅是使能,还要配合中断分组、屏蔽、IO 口映射等设置才算是一个完整的中断设置
ICER[8]:全称是:Interrupt Clear-Enable Registers。该寄存器组与 ISER 的作用恰好相反,是用来清除某个中断的使能的。其对应位的功能,也和 ICER 一样。
这里要专门设置一个 ICER 来清除中断位,而不是向 ISER 写 0 来清除,是因为 NVIC 的这些寄存器都是写 1 有效的,写 0 是无效的。
ISPR[8]:全称是:Interrupt Set-Pending Registers,
每个位对应的中断和 ISER 是一样的。通过置 1,可以将正在进行的中断挂起,而执行同级或更高级别的中断。写 0 是无效的。
ICPR[8]:全称是:Interrupt Clear-Pending Registers
其作用与 ISPR 相反,对应位也和 ISER 是一样的。通过设置 1,可以将挂起的中断接挂。写 0 无效。
IABR[8]:全称是:Interrupt Active Bit Registers。对应位所代表的中断和 ISER 一样,如果为 1,则表示该位所对应的中断正在被执行。这是一个只读寄存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。
(External interrupt/event controller)—外部中断/事件控制器
STM32F103 的中断控制器支持 19 个外部中断/事件请求。每个中断设有状态位,每个中断/事件都有独立的触发和屏蔽设置。
19 个外部中断为:
线 0~15:对应外部 IO 口的输入中断。
线 16:连接到 PVD 输出。
线 17:连接到 RTC 闹钟事件。
线 18:连接到 USB 唤醒事件。
软件中断事件寄存器是小开关
中断屏蔽寄存器是一个中开关
后面NVIC还有一个大开关
事件:ADC采集,定时器计时等
STM32F407 的中断控制器支持 22个外部中断/事件请求。每个中断设有状态位,每个中断/事件都有独立的触发和屏蔽设置。
STM32F407 的 22 个外部中断为:
EXTI 线 0~15:对应外部 IO 口的输入中断。
EXTI 线 16:连接到 PVD 输出。
EXTI 线 17:连接到 RTC 闹钟事件。
EXTI 线 18:连接到 USB OTG FS 唤醒事件。
EXTI 线 19:连接到以太网唤醒事件。
EXTI 线 20:连接到 USB OTG HS(在 FS 中配置)唤醒事件。
EXTI 线 21:连接到 RTC 入侵和时间戳事件。
EXTI 线 22:连接到 RTC 唤醒事件。
中断响应:申请中断,让CPU执行中断函数
事件响应:外部中断的信号不通向CPU,转而通向其他外设,触发其他外设的操作,如:ADC转换,DMA等。
EXPORT EXTI0_IRQHandler
EXPORT EXTI1_IRQHandler
EXPORT EXTI2_IRQHandler
EXPORT EXTI3_IRQHandler
EXPORT EXTI4_IRQHandler
EXPORT EXTI9_5_IRQHandler
EXPORT EXTI15_10_IRQHandler
中断线 0-4 每个中断线对应一个中断函数
中断线 5-9 共用中断函数 EXTI9_5_IRQHandler
中断线 10-15 共用中断函数 EXTI15_10_IRQHandler
第一个函数是判断某个中断线上的中断是否发生(标志位是否置位):
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
这个函数一般使用在中断服务函数的开头判断中断是否发生。
另一个函数是清除某个中断线上的中断标志位:
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
这个函数一般应用在中断服务函数结束之前,清除中断标志位。
所谓AFIO,即alternate function IO 复用IO端口
对寄存器 AFIO_EVCR , AFIO_MAPR 和 AFIO_EXTICRX 进行读写操作前,应当首先打开 AFIO的时钟。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
然后使用GPIO_EXTILineConfig函数进行挂载。例如:
GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource4);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
然后进行重映射
GPIO_PinRemapConfig(GPIO_Remap_USART1,ENABLE);
该寄存器共有四个,分别对应广大GPIO引脚中断配置。
SWIER :19位有效,配置线x上的软件中断。配置1的时候将EXTI_PR中的相应中断挂起。
挂起代表中断来了,捕捉到了。
在库函数中,配置 GPIO 与中断线的映射关系的函数 GPIO_EXTILineConfig()来实现的:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);//先打开时钟
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
该函数将 GPIO 端口与中断线映射起来,使用范例是:
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource2);
将中断线 2 与 GPIOE 映射起来,很显然是 GPIOE.2 与 EXTI2 中断线连接了。
初始化线上中断,设置触发条件。中断线上中断的初始化是通过函数 EXTI_Init()实现的。EXTI_Init()函数的定义是:
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
使用方法
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line=EXTI_Line4;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure); //根据 EXTI_InitStruct 中指定的
//参数初始化外设 EXTI 寄存器
中断线的标号
F103取值范围为EXTI_Line0~EXTI_Line15。
F407取值范围为EXTI_Line0~EXTI_Line23。
中断模式,可选值为中断 EXTI_Mode_Interrupt 和事件 EXTI_Mode_Event。
触发方式,可以是下降沿触发 EXTI_Trigger_Falling,上升沿触发 EXTI_Trigger_Rising,或者任意电平(上升沿和下降沿)触发EXTI_Trigger_Rising_Falling,
设置好中断线和 GPIO 映射关系,然后又设置好了中断的触发模式等初始化参数。既然是外部中断,涉及到中断我们当然还要设置 NVIC 中断优先级。
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //使能按键外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级 2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //子优先级 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //中断优先级分组初始化
这里先打断一下思路,可以看一下后文NVIC的介绍。
常用的中断服务函数格式为:
void EXTI3_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line3)!=RESET)//判断某个线上的中断是否发生
{
中断逻辑…
EXTI_ClearITPendingBit(EXTI_Line3); //清除 LINE 上的中断标志位
}
}
第一个函数是判断某个中断线上的中断是否发生(标志位是否置位):
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
这个函数一般使用在中断服务函数的开头判断中断是否发生。
另一个函数是清除某个中断线上的中断标志位:
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
这个函数一般应用在中断服务函数结束之前,清除中断标志位。
固件库还提供了两个函数用来判断外部中断状态以及清除外部状态
标志位的函数 EXTI_GetFlagStatus 和 EXTI_ClearFlag,他们的作用和前面两个函数的作用类似。
STM32F4 所有中断服务函数的名字,都已经在startup_stm32f40_41xx.s 里面定义好了,如果有不知道的,去这个文件里面找就可以了
SYSCFG与F1的AFIO类似但有不同,主要管理内存空间的映射及其EXTI中断等配置。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
然后使用SYSCFG_EXTILineConfig进行挂载。
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource2);
需要注意的一点是IO开启中断之后,不影响正常使用,也可读取状态。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);//使能 SYSCFG 时钟
设置 IO 口与中断线的映射关系
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
设置中断结构体,初始化线上中断
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line=EXTI_Line4;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure); //初始化外设 EXTI 寄存器
与F103的中断的结构体一致
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //使能按键外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级 2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //响应优先级 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //中断优先级分组初始化
103的参数凑合看一下,407的可以在F4中文参考手册指南10.2的234页浏览。
优先级
参考前文的 : STM32的IO中断处理函数
编写中断服务函数中的常用函数与103是一样的。
第一个函数是判断某个中断线上的中断是否发生(标志位是否置位):
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
这个函数一般使用在中断服务函数的开头判断中断是否发生。
另一个函数是清除某个中断线上的中断标志位:
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
这个函数一般应用在中断服务函数结束之前,清除中断标志位。
KEY0->PE4 上拉输入
KEY1->PE3 上拉输入
KEY2->PE2 上拉输入
WK_UP->PA0 下拉输入
//外部中断4服务程序
void EXTI4_IRQHandler(void)
{
delay_ms(10); //消抖
if(KEY0==0)
{
LED0=!LED0;
LED1=!LED1;
}
EXTI_ClearITPendingBit(EXTI_Line4);//清除LINE4上的中断标志位
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
delay_init(168); //初始化延时函数
uart_init(115200); //串口初始化
LED_Init(); //初始化LED端口
BEEP_Init(); //初始化蜂鸣器端口
EXTIX_Init(); //初始化外部中断输入
LED0=0; //先点亮红灯
while(1)
{
printf("OK\r\n"); //打印OK提示程序运行
delay_ms(1000); //每隔1s打印一次
}
}