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 响应中断时的完整流程。

中断向量表结构与响应流程 中断向量表结构与响应流程 向量表(Flash 起始) 0x0000: 栈顶地址 0x0004: 复位函数地址 0x0008: NMI 处理函数 0x000C: HardFault 处理 0x0040: SysTick 处理 0x0044: EXTI0 处理 0x0048: EXTI1 处理 CPU 响应中断流程 ① 外设触发中断请求 ② CPU 保存当前上下文(压栈) ③ 查向量表,获取 ISR 地址 ④ 跳转到 ISR 执行 ⑤ 恢复上下文,返回主程序 查表

2.5 常见问题与避坑

  • ISR 函数名写错:启动文件里用的是弱符号,你写错名字不会报错,但中断来了会跑进 Default_Handler 死循环。我建议每次新建工程时,先对照参考手册核对一遍函数名。
  • 中断标志未清除:这是新手最常犯的错误。不清标志,中断会反复触发,CPU 永远在 ISR 里打转。
  • 向量表对齐:有些 MCU 要求向量表必须按 256 字节或 512 字节对齐。重映射时如果地址没对齐,中断会异常。
  • 优先级分组不一致:整个系统只能用一种优先级分组方式。如果不同模块设置了不同的分组,中断优先级会乱套。

总结一下:中断向量表是 MCU 中断系统的基石。理解它的结构,规范地编写 ISR,正确地配置向量表,这三件事做好了,中断系统就稳了一半。剩下的就是多练、多踩坑、多总结。