在上一期的教程《从“点亮”到“感知”:STM32F103按键输入入门》中,我们教会了开发板通过“轮询”的方式,一刻不停地“盯着”按键,随时准备听候指令。但这种方式就像一个人死死盯着门口,虽然能第一时间发现访客,但这段时间里他却什么别的事也做不了,效率实在太低。

有没有更聪明的方法?当然有!这就是我们今天的主角——外部中断。它能让你的STM32从“主动询问”变为“被动等待”,当外部事件(比如按键按下)发生时,它会主动暂停手头的工作,优先处理紧急任务,处理完成后又回去继续原来的工作。

这种“主动汇报”的机制,是构建高效、实时嵌入式系统的核心。今天,就让我们继续拿起那块“战损版”的开发板,为它赋予这种“中断响应”的超能力。

STM32的外部中断是个很实用的功能,它能让单片机及时响应外部事件(比如按键按下)。下面这份教程从基本原理开始,带你一步步掌握它的配置和使用。

 一、什么是外部中断?先搞懂核心思想

想象一下这个场景:

  • 轮询模式(你之前的方式):你正在厨房做饭,但每隔5秒就要跑到客厅看一眼门铃有没有响。饭没做好,人也累得够呛。

  • 中断模式(今天要学的):你在厨房安心做饭,门铃一响,你立刻放下锅铲去开门,开完门再回来继续做饭。饭做好了,人也轻松。

在STM32的世界里,CPU就是那个做饭的你,外部中断就是那个门铃。当CPU正在while(1)里循环执行主任务时,一旦检测到指定的引脚有电平变化(上升沿/下降沿),它就会:

  1. 暂停当前任务(保护好现场)。

  2. 跳转去执行一个特殊的函数——中断服务函数。

  3. 处理完紧急事件(比如记录按键、控制LED)后,返回主程序被打断的地方,继续往下执行。

二、深入硬件:中断线路与映射规则

STM32的每个GPIO引脚都可以作为外部中断的触发源,但中断线的资源是有限的(通常只有16条,即EXTI0 ~ EXTI15)。这就好比一栋大楼有很多个房间(GPIO引脚),但只有16个服务窗口(中断线)。同一时间,一个窗口只能为一个房间服务。

这就引出了一个重要的规则:引脚共享中断线,但同一时间只能使能一个。

比如,PA0PB0都连接在 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

这里需要注意一个细节:由于中断线数量多于中断服务函数数量,EXTI5EXTI9共用 EXTI9_5_IRQHandler ,EXTI10EXTI15共用 EXTI15_10_IRQHandler。在共用的中断函数里,你需要通过判断标志位来区分具体是哪一根线触发了中断。


三、不止于按键:那些“隐藏”的专用中断线

STM32的EXTI功能远不止于此。为了处理芯片内部关键外设的事件,它还拥有EXTI16 ~ EXTI31这些“专用中断线”。它们不连接GPIO引脚,而是连接芯片内部的“传感器”和“闹钟”,主要用于系统监控和低功耗唤醒。

这些功能在构建复杂、低功耗的工业设备时至关重要:

核心功能一:系统监控与报警

这类中断线连接的是芯片内部的监测模块,用于在关键参数异常时第一时间通知 CPU。

EXTI线16 —— 可编程电压监测器 (PVD)
这是最常用的专用中断之一。PVD 用于监测芯片的供电电压(VDD)。你可以设置一个阈值,当电压低于或高于该阈值时,就会触发中断。
    典型应用:在电池供电的设备(如手机、无线传感器)中,当检测到电池电压过低时,触发 PVD 中断,CPU 可以立即执行紧急数据保存,然后安全关机,防止数据丢失。
    EXTI线21 —— RTC入侵检测与时间戳


    简单来说,这些“专用中断线”的本质是将芯片内部关键外设(如电源管理、实时时钟等)产生的信号,也纳入到 EXTI 这个统一的“中断/事件分发网络”中来管理。这样做的好处是,可以用一套相同的机制(边沿检测、屏蔽、挂起)来处理内部事件,更重要的是,让这些内部事件也能具备将MCU从低功耗模式中唤醒的能力。它们不用于普通的 GPIO 引脚中断,主要服务于两大核心功能:系统监控和低功耗唤醒。

    这个中断与实时时钟(RTC)的入侵检测功能绑定。当芯片的特定引脚(RTC入侵引脚)检测到外部信号(如机箱被打开)时,会触发此中断。
    典型应用:在需要防篡改的设备(如智能电表、金融终端)中,一旦检测到入侵事件,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来接收和处理数据。
    • 深入理解:两种触发类型

      从设计上看,这些专用中断线可以分为两类。

      1. 可配置型:如同GPIO中断一样,你可以配置它的触发边沿(上升沿/下降沿)、开启/关闭中断屏蔽。PVD和大部分RTC事件都属于此类。

      2. 直接连接型:这类线主要给特定外设用于产生唤醒事件。它们不需要软件进行边沿等配置,只要外设产生了唤醒信号,就会直接通过EXTI向电源管理模块发出请求。很多串口(USART)的唤醒线就属于这种。

      总而言之,这些专用中断线就像是 STM32 内部的“传感器网络”和“唤醒铃”,让芯片能够自我监控,并在需要时从沉睡中被及时叫醒,是构建高效、可靠嵌入式系统的关键。

      这些专用中断线中的大部分(如 EXTI16 对应 PVD)通常拥有独立的中断服务函数,比如 PVD_IRQHandler


    了解外部中断 (EXTI)
    STM32的每个GPIO引脚都可以作为外部中断的触发源它的核心思想是:当引脚上检测到指定的电平变化(如上升沿、下降沿)时,会触发中断,暂停当前主程序,跳转去执行预设的中断服务函数,处理完后再回来
    由于STM32的中断线(EXTI线)数量有限(通常是16条,对应0~15),多个GPIO引脚需要共用一条中断线。例如,PA0和PB0都连接在 EXTI0 线上。因此,同一时刻,一条中断线上只能有一个引脚被使能。你需要通过 SYSCFG_EXTILineConfig() (标准外设库) 或CubeMX的图形化配置,将特定的GPIO引脚映射到对应的中断线上。

    四、代码实战:标准库方式配置按键中断

    现在,我们以正点原子精英版上的按键为例,用标准外设库来手把手配置外部中断。我们的目标是:按下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寄存器
     
    }
    编写中断服务函数
    当外部中断触发后,程序会跳转到对应的中断服务函数中执行。你需要在该函数中清除中断标志位,否则中断会一直触发
    // EXTI0的中断服务函数名是固定的,可在启动文件 startup_stm32f40xx.s 中查到
    //外部中断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);	  
    	}
     }
    补充:使用STM32CubeMX和HAL库
    如果你使用STM32CubeMX和HAL库,配置过程会更加图形化和简单:
    引脚配置:在Pinout视图中,将目标引脚(如PC13)模式设置为 GPIO_EXTI13
    参数设置:在GPIO设置中,配置触发方式(上升/下降沿)、上拉/下拉等
    NVIC使能:在NVIC设置中,使能 EXTI line[15:10] interrupts 并设置优先级
    编写回调函数:HAL库使用弱定义的回调函数来处理中断。你需要在用户代码中重写这个函数
    // 重写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);
        }
    }
    这种方式的好处是HAL库已经帮你处理了中断标志位的清除和中断入口等底层操作,你只需关注业务逻辑

    五、总结与思考

    从轮询到中断,我们的开发板完成了一次思维方式的跃迁。它不再是被动地、低效地“盯着”外部事件,而是建立了事件驱动的思维模型。

    回顾今天的收获:

    1. 理解了中断的核心思想:暂停、处理、返回。

    2. 掌握了GPIO中断的映射规则:不同引脚可以共用中断线,但同一时间只能使能一个。

    3. 实战了标准库的中断配置:从GPIO、AFIO到EXTI、NVIC,走通了完整的配置链路。

    4. 编写了中断服务函数:并时刻记得清除中断标志位这个关键动作。

    5. 了解了专用中断线:看到了外部中断在系统监控和低功耗领域的广阔应用。

    看着这块屏幕碎裂但依旧坚挺的“老将”,从点亮LED的“输出”,到识别按键的“感知”,再到今天学会“中断响应”,它仿佛被一点点注入了灵魂。这正是嵌入式的魅力所在:每一行代码,都在赋予硬件生命。