
STM32 中断与异常深度详解:从概念到 NVIC 寄存器
1. 什么是中断
1.1 生活类比
你正在厨房炒菜,门铃响了——放下锅铲去开门,处理完回来继续炒菜。
这就是中断:
- 主程序 = 炒菜
- 中断事件 = 门铃响
- 中断服务程序(ISR) = 去开门
- 中断返回 = 回来继续炒菜
定义:
CPU 正在执行主程序,突然收到紧急信号,暂停当前工作,转而去处理这个紧急事件,处理完后回到原来暂停的地方继续执行。
1.2 完整执行流程
① 中断请求(IRQ) → 外设/事件发出中断信号
② 中断响应 → CPU 判断优先级,决定是否响应
③ 保存现场 → 硬件自动压栈:xPSR、PC、LR、R0~R3、R12
④ 跳转到 ISR → 查向量表,找到中断服务函数入口
⑤ 执行 ISR → 用户自己写的中断处理代码
⑥ 恢复现场 → 硬件自动出栈
⑦ 中断返回 → 通过 EXC_RETURN 回到主程序断点
1.3 中断 vs 轮询
| CPU 占用 | 一直查,浪费 | 事件来了才处理 |
| 实时性 | 取决于轮询周期 | 几乎实时响应 |
| 功耗 | 一直跑,高 | 可休眠等待唤醒 |
| 代码复杂度 | 简单但臃肿 | 需要理解中断机制 |
2. STM32 中,中断的意义
2.1 轮询的问题
// 轮询:CPU 死等在 while 里
while(1) {
if (按键按下) { 处理按键; }
// 这里干不了别的事
}
2.2 中断的优势
// 中断:主程序干自己的事
int main(void) {
while(1) {
__WFI(); // 休眠,等中断唤醒
}
}
void EXTI0_IRQHandler(void) { // 硬件自动调用
if (EXTI_GetITStatus(EXTI_Line0)) {
处理按键;
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
三大价值:
3. 谁会产生中断

3.1 内核异常(Exception)

Cortex-M 内核自身产生,编号 1~15:
| 1 | Reset | -3(最高) | 复位 |
| 2 | NMI | -2 | 不可屏蔽中断 |
| 3 | HardFault | -1 | 硬件错误(除零、非法访问) |
| 4 | MemManage | 可编程 | MPU 违规 |
| 5 | BusFault | 可编程 | 总线错误 |
| 6 | UsageFault | 可编程 | 未对齐、未定义指令 |
| 11 | SVCall | 可编程 | 系统调用(RTOS 用) |
| 14 | PendSV | 可编程 | 可挂起系统调用(RTOS 上下文切换) |
| 15 | SysTick | 可编程 | 系统滴答定时器 |
3.2 外部中断(IRQ)
外设产生,编号从 16 开始(IRQn = 中断号 – 16):
- GPIO:EXTI0~EXTI15
- 定时器:TIM1~TIM14
- 串口:USART1~UART5
- ADC、DMA、I2C、SPI、RTC、USB、CAN 等
区分:异常是内核级(1~15),IRQ 是外设级(16 起),都由 NVIC 统一管理。
4. 中断向量表
4.1 什么是向量表
向量表 = 函数指针数组,每项对应一个中断的服务函数入口地址。
中断发生时,CPU 根据中断号查表跳转,不需要自己判断"该找谁"。
| 0x00 | 初始 MSP 值 | 栈顶指针(不是中断!) |
| 0x04 | Reset_Handler | 复位入口 |
| 0x08 | NMI_Handler | NMI |
| 0x0C | HardFault_Handler | 硬件错误 |
| … | … | … |
| 0x40 | IRQ0_Handler | 外部中断 0(WWDG) |
| … | … | … |
注意:
第 0 项是初始栈指针,不是中断向量。
CPU 上电先从 0x00 取栈顶,从 0x04 取复位地址。
4.2 C 源文件和汇编混合链接
4.2.1 启动文件 startup_stm32f103xb.s 核心结构
; 1. 栈空间
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; 2. 向量表(放在 Flash 最开头)
AREA RESET, DATA, READONLY
EXPORT __Vectors
__Vectors DCD __initial_sp ; 0x00: 栈顶
DCD Reset_Handler ; 0x04: 复位
DCD NMI_Handler ; 0x08
DCD HardFault_Handler ; 0x0C
; … 更多异常 …
DCD WWDG_IRQHandler ; IRQ0
DCD PVD_IRQHandler ; IRQ1
; … 更多外设中断 …
; 3. Reset_Handler:复位后第一个执行
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0 ; 先配时钟
LDR R0, =__main
BX R0 ; 跳 C 库,最终进 main()
ENDP
; 4. 默认中断函数:弱定义,死循环
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B . ; 死循环
ENDP
4.2.2 关键机制:[WEAK] 弱符号
- 启动文件用 [WEAK] 定义了所有中断函数的默认实现(死循环)
- 你在 C 文件里写同名函数(如 USART1_IRQHandler()),链接器优先用你的版本,覆盖弱定义
- 不写就用默认死循环版本
这就是为什么写个函数名完全匹配的 ISR 就能自动被调用——不需要注册。
4.2.3 内存布局
Flash (0x08000000) RAM (0x20000000)
┌──────────────┐ ┌──────────────┐
│ 中断向量表 │ │ .data 段 │
├──────────────┤ ├──────────────┤
│ .text 代码 │ │ .bss 段 │
├──────────────┤ ├──────────────┤
│ .data 初值 │ │ 堆 Heap │
└──────────────┘ ├──────────────┤
│ 栈 Stack │
│ ↓ 向下生长 │
└──────────────┘
4.3 STM32 工程源文件提供者

| 启动文件 | startup_stm32f103xb.s | ST | 向量表、栈堆初始化 |
| 系统文件 | system_stm32f1xx.c | ST | SystemInit() 配时钟 |
| 库文件 | stm32f1xx_gpio.c 等 | ST(标准库/HAL) | 外设操作封装 |
| 中断模板 | stm32f1xx_it.c | ST | ISR 集中存放点 |
| 用户文件 | main.c 等 | 你 | 应用逻辑 |

4.4 中断向量表在哪里

4.4.1 默认位置
上电后 CPU 从 0x00000000 取向量表,通过 BOOT 引脚映射到不同区域:
| 0 | X | 主 Flash | 0x08000000 |
| 1 | 0 | 系统存储器(ISP) | 0x1FFFF000 |
| 1 | 1 | 内置 SRAM | 0x20000000 |
4.4.2 向量表偏移寄存器 VTOR
// 把向量表搬到 SRAM(Bootloader 常用)
SCB->VTOR = 0x20000000;
地址必须对齐(向量表大小向上取整到 2 的幂)。
5. 中断号

5.1 中断号到底是什么?
一句话:中断号就是向量表这个"数组"的下标。
你把中断向量表想象成一个超大的数组 vector_table[]:
vector_table[0] = __initial_sp ← 初始栈顶(不是中断!)
vector_table[1] = Reset_Handler ← 中断号 1
vector_table[2] = NMI_Handler ← 中断号 2
vector_table[3] = HardFault_Handler ← 中断号 3
vector_table[4] = MemManage_Handler ← 中断号 4
…
vector_table[16] = WWDG_IRQHandler ← 中断号 16(第一个外设中断)
vector_table[17] = PVD_IRQHandler ← 中断号 17
…
vector_table[53] = USART1_IRQHandler ← 中断号 53
中断号 = 这个数组的下标
就这么简单。CPU 不知道"串口1中断"叫什么名字,它只认编号。发生中断时,硬件给 CPU 一个编号,CPU 就拿这个编号去数组里查表,找到函数地址跳过去。
5.2 为什么下标从 1 开始?0 去哪了?
这是最容易困惑的点。
Cortex-M 内核规定:
向量表第 0 项(偏移 0x00)不是中断向量,而是初始栈指针(MSP)。
为什么这么设计?
因为 CPU 上电复位后,做的第一件事不是执行代码,而是要初始化栈。
栈指针从哪来?就从向量表第 0 项取。
CPU 上电启动流程:
1. 从地址 0x00 读 4 字节 → 当作初始栈顶 MSP
2. 从地址 0x04 读 4 字节 → 当作复位 Reset_Handler 地址
3. 跳过去执行 Reset_Handler → 进入 main()
所以地址 0x00 这个位置被"占用"了,只能放栈顶,不能放中断向量。
于是中断号只能从 1 开始编号。
就像你家大楼,一楼是大堂(放门禁的),真正的住户从二楼开始。
地址 0 是大堂,住户编号从 1 开始。
5.3 为什么 1~15 是"内核异常",16 起才是"外设中断"?
这是 Cortex-M 内核设计者定死的编号规则,不是 ST 定的。
Cortex-M3/M4 内核自己需要一些"内部中断"来处理错误、系统调用这些事,这些编号被内核预留了:

前 15 个编号是 Cortex-M 内核"自留地",所有 Cortex-M3/M4 芯片都一样。
从 16 开始,才是 ST(或其他芯片厂商)自己加的外设中断。
ST 加了 WWDG、PVD、EXTI、USART、TIM……每家厂商加的不一样,数量也不一样。
就像手机系统:前几个按键(电源、音量)是所有手机都有的"系统键",后面的快捷键是各品牌自定义的。
5.4 IRQn_Type 枚举:代码里看到的是什么?
打开 STM32 的头文件 stm32f10x.h,你会看到这样一段:
typedef enum IRQn
{
/***** Cortex-M3 内核异常(编号就是绝对值,负数) *****/
NonMaskableInt_IRQn = -14, // NMI,中断号 = 16 + (-14) = 2
MemoryManagement_IRQn = -12, // MemManage,中断号 = 16 + (-12) = 4
BusFault_IRQn = -11, // BusFault,中断号 = 16 + (-11) = 5
UsageFault_IRQn = -10, // UsageFault,中断号 = 16 + (-10) = 6
SVCall_IRQn = -5, // SVCall,中断号 = 16 + (-5) = 11
PendSV_IRQn = -2, // PendSV,中断号 = 16 + (-2) = 14
SysTick_IRQn = -1, // SysTick,中断号 = 16 + (-1) = 15
/***** STM32 外设中断(从 0 开始正数) *****/
WWDG_IRQn = 0, // 中断号 = 16 + 0 = 16
PVD_IRQn = 1, // 中断号 = 17
TAMPER_IRQn = 2, // 中断号 = 18
RTC_IRQn = 3, // 中断号 = 19
EXTI0_IRQn = 16, // 中断号 = 16 + 16 = 32
USART1_IRQn = 37, // 中断号 = 16 + 37 = 53
// …
} IRQn_Type;
重点理解这里的"负数设计":
外设中断从 0 开始数(WWDG=0, PVD=1…)
内核异常用负数表示(NMI=-14, HardFault 没在这里列,它是 -13 对应的编号 3…)
CPU 真正用来查表的"绝对中断号"怎么算?
绝对中断号 = IRQn_Type 的值 + 16
为什么要 +16?
因为外设 IRQn 从 0 开始,对应的绝对中断号是 16。这个 +16 的偏移是 Cortex-M 内核定死的。
| -14 | NMI | 2 |
| -12 | MemManage | 4 |
| 0 | WWDG | 16 |
| 16 | EXTI0 | 32 |
| 37 | USART1 | 53 |
这样设计的好处:
库函数里只需要传 IRQn(比如 NVIC_EnableIRQ(USART1_IRQn)),
库函数内部自动 +16 算出绝对中断号去操作寄存器。你不用记绝对编号。
5.5 中断号怎么用?完整链路走一遍
假设 USART1 收到一个字节,触发了中断。整个过程是:
第 1 步:硬件标记"挂起"
USART1 外设收到数据,硬件把 NVIC 里 ISPR(中断挂起寄存器)的第 37 位置 1(因为 USART1 的 IRQn 是 37)。
ISPR[0] 寄存器的 bit 37 = 1 → 表示 USART1 中断正在等待处理
第 2 步:NVIC 判断优先级
NVIC 看一下:
都满足 → 决定响应。
第 3 步:CPU 用中断号查表
CPU 拿到绝对中断号 53,开始算向量表地址:
向量表基地址(VTOR) = 0x08000000(默认在 Flash)
USART1 的向量地址 = VTOR + 绝对中断号 × 4
= 0x08000000 + 53 × 4
= 0x08000000 + 212
= 0x080000D4
CPU 从 0x080000D4 这个地址读 4 字节,读到的就是 USART1_IRQHandler 这个函数的地址。
第 4 步:跳过去执行
CPU 把当前现场压栈,然后 PC 寄存器改成 USART1_IRQHandler 的地址,开始执行你的中断函数。
5.6 用一个例子彻底搞懂
为什么 USART1 的中断函数必须叫 USART1_IRQHandler,而不能叫 My_Uart_Handler?
答案:因为启动文件的向量表里写死了:
__Vectors DCD __initial_sp ; 偏移 0x00
DCD Reset_Handler ; 偏移 0x04(中断号 1)
DCD NMI_Handler ; 偏移 0x08(中断号 2)
…
DCD USART1_IRQHandler ; 偏移 0xD4(中断号 53)
当 USART1 中断发生,CPU 去 0xD4 读地址,读到的就是 USART1_IRQHandler。
如果你把函数名写成 My_Uart_Handler,向量表里那个位置还是弱定义的默认死循环函数——CPU 跳过去就死循环了,根本不会调到你的函数。
函数名必须和向量表里的名字一致,链接器才能把你的函数地址填到那个位置上。
5.7 一张图总结
绝对中断号 向量表偏移 内容
───────── ───────── ──────────────────
0 0x00 初始栈顶 MSP(不是中断!)
1 0x04 Reset_Handler
2 0x08 NMI_Handler
3 0x0C HardFault_Handler
… … …
15 0x3C SysTick_Handler
16 0x40 WWDG_IRQHandler ← 外设开始
17 0x44 PVD_IRQHandler
… … …
32 0x80 EXTI0_IRQHandler
… … …
53 0xD4 USART1_IRQHandler
核心公式:
这就是中断号的全部本质——它就是向量表这个数组的下标,CPU 靠它找到该执行哪个函数。
6. NVIC

6.1 谁来管理中断 — NVIC
NVIC(Nested Vectored Interrupt Controller) 是 Cortex-M 内核的一部分:
- 中断使能/失能
- 优先级设置
- 挂起状态管理
- 嵌套判断
- 响应与返回控制
NVIC 是内核紧耦合的,响应极快——从中断请求到进入 ISR 仅需约 12 个时钟周期。
6.2 中断嵌套
6.2.1 什么是嵌套
高优先级中断可以打断低优先级中断的执行。
主程序: ──────█~~~~~~~~~~~~~~~~~~~~~~~~────
↓ 低优先级中断 A
中断 A: █~~~~~~~~~~~~~~~~~█
↓ 高优先级中断 B 来了
中断 B: █~~~~~█
中断 A: █~~~~~█
主程序: ──────█~~~~~~~~~~~~~~~~~~~~~~~~────
6.2.2 嵌套条件
对比 ARM7:
ARM7 默认不能嵌套,要手动在 ISR 里开中断,很麻烦。Cortex-M 把嵌套硬件化了。
6.3 中断优先级
6.3.1 抢占优先级 + 子优先级
4 个 bit 分成两组:
- 抢占优先级:决定能不能嵌套——数值越小优先级越高
- 子优先级:同时挂起时谁先执行——不能嵌套
6.3.2 优先级分组
| Group_0 | 0 | 4 | 1 | 16 |
| Group_1 | 1 | 3 | 2 | 8 |
| Group_2 | 2 | 2 | 4 | 4 |
| Group_3 | 3 | 1 | 8 | 2 |
| Group_4 | 4 | 0 | 16 | 1 |
推荐 Group_2:4 级抢占 + 4 级子优先级,兼顾嵌套深度和并发处理。
6.3.3 判断规则
注意:优先级数值越小,优先级越高。0 是最高,15 是最低。
6.3.4 代码示例
// 分组:2 bit 抢占 + 2 bit 子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// USART1:抢占 1,子 0
NVIC_InitTypeDef n;
n.NVIC_IRQChannel = USART1_IRQn;
n.NVIC_IRQChannelPreemptionPriority = 1;
n.NVIC_IRQChannelSubPriority = 0;
n.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&n);
// TIM2:抢占 0(比 USART1 高,能打断它)
n.NVIC_IRQChannel = TIM2_IRQn;
n.NVIC_IRQChannelPreemptionPriority = 0;
n.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&n);
6.4 理解 NVIC
6.4.1 工作流程
外设中断请求 → 挂起(Pending)
↓
优先级判断
↓
响应?
├─ 是 → 硬件压栈 → 跳向量表 → 执行 ISR → 硬件出栈 → 返回
└─ 否 → 保持挂起,等当前中断结束
6.4.2 Tail-Chaining(尾链)
中断 A 刚结束,中断 B 已挂起 → 直接从 ISR A 跳到 ISR B,不用先恢复再保存,省约 12 个周期。
6.4.3 Late Arriving(迟到中断)
压栈过程中来了更高优先级的中断 → 硬件先响应高优先级的,压栈只做一次。
6.5 NVIC 内部寄存器
NVIC 基地址 0xE000E100:
| ISER0~7 | 0x000 | 中断使能(写 1 使能) |
| ICER0~7 | 0x080 | 清除使能(写 1 失能) |
| ISPR0~7 | 0x100 | 挂起置位 |
| ICPR0~7 | 0x180 | 挂起清除 |
| IABR0~7 | 0x200 | 活动位(看是否正在执行) |
| IPR0~59 | 0x300 | 优先级(每个 8 bit,用高 4 bit) |
6.5.1 优先级寄存器位域
IPR[x] 每个中断 1 字节:
bit 7 6 5 4 3 2 1 0
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ 抢占优先级 │ 子优先级 │0│
└──┴──┴──┴──┴──┴──┴──┴──┘
← 有效 → ←保留→
低 4 bit 保留,直接写寄存器时优先级值要左移 4 位。
6.5.2 内核辅助寄存器
| PRIMASK | 写 1 屏蔽所有可屏蔽中断(只剩 NMI/HardFault) |
| FAULTMASK | 屏蔽所有异常(包括 HardFault) |
| BASEPRI | 设阈值,低于该优先级的全屏蔽 |
__disable_irq(); // PRIMASK=1,关中断
__enable_irq(); // PRIMASK=0,开中断
__set_BASEPRI(0x60); // 屏蔽优先级数值 ≥ 6 的中断
RTOS 临界区常用 BASEPRI 而不是 PRIMASK——保留高优先级中断(SysTick、NMI)的响应能力。
总结
| 中断 | 硬件自动暂停主程序 → 跳 ISR → 返回 |
| 意义 | 实时响应、CPU 高效、低功耗唤醒 |
| 中断源 | 内核异常(1~15)+ 外设 IRQ(16 起) |
| 向量表 | 函数指针数组,在 Flash 开头 |
| 启动文件 | 汇编写向量表,[WEAK] 让用户函数覆盖 |
| 中断号 | 偏移 = 中断号 × 4 |
| NVIC | 管理使能、优先级、嵌套 |
| 优先级 | 抢占决定嵌套,子优先级决定并发顺序 |
| 嵌套 | 高抢占打断低抢占,硬件自动完成 |
| 优化 | Tail-Chaining + Late Arriving |
面试题精选
一、基础概念题
1. 什么是中断?中断和轮询有什么区别?
答:中断是 CPU 在正常执行程序时,收到硬件/软件的紧急信号,暂停当前程序,转而去处理紧急事件,处理完后回到断点继续执行的机制。
| CPU 占用 | 一直查询,空转浪费 | 事件驱动,按需处理 |
| 实时性 | 取决于轮询周期,最差延迟大 | 纳秒~微秒级响应 |
| 功耗 | CPU 一直全速运行 | 可进入低功耗休眠,中断唤醒 |
| 代码 | 简单,但主循环臃肿 | 需理解中断机制,架构清晰 |
2. 中断向量表是什么?放在哪里?
答:中断向量表是一张"函数指针数组",每个表项对应一个中断/异常的服务程序入口地址。
- 默认位置:Flash 起始地址 0x08000000(由 BOOT 引脚决定映射到 0x00000000)
- 第 0 项:不是中断向量,而是初始栈指针 MSP(CPU 上电第一件事就是取它初始化栈)
- 第 1 项起:Reset_Handler、NMI_Handler、HardFault_Handler……
- 可重定位:通过 SCB->VTOR 寄存器把向量表搬到 SRAM 中(Bootloader 场景常用)
3. 中断号是什么?异常编号和 IRQ 编号有什么关系?
答:中断号就是向量表这个数组的下标,CPU 靠它查表找到对应的 ISR 地址。
- 1~15:Cortex-M 内核异常(Reset、NMI、HardFault、SysTick 等),所有 Cortex-M3/M4 芯片都一样
- 16 起:外部外设中断(IRQ),各厂商自定义,ST 的 STM32 从 WWDG(IRQ0) 开始
代码里用 IRQn_Type 枚举:外设 IRQn 从 0 开始,内核异常用负数。
绝对中断号 = IRQn_Type 值 + 16(因为外设 IRQ0 对应绝对编号 16)。
公式:向量地址 = VTOR + 绝对中断号 × 4
4. Cortex-M 的异常和外部中断有什么区别?
答:
| 来源 | Cortex-M 内核自身 | 片上外设 |
| 编号 | 1~15 | 16 起 |
| 例子 | Reset、HardFault、SysTick、PendSV | USART1、EXTI0、TIM2 |
| 谁管理 | NVIC | NVIC |
本质上两者都走同一个 NVIC,只是来源不同——异常是内核内部事件,IRQ 是外设发来的信号。
二、NVIC 与优先级
5. NVIC 是什么?有什么作用?
答:NVIC(Nested Vectored Interrupt Controller,嵌套向量中断控制器)
是 Cortex-M 内核紧耦合的中断管理单元,功能包括:
关键特点:和内核紧耦合,响应极快——从中断请求到进入 ISR 仅需约 12 个时钟周期。
6. STM32 的中断优先级是怎么分组的?
答:STM32 用 4 bit 表示优先级,分成两组:
- 抢占优先级:决定能不能嵌套——数值越小优先级越高,高抢占能打断低抢占
- 子优先级:同时挂起时谁先执行——不能嵌套
通过 NVIC_PriorityGroupConfig() 选择分组方式:
| Group_0 | 0 | 4 | 1 | 16 |
| Group_1 | 1 | 3 | 2 | 8 |
| Group_2 | 2 | 2 | 4 | 4 |
| Group_3 | 3 | 1 | 8 | 2 |
| Group_4 | 4 | 0 | 16 | 1 |
推荐 Group_2:兼顾嵌套深度和并发处理能力。
7. 中断嵌套是什么?什么条件下发生?
答:高优先级中断可以打断低优先级中断的执行,形成嵌套。
主程序 ────────────────
↓ 低优先级中断 A
中断 A ────────────────
↓ 高优先级中断 B 来了
中断 B ──────
中断 A ──────
主程序 ────────────────
嵌套条件:
子优先级相同的两个中断不能互相嵌套——哪怕子优先级不同也不行,只有抢占优先级才能决定嵌套。
8. 优先级数值越大优先级越高,还是越小越高?
答:数值越小,优先级越高。
- 0 是最高(除了内核的 -3、-2、-1)
- 15 是最低
这是 Cortex-M 的设计——硬件判断时直接做无符号数比较,小的赢,效率最高。
三、现场保护与返回
9. 中断发生时,硬件自动做了哪些事?
答:进入中断时硬件自动压栈 8 个寄存器:
压栈顺序(从高地址到低地址):
xPSR ← 程序状态寄存器
PC ← 程序计数器(断点地址)
LR ← 链接寄存器
R12
R3
R2
R1
R0 ← 当前 SP 指向这里
这就是为什么中断响应快——8 个寄存器的入栈是硬件一条指令完成的,软件不用管。
退出中断时:硬件自动出栈恢复这 8 个寄存器,然后根据 EXC_RETURN 值判断回到哪个模式(线程模式/ Handler 模式,用 MSP/PSP)。
10. EXC_RETURN 是什么?
答:中断返回时,LR 寄存器会被硬件自动填入一个特殊值叫 EXC_RETURN(比如 0xFFFFFFF9 / 0xFFFFFFFD)。
它告诉内核:
- 退出后回到线程模式还是 Handler 模式
- 用 MSP 还是 PSP
- 是否发生了浮点上下文
CPU 执行 BX LR 时,看到 LR 是 EXC_RETURN 值,就知道这是中断返回,自动走出栈流程。
四、启动文件与弱符号
11. 为什么中断服务函数的名字必须完全匹配?
答:因为启动文件的向量表里用函数名做"索引"。
启动文件里写了:
__Vectors DCD USART1_IRQHandler ; 中断号 53 对应的位置
当 USART1 中断发生,CPU 去向量表第 53 项读地址,读到的就是 USART1_IRQHandler 的地址。
如果你写的函数叫 My_Uart_Handler,链接器不会自动替换——向量表里还是启动文件里的弱定义死循环,CPU 跳过去就卡死了。
12. 什么是 [WEAK] 弱符号?
答:启动文件用 [WEAK] 关键字定义了所有中断函数的默认实现、
(通常是死循环 B .):
USART1_IRQHandler PROC
EXPORT USART1_IRQHandler [WEAK]
B .
ENDP
链接规则:
- 你在 C 文件里写了同名函数 → 链接器优先用你的版本,覆盖弱定义
- 你没写 → 用弱定义的默认死循环版本
这就是为什么你不需要在任何地方"注册"中断函数——只要函数名对,链接器自动帮你填进向量表。
五、进阶原理题
13. 什么是 Tail-Chaining(尾链)?
答:Cortex-M 的中断优化机制——如果中断 A 刚执行完,中断 B 已经挂起,CPU 不需要先恢复现场再保存现场,而是直接从 ISR A 跳到 ISR B。
节省了两次出栈/入栈的时间(约 12 个时钟周期),中断响应更快。
14. 什么是 Late Arriving(迟到中断)?
答:在压栈过程中,如果来了一个优先级更高的中断,CPU 会先响应这个更高优先级的中断,压栈只做一次。
服务完高优先级中断后,再服务那个低优先级的。
这是硬件自动处理的,软件不需要干预。
15. PRIMASK、FAULTMASK、BASEPRI 有什么区别?
答:这三个是 Cortex-M 的中断屏蔽寄存器:
| PRIMASK | 写 1 → 屏蔽所有可屏蔽中断(只剩 NMI、HardFault) | 极短临界区 |
| FAULTMASK | 写 1 → 屏蔽所有异常(包括 HardFault) | 极少用 |
| BASEPRI | 设置优先级阈值 → 优先级数值 ≥ 阈值的全屏蔽 | RTOS 临界区 |
为什么 RTOS 用 BASEPRI 而不是 PRIMASK?
因为 BASEPRI 可以保留高优先级中断的响应能力(比如 SysTick 节拍中断、NMI),只屏蔽低优先级的——临界区更短,实时性更好。
__disable_irq(); // PRIMASK=1,关所有中断
__set_BASEPRI(0x60); // 屏蔽优先级数值 ≥ 6 的,高优先级还能响应
16. 中断延迟是什么?受哪些因素影响?
答:从中断请求发出,到 ISR 第一条指令执行的时间,叫中断延迟。
Cortex-M3 典型中断延迟是 12 个时钟周期(包括压栈 8 个寄存器 + 向量取指)。
影响因素:
六、实战题
17. 中断服务函数里能做什么?不能做什么?
答:
✅ 可以做:
- 读寄存器清中断标志
- 简单的数据处理
- 置标志位/发信号量通知主循环
- 短时间的延时(短!)
❌ 不能做:
- 不要做长延时(HAL_Delay() 这种)——会卡住低优先级中断
- 不要打印串口(printf)——太慢,可能丢数据
- 不要做复杂计算——会影响实时性
- 不要在 ISR 里调用会阻塞的函数(RTOS 的 vTaskDelay())
原则:中断服务函数要短、快、只做必要的事,复杂工作交给主循环或任务去做。
18. 怎么设计一个好的中断处理?
答:标准模式——"中断只置标志,主循环做处理":
volatile uint8_t uart_rx_flag = 0;
uint8_t uart_rx_data;
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
uart_rx_data = USART_ReceiveData(USART1); // 读数据
uart_rx_flag = 1; // 置标志
// 不在这里做复杂处理!
}
}
int main(void) {
while(1) {
if (uart_rx_flag) {
uart_rx_flag = 0;
// 这里慢慢处理数据,打印、解析都行
process_uart_data(uart_rx_data);
}
}
}
volatile 关键字必须加——因为这个变量在 ISR 和主循环里都用,编译器优化可能会把它缓存到寄存器,导致主循环读不到 ISR 更新的值。
19. 为什么中断里的标志位变量要加 volatile?
答:编译器优化时,会把频繁访问的变量缓存到 CPU 寄存器里。
如果主循环里读 flag,编译器可能优化成"只读一次,后面一直用寄存器里的值"——但 ISR 里改了内存里的 flag,主循环根本看不到。
加 volatile 告诉编译器:这个变量随时可能被改变,每次都必须从内存重新读,不许优化缓存。
20. 写中断服务函数时,为什么要手动清中断标志?
答:绝大多数 STM32 外设中断,硬件不会自动清标志——如果你不写 EXTI_ClearITPendingBit() 或 USART_ClearITPendingBit(),
中断返回后会立刻再次触发同一个中断,CPU 就卡在 ISR 里出不来了,主程序完全不跑。
例外:
有些中断标志是读/写清零的(比如 USART 的 RXNE,读 DR 寄存器就自动清),但最好还是手动显式清,养成习惯,避免不同外设行为不一致踩坑。
高频考点速记
| 中断号本质 | 向量表数组下标,偏移 = 中断号 × 4 |
| 向量表第 0 项 | 初始栈指针 MSP,不是中断 |
| 优先级分组 | 抢占决定嵌套,子优先级决定并发顺序 |
| 优先级数值 | 越小越高 |
| 嵌套条件 | 新中断抢占优先级严格更高 |
| [WEAK] 作用 | 用户写同名函数自动覆盖默认死循环 |
| 硬件自动压栈 | 8 个寄存器:xPSR/PC/LR/R12/R0-R3 |
| 中断延迟 | Cortex-M3 典型 12 个时钟周期 |
| volatile | 防止编译器优化缓存 ISR 修改的变量 |
| ISR 原则 | 短、快、清标志、复杂工作交给主循环 |
| Tail-Chaining | 连续中断直接尾接,省去恢复/保存现场 |
| BASEPRI vs PRIMASK | BASEPRI 保留高优先级,RTOS 临界区用它 |




