在上一章中,我们完成了OLED显示屏的调试,掌握了如何通过显示屏直观地查看单片机内部的数据变化。然而在实际的嵌入式开发中,仅仅依靠轮询方式去检测外部事件往往效率低下,甚至可能错过关键信号。这时,就需要引入中断机制——让CPU在忙于主程序的同时,能够及时响应外部突发的事件。本章将围绕STM32的EXTI外部中断展开,从中断系统的基础概念讲起,逐步深入到NVIC中断控制器、EXTI外部中断的配置流程,并通过对射式红外传感器计次和旋转编码器计次两个实战案例,帮助大家掌握外部中断的完整开发方法。
如果觉得有收获可以为我的文章点点小赞,这将激励我创作更好的内容!谢谢!
一、EXTI外部中断
1.1 中断系统
中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源
中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回
1.2 中断执行流程
1.3 STM32中断
68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设
使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级
1.4 NVIC基本结构
1.5 NVIC优先级分组
NVIC的中断优先级由优先级寄存器的4位(0~15)决定,越低位优先级越高,第0位优先级最高,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级
抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队
1.6 EXTI简介
EXTI(Extern Interrupt)外部中断
EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序
支持的触发方式:上升沿/下降沿/双边沿/软件触发
支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断,例如PA0和PB0不能同时触发中断
通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒
触发响应方式:中断响应/事件响应(事件响应:STM32额外功能,外部中断的信号通向其他外设,用来触发其他外设的事件/操作,属于外设之间的联合工作)
1.7 EXTI基本结构
1.8 AFIO复用IO口
AFIO主要用于引脚复用功能的选择和重定义
在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择
1.9 EXTI框图
1.10 旋转编码器简介
旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向
类型:机械触点式/霍尔传感器式/光栅式
1.11 硬件电路
1.12 实例一 5.1.1 对射式红外传感器计次
如何配置外部中断/外部中断的配置流程:
(1)第一步,配置RCC
配置RCC,把外部中断涉及的外设的时钟都打开;
//配置RCC RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);(2)第二步,配置GPIO
配置GPIO,选择我们的端口为输入模式;
//配置GPIO GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStructure);(3)第三步,配置AFIO
配置AFIO,选择我们用的这一路的GPIO,连接到后面的EXTI;
我们可能会使用以下与AFIO有关的库函数:
//复位AFIO外设,清除AFIO全部的外设配置
void GPIO_AFIODeInit(void);
//锁定选中引脚的GPIO配置,防止外部更改
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
//配置AFIO的事件输出功能
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);
//配置AFIO的数据选择器,来选择我们想要的中断引脚(CountSensor.c需要使用)
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
//配置AFIO
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
(4)第四步,配置EXTI
配置EXTI,选择边沿触发方式,比如上升沿,下降沿,双边沿。并选择响应方式,可以选择中断响应和事件响应;
我们可能会使用以下与EXTI有关的库函数:
//清除EXTI全部的外设配置,恢复成上电默认的状态
void EXTI_DeInit(void);
//根据所给结构体的参数配置EXTI外设(CountSensor.c需要使用)
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
//把参数传递的结构体变量赋一个默认值
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);
//用来软件触发外部中断的,调用这个函数,参数给一个指定的中断线,就能软件触发一次这个外部中断
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);
//在主程序里,GetFlagStatus——获取指定的标志位是否被置1了
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);
//在主程序里,ClearFlag——可以对置1的标志位进行清除
void EXTI_ClearFlag(uint32_t EXTI_Line);
//在中断函数里,GetITStatus——获取中断标志位是否被置1了
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
//在中断函数里,ClearITPendingBit——清除中断挂起标志位
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
//配置EXTI
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line14;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
(5)第五步,配置NVIC
配置NVIC,给中断选择一个合适的优先级;
//用来中断分组
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
//根据结构体里面指定的参数初始化NVIC
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);
//配置NVIC
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
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);
(6)第六步,接收外部中断信号
最后,通过NVIC,外部中断信号就能进入CPU了。这样CPU才能收到中断信号,才能跳转到中断函数里执行我们从启动文件startup_stm32f10x_md.s复制的对应名字的中断程序。
void EXTI15_10_IRQHandler(void) { if( EXTI_GetFlagStatus(EXTI_Line14)==SET ) { Sensor_TriggerFlag=1; EXTI_ClearITPendingBit(EXTI_Line14); } }完整的外部中断实例代码函数:
CountSensor.c(计数传感器)↓
-----------------------------
#include "stm32f10x.h" // Device header #include "Delay.h" uint16_t CountSensor_Count=0; //全局变量,用于计数 uint8_t Sensor_TriggerFlag=0; //获取标志,读完清零,消除按键抖动 /** 函 数:计数传感器初始化 参 数:无 返 回 值:无 */ void CountSensor_Init(void) { /配置RCC,开启时钟/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启GPIOB的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟 /配置GPIO,GPIO初始化/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //将PB14引脚初始化为上拉输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStructure); /配置AFIO,AFIO选择中断引脚/ GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚 /配置EXTI,EXTI初始化/ EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line = EXTI_Line14; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发 EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设 /配置NVIC,NVIC中断分组/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2 //即抢占优先级范围:0~3,响应优先级范围:0~3 //此分组配置在整个工程中仅需调用一次 //若有多个中断,可以把此代码放在main函数内,while循环之前 //若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置 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_Init,配置NVIC外设 } /** 函 数:获取计数传感器的计数值 参 数:无 返 回 值:计数值,范围:0~65535 */ uint16_t CountSensor_Get(void) { return CountSensor_Count; } //获取标志,读完自动清零 uint8_t CountSensor_FlagRead(void) { uint8_t temp = Sensor_TriggerFlag; Sensor_TriggerFlag = 0; return temp; } /** 函 数:EXTI15_10外部中断函数 参 数:无 返 回 值:无 注意事项:此函数为中断函数,无需调用,中断触发后自动执行 函数名为预留的指定名称,可以从启动文件复制 请确保函数名正确,不能有任何差异,否则中断函数将不能进入 */ void EXTI15_10_IRQHandler(void) { if( EXTI_GetFlagStatus(EXTI_Line14)==SET )//判断是否是外部中断14号线触发的中断 { Sensor_TriggerFlag=1; EXTI_ClearITPendingBit(EXTI_Line14);//清除外部中断14号线的中断标志位 //中断标志位必须清除 //否则中断将连续不断地触发,导致主程序卡死 } }-----------------------------
main.c↓
-----------------------------
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "CountSensor.h" extern uint16_t CountSensor_Count; extern uint8_t Sensor_TriggerFlag; int main(void) { OLED_Init(); CountSensor_Init(); OLED_ShowString(1,1,"Count:"); while(1) { if( CountSensor_FlagRead() == 1 ) { Delay_ms(20); //消抖延时放在主循环,不在中断! if( GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_14) == 0 ) //确认现在依旧是遮挡低电平 { CountSensor_Count++; } } OLED_ShowNum(1,7,CountSensor_Get(),5); } }-----------------------------
1.13 实例二 5.2.1 旋转编码器计次
在不同引脚产生下降沿进入外部中断的同时使用GPIO_ReadInputDataBit();库函数读取另一引脚的电平,如果能读取到低电平,说明现在触发中断的引脚出现下降沿的时间晚于另一引脚出现下降沿,我们可以利用这种方法自己定义正反转
再将计数的“变化的数值”封装成函数供主程序使用
Encoder.c(计数传感器)↓
-----------------------------
#include "stm32f10x.h" // Device header int16_t Encoder_Count=0; //全局变量,用于计数旋转编码器的增量值 /** 函 数:旋转编码器初始化 参 数:无 返 回 值:无 */ void Encoder_Init(void) { /配置RCC,开启时钟/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启GPIOB的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟 /配置GPIO,GPIO初始化/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//将PB0和PB1引脚初始化为上拉输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStructure); /配置AFIO,AFIO选择中断引脚/ GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1); /配置EXTI,EXTI初始化/ EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line = EXTI_Line0 | 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); //将结构体变量交给EXTI_Init,配置EXTI外设 /配置NVIC,NVIC中断分组/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置NVIC为分组2 //即抢占优先级范围:0~3,响应优先级范围:0~3 //此分组配置在整个工程中仅需调用一次 //若有多个中断,可以把此代码放在main函数内,while循环之前 //若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置 NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量 NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //选择配置NVIC的EXTI0线 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1 NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设 NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //指定NVIC线路的响应优先级为2 NVIC_Init(&NVIC_InitStructure); } /** 函 数:旋转编码器获取增量值 参 数:无 返 回 值:自上此调用此函数后,旋转编码器的增量值 / /使用Temp变量作为中继,目的是返回Encoder_Count后将其清零/ /在这里,也可以直接返回Encoder_Count 但这样就不是获取增量值的操作方法了 也可以实现功能,只是思路不一样/ int16_t Encoder_Get(void) { int16_t Temp; Temp=Encoder_Count; Encoder_Count=0; return Temp; } /* 函 数:EXTI0外部中断函数 参 数:无 返 回 值:无 注意事项:此函数为中断函数,无需调用,中断触发后自动执行 函数名为预留的指定名称,可以从启动文件复制 请确保函数名正确,不能有任何差异,否则中断函数将不能进入 */ void EXTI0_IRQHandler(void)//EXTI_Line1电平先下降,EXTI_Line0后下降条件if才成立 { if( EXTI_GetFlagStatus(EXTI_Line0)==SET ) { if( GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0 )//PB0的下降沿触发中断,此时检测另一相PB1的电平,目的是判断旋转方向 { Encoder_Count--; //此方向定义为反转,计数变量自减 } EXTI_ClearITPendingBit(EXTI_Line0); //清除外部中断0号线的中断标志位 } } /** 函 数:EXTI1外部中断函数 参 数:无 返 回 值:无 注意事项:此函数为中断函数,无需调用,中断触发后自动执行 函数名为预留的指定名称,可以从启动文件复制 请确保函数名正确,不能有任何差异,否则中断函数将不能进入 */ void EXTI1_IRQHandler(void)//EXTI_Line0电平先下降,EXTI_Line1后下降条件if才成立 { if( EXTI_GetFlagStatus(EXTI_Line1)==SET ) { if( GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0)==0 ) //PB1的下降沿触发中断,此时检测另一相PB0的电平,目的是判断旋转方向 { Encoder_Count++; //此方向定义为正转,计数变量自增 } EXTI_ClearITPendingBit(EXTI_Line1); //清除外部中断1号线的中断标志位 } }-----------------------------
main.c↓
-----------------------------
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Encoder.h" int16_t Num; int main(void) { OLED_Init(); Encoder_Init(); OLED_ShowString(1,1,"Num:"); while(1) { Num += Encoder_Get(); OLED_ShowSignedNum(1,5,Num,5); } }-----------------------------
二、总结
本文围绕STM32的EXTI外部中断展开,系统梳理了从中断基础概念到实际工程应用的完整链路。首先介绍了中断、中断优先级和中断嵌套的基本概念,明确了CPU响应外部事件的机制;随后讲解了STM32的NVIC中断控制器,包括其基本结构、16级可编程优先级以及抢占优先级与响应优先级的分组规则,为合理分配中断资源打下基础。
在EXTI部分,重点说明了外部中断的触发方式、支持的GPIO引脚、通道数量以及中断响应与事件响应的区别,并结合AFIO的引脚选择与重映射功能,理清了从GPIO到EXTI再到NVIC的信号通路。通过两个完整的实战案例——对射式红外传感器计次和旋转编码器计次,详细演示了外部中断的标准配置流程:开启RCC时钟、初始化GPIO输入模式、通过AFIO选择中断引脚、配置EXTI边沿触发方式、设置NVIC优先级,最后在中断服务函数中完成标志位判断与清除。两个案例还分别展示了如何利用中断标志位配合主循环消抖,以及如何通过读取另一相引脚电平来判断旋转方向。
总的来说,掌握EXTI外部中断的关键在于理解中断响应的完整链路,并严格按照“RCC→GPIO→AFIO→EXTI→NVIC→中断函数”的顺序进行配置。在实际开发中,还需注意中断标志位必须及时清除、NVIC分组配置整个工程只需调用一次、以及相同Pin不能同时触发中断等细节。希望本文能帮助读者快速上手外部中断的开发,为后续学习定时器、串口等更复杂的中断应用打下坚实基础。
都看到这了点点赞吧!