中断入门 --- PIR 中断控制板载 LED
配套硬件 :DshanMCU-F407(STM32F407ZGT6)+ 人体红外感应模块(PIR)
前置知识 :已完成 PIR 轮询控制 LED 实验
学习理念:理解"CPU 主动去问 vs 外设主动通知"的本质区别
文章目录
- [中断入门 --- PIR 中断控制板载 LED](#中断入门 — PIR 中断控制板载 LED)
-
- [1. 中断------是什么?](#1. 中断——是什么?)
-
- [1.1 生活中的例子](#1.1 生活中的例子)
- [1.2 中断 vs 轮询](#1.2 中断 vs 轮询)
- [1.3 中断的完整流程(6 步)](#1.3 中断的完整流程(6 步))
- [2. STM32 中断体系](#2. STM32 中断体系)
-
- [2.1 NVIC(嵌套向量中断控制器)](#2.1 NVIC(嵌套向量中断控制器))
- [2.2 EXTI(外部中断/事件控制器)](#2.2 EXTI(外部中断/事件控制器))
- [2.3 中断优先级](#2.3 中断优先级)
- [3. 硬件接线](#3. 硬件接线)
-
- [3.1 接线表](#3.1 接线表)
- [3.2 PIR 模块本身的三样调节](#3.2 PIR 模块本身的三样调节)
- [4. CubeMX 配置(中断版)](#4. CubeMX 配置(中断版))
-
- [4.1 新建工程](#4.1 新建工程)
- [4.2 引脚配置](#4.2 引脚配置)
- [4.3 GPIO 参数配置](#4.3 GPIO 参数配置)
- [4.4 NVIC 配置(重要!)](#4.4 NVIC 配置(重要!))
- [4.5 时钟配置](#4.5 时钟配置)
- [4.6 生成工程](#4.6 生成工程)
- [5. 中断代码逐层解读](#5. 中断代码逐层解读)
-
- [5.1 完整的调用链](#5.1 完整的调用链)
- [5.2 EXTI0_IRQHandler(由 CubeMX 生成在 stm32f4xx_it.c)](#5.2 EXTI0_IRQHandler(由 CubeMX 生成在 stm32f4xx_it.c))
- [5.3 HAL_GPIO_EXTI_IRQHandler(由 CubeMX 生成,HAL 库内部)](#5.3 HAL_GPIO_EXTI_IRQHandler(由 CubeMX 生成,HAL 库内部))
- [5.4 你写的回调函数 HAL_GPIO_EXTI_Callback](#5.4 你写的回调函数 HAL_GPIO_EXTI_Callback)
- [5.5 全局变量 vs 局部变量](#5.5 全局变量 vs 局部变量)
- [6. 遇到的问题全记录](#6. 遇到的问题全记录)
-
- [问题 ①:LED 一直亮,手怎么晃都没反应](#问题 ①:LED 一直亮,手怎么晃都没反应)
- [问题 ②:H/L 跳线帽搞不清](#问题 ②:H/L 跳线帽搞不清)
- [问题 ③:TIME 旋钮的作用](#问题 ③:TIME 旋钮的作用)
- [问题 ④:为什么调试时看不到 while(1) 被跳出](#问题 ④:为什么调试时看不到 while(1) 被跳出)
- [问题 ⑤:中断优先级怎么理解](#问题 ⑤:中断优先级怎么理解)
- [问题 ⑥:刚上电 LED 就亮](#问题 ⑥:刚上电 LED 就亮)
- [7. PIR 模块硬件详解](#7. PIR 模块硬件详解)
-
- [7.1 模块上的三个可调元件](#7.1 模块上的三个可调元件)
- [7.2 什么时候需要等 30 秒?](#7.2 什么时候需要等 30 秒?)
- [7.3 为什么盖布没用?](#7.3 为什么盖布没用?)
- [8. 轮询 vs 中断对比](#8. 轮询 vs 中断对比)
-
- [8.1 核心代码对比](#8.1 核心代码对比)
- [8.2 CPU 占用对比](#8.2 CPU 占用对比)
- [8.3 什么时候用轮询,什么时候用中断?](#8.3 什么时候用轮询,什么时候用中断?)
- [9. 扩展思考](#9. 扩展思考)
-
- [9.1 如果改为下降沿触发会怎样?](#9.1 如果改为下降沿触发会怎样?)
- [9.2 如果要实现"人来了亮,人走了灭"?](#9.2 如果要实现"人来了亮,人走了灭"?)
- [9.3 中断里面能不能用 HAL_Delay?](#9.3 中断里面能不能用 HAL_Delay?)
- [9.4 后续你学到什么](#9.4 后续你学到什么)
微信视频2026-05-26_213809_618
1. 中断------是什么?
⭐⭐⭐ MUST MASTER
1.1 生活中的例子
你在看书(CPU 在跑 while(1))
邮递员按门铃(中断信号来了)
你放下书,跑去开门(CPU 暂停当前任务,跳去执行中断服务函数)
处理完了,回来继续看书(CPU 回到 while(1) 继续跑)
核心点:你不用每 5 秒去门口看一眼有没有人(轮询),门铃响了自然会叫你(中断)。
1.2 中断 vs 轮询
| 对比 | 轮询(Polling) | 中断(Interrupt) |
|---|---|---|
| CPU 怎么知道外设需要处理? | 反复去问 | 外设主动通知 |
| CPU 在等待时在做什么? | 一直在查寄存器 | 可以干别的事,也可以休眠 |
| 响应速度 | 取决于轮询间隔 | 即时 |
| 适合场景 | 外设状态变化慢、不追求实时 | 外设随时可能产生数据 |
| 代码复杂度 | 简单 | 稍复杂(需要了解中断机制) |
1.3 中断的完整流程(6 步)
第 1 步:外设产生中断信号
PIR 检测到人 → PE0 从低变高
第 2 步:CPU 检测到中断请求(IRQ)
检查当前是否有更高优先级的中断
第 3 步:CPU 自动保护"现场"
保存当前正在执行的指令地址、寄存器到栈中
第 4 步:跳转到中断向量表
找到 EXTI0_IRQHandler(PE0 对应的中断服务函数入口)
第 5 步:执行中断服务函数
EXTI0_IRQHandler → HAL_GPIO_EXTI_IRQHandler → 你写的 Callback
第 6 步:恢复"现场",返回原来被打断的地方
继续执行 while(1) 中被中断的那条指令的下一条
2. STM32 中断体系
⭐⭐⭐ MUST MASTER
2.1 NVIC(嵌套向量中断控制器)
NVIC = Nested Vectored Interrupt Controller
它是 STM32 内部管理所有中断的"总调度中心",负责:
- 决定哪个中断更重要(优先级)
- 中断来了,该跳到哪个函数去执行(向量)
- 处理高优先级中断打断低优先级中断(嵌套)
2.2 EXTI(外部中断/事件控制器)
EXTI = External Interrupt/Event Controller
检测 GPIO 引脚上的边沿信号(上升沿、下降沿),产生中断请求。
GPIO 和 EXTI 线的对应关系:
PA0 ─── EXTI0 ─── NVIC ─── CPU
PB0 ─── EXTI0 │
PE0 ─── EXTI0 │ 所有 GPIO 的 0 号引脚都共用 EXTI0
PG0 ─── EXTI0 │ 但同一时刻只能选一个
↓
EXTI0_IRQHandler
重要限制:同一编号的引脚只能有一个配成 EXTI 模式。比如配了 PE0 为 EXTI0,就不能再配 PA0/PB0/PF0 为 EXTI0。
2.3 中断优先级
STM32F4 用 4 位二进制表示优先级,共 16 级(0~15)。数值越小优先级越高。
优先级分组:
| 组 | 抢占优先级位数 | 子优先级位数 | 说明 |
|---|---|---|---|
| Group 0 | 0 | 4 | 所有中断同级,没有抢占 |
| Group 1 | 1 | 3 | 2 级抢占,每级 8 个子优先级 |
| Group 2 | 2 | 2 | 4 级抢占,每级 4 个子优先级 |
| Group 3 | 3 | 1 | 8 级抢占,每级 2 个子优先级 |
| Group 4 | 4 | 0 | 16 级抢占,无子优先级(默认) |
CubeMX 默认用 Group 4,即 16 个独立的优先级等级。
3. 硬件接线
3.1 接线表
和 PIR 轮询实验完全一样:
| PIR 模块 | F407 | 说明 |
|---|---|---|
| VCC(红) | 3.3V | 供电 |
| GND(黑) | GND | 共地 |
| OUT(绿) | PE0 | 中断输入脚 |
3.2 PIR 模块本身的三样调节
┌──────────────────────────────────────┐
│ PIR 模块(背面) │
│ │
│ ┌──┐ ┌──┐ │
│ │ │ SENS │ │ TIME │
│ │ │(距离) │ │(延时) │
│ └──┘ └──┘ │
│ │
│ ┌─────┐ │
│ │ H/L │ ← 跳线帽(触发模式) │
│ └─────┘ │
└──────────────────────────────────────┘
本次实验推荐设置:
-
跳线帽 → 拔掉 (等价 L / 不可重复触发)

-
TIME 旋钮 → 逆时针拧到底(最短延时~2秒)

-
SENS 旋钮 → 拧到中间(距离~5米)
4. CubeMX 配置(中断版)
4.1 新建工程
- 打开 CubeMX → New Project
- 搜索
STM32F407ZGTx→ 双击选中
4.2 引脚配置
| 引脚 | 设置 | 说明 |
|---|---|---|
| PE0 | GPIO_EXTI0 | PIR 输出,配成外部中断 |
| PF9 | GPIO_Output | 板载 LED1 |
配完后 PE0 引脚上会多出一个 EXTI 标签。
4.3 GPIO 参数配置
左边菜单栏 → GPIO → 找到 PE0:
| 参数 | 设置值 | 说明 |
|---|---|---|
| GPIO mode | External Interrupt Mode with Rising edge trigger detection | 上升沿触发(从低到高时中断) |
| GPIO Pull-up/Pull-down | Pull-down | 没人时默认低电平 |
| Maximum output speed | Low(保存默认) | 输入不需要速度配置 |
| User Label | 可留空或填写 "PIR_IN" | 方便识别 |
为什么选 Rising edge(上升沿)?
PIR_OUT: ───────┬──────────────
│
低→高的瞬间就是"人进入检测范围"
在这个瞬间触发一次中断就够用了
选 Falling edge(下降沿)就是检测人离开
选 Both 就是进入和离开都触发
4.4 NVIC 配置(重要!)
左边菜单栏 → System Core → NVIC:
找到 EXTI line 0 interrupt → 打勾 Enabled
☑ EXTI line 0 interrupt ← 这个必须勾上!
Preemption Priority: 5 ← 默认就好,不冲突
Sub Priority: 0
为什么必须在 NVIC 打勾?
不打勾 = CPU 知道中断来了,但是"不理会" = 中断永远不会被响应。
这是 STM32 中断的"总闸",永远记得要在这里打勾。
4.5 时钟配置
Clock Configuration → HCLK = 168 MHz
4.6 生成工程
Project Manager:
- Project Name:
pir_interrupt - Toolchain/IDE: MDK-ARM
- 点击 GENERATE CODE
5. 中断代码逐层解读
5.1 完整的调用链
硬件检测到 PE0 上升沿
↓
CPU 暂停 while(1),跳到中断向量表
↓
向量表找到 EXTI0_IRQHandler(在 stm32f4xx_it.c)
↓
EXTI0_IRQHandler 调用 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0)
↓
HAL_GPIO_EXTI_IRQHandler 清除中断标志位
↓
HAL_GPIO_EXTI_IRQHandler 调用你写的 HAL_GPIO_EXTI_Callback(GPIO_PIN_0)
↓
你的回调函数执行:Toggle LED
↓
返回 while(1),继续执行
5.2 EXTI0_IRQHandler(由 CubeMX 生成在 stm32f4xx_it.c)
c
/**
* @brief EXTI line0 中断服务函数
* @note 当 PE0 检测到上升沿时,硬件自动跳到这里
*/
void EXTI0_IRQHandler(void)
{
/* USER CODE BEGIN EXTI0_IRQn 0 */
/* USER CODE END EXTI0_IRQn 0 */
// 这行是核心:调 HAL 库函数做实际处理
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
/* USER CODE BEGIN EXTI0_IRQn 1 */
/* USER CODE END EXTI0_IRQn 1 */
}
5.3 HAL_GPIO_EXTI_IRQHandler(由 CubeMX 生成,HAL 库内部)
这个函数你做不了任何修改,它在 HAL 库里。它的工作:
- 检查
EXTI_PR寄存器中对应位是否置 1(确认中断确实发生了) - 清除中断标志位(这一步必须做,否则 CPU 以为中断一直在持续)
- 调用你写的回调函数
5.4 你写的回调函数 HAL_GPIO_EXTI_Callback
c
/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
// 判断是哪个引脚触发了中断
// 如果多个引脚都配了中断,用这个区分
if (GPIO_Pin == GPIO_PIN_0)
{
// 翻转 LED 状态
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9);
}
}
/* USER CODE END 4 */
为什么这里不用 while(1) 一直跑?
因为中断的回调函数是被动调用 的------没有中断发生时,CPU 在 while(1) 里正常执行。只有 PE0 出现上升沿时,回调函数才被触发执行。执行完立即返回 while(1)。
5.5 全局变量 vs 局部变量
你踩过的坑:
c
// ❌ 错误写法:变量定义在回调内部,每次进来重新声明
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
volatile int val = 0; // 每次中断进来都是 0
val = val + 1; // 每次都是 1
// val 永远到不了 2!
}
c
// ✅ 正确写法:变量定义在全局区,永远不会被销毁
/* USER CODE BEGIN PV */
volatile int irq_count = 0; // 全局变量
/* USER CODE END PV */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
irq_count++; // 每次中断进来,值增加
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9);
}
}
为什么加 volatile?
volatile告诉编译器:这个变量可能被中断修改- 防止编译器优化(编译器看到 while(1) 里没有改
irq_count,可能直接把它优化掉,导致永远读到 0)
6. 遇到的问题全记录
问题 ①:LED 一直亮,手怎么晃都没反应
现象:烧录中断代码后,板载 LED1 一直亮着,手在 PIR 前晃动无变化。
排查过程:
- 怀疑中断没触发 → 加
val计数 Watch 窗口看 → 计数值不动 - 怀疑 NVIC 没配置 → 打开 CubeMX 检查 → 发现没在 NVIC 给 EXTI0 打勾
- 打勾后重新生成 → 问题仍然存在
- 怀疑 PIR 模块坏了 → 烧回轮询代码测 → 轮询版 LED 也一直亮
- 原因:PIR 模块的三个调节硬件
最终解决:
- 拔掉跳线帽(H/L 模式选择)
- TIME 旋钮逆时针拧到底(延时最短)
- 重新上电,等待 30 秒让 PIR 稳定
经验和教训:
- 硬件问题(PIR 模块上的物理调节)影响了所有软件测试
- 轮询版和中断版都"死"是因为 PIR 一直输出高电平,不是中断代码的问题
- 遇到"都不工作"的情况,优先检查硬件
问题 ②:H/L 跳线帽搞不清
现象:跳线帽插在 H 时,PIR 触发一次后 LED 就锁死了。拔掉(L 模式)就正常。
原理:
| 跳线帽位置 | 模式 | 行为 |
|---|---|---|
| H | 可重复触发 | 人一直在感应区 → OUT 持续输出高电平 |
| L | 不可重复触发 | 检测到人 → OUT 输出一个高电平脉冲(约 2-3 秒)→ 自动恢复低电平 |
| 拔掉 | 等价 L 模式 | 同上 |
为什么 H 模式会让 LED 卡住?
H 模式 CPU 眼中的 PE0:
人进入 → ──────── 高(持续)──────────→ 人离开才恢复低
↑ 第 1 次触发中断(Toggle:亮)
↑ 不会再触发
↑ 第 2 次中断一直不来(Toggle 不再执行)
L 模式 CPU 眼中的 PE0:
人进入 → ──── 高(2 秒)────→ 低 → 人再进入 → ── 高(2 秒)──→ 低
↑ 触发中断(亮) ↑ 又触发中断(灭)
结论:中断实验推荐用 L 模式(拔掉跳线帽或跳到 L)。
问题 ③:TIME 旋钮的作用
现象:一个银色的十字旋钮,往一边拧 PIR 很灵敏,往另一边拧就没反应。
原理 :TIME 旋钮控制 OUT 输出高电平的持续时间。
逆时针拧到底(箭头指向 "-")→ 延时 ~2 秒
↓
────┐ ┌──── 低(2 秒后自动恢复)
└──────────┘
高电平(2 秒)
顺时针拧到底(箭头指向 "+")→ 延时 ~30 秒
↓
────────────────────────── 高(30 秒都不恢复)
另一个银色旋钮 SENS:控制检测距离(检测范围),不是灵敏度。
问题 ④:为什么调试时看不到 while(1) 被跳出
现象:在 Keil 里按 F5 全速运行,手靠近 PIR,看代码窗口------光标永远在 while(1) 里,看不到跳去中断函数。
原因:中断的响应速度极快。
- 从 PE0 产生上升沿 → CPU 跳进 EXTI0_IRQHandler → 执行完回调 → 回到 while(1)
- 整个过程约 几个微秒(1 微秒 = 0.000001 秒)
- Keil 的代码高亮刷新率跟不上,你根本看不到这个瞬间
想看的话:在回调函数这行加断点
c
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
// 双击左边栏,出现红色圆点就是断点
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9); ← 断点加这行
}
}
加好断点后按 F5 全速运行,手靠近 PIR,Keil 就会停在断点处,这时你能看到:
- 代码停在回调函数里
- 左边 Call Stack 窗口显示 main() → ... → HAL_GPIO_EXTI_Callback
- 说明程序确实"跳出"了 while(1),进入了中断回调
按一下 F8(单步执行)走完这一行,按 F5 继续运行。
问题 ⑤:中断优先级怎么理解
现象:NVIC 打勾了 EXTI0 中断,Preemption Priority 保持默认 5。
解释 :优先级 5 表示中等优先级(0 最高,15 最低)。在只有一个外设使用中断时,优先级是多少并不重要------只要打勾了就会执行。优先级只在多个中断同时发生时才起作用(谁优先级高就先响应谁)。
问题 ⑥:刚上电 LED 就亮
现象:烧录完程序,板载 LED1 一上电就亮着。
原因:CubeMX 默认 PF9 初始输出状态是 Low(低电平),而 DshanMCU-F407 的板载 LED 是低电平亮。
解决 :在 USER CODE BEGIN 2 加一行初始化:
c
/* USER CODE BEGIN 2 */
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_SET); // 初始灭
/* USER CODE END 2 */
7. PIR 模块硬件详解
7.1 模块上的三个可调元件
┌────────────────────────────────────┐
│ PIR 模块(RE200B) │
│ │
│ 跳线帽 H/L ← 触发模式选择 │
│ │ │
│ SENS 旋钮 │ TIME 旋钮 │
│ (检测距离) │ (输出延时) │
│ │
│ 逆时针=距离短 │ 逆时针=延时短 │
│ 顺时针=距离远 │ 顺时针=延时长 │
└────────────────────────────────────┘
7.2 什么时候需要等 30 秒?
PIR 模块刚上电时,内部有一个"稳定期"(约 30~60 秒):
- 模块在自检、校准环境红外背景
- 这段时间内模块可能输出不稳定(一直高或一直低)
- 30 秒后进入正常工作状态
所以你每次给 PIR 重新上电后,等 30 秒再做测试,不要一上电就急着试。
7.3 为什么盖布没用?
PIR 检测的是人体红外辐射(热信号) ,不是可见光。用布盖住没用的------布会保持你的手温,PIR 仍然能检测到。要测试 PIR,需要人离开感应范围,不是"挡住"。
8. 轮询 vs 中断对比
8.1 核心代码对比
| 轮询版本 | 中断版本 |
|---|---|
while(1) 里每 100ms 读一次 |
while(1) 里什么都不用做 |
| CPU 不断检查寄存器 | CPU 空闲,等外设叫它 |
| 响应延迟 = 轮询间隔(最大 100ms) | 响应延迟 = 微秒级 |
8.2 CPU 占用对比
轮询 CPU 占用:
┌────────────────────────────────────────────┐
│ 读 PE0 │ 写 PF9 │ 读 PE0 │ 写 PF9 │ 读 PE0 │
└────────────────────────────────────────────┘
CPU 80% 时间都在"读 PE0"这件事上
中断 CPU 占用:
┌────────────────────────────────────────────┐
│ while(1) 空转(或休眠) │
│ │
│ ← 中断来了 → 写 PF9 → 回去 │
│ │
│ while(1) 继续空转 │
└────────────────────────────────────────────┘
CPU 99% 的时间是空闲的
8.3 什么时候用轮询,什么时候用中断?
| 场景 | 推荐 | 原因 |
|---|---|---|
| 按键检测 | 中断或轮询 | 看需求,按键抖动需要消抖 |
| PIR 检测 | 中断或轮询 | PIR 变化慢,两种都可以 |
| 串口接收数据 | 中断 | 数据随时可能来,轮询会丢 |
| GPS 接收 | 中断(DMA+IDLE) | 数据持续不断,轮询 CPU 扛不住 |
| 超声波测距 | 中断(输入捕获) | 需要精确测量脉宽,微秒级响应 |
9. 扩展思考
9.1 如果改为下降沿触发会怎样?
c
// CubeMX 中把 GPIO mode 改为:
External Interrupt Mode with Falling edge trigger detection
这样 PIR 检测到有人进去的那一瞬间不触发,人离开时触发。所以 PIR 检测到人 → LED 不亮,人离开 → LED 亮。
9.2 如果要实现"人来了亮,人走了灭"?
c
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
// 读取当前电平,而不是翻转
if (HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_0) == GPIO_PIN_SET)
{
// 上升沿:人来了 → 开灯
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_RESET);
}
else
{
// 下降沿:人走了 → 关灯
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_SET);
}
}
}
9.3 中断里面能不能用 HAL_Delay?
不能 。HAL_Delay 依赖 SysTick 中断,而中断里的优先级如果高于 SysTick,SysTick 就无法执行,HAL_Delay 会死锁。
如果需要延时,用定时器代替,或者用"状态机"思想------在中断里只设一个标志位,在 while(1) 里检测标志位后再执行延时代码。
c
volatile uint8_t pir_triggered = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
pir_triggered = 1; // 只设标志,不做事
}
}
int main()
{
while (1)
{
if (pir_triggered)
{
pir_triggered = 0;
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9);
HAL_Delay(500); // 在 while(1) 里可以用延时
}
}
}
9.4 后续你学到什么
06-4: OLED调试(你没OLED,可以用LCD替代或串口printf)
06-5-1: 定时器消抖(替代 HAL_Delay 做延时,更精确)
06-5-2: 环形缓冲区(存储连续的中断触发时间戳)
06-5-3: 防按键丢失(用中断+缓冲区保证数据不丢)
编写日期 :2026年5月25日
适用硬件 :DshanMCU-F407(STM32F407ZGT6)+ PIR 人体红外模块
配套视频 :06-1 中断概念 → 06-2 中断体系 → 06-3 GPIO 中断编程
踩坑记录:PIR H/L 模式选择、TIME 旋钮调节、NVIC 勾选、局部变量 vs 全局变量、Keil 调试看不到中断跳转