从“轮询”到“触发”:STM32外部中断入门——让你的开发板学会“主动汇报”
有没有更聪明的方法?当然有!这就是我们今天的主角——外部中断。它能让你的STM32从“主动询问”变为“被动等待”,当外部事件(比如按键按下)发生时,它会主动暂停手头的工作,优先处理紧急任务,处理完成后又回去继续原来的工作。
这种“主动汇报”的机制,是构建高效、实时嵌入式系统的核心。今天,就让我们继续拿起那块“战损版”的开发板,为它赋予这种“中断响应”的超能力。
一、什么是外部中断?先搞懂核心思想
想象一下这个场景:
轮询模式(你之前的方式):你正在厨房做饭,但每隔5秒就要跑到客厅看一眼门铃有没有响。饭没做好,人也累得够呛。
中断模式(今天要学的):你在厨房安心做饭,门铃一响,你立刻放下锅铲去开门,开完门再回来继续做饭。饭做好了,人也轻松。
在STM32的世界里,CPU就是那个做饭的你,外部中断就是那个门铃。当CPU正在while(1)里循环执行主任务时,一旦检测到指定的引脚有电平变化(上升沿/下降沿),它就会:
暂停当前任务(保护好现场)。
跳转去执行一个特殊的函数——中断服务函数。
处理完紧急事件(比如记录按键、控制LED)后,返回主程序被打断的地方,继续往下执行。
二、深入硬件:中断线路与映射规则
STM32的每个GPIO引脚都可以作为外部中断的触发源,但中断线的资源是有限的(通常只有16条,即EXTI0 ~ EXTI15)。这就好比一栋大楼有很多个房间(GPIO引脚),但只有16个服务窗口(中断线)。同一时间,一个窗口只能为一个房间服务。
这就引出了一个重要的规则:引脚共享中断线,但同一时间只能使能一个。
比如,PA0和PB0都连接在 EXTI0 这根线上。如果你想用PA0做中断,就不能同时用PB0做中断。你需要通过配置,将具体的引脚映射到对应的中断线上。
下面是经典的映射关系表,记下这张表,以后查起来会很方便:
| GPIO引脚 | 所属中断线 (EXTI Line) | 对应的中断服务函数 (IRQHandler) |
| PA0, PB0, PC0, ... , PG0 | EXTI0 | EXTI0_IRQHandler |
| PA1, PB1, PC1, ... , PG1 | EXTI1 | EXTI1_IRQHandler |
| PA2, PB2, PC2, ... , PG2 | EXTI2 | EXTI2_IRQHandler |
| PA3, PB3, PC3, ... , PG3 | EXTI3 | EXTI3_IRQHandler |
| PA4, PB4, PC4, ... , PG4 | EXTI4 | EXTI4_IRQHandler |
| PA5, PB5, PC5, ... , PG5 | EXTI5 | EXTI9_5_IRQHandler |
| PA6, PB6, PC6, ... , PG6 | EXTI6 | |
| PA7, PB7, PC7, ... , PG7 | EXTI7 | |
| PA8, PB8, PC8, ... , PG8 | EXTI8 | |
| PA9, PB9, PC9, ... , PG9 | EXTI9 | |
| PA10, PB10, PC10, ... , PG10 | EXTI10 | EXTI15_10_IRQHandler |
| PA11, PB11, PC11, ... , PG11 | EXTI11 | |
| PA12, PB12, PC12, ... , PG12 | EXTI12 | |
| PA13, PB13, PC13, ... , PG13 | EXTI13 | |
| PA14, PB14, PC14, ... , PG14 | EXTI14 | |
| PA15, PB15, PC15, ... , PG15 | EXTI15 |
这里需要注意一个细节:由于中断线数量多于中断服务函数数量,EXTI5到EXTI9共用 EXTI9_5_IRQHandler ,EXTI10到EXTI15共用 EXTI15_10_IRQHandler。在共用的中断函数里,你需要通过判断标志位来区分具体是哪一根线触发了中断。
三、不止于按键:那些“隐藏”的专用中断线
STM32的EXTI功能远不止于此。为了处理芯片内部关键外设的事件,它还拥有EXTI16 ~ EXTI31这些“专用中断线”。它们不连接GPIO引脚,而是连接芯片内部的“传感器”和“闹钟”,主要用于系统监控和低功耗唤醒。
这些功能在构建复杂、低功耗的工业设备时至关重要:
核心功能一:系统监控与报警
这类中断线连接的是芯片内部的监测模块,用于在关键参数异常时第一时间通知 CPU。
EXTI线16 —— 可编程电压监测器 (PVD)这是最常用的专用中断之一。PVD 用于监测芯片的供电电压(VDD)。你可以设置一个阈值,当电压低于或高于该阈值时,就会触发中断。
EXTI线21 —— RTC入侵检测与时间戳
简单来说,这些“专用中断线”的本质是将芯片内部关键外设(如电源管理、实时时钟等)产生的信号,也纳入到 EXTI 这个统一的“中断/事件分发网络”中来管理。这样做的好处是,可以用一套相同的机制(边沿检测、屏蔽、挂起)来处理内部事件,更重要的是,让这些内部事件也能具备将MCU从低功耗模式中唤醒的能力。它们不用于普通的 GPIO 引脚中断,主要服务于两大核心功能:系统监控和低功耗唤醒。
典型应用:在需要防篡改的设备(如智能电表、金融终端)中,一旦检测到入侵事件,CPU 可以立即清除敏感密钥或记录下精确的“时间戳”,锁定证据。
核心功能二:低功耗唤醒
在低功耗模式下,CPU 的主时钟可能都停止了。但这些专用中断线可以绕过 CPU,当特定条件满足时,直接唤醒整个系统。这是现代微控制器实现超低功耗的关键。
1、RTC相关事件 (闹钟、唤醒、时间戳)
- EXTI线17 / 18:连接到 RTC闹钟 或 RTC闹钟事件。可以设置RTC在特定时间产生一个闹钟事件。即使MCU在深度睡眠,这个信号也能通过EXTI线17把它唤醒,去执行定时任务。
- EXTI线20 / 22:连接到 RTC唤醒事件。这是 RTC 的一个周期性唤醒功能,可以配置为每秒钟或每分钟唤醒一次MCU,进行短暂的数据采集后继续睡眠,极大地节省电能。
2、外设唤醒事件 (USB、以太网、串口等)
- EXTI线18/19/20:连接到 USB 或 以太网 的唤醒事件。对于USB设备,当主机发出唤醒信号时,STM32能通过这根中断线从睡眠中醒来,恢复通信。
- EXTI线23-30/31:连接到 I2C 或 USART/LPUART(低功耗串口) 的唤醒事件。当串口总线上检测到特定数据或起始位时,对应的EXTI线可以唤醒MCU来接收和处理数据。
可配置型:如同GPIO中断一样,你可以配置它的触发边沿(上升沿/下降沿)、开启/关闭中断屏蔽。PVD和大部分RTC事件都属于此类。
直接连接型:这类线主要给特定外设用于产生唤醒事件。它们不需要软件进行边沿等配置,只要外设产生了唤醒信号,就会直接通过EXTI向电源管理模块发出请求。很多串口(USART)的唤醒线就属于这种。
深入理解:两种触发类型
从设计上看,这些专用中断线可以分为两类。
总而言之,这些专用中断线就像是 STM32 内部的“传感器网络”和“唤醒铃”,让芯片能够自我监控,并在需要时从沉睡中被及时叫醒,是构建高效、可靠嵌入式系统的关键。
这些专用中断线中的大部分(如 EXTI16 对应 PVD)通常拥有独立的中断服务函数,比如 PVD_IRQHandler
四、代码实战:标准库方式配置按键中断
现在,我们以正点原子精英版上的按键为例,用标准外设库来手把手配置外部中断。我们的目标是:按下KEY0、KEY1、KEY2(低电平有效)或WK_UP(高电平有效),通过中断方式控制LED和蜂鸣器。
1. 硬件回顾与准备
根据上一节的原理图:
KEY0/KEY1/KEY2:分别接在 PE4/PE3/PE2,按下为低电平(下降沿触发)。
WK_UP:接在 PA0,按下为高电平(上升沿触发)。
请确保你的工程中已经包含了delay.c和基础的gpio.c(含LED和按键IO初始化,可参考上一篇文章)。
//按键初始化函数
void KEY_Init(void) //IO初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);//使能PORTA,PORTE时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;//KEY0-KEY2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIOE2,3,4
//初始化 WK_UP-->GPIOA.0 下拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0设置成输入,默认下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0
}
//外部中断0服务程序
void EXTIX_Init(void)
{
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
KEY_Init(); // 按键端口初始化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //使能复用功能时钟
//GPIOE.2 中断线以及中断初始化配置 下降沿触发
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource2);
EXTI_InitStructure.EXTI_Line=EXTI_Line2; //KEY2
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寄存器
//GPIOE.3 中断线以及中断初始化配置 下降沿触发 //KEY1
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3);
EXTI_InitStructure.EXTI_Line=EXTI_Line3;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
//GPIOE.4 中断线以及中断初始化配置 下降沿触发 //KEY0
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource4);
EXTI_InitStructure.EXTI_Line=EXTI_Line4;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
//GPIOA.0 中断线以及中断初始化配置 上升沿触发 PA0 WK_UP
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);
EXTI_InitStructure.EXTI_Line=EXTI_Line0;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //使能按键WK_UP所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //使能按键KEY2所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //子优先级2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn; //使能按键KEY1所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; //子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn; //使能按键KEY0所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
}//外部中断0服务程序
void EXTI0_IRQHandler(void)
{
delay_ms(10);//消抖
if(WK_UP==1) //WK_UP按键
{
BEEP=!BEEP;
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除LINE0上的中断标志位
}
//外部中断2服务程序
void EXTI2_IRQHandler(void)
{
delay_ms(10);//消抖
if(KEY2==0) //按键KEY2
{
LED0=!LED0;
}
EXTI_ClearITPendingBit(EXTI_Line2); //清除LINE2上的中断标志位
}
//外部中断3服务程序
void EXTI3_IRQHandler(void)
{
delay_ms(10);//消抖
if(KEY1==0) //按键KEY1
{
LED1=!LED1;
}
EXTI_ClearITPendingBit(EXTI_Line3); //清除LINE3上的中断标志位
}
void EXTI4_IRQHandler(void)
{
delay_ms(10);//消抖
if(KEY0==0) //按键KEY0
{
LED0=!LED0;
LED1=!LED1;
}
EXTI_ClearITPendingBit(EXTI_Line4); //清除LINE4上的中断标志位
}int main(void)
{
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(115200); //串口初始化为115200
LED_Init(); //初始化与LED连接的硬件接口
BEEP_Init(); //初始化蜂鸣器端口
KEY_Init(); //初始化与按键连接的硬件接口
EXTIX_Init(); //外部中断初始化
LED0=0; //点亮LED0
while(1)
{
printf("OK\r\n");
delay_ms(1000);
}
}// 重写HAL库的弱定义回调函数
void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_13)
{
// 处理按键按下事件,例如翻转LED
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
}五、总结与思考
从轮询到中断,我们的开发板完成了一次思维方式的跃迁。它不再是被动地、低效地“盯着”外部事件,而是建立了事件驱动的思维模型。
回顾今天的收获:
理解了中断的核心思想:暂停、处理、返回。
掌握了GPIO中断的映射规则:不同引脚可以共用中断线,但同一时间只能使能一个。
实战了标准库的中断配置:从GPIO、AFIO到EXTI、NVIC,走通了完整的配置链路。
编写了中断服务函数:并时刻记得清除中断标志位这个关键动作。
了解了专用中断线:看到了外部中断在系统监控和低功耗领域的广阔应用。
看着这块屏幕碎裂但依旧坚挺的“老将”,从点亮LED的“输出”,到识别按键的“感知”,再到今天学会“中断响应”,它仿佛被一点点注入了灵魂。这正是嵌入式的魅力所在:每一行代码,都在赋予硬件生命。
评论 (0)
登录后即可发表评论