stm32–中断相关
1. NVIC介绍
NVIC(Nested Vectored Interrupt Controller 嵌套向量中断控制器),作用是管理中断嵌套,核心任务是管理中断优先级.
stm32中有很多中断通道,当同一时刻有多个中断发生时,哪个中断先上CPU进行处理?当在处理一个中断时,又有一个中断产生,新的中断是否更加紧急、是否该插队进行处理?这个顺序问题如果由CPU处理的话无疑加大了CPU的任务量,导致CPU效率低下,于是便有了NVIC。 NVIC的作用就是将一堆争先恐后想要使用CPU的中断缕缕顺,让这群中断排队,让紧急的中断插队,确保CPU只需要专心进行运算工作,这就是NVIC的作用。
68个可屏蔽中道(不包含16个Cortex-M3的中断线)
16个可编程的优先等级(4位中断优先级 4位-> 2^4个)
低延迟的异常和中断处理
电源管理控制
系统控制寄存器的实现
1.1 中断 及 中断嵌套 的概念:
中断(Interrupt):当中断发生时,会暂停当前正在执行的程序,将程序的执行流程转移到对应的中断服务程序(ISR,Interrupt Service Routine)中进行处理。中断服务程序执行完之后,再返回到原来被中断的程序继续执行。例如:当外部按键连接到一个EXTI引脚,按下按键产生中断,CPU暂停当前任务并保护当前任务的执行现场,转头去执行按键处理的中断服务程序,如更新计数器的值或者改变某个输出状态,执行结束后恢复之前被中断的程序的执行现场并继续该程序的执行。
中断嵌套:是指在中断处理过程中,允许更高优先级的中断请求打断当前正在执行的低优先级中断服务程序(ISR),转而执行高优先级中断的服务程序,当高优先级中断处理完毕之后,再返回到被打断的低优先级中断服务程序继续执行的机制。
1.2 嵌套过程:
当一个低优先级中断发生时,CPU暂停当前正在执行的主程序,进入低优先级中断服务程序。在低优先级中断服务程序执行期间,如果有更高优先级的中断请求到来,并且系统允许中断嵌套(相关的中断嵌套使能位被设置),那么CPU会暂停低优先级中断服务程序的执行,转而处理高优先级中断,进入高优先级中断服务程序。
在高优先级中断服务程序执行完相应的处理后,CPU会从高优先级中断服务程序返回,继续执行被打断的低优先级中断服务程序。低优先级中断服务程序执行完毕之后,CPU再返回到最初被中断的主程序继续执行。
1.3 优先级相关:
NVIC给每个中断赋予抢占优先级和响应优先级。关系如下:
- 拥有较高抢占优先级的中断可以打断抢占优先级较低的中断
- 若两个抢占优先级的中断同时挂起,则优先执行响应优先级较高的中断
- 若挂起的两个中断,它们各自的响应优先级、抢占优先级都相同,则优先执行位于中断向量表中位置较高的中断
- 响应优先级不会造成中断嵌套,也就是说中断嵌套是由抢占优先级决定的
对第4点进行解释:有这样的两个中断:A(响应优先级:1 抢占优先级:0)B(响应优先级:0 抢占优先级:0);首先我们需要知道,等级系数数字越小,等级越高。当中断A在被处理时,产生了中断B,但是由于二者的抢占优先级相同,所以并不会发生中断嵌套,即A仍旧在执行。如果中断AB同时发生,在面临选择时,遵循上述原则第2点,即相同抢占优先级 选择响应优先级较高的B。
优先级划分:
经过上面的介绍我们应该对中断和优先级有了初步的认识:优先级是中断固有的性质,优先级有两种分类,分别是响应优先级和抢占优先级。那么具体在硬件层面这两种优先级是如何表示的呢?
在STM32中,NVIC的两种中断优先级由4位优先级寄存器决定,可以根据实际需求将这4bit进行划分:高n位表示抢占优先级,剩余的低4-n位表示相应优先级。因此便有5种划分方式:
分组方式 | 抢占优先级 | 响应优先级 |
分组1 | 0位 | 4位,取值可为0~15 |
分组2 | 1位,取值可为0~1 | 3位,取值可为0~7 |
分组3 | 2位,取值可为0~3 | 2位,取值可为0~3 |
分组4 | 3位,取值可为0~7 | 1位,取值可为0~1 |
分组5 | 4位,取值可为0~15 | 0位 |
配置分组的函数:
只需要调用这个函数便可实现优先级分组的划分,函数使用的相关细节会在本文后续涉及。
2. EXTI简介
EXTI(External Interrupt) 外部中断: EXTI可监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC(Nested Vectored Interrupt Controller 嵌套向量中断控制器)发出中断申请,经过NVIC裁决之后即可中断CPU主程序,使CPU执行EXTI对应的中断程序。
支持的触发方式:上升沿/下降沿/双边延/软件触发
支持的GPIO口:所有的GPIO口,但是相同的pin不能同时触发中断
通道数:16个GPIO_pin,PVD(电源电压监测器)输出,RTC闹钟,USB唤醒,以太网唤醒
触发响应方式:中断响应/事件响应 (区别在于中断响应需要上CPU处理,事件响应是不需要CPU处理而是触发其他外设进行操作)
EXTI基本结构:
前面提到EXTI支持所有的GPIO口,①处每个GPIO都引出了16条线说明每组GPIO外设的每条线都是可以被选择被配置为EXTI通道的;但是相同的pin不能同时触发中断,这意味着即使每条GPIO端口都具备被选择为EXTI通道的能力,但是在选择时重复的pin编号是不被允许的(例如在GPIOA组里选择了GPIOA_Pin_0、GPIOA_Pin_1,GPIOA_Pin_2,那么在GPIOB组里pin为0、1、2的就不能再选了,其余的都是可用的;若GPIOB组里选择了GPIOB_Pin_5、GPIOA_Pin_6、GPIOA_Pin_7;那么在GPIOC组里就不能选择pin为0,1,2和5,6,7的了,其余pin可用)。这就是通道数中提到的16个GPIO_pin的来源。
上面这段文字描述了GPIO通道被选择为EXTI通道的选择原则,那么该怎么进行选择操作呢?选择操作是由AFIO(复用功能I/O)外设模块来完成,AFIO模块并不是专用来选择EXTI通道的,它还有其他功能,在此不过多赘述。AFIO在这里的主要作用是将GPIO端口和外部中断线建立映射关系。具体的映射关系就是:GPIOx的pin号就是中断线编号,比如我们将GPIOA的0、1、2经由AFIO映射为EXTI通道,那么它们就对应EXTI0、EXTI1、EXTI2这三条EXTI通道。经由AFIO选择之后最多可有16条EXTI通道,再加上③中PVD(电源电压监测器)输出,RTC闹钟,USB唤醒,以太网唤醒4个通道总共20个EXTI通道通向右侧的EXTI边沿检测及控制模块。
在④中的边沿检测及控制模块可以对中断通道进行配置,我们可以在此模块中:选择需要配置的目标通道,配置目标通道的触发方式是什么(上升沿/下降沿/双边延/软件触发),配置目标通道的响应方式是什么(中断响应/事件响应 )。如果选择为中断响应,意味着需要让CPU进行处理,意味着需要NVIC进行先后顺序、嵌套与否的决断,所以图中可以看出20个通道都可以通向NVIC;如果选择事件响应,便不需要NVIC进行决断,可直接通向其他外设,图中体现为20条线接到其他外设。两个通向(NIVC/其他外设)意味着两种响应方式(中断响应/事件响应)。此外:stm32在设计时将EXTI通道编号5~9合并为一条(EXTI9_5),将EXTI通道编号10~15合并为一条(EXTI15_10)这意味着在区间内的中断通道通向同一个中断函数,可以在进入中断函数之后根据中断标志位再进一步区分是哪个中断通道触发的中断。
需要注意的是,图中列举的只是接入NVIC的外部中断,除了外部中断还有很多其他类型的中断,例如:定时器中断、串口中断、I2C中断、SPI中断、ADC中断以及DMA中断。只要涉及中断,就一定涉及NVIC;但涉及NVIC不一定涉及外部中断。所以这样来看,EXTI外部中断和NVIC嵌套向量中断控制器并不是对等关系,NVIC更像是EXTI的上级,起到对各种中断的管理作用。
3. 函数解读及配置流程
3.2 函数解读
3.2.1 配置AFIO相关:
stm32f10x_gpio.h:(只用到了这个头文件中的个别函数,但是为了方便大家在自己的工程文件中锁定位置进行跳转,所以都复制过来了)
void GPIO_DeInit(GPIO_TypeDef* GPIOx);
void GPIO_AFIODeInit(void);
//复位AFIO外设,调用此函数会清除AFIO配置
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
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);
//引脚重映射
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
//将GPIO端口和外部中断险EXTI_Line建立映射关系,虽然是GPIO命名,本质是AFIO在工作
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);
3.2.2 配置EXTI相关:
stm32f10x_exti.h:
void EXTI_DeInit(void);
//清除EXTI配置,恢复至默认状态
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
//初始化EXTI,用结构体中的参数进行配置
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);
//给EXTI_InitStructure初始化结构体赋默认值
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);
//软件触发一次指定外部中断,参数指定目标中断线
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);
/*检查指定外部中断线的标志状态,当外部中断线上发生了特定事件(上升/下降/双边沿)
相应的标志位会被置位*/
void EXTI_ClearFlag(uint32_t EXTI_Line);
//清除指定外部中断线的标志位,也就是上面这个函数所查看的标志位
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
/*此函数用于检查指定外部中断线的挂起状态,当外部中断线发生了特定时间(上升/下降/双边沿)
且该中断未被屏蔽时,中断挂起位会被置位*/
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
//清除指定外部中断线的中断挂起位,对应上面这个函数所查看的挂起标志位
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line)和
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line)的区别:
EXTI_GetFlagStatus()这个函数用于检查指定的外部中断线的标志状态。当外部中断线上发生了特定事件,相应的标志位会被置位。这个函数可以查询该标志位以确定是否有中断事件发生。EXTI_GetITStatus()这个函数用于检查指定外部中断线的中断挂起状态。当外部中断线上发生了中断事件并且该中断未被屏蔽时,中断挂起位会被置位。这个函数可以查询该挂起位以确定是否有中断等待处理。
3.2.3 配置NVIC相关:
misc.h:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
//划分中断分组,也就是上文中提到的4bit如何划分
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);
//配置SysTick系统抵达定时器时钟源
需要注意的是:中断分组划分这个函数最好确保它在整个项目中只执行一次,最好放在main.c进入循环之前。
3.2 配置流程
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
//开启AFIO时钟,因为要使用AFIO完成外部中断线和GPIO端口映射功能
/*GPIO端口根据需求配置*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11|GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
/*AFIO实现外部中断线映射*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource10);
//将GPIOB_Pin10映射到EXTI10中断线
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource11);
//将GPIOB_Pin11映射到EXTI11中断线
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
//将GPIOB_Pin10映射到EXTI1中断线
/*使用结构体配置EXTI*/
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line10|EXTI_Line11|EXTI_Line1;
//选择要进行配置的中断线
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
//使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
//选择响应方式,此处为中断响应
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
//选择触发方式,此处为下降沿触发
EXTI_Init(&EXTI_InitStructure);
/*中断分组划分*/
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_InitStructure.NVIC_IRQChannelSubPriority = 1;
//设置响应优先级
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
在之前的EXTI基本结构图中我们可以看到外部中断线5~9和外部中断线10~15在进行向中断函数跳转时被合并在一起,以10~15线为例,触发其中某个中断时会根据中断向量表跳转到同一个中断服务函数(EXTI15_10_IRQHandler)。在这个中断服务函数中,需要查询各个中断的挂起位来确定具体是哪一个中断线触发了中断,然后进行相应的处理。相当于它们的中断线是独立的,中断触发后跳转的的中断服务函数所在区域是公共的(10~15都会来这里),来到公共区域后再进行自检,看他们独立的中断线,具体是哪根被触发了。
例如:
void EXTI15_10_IRQHandler()
{
if(EXTI_GetITStatus(EXTI_Line10) == SET)
{
//进行LINE10对应的中断处理逻辑
EXTI_ClearITPendingBit(EXTI_Line10);
}
if(EXTI_GetITStatus(EXTI_Line11) == SET)
{
//进行LINE11对应的中断处理逻辑
EXTI_ClearITPendingBit(EXTI_Line11);
}
}
void EXTI1_IRQHandler()
{
if(EXTI_GetITStatus(EXTI_Line1) == SET)
{
//进行LINE1对应的中断处理逻辑
}
EXTI_ClearITPendingBit(EXTI_Line1);
}
另外需要注意的是,只要进入中断,就必须进行手动清除挂起标志位。
作者:此昵称己被使用