2. 中断向量表:结构、编写规范与配置
中断向量表,说白了就是一张“紧急事件处理清单”。CPU 遇到中断信号后,会立刻翻这张表,找到对应的处理函数地址,跳过去执行。我刚开始学单片机时,总觉得这玩意儿很神秘,后来自己动手写过一次启动文件,才真正搞明白——它其实就是一段连续的内存,存着一堆函数指针。
2.1 中断向量表的结构
这张表通常放在程序存储器的起始地址(比如 0x00000000)。每个表项占 4 字节(32 位 MCU),存放的是对应中断服务函数的入口地址。
| 偏移地址 | 中断源 | 说明 |
|---|---|---|
| 0x0000 | 复位向量 | 系统上电或复位后,PC 指向这里 |
| 0x0004 | 不可屏蔽中断 (NMI) | 硬件严重错误,比如电源跌落 |
| 0x0008 | 硬错误 (HardFault) | 程序跑飞、访问非法地址 |
| 0x000C | 存储器管理错误 | MPU 保护违规 |
| 0x0010 | 总线错误 | 数据/指令总线访问异常 |
| 0x0014 | 用法错误 | 未定义指令、对齐错误 |
| 0x0040 | SysTick 定时器 | 系统滴答,常用于 OS 调度 |
| 0x0044 | 外部中断 0 (EXTI0) | GPIO 引脚触发 |
| ... | ... | ... |
关键点:向量表的前 16 个表项(0x0000~0x003C)是系统异常,从 0x0040 开始才是外设中断。不同芯片的具体编号有差异,一定要查对应型号的参考手册。
我在项目中遇到过一个问题:芯片上电后一直跑飞,查了半天发现是复位向量写错了。嗯,向量表第一个字必须是栈顶地址,第二个字才是复位函数地址。顺序搞反,CPU 直接懵掉。
2.2 中断服务函数(ISR)的编写规范
写 ISR 和写普通函数不一样。你想想看,ISR 是在任意时刻被硬件调用的,它不能破坏主程序的现场。所以有几个硬性规矩必须遵守。
2.2.1 基本规则
- 短小精悍:ISR 里只做最紧急的事,比如置标志位、清中断标志。耗时操作(比如打印、延时)放到主循环里处理。
- 不可阻塞:不能在 ISR 里等信号量、等消息队列。我曾经见过有人把 printf 写在 ISR 里,结果串口发送阻塞,整个系统卡死。
- 保护共享数据:ISR 和主循环共享的变量,必须用
volatile修饰,防止编译器优化掉。 - 可重入:如果同一个中断可能嵌套触发,ISR 必须是可重入的。一般做法是 ISR 里只操作局部变量或原子操作。
2.2.2 标准 ISR 模板
// 标准 ISR 写法(以 STM32 为例)
void EXTI0_IRQHandler(void)
{
// 1. 检查中断标志位
if (EXTI->PR & EXTI_PR_PR0)
{
// 2. 清除中断标志(必须!否则会一直触发)
EXTI->PR = EXTI_PR_PR0;
// 3. 做最少的处理
g_button_pressed = 1; // 置标志位,主循环去处理
}
}
避坑指南:我曾经在调试一个电机驱动项目时,忘记在 ISR 里清中断标志位。结果中断一直触发,CPU 卡在 ISR 里出不来,主循环完全没机会跑。后来用示波器看引脚波形,才发现中断信号根本没停过。
2.2.3 中断优先级与嵌套
大多数 MCU 支持中断优先级分组。高优先级中断可以打断低优先级中断,形成嵌套。我个人习惯把时间敏感的中断(比如定时器捕获)设成高优先级,把非紧急的中断(比如按键)设成低优先级。
| 优先级分组 | 抢占优先级位数 | 子优先级位数 | 典型应用 |
|---|---|---|---|
| 组 0 | 0 | 4 | 所有中断同优先级,按编号响应 |
| 组 1 | 1 | 3 | 2 级抢占,8 级子优先级 |
| 组 2 | 2 | 2 | 4 级抢占,4 级子优先级(常用) |
| 组 3 | 3 | 1 | 8 级抢占,2 级子优先级 |
| 组 4 | 4 | 0 | 16 级抢占,无子优先级 |
2.3 中断向量表的配置
配置向量表有两种方式:一种是使用芯片厂商提供的启动文件(默认方式),另一种是手动重映射向量表。我建议初学者先用默认方式,等熟悉了再尝试手动配置。
2.3.1 使用启动文件(默认方式)
大部分 IDE 会自动包含启动文件。你只需要在代码里实现对应的 ISR 函数名,链接器会自动把函数地址填入向量表。比如 STM32 的启动文件 startup_stm32f4xx.s 里已经声明了所有中断向量,你只要写同名的函数就行。
// 启动文件中预定义的弱函数(示例)
void EXTI0_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
// 你在用户代码中重新实现
void EXTI0_IRQHandler(void)
{
// 你的处理逻辑
}
2.3.2 手动重映射向量表
有些场景需要把向量表搬到 RAM 里,比如做 IAP(在应用编程)升级时。这时需要修改向量表偏移寄存器(如 SCB->VTOR)。
// 手动重映射向量表到 RAM 地址 0x20000000
#define VECTOR_TABLE_RAM_BASE 0x20000000
void vector_table_remap(void)
{
// 1. 关闭全局中断
__disable_irq();
// 2. 将向量表复制到 RAM
memcpy((void*)VECTOR_TABLE_RAM_BASE,
(void*)SCB->VTOR,
sizeof(uint32_t) * 16); // 只复制前 16 个系统异常
// 3. 设置 VTOR 寄存器指向 RAM 中的新向量表
SCB->VTOR = VECTOR_TABLE_RAM_BASE;
// 4. 使能全局中断
__enable_irq();
}
小技巧:手动重映射后,记得用 __DSB() 和 __ISB() 指令做内存屏障和指令同步,确保 CPU 使用最新的向量表。我踩过这个坑——重映射后中断没生效,查了半天发现是缓存没刷新。
2.4 中断向量表结构图
下面这张图展示了向量表在内存中的布局,以及 CPU 响应中断时的完整流程。
2.5 常见问题与避坑
- ISR 函数名写错:启动文件里用的是弱符号,你写错名字不会报错,但中断来了会跑进
Default_Handler死循环。我建议每次新建工程时,先对照参考手册核对一遍函数名。 - 中断标志未清除:这是新手最常犯的错误。不清标志,中断会反复触发,CPU 永远在 ISR 里打转。
- 向量表对齐:有些 MCU 要求向量表必须按 256 字节或 512 字节对齐。重映射时如果地址没对齐,中断会异常。
- 优先级分组不一致:整个系统只能用一种优先级分组方式。如果不同模块设置了不同的分组,中断优先级会乱套。
总结一下:中断向量表是 MCU 中断系统的基石。理解它的结构,规范地编写 ISR,正确地配置向量表,这三件事做好了,中断系统就稳了一半。剩下的就是多练、多踩坑、多总结。