从”能用”到”理解”
上一篇博客中,我们完成了外部中断的功能:红灯闪烁,按下 KEY1 触发中断,翻转绿灯。
但当时更多的只是照着教程写了代码,对于”为什么这样配置就能触发中断”、”从按键按下到 ISR 执行中间发生了什么”,只有一个模糊的概念。
这篇就来填补这个空白——沿着信号的路径,从 KEY1 引脚一直追踪到 CPU 执行中断服务函数,搞懂完整的中断触发流程。
完整信号路径概览
从 KEY1(PB12)引脚的电平变化到执行中断服务函数,总共经过 5 个阶段:
1 | GPIO 引脚(PB12) |
接下来逐一深入每个阶段。
第一阶段:GPIO 输入驱动器
信号从 KEY1(PB12)引脚进入芯片,首先经过的是 GPIO 输入驱动器。
输入驱动器的结构
每个 GPIO 引脚内部都有一套输入驱动电路,核心是两个串接的施密特触发器:
1 | PB12 引脚 |
施密特触发器的作用是整形——把缓慢变化或不干净的电平信号转换成干净的数字方波:
想象一下,机械按键按下时电平不是瞬间从高跳到低的,中间会有抖动和毛刺。施密特触发器的作用就是”过滤”这些不稳定的中间态——低于阈值判为 0,高于阈值判为 1,输出一个干净的数字信号。
输入驱动器在做什么
不管 PB12 配置成普通 GPIO 输入还是 EXTI 中断模式,GPIO 的输入驱动器都在做同一件事——信号整形:
- 施密特触发器接收引脚上的模拟电平
- 将其转换成干净的 0/1 数字信号(低于阈值判 0,高于判 1,过滤中间抖动)
- 整形后的信号分两路输出:一路到 **GPIO 输入数据寄存器 (IDR)**,另一路到 片上外设(这里是 EXTI 模块的边沿检测电路)
1 | PB12 引脚上的模拟电平(可能带抖动) |
这也解释了为什么上一篇中可以在 ISR 里用 HAL_GPIO_ReadPin() 再次确认按键状态——因为有单独一路信号始终通往 IDR,不依赖 EXTI 的工作状态。
注意:边沿检测(下降沿/上升沿)不在这里发生,那是下一阶段 EXTI 电路的工作。输入驱动器的职责到此为止——只负责整形和转发。
关于 GPIO 八大模式的详细解析,参考之前那篇 GPIO 模式的博客。
第二阶段:边沿检测电路
经过输入驱动器整形后的干净信号,同时送给上升沿检测和下降沿检测两条路径:
两个触发选择寄存器
EXTI 模块有两个触发选择寄存器,分别控制上升沿和下降沿的使能:
| 寄存器 | 全称 | 作用 |
|---|---|---|
EXTI_RTSR |
Rising Trigger Selection Register | 对应位写 1 → 使能该线的上升沿触发 |
EXTI_FTSR |
Falling Trigger Selection Register | 对应位写 1 → 使能该线的下降沿触发 |
每个寄存器都有 20 位(bit 0~19),每 1 位对应一条 EXTI 线。默认所有位都是 0(不使能),只有我们配置的那一位会被设为 1。
拿我们的配置举例——选择了下降沿触发,所以下降沿寄存器状态是:
1 | EXTI_FTSR 寄存器(20位): |
而 EXTI_RTSR 寄存器则保持全 0,因为我们没有使能上升沿触发。
我们的配置
上一篇我们在 CubeMX 中选择了 Falling edge trigger detection(下降沿触发),这意味着:
1 | EXTI_FTSR 的 bit 12 = 1 (使能下降沿检测) |

在生成的代码中,HAL_GPIO_Init() 内部通过位运算来操作这两个寄存器。拿我们的下降沿触发举例,等价于做了这样一件事:
1 | // HAL_GPIO_Init 内部对 FTSR 的操作(以 PB12 为例) |
这行代码的意思是:读取 FTSR 寄存器的当前值 → 把第 12 位改成 1 → 写回去。
操作前后的寄存器变化:
1 | 执行前: FTSR = 0x0000 0000 (所有位都是 0,所有 EXTI 线都没使能) |
GPIO_PIN_12 的值是 0x1000(即 1 << 12),这个值与 FTSR 做按位或运算,结果就是只把第 12 位设成 1,不影响其他位。
同理,如果配置的是上升沿触发,操作的就是 RTSR 寄存器:
1 | EXTI->RTSR |= GPIO_PIN_12; // RTSR 的第 12 位置 1 |
边沿检测电路的工作原理
1 | 当前电平 → 延迟一个时钟周期 → 旧电平 |
具体过程:
- 每个时钟周期,电路比较当前电平和上一周期的电平
- 如果当前电平 > 上一周期电平 = 上升沿
- 如果当前电平 < 上一周期电平 = 下降沿
- 检测到的边沿再与
RTSR/FTSR对应位做与运算 - 如果该位为 1(使能了对应边沿检测),与门输出 1
- 两个与门的输出经过或门合并
所以对于我们的场景:KEY1 按下(高→低),下降沿被检测到,FTSR bit12 = 1,与门输出 1,或门输出 1 → 向后传递一个高电平信号。
第三阶段:EXTI 寄存器组
第二阶段输出的高电平信号到达了 EXTI 的核心寄存器组:
请求挂起寄存器 (EXTI_PR)
EXTI_PR(Pending Register),也叫挂起寄存器或待决寄存器。
当边沿检测电路传来高电平信号时,EXTI_PR 的对应位被置为 1:
| 位 | 含义 |
|---|---|
| bit 12 = 1 | EXTI12 线有中断请求正在等待处理 |
只要 PR 的某一位为 1,就表示该中断线有一个”未处理”的中断请求。
这个寄存器很关键——CPU 正是通过查询 PR 知道”谁触发了中断”的。
这就像外卖到了,门卫(PR)在本子上记下”12 号房有外卖”,然后通知你。
中断屏蔽寄存器 (EXTI_IMR)
EXTI_IMR(Interrupt Mask Register),决定是否允许某条中断线向 NVIC 发送请求:
- 某位 = 1:允许该中断线继续向后传递(放行)
- 某位 = 0:屏蔽该中断线(拦截)
CubeMX 在配置 PB12 为 GPIO_EXTI12 时,会自动把 IMR 的 bit 12 设为 1。
1 | // HAL_GPIO_Init 内部使能 IMR 的底层代码 |
PR 和 IMR 的与门组合
PR 的输出和 IMR 的输出经过一个与门:
1 | EXTI_PR bit12 = 1 ───┐ |
只有 两者都是 1 时,与门才输出 1,中断信号才能到达 NVIC。
这就是中断屏蔽的原理——把 IMR 的某位设为 0,即使 PR 被置 1,与门也会输出 0,相当于”关掉了”这条中断线。
第四阶段:NVIC(嵌套向量中断控制器)
第三阶段输出的信号终于到了中断的”最高层”——NVIC(Nested Vectored Interrupt Controller)。
NVIC 的角色
先补充一个概念:Cortex-M3 是什么?
STM32F103 芯片内部可以分成两层:
- 内核(Core)——ARM 公司设计的 CPU 核心,负责执行指令、处理运算。STM32F103 用的是 Cortex-M3 内核,属于 ARM 的”M 系列”(Microcontroller),专门为单片机设计的低功耗、高性能内核
- 外设(Peripheral)——ST 公司在内核周围添加的功能模块,比如 GPIO、EXTI、定时器、UART 等。这些是 ST 自己设计的,不同型号的 STM32 外设各有不同
打个比方:Cortex-M3 内核就像汽车的发动机,外设就像车轮、方向盘、空调——发动机决定了这辆车的基础性能,而外设决定了这辆车能干什么。
NVIC 是 Cortex-M3 内核的一部分,负责:
- 管理所有中断的使能和屏蔽
- 管理中断优先级——当多个中断同时触发时决定先处理谁
- 找到中断向量——根据中断号找到对应的中断服务函数地址
- 支持中断嵌套——高优先级中断可以打断低优先级中断
中断向量
每个中断源都有一个唯一的中断号,对应中断向量表中的一项。EXTI12 的中断号是 EXTI15_10_IRQn(因为 EXTI 线 10~15 共享同一个中断入口)。
中断向量表存储在各个中断服务函数的地址:
| 中断号 | 中断名 | 函数地址 |
|---|---|---|
| 23 | EXTI15_10_IRQn | → EXTI15_10_IRQHandler() |
当 NVIC 检测到 EXTI12 的中断信号时,它取出向量表中 EXTI15_10 对应的地址,然后 CPU 跳转到那里执行。
NVIC 相关的 CubeMX 配置代码
在 MX_GPIO_Init() 中,CubeMX 生成了两行关键代码:
1 | /* EXTI interrupt init*/ |
这两个函数的背后操作:
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn) 内部操作的是 NVIC_ISER(Interrupt Set Enable Register)寄存器:
1 | // 简化版实现 |
ISER 寄存器有 3 个(共 96 位),每一位对应一个中断号。EXTI15_10_IRQn 的中断号是 23,所以 ISER[0] 的第 23 位被置 1。
NVIC 总共有三个关键使能寄存器:
- ISER:Interrupt Set Enable Register——置 1 使能中断
- ICER:Interrupt Clear Enable Register——置 1 禁能中断
- ISPR/ICPR:中断挂起/清除挂起
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 15, 0) 设置优先级,后面会展开讲。
第五阶段:执行 ISR 与清标志位
NVIC 把中断信号送达 CPU 内核后,CPU 的响应流程:
1 | NVIC 发出中断请求 |
为什么必须清 PR?
这是最关键的一步,也是最容易忘的一步。
EXTI_PR 寄存器的作用是”记住”中断请求。如果不主动清零:
- ISR 执行完毕后,PR bit12 仍然为 1
- 与门另一端的 IMR bit12 也为 1(没关中断)
- 与门持续输出高电平到 NVIC
- NVIC 认为又有新中断来了
- CPU 再次进入 ISR
- 死循环!
所以在 ISR 末尾必须调用:(当然,好心的cubemx会自动帮我们生成这行代码)
1 | HAL_GPIO_EXTI_IRQHandler(KEY1_Pin); |
这个函数内部会检查 PR 对应位,如果为 1 就将它清零:
1 | void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin) |
清零的底层操作:
1 | // __HAL_GPIO_EXTI_CLEAR_IT(KEY1_Pin) 展开后 |
⚠️ 规律:向 PR 写 1 来清除中断标志位。 这是 STM32 中常见的寄存器设计——读某位判断状态,写 1 清除该位。不要写成写 0,那不会生效。
深入中断优先级
4 位优先级的分配
上一篇博客只提了一句”配置优先级”,现在我们来看看背后发生了什么。
STM32F1 为每个中断向量分配了 4 个二进制位来存储优先级信息:
1 | NVIC->IP[IRQn] = [bit7][bit6][bit5][bit4][bit3][bit2][bit1][bit0] |
CubeMX 中的 优先级分组(Priority Group) 决定了这 4 位如何分配到抢占优先级和子优先级:
| 分组 | 抢占优先级(bit) | 子优先级(bit) | 抢占级数量 | 子优先级数量 |
|---|---|---|---|---|
| 组 0 | 0 | 4 | 1 | 16 |
| 组 1 | 1 | 3 | 2 | 8 |
| 组 2 | 2 | 2 | 4 | 4 |
| 组 3 | 3 | 1 | 8 | 2 |
| 组 4 | 4 | 0 | 16 | 1 |
CubeMX 默认使用组 4,即全部 4 位用于抢占优先级,没有子优先级。
优先级编码
CubeMX 中配的”Preemption Priority = 15”并不是直接写进寄存器的值,中间有一个编码过程。
以组 4 为例,4 位全部是抢占优先级:
| 中断 | CubeMX 中配的优先级 | 编码到 IP 寄存器高 4 位 | 实际优先级值 |
|---|---|---|---|
| SysTick | 14 | 1110 | 14(最高) |
| EXTI15_10 | 15 | 1111 | 15(最低) |
HAL_NVIC_SetPriority 内部做编码:
1 | // HAL_NVIC_SetPriority 简化逻辑 |
中断嵌套
如果多个中断被触发,NVIC 根据优先级决定处理顺序:
- 数值越小,优先级越高
- 高优先级中断可以打断低优先级中断(嵌套)
- 同优先级中断按中断号决定,号小的先执行
我们配置的优先级关系:
| 中断 | 优先级 | 说明 |
|---|---|---|
| SysTick | 14 | 最高,可以抢占其他所有中断 |
| EXTI15_10 | 15 | 最低 |
所以上一篇中在 EXTI 的 ISR 里调用 HAL_Delay(10) 是安全的——SysTick(优先级 14)可以打断 EXTI(优先级 15)来更新 uwTick,不会死锁。
完整映射:CubeMX 配置 → 底层寄存器
用一张表把 CubeMX 配置和底层寄存器对应起来:
| CubeMX 配置项 | 影响的寄存器 | 对应代码 |
|---|---|---|
| PB12 → GPIO_EXTI12 | AFIO_EXTICR3(选择 PB12 连接到 EXTI12) | HAL_GPIO_Init() 内部 |
| GPIO mode → IT Falling | EXTI_FTSR bit12 = 1 | EXTI->FTSR |= (1<<12) |
| EXTI line[15:10] NVIC Enabled | NVIC_ISER[0] bit23 = 1 | HAL_NVIC_EnableIRQ(EXTI15_10_IRQn) |
| Preemption Priority = 15 | NVIC_IP[23] 高 4 位 = 1111 | HAL_NVIC_SetPriority(EXTI15_10_IRQn, 15, 0) |
| (自动生成) | EXTI_IMR bit12 = 1 | EXTI->IMR |= (1<<12) |
| (自动生成) | EXTI_PR bit12 清 0 | EXTI->PR = (1<<12) |
总结
完整信号路径回顾
1 | KEY1 按下 (PB12 从高到低) |
核心要点
| 要点 | 说明 |
|---|---|
| EXTI_PR | 记录中断请求,ISR 中必须清零,否则反复触发 |
| EXTI_IMR | 中断屏蔽开关,1 = 放行,0 = 屏蔽 |
| EXTI_RTSR/FTSR | 选择上升沿/下降沿触发 |
| NVIC | 总控制器,管理所有中断的使能和优先级 |
| 优先级组 | 决定 4 位中多少位给抢占、多少位给子优先级 |
| 中断嵌套 | 高优先级可打断低优先级,SysTick > EXTI |
与其他中断的共性
虽然这篇以外部中断(EXTI)为例,但所有中断的流程都有类似的框架:
- 外设产生中断事件 → 对应挂起寄存器置位
- 中断使能/屏蔽寄存器决定是否放行
- 信号到达 NVIC,根据优先级排队
- NVIC 让 CPU 跳转到对应的中断服务函数
- ISR 中必须清除挂起标志
后续学习定时器、UART、DMA 等外设的中断时,它们的流程都遵循这个模式。







