背景:为什么需要中断?
在前一篇按键输入博客中,KEY2 的”按一下翻转”功能用了轮询方式:
1 | // 轮询:CPU 主动、反复地问"按键被按了吗?" |
轮询的缺点很明显——如果 CPU 正在执行某个耗时操作,按键按下后要等到下一个循环才能被检测到,响应不及时。
中断的思路完全不同:
1 | 轮询: CPU → "按了吗?" → "没按" → "按了吗?" → "没按" → "按了吗?" → "按了!" → 处理 |
打个比方:
- 轮询 = 你在写作业,每 5 秒抬头看一眼有没有人敲门
- 中断 = 你在写作业,门铃响了(中断信号),你去开门,回来继续写
硬件原理
硬件与之前博客完全一样——KEY1(PB12,外部上拉)控制中断触发,红灯(PB0) 和 绿灯(PA7) 作为输出,高电平点亮。
CubeMX 配置
新建项目
芯片选择 STM32F103C8T6,步骤与之前一样,不重复。
配置引脚
| 引脚 | 功能 | User Label | 详细参数 |
|---|---|---|---|
| PB0 | GPIO_Output | LED_RED |
推挽输出、无上下拉、低速 |
| PA7 | GPIO_Output | LED_GREEN |
推挽输出、无上下拉、低速 |
| PB12 | GPIO_EXTI12 | KEY1 |
下降沿触发中断、无上下拉(外部已有上拉) |
关键步骤:配置 PB12 的中断模式
在芯片图上找到 PB12 → 点击 → 选择 GPIO_EXTI12,顺便把user label设置成key1
然后到左侧 GPIO 设置中,把 PB12 的具体参数配好:
注意这里选择的是 External Interrupt Mode with Falling edge trigger detection(外部中断模式,下降沿触发)——按键按下时电平从高变低,正好产生下降沿,与前面的硬件原理匹配.
配置 NVIC(中断控制器)
GPIO 配好后,还需要告诉 NVIC(嵌套向量中断控制器)”我要使用这个中断”。
切换到 System Core → NVIC,找到 EXTI line[15:10] interrupts,在旁边那个小方框里打上勾:
这里有两个参数需要关注:
| 参数 | 值 | 说明 |
|---|---|---|
| Preemption Priority | 15 | 抢占优先级(数字越小优先级越高) |
| Sub Priority | 0 | 子优先级 |
关于优先级的具体原理,后面会单独说明。
配调试接口 & 生成代码
老规矩:System Core → SYS → Debug → Serial Wire,防止烧录一次后 SWD 被锁。
Project Manager 设置:
| 设置项 | 值 |
|---|---|
| Project Name | Interrupt |
| Toolchain / IDE | CMake |
点击 Generate Code,用 VS Code 打开。
代码实现
引脚宏定义(main.h)
CubeMX 根据 User Label 生成了以下宏:
1 | // 绿色 LED — PA7 |
main.c —— 红灯持续闪烁
1 | int main(void) |
逻辑很简单:PB0 每 2 秒翻转一次电平,红灯持续闪烁。
主循环里无需任何与按键相关的代码——按键检测完全由中断处理。
stm32f1xx_it.c —— 外部中断服务函数
当 KEY1 按下时,EXTI15_10 中断被触发,CPU 跳转到 EXTI15_10_IRQHandler 执行:
1 | /** |
逐行解释:
| 行号 | 代码 | 作用 |
|---|---|---|
| ① | HAL_Delay(10) |
机械按键按下瞬间会有抖动,电平在几毫秒内反复跳变。等 10ms 让电平稳定下来再读 |
| ② | HAL_GPIO_ReadPin(...) |
再次读取 KEY1 的电平,确认是真的按下了(低电平)。如果只是噪声干扰,不会误触发 |
| ③ | HAL_GPIO_TogglePin(...) |
翻转绿灯 PA7 的电平——原来亮就灭,原来灭就亮 |
| ④ | HAL_GPIO_EXTI_IRQHandler(KEY1_Pin) |
非常重要 清除 EXTI 的中断挂起标志位。如果不清除,中断会反复触发,程序卡死。函数里本身自带,不能删除。 |
⚠️ HAL_GPIO_EXTI_IRQHandler 的两种写法
上面这个函数内部其实干了三件事:
1 | void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin) |
也就是说,你也可以不在 ISR 里直接写逻辑,而是把逻辑写在 HAL_GPIO_EXTI_Callback 里,CubeMX 会在 main.c 中留出 USER CODE BEGIN 4 给你写回调:
1 | /* USER CODE BEGIN 4 */ |
MX_GPIO_Init —— GPIO 与 NVIC 初始化
MX_GPIO_Init 函数中,CubeMX 不仅配了 GPIO,还配了 NVIC:
1 | static void MX_GPIO_Init(void) |
关键点:
GPIO_MODE_IT_FALLING—— 配置 PB12 为外部中断模式,下降沿触发GPIO_NOPULL—— 因为板子已经有外部上拉电阻,不需要内部上拉HAL_NVIC_EnableIRQ(EXTI15_10_IRQn)—— 使能 NVIC 中的 EXTI15_10 中断通道。这步非常重要,漏了中断不会触发
关于中断优先级
CubeMX 中配置的优先级和实际生效的值之间有一个”编码”过程,理解了这个才能搞清中断嵌套行为。
优先级分组
STM32 的 NVIC 支持优先级分组,决定了抢占优先级和子优先级各占几位:
| 分组宏 | 抢占优先级 | 子优先级 | 适用场景 |
|---|---|---|---|
NVIC_PRIORITYGROUP_0 |
0 bit | 4 bits | 所有中断同级,不能嵌套 |
NVIC_PRIORITYGROUP_1 |
1 bit | 3 bits | 最多 2 级嵌套 |
NVIC_PRIORITYGROUP_2 |
2 bits | 2 bits | 最多 4 级嵌套 |
NVIC_PRIORITYGROUP_3 |
3 bits | 1 bit | 最多 8 级嵌套 |
NVIC_PRIORITYGROUP_4 |
4 bits | 0 bits | 最多 16 级嵌套(最常用) |
CubeMX 默认使用 NVIC_PRIORITYGROUP_4——4 位全是抢占优先级,没有子优先级。
本次配置的优先级
| 中断 | PreemptPriority | 编码值 | 实际优先级 |
|---|---|---|---|
| SysTick | 14 | 0x00 | 0(最高) |
| EXTI15_10 | 15 | 0x80 | 8 |
数值越小优先级越高。SysTick 的优先级高于 EXTI,所以在 EXTI 中断服务函数中调用 HAL_Delay() 是安全的——SysTick 可以抢占 EXTI 来更新 uwTick 变量,不会死锁。
编译 & 烧录
- VS Code 底部工具栏点击齿轮图标 → 生成
- 连接 ST-Link
- 点击 运行和调试 → 选择
STM32Cube:STM32 Launch STLink GDB Server - 点击继续按钮,程序开始运行
如果遇到烧录问题,参考上一篇或波特律动 FAQ。
效果展示
烧录成功后:
- 🔴 红灯持续闪烁——每 2 秒亮灭一次
- 按下 KEY1(PB12) → 🟢 绿灯翻转状态(亮↔灭)
- 每次按键,绿灯变化一次,红灯不受影响,继续闪烁
- 如果绿灯和红灯一起亮起,则颜色变化成黄色。
中断 vs 轮询的对比
这个 demo 正好可以和上一篇按键博客做一个对比:
| 对比项 | 轮询(上一篇 KEY2) | 中断(本篇) |
|---|---|---|
| CPU 占用 | 高(一直在检测) | 低(平时干正事,有事件才处理) |
| 响应速度 | 取决于循环周期 | 立即响应 |
| 代码位置 | main 循环里 | 中断服务函数中 |
| 主循环复杂度 | 按键逻辑和主逻辑混在一起 | 主循环只关心本职工作 |
| 适用场景 | 低频检测、简单系统 | 实时性要求高的场景 |
当然,中断也有代价——每次进入和退出中断都有上下文切换开销(保存现场、恢复现场),并且中断函数要尽量简短,否则会影响其他实时任务的响应。
进阶思路
1. 用回调函数分离逻辑
前面提到过,可以把中断处理逻辑写到 HAL_GPIO_EXTI_Callback 里,中断服务函数保持整洁。当项目中多个引脚共用一个 EXTI 中断线时(比如 EXTI15_10 同时服务 PB12 和 PB13),回调函数里用 GPIO_Pin 区分来源,代码结构更清晰。
2. 中断中只设标志位,主循环处理
标准的嵌入式写法——中断里只设一个 volatile 标志位:
1 | volatile uint8_t key1_flag = 0; |
主循环检测到标志位置位后,再执行消抖、翻转等操作。这样 ISR 的执行时间最短,对系统实时性的影响最小。
3. 上升沿 + 下降沿双触发
按键按下和松开都产生中断,可以用来检测”长按”和”短按”,或者做更精细的状态控制。
4. 优先级嵌套测试
可以给不同的外设中断配置不同的抢占优先级,观察高优先级中断打断低优先级中断的行为,加深对中断嵌套的理解。
总结
这次的收获:
- 外部中断(EXTI)的基本配置方法——CubeMX 里配引脚模式 + NVIC 设置
- 中断服务函数的写法——消抖 → 读引脚确认 → 执行逻辑 → 清标志位
- 中断优先级——优先级分组、优先级编码、抢占与嵌套的基本概念
- CMake 生成器的注意事项——NVIC 配置可能不会自动生成,需要手动检查
中断是嵌入式开发中非常重要的概念,后面学定时器、UART、I2C、SPI 等外设时都会用到中断机制。理解了这个 demo,后面的路会顺畅很多。




