【STM32 CubeMX + Keil】 中断、NVIC 、EXTI
本篇前半部分以介绍中断概念为主,内容较为理论,篇幅稍长;
后半部分则通过按键实例,具体展示中断的配置流程、回调函数编写以及可能出现的冲突与解决方案。
目录
一、前言
二、中断 概述
三、NVIC:中断控制
四、EXTI:外部中断控制器
五、CubeMX 中断触发配置示范
六、keil 中编写回调函数
七、优先级冲突
一、前言
STM32 入门通常需要跨过五道坎:新建工程、GPIO 控制、NVIC、EXTI 和 UART 通信。
跨过这几关,基本就掌握了开发的主流程。之后再学习其他功能,往往会顺利很多——毕竟可以参考大量现成的代码(说白了,就是“合理借鉴”)。
本篇将重点讲解 NVIC 和 EXTI。
这两部分本身并不复杂,但由于概念相对抽象、关联知识较多,成了很多初学者的难点。
以前使用标准库的时候,就算理解不深,至少手动配置中断的过程,也能让人留下印象。
而现在借助 CubeMX 自动生成代码,NVIC 的配置常常简化为“勾选一下”,导致中断的重要性容易被忽视。结果往往在项目做到一半时,才突然掉进坑里。
本文反复写了近一周,内容高度浓缩,只保留实际开发中最需要的干货。我们将避免枯燥的理论阐述,力求在十分钟内帮助您掌握 STM32 中断的应用。
二、中断 概述
1、先讲故事
公司饭堂。
一个打饭阿姨 (CPU),一个饭堂经理 (NVIC)。
一群打饭的人 (主程序代码)。
左边窗口:用于正常排队打饭 ;
右边窗口:插队专用 !预留给有重要任务的团队(多数团队只有1人、少量团队有多人) 。
阿姨平时在左边窗口打饭。当右边窗口来人插队了,她需放下左边的饭盒,过去右边优先给插队者打饭。
阿姨一个人如何兼顾两个窗口呢?
简单!左边打饭时,每隔5秒,扭头看看右边是否有人排队。(低效的查询法)
这是STM32入门时最常用的方法:在main的while循环里,间隔5秒,if 判断某个状态标志是否达成,以执行对应工作。
这方法的坏处是,当经理要求每隔0.01秒检查一次时,阿姨工作就得卡壳,99%的时间都浪费在扭头检查右边窗口这事上, 无法专注打饭。
如何改进呢?
经理NVIC引入两则规则提升效率:
1、安装门铃:右边窗口装上门铃,阿姨只需在听到铃声时过来处理插队请求。
2、工牌等级制度:插队者需佩戴工牌,标注团队优先级(0~15,数字越小优先级越高)。
现在的工作流程:
1、阿姨安心处理左边队伍,无需主动检查右边。
2、右边来人A君,经理查看其工牌等级3,按铃,通知阿姨立即暂停左边工作,过来为A君打饭。
3、右边再来D5君(优先级4),经理比较优先级后,安排D5排在A君后方。
4、又来了两人,B君(优先级0)、D2君(优先级5)。经理要求全体后退,让优先级最高的B君排到第一。其余按优先级和到达时间排序:B → A → D5 → D2。(级别高的排前、同级先到先排)
5、B君完成后,阿姨继续为之前中断的A君服务(恢复现场)。
6、右边所有请求处理完毕后,阿姨返回左边继续之前工作。
不妨在阅读完后续内容后,再回看一遍这个“公司管理”的比喻,相信你会对 STM32 的中断机制有更深刻的理解。
2、中断是什么?
中断是一种让 CPU 处理紧急任务的机制:当系统正在执行主程序时,若有重要事件发生(如数据到达、异常出现),CPU 会立即暂停当前任务,转去处理该事件,处理完毕后再返回原任务继续执行。
在实际开发中,我们通常将中断机制、系统异常、内部中断、外部中断、事件等,统一称为“中断”。具体含义,在不同的场合需结合上下文理解:
- 可指整个中断处理体系(如 NVIC 管理机制);
- 也可指某个具体的中断源(如串口接收中断、EXTI 线路中断)。
2、中断事件分类
STM32 拥有近百个可中断事件(可理解为有权“插队”的任务),从开发者角度可简单分为三类:
- 内部中断(约10个):包括不可控的复位、硬件错误中断,以及可控的 SysTick、PendSV 等系统中断。
- 外设中断(约80个):如 UART 空闲中断、DMA 传输结束等由外设触发的中断。
- 外部中断(约19个):由 GPIO 电平或边沿变化触发,经 EXTI 控制器检测并上报。(本文重点讲解此类)
需要再次强调的是,“中断”这一术语在不同语境下具有不同的含义,必须结合上下文来理解:
-
它可以指整个中断处理体系,包括 NVIC 的管理机制、系统异常、内部中断、外部中断以及事件等,这些常被统称为“中断”;
-
它也可以指某个具体的中断源,例如串口接收中断、某一条 EXTI 线路中断等。
三、NVIC:中断控制
简单来说,STM32 的各个外设都可能产生紧急任务,而 NVIC 就是负责处理这些“插队请求”的调度中心,根据优先级决定处理顺序。
1、NVIC 概述
NVIC(Nested Vectored Interrupt Controller),即嵌套向量中断控制器,是 Cortex-M 内核中用于统一管理中断的硬件模块。
在 STM32 运行时,存在两套并行的工作机制:
- 主程序执行流:正常顺序执行的代码
- 中断系统:由 NVIC 管理和调度的紧急事务处理机制
NVIC 负责实时监控各类中断事件。一旦发生中断,它会立即保存当前程序现场、暂停主程序、转去执行相应的中断服务程序,处理完毕后再恢复主程序的运行。
2、NVIC 与各类中断的关系
可以把中断管理系统看作一家公司。
NVIC 是经理,是核心调度者,负责统一接收和调度所有紧急事务(中断)。
它管理着三个主要部门:
- 内部中断:如复位中断、硬件错误中断、 SysTick中断等
- 外设中断:如定时器溢出、串口收发完成等
- EXTI中断:专门处理来自 GPIO 引脚的外部信号
所有中断请求——无论来自芯片内部还是外部引脚——最终都会汇总到经理(NVIC)这里。由它根据优先级规则进行仲裁,决定哪些中断可以立即“插队”处理,并安排给 CPU 执行。
3、优先级管理
无论是使用寄存器、标准库还是 HAL 库,底层硬件架构相同,但操作方式完全不同。
传统配置方式(寄存器/标准库):
- 需掌握 STM32F4 的 60 个可屏蔽中断 + 10 个系统异常
- 支持中断优先级分组(0-4 组)
- 优先级规则复杂:抢占优先级 > 子优先级 > 硬件编号
- 需设置系统优先级分组(整个系统只设置一次)
- 需初始化
NVIC_InitTypeDef
结构体并手动使能中断
CubeMX 配置方式:
- 自动生成配置代码,极大简化操作流程
- 默认配置为 16 级抢占优先级,不再区分子优先级
- 优先级数值 0-15,数字越小优先级越高
- 高优先级(数值小)可打断低优先级(数值大)
- 相同优先级的中断按到达顺序依次执行
- 所有中断默认优先级为 0,建议根据实际需求在 NVIC 配置中重新分配(0-15)
四、EXTI:外部中断控制器
EXTI 是中断系统的“三大部门”之一,专门负责处理来自 GPIO 引脚的外部信号变化。
1、EXTI 简介
EXTI(External Interrupt/Event Controller)是 STM32 中专门用于监控 GPIO 引脚电平变化的硬件模块。
其核心作用是将外部信号的跳变(如上升沿、下降沿)转换为内核可识别和处理的中断或事件请求(IRQ)。
2、主要特点:
- 23 条中断线:其中 16 条连接 GPIO 引脚,其余用于 RTC、USB 等内部外设
- 灵活触发:每条线可独立配置为上升沿、下降沿、双边沿触发
- 中断模式:产生中断,由 CPU 执行中断服务函数
- 事件模式:直接触发其他外设(如 DMA)或唤醒内核,无需 CPU 参与
- 硬件级响应:检测和响应速度极快,远优于软件轮询
3、典型应用场景
- 按键唤醒与检测
- 旋转编码器解码
- 紧急安全信号检测(如急停开关)
- 脉冲计数与捕获:结合定时器输入捕获功能,可实现高精度脉冲宽度或频率测量。
- 外设同步与低功耗定时唤醒:通过RTC闹钟等事件实现定时唤醒,或借助通信同步信号触发数据传输。
4、共中断线共享机制
STM32 的 EXTI 中断线数量有限,因此多个引脚共享同一条中断线。
你需要特别注意中断源的区分。
中断线共享规则:
- 所有引脚0,共享一条线:EXTI0 (覆盖:PA0、PB0、PC0… )
- 所有引脚1,共享一条线:EXTI1 (覆盖:PA1、PB1、PC1… )
- 所有引脚2,共享一条线:EXTI2 (覆盖:PA2、PB2、PC2… )
- 所有引脚3,共享一条线:EXTI3 (覆盖:PA3、PB3、PC3… )
- 所有引脚4,共享一条线:EXTI4 (覆盖:PA4、PB4、PC4… )
- 引脚5至9, 共享一条线:EXTI9_5(覆盖PA5-PA9, PB5-PB9等)。
- 引脚10至15, 共享一条线:EXTI15_10(覆盖PA10-PA15, PB10-PB15等)。
可能引发的问题:
当多个配置为中断模式的引脚共享同一条 EXTI 线(如 PA1 和 PB1)时,产生中断后系统无法直接区分具体是哪个引脚触发了中断。
解决方法:
- 在共用的中断服务函数中,通过软件方式依次检查各引脚的电平状态来判断中断来源
- 在项目规划阶段,尽量避免将多个需要同时使用的关键中断源分配到同一条中断线上
总结一下:
EXTI 就是一个高效的事件触发器。对于需要快速响应外部信号(如按键、报警、状态切换)的场景,中断法是正确且高效的选择。你只需要告诉CubeMX用哪个引脚、何时触发(上升沿/下降沿)以及在keil里决定触发后干什么即可,剩下的硬件会自动搞定。
五、CubeMX 中断触发配置示范
下面以按键为例,演示如何使用 STM32CubeMX 配置 GPIO 引脚,通过外部按键的电平变化来触发中断。
1. 硬件连接确认
查看原理图,确认按键连接的 GPIO 引脚及其默认电平状态。
本篇所用开发板,其按键1的原理图如下所示:
- 按键1连接的是 PA0
- 当按键按下后,PA0 为高电平
- 电路设计中未为 PA0 设置外部下拉电阻。为确保该引脚在空闲时保持稳定的低电平,需开启内部下拉电阻
- 按键两端并联有电容,可实现硬件消抖,因此无需在软件中额外添加消抖延时逻辑
综上,PA0 应配置为:下拉输入模式,上升沿触发。
2、配置引脚为EXTI
若已有 CubeMX 工程,可直接双击打开项目目录中的 .ioc
配置文件。
若无现有工程,也可新建一个工程用于测试。
具体操作:
- 在芯片引脚示意图上,找到 PA0 引脚(通常位于左下角)。
- 单击 PA0 引脚,在弹出的功能菜单中选择 GPIO_EXTI0 选项。
3、配置引脚工作参数
进入GPIO页面, 点击对应的引脚,打开该引脚的详细配置:
- 触发模式:选 ....Interrupt...Rising..., (上升沿触发中断)
- 上、下拉:选Pull-down, (开启内部下拉电阻)
GPIO Mode(可触发模式)
常用模式为边沿触发,主要选项包括:
- External Interrupt Mode with Rising edge trigger detection:上升沿触发(适用于引脚电平由低变高的瞬间)
- External Interrupt Mode with Falling edge trigger detection:下降沿触发(适用于引脚电平由高变低的瞬间)
GPIO Pull-up/Pull-down(上下拉模式)
共三种配置选项:
No pull-up and no pull-down:浮空输入。引脚空闲电平完全由外部电路决定。
Pull-up: 启用内部上拉电阻。确保引脚空闲时保持高电平。
Pull-down:启用内部下拉电阻。确保引脚空闲时保持低电平。
4、使能中断线、配置优先级
进入到NVIC页面,找到对应的中断线,打勾,NVIC即可响应它的中断请示。
- 打勾:EXTI line0 interrupt
- 优先级:默认 0; 即最高优先级,我们先作不修改,使用默认配置。
5、生成工程配置
完成上述三步配置后,点击"Generate Code"生成工程代码。
六、keil 中编写回调函数
1、中断回调函数机制
使用标准库开发时,我们需要手动编写与启动文件中名称严格匹配的中断服务函数。若未编写或函数名不一致,中断发生时程序将无法找到入口地址,导致运行时错误。
使用 CubeMX 后,它会自动生成完整的中断服务函数,并在其中调用相应的回调函数。这些回调函数在生成代码中已以弱定义(weak)形式存在,有效避免了因遗漏中断服务函数而引发的程序跑飞问题。
我们只需在外部重新实现所需的中断回调函数即可。当相应中断触发时,中断服务函数会自动调用我们编写的回调函数。
重要提示:所有 EXTI 线的中断最终都会调用同一个回调函数:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);
2、编写回调函数
- 打开
main.c
文件,在/* USER CODE BEGIN 4 */
和/* USER CODE END 4 */
之间添加以下代码 (也可在其他工程源文件中编写,确保编译通过即可):
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{// 当按键触发中断时,反转LED灯的电平状态HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_2);
}
完成后,是这个样子的
编译并烧录程序后,按下按键即可观察到 LED 灯状态反转,这表明已成功实现了基本的中断触发功能。
3、相同 EXTI 线 不同引脚触发的处理
如前所述,多个 GPIO 引脚共享同一条 EXTI 线:
- 所有引脚0,共享一条线:EXTI0 (覆盖:PA0、PB0、PC0… )
- 所有引脚1,共享一条线:EXTI1 (覆盖:PA1、PB1、PC1… )
- 所有引脚2,共享一条线:EXTI2 (覆盖:PA2、PB2、PC2… )
- 所有引脚3,共享一条线:EXTI3 (覆盖:PA3、PB3、PC3… )
- 所有引脚4,共享一条线:EXTI4 (覆盖:PA4、PB4、PC4… )
- 引脚5至9, 共享一条线:EXTI9_5(覆盖PA5-PA9, PB5-PB9等)。
- 引脚10至15, 共享一条线:EXTI15_10(覆盖PA10-PA15, PB10-PB15等)。
回顾刚才编写的中断回调函数:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{// 当按键触发中断时,反转LED灯的电平状态HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_2);
}
存在一个问题:
如果项目中同时使用了 PA0 和 PC0(两者均属于 EXTI0),或者同时使用了 PA5、PA8、PB6、PC9(同属 EXTI9_5),应如何区分具体是哪个引脚触发的中断?
解决方法:
由于所有共享中断线的引脚最终都会调用同一个回调函数,可以在函数内部通过判断引脚的电平状态来确定触发源。
示例代码:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{// 判断PA0是否为高电平if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 1){HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_2); // 反转LED}// 判断PB0是否为高电平if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == 1){HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_2); // 反转LED}// 判断PD8是否为低电平if (HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_8) == 0){HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_2); // 反转LED}
}
注意:
在使用 HAL_GPIO_ReadPin()
判断触发源时,该函数读取的是引脚当前的实时电平,而非按键是否被按下的逻辑状态。
实际电平与按键状态的关系完全取决于硬件电路设计:
-
有些电路设计中,按键按下时引脚为高电平
-
有些电路设计中,按键按下时引脚为低电平
因此,务必根据实际电路确定应与何种电平状态进行比较判断。
很多从事纯软件开发的开发者在初次接触嵌入式中断时,常有一个疑问:
这个回调函数最初是由EXTI0_IRQHandler()
调用的,但EXTI0_IRQHandler()
又是被谁调用的呢?这是一个从纯软件转向嵌入式开发时容易困惑的问题。
请注意:中断服务函数是由硬件直接调用的,而不是由某个软件函数发起的。在完成中断配置后,当 EXTI 中断触发时,硬件会向 NVIC(嵌套中断向量控制器)发送请求,NVIC 再根据中断向量表中对应的入口地址,引导 CPU 跳转至相应的中断服务函数开始执行。
七、优先级冲突
如前所述,中断优先级数值越小,优先级越高。
CPU 会优先处理高优先级任务,然后再处理低优先级任务。
这一概念理解起来并不复杂,但在实际开发中,新手很容易遇到优先级冲突问题,且这类问题往往难以排查。
我们对之前的 EXTI 回调函数稍作修改:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{ if (1 == HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)){for (uint8_t i = 0; i < 20; i++){HAL_Delay(500); // 间隔延时HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_2); // 当按键触发中断时,反转LED灯的电平状态}}
}
这段代码的意图是:触发中断后让 LED 闪烁 20 次。
从逻辑上看似乎没有问题。
尝试编译并烧录运行后,会发现按键触发中断后程序会出现卡死现象。为什么?
如果仅检查 PA0 引脚的工作模式、中断配置或程序逻辑,可能无法找到问题根源,甚至可能怀疑芯片硬件故障。
而有经验的开发者会首先关注 HAL_Delay()
函数,因为它是依靠 SysTick 进行计时的。
虽然在我们的工程中没有显式配置 SysTick,但在 CubeMX 生成的代码中,它已被默认配置,且SysTick 的中断优先级默认为 15(最低优先级)。回顾之前在 NVIC 配置中的设置,EXTI 的中断优先级为 0(最高优先级)。
问题根源:
EXTI 中断(优先级 0)在执行过程中调用了 HAL_Delay()
,而该函数依赖于低优先级(15)的 SysTick 中断。由于高优先级中断无法被低优先级中断打断,导致 SysTick 中断永远无法执行,HAL_Delay()
也就无法正常返回。
解决方案:
如果中断 A 的执行需要依赖另一个中断 B,必须将 B 的优先级设置为高于 A(数值更小)。同级优先级也不行,必须确保 B 有权限打断 A,才能保证系统正常运行。
作如上图的修改,编译、烧录后,程序装正常运行。
如有错漏,欢迎指正!!~