3. 中断向量表:中断向量表的定义、中断服务函数(ISR)的编写规范

中断向量表,说白了就是一张“紧急事件处理人员名单”。

CPU 平时按部就班地执行主程序,突然某个外设(比如定时器、串口)发出了中断请求。CPU 得知道该跳到哪里去处理这个请求。这时候,中断向量表就派上用场了——它告诉 CPU:每种中断对应哪个处理函数,地址是多少。

3.1 中断向量表的定义与结构

中断向量表通常放在程序存储器的起始地址(比如 0x0000 或 0x00000000)。每个中断源占用一个固定位置,里面存放的是该中断服务函数(ISR)的入口地址。

我刚开始做项目时,总觉得向量表是编译器自动搞定的,不用管。直到有一次调试一个串口接收丢数据的问题,查了两天才发现是向量表被意外覆盖了……从那以后,我对向量表的态度就认真多了。

中断源 向量地址(以 STM32F1 为例) 说明
复位 0x0000_0004 系统上电或复位后跳转
NMI 0x0000_0008 不可屏蔽中断
HardFault 0x0000_000C 硬件错误处理
SysTick 0x0000_003C 系统滴答定时器
EXTI0 0x0000_0058 外部中断线 0
TIM1_UP 0x0000_006C 定时器 1 更新中断

这张表是硬件设计时就定好的。你写代码时,只需要把对应的 ISR 函数名放到正确的位置。编译器会帮你完成地址填充。

核心要点:中断向量表是 CPU 与中断服务函数之间的“桥梁”。桥断了,中断就进不去。

3.2 中断服务函数(ISR)的编写规范

写 ISR 和写普通函数不一样。我见过不少新手把 ISR 写成几百行的庞然大物,结果系统响应慢得像蜗牛。嗯,这里要注意几个硬性规则。

3.2.1 ISR 的基本结构

void TIM2_IRQHandler(void)
{
    // 1. 检查中断标志位
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        // 2. 用户处理逻辑(尽量精简)
        led_toggle();

        // 3. 清除中断标志位
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

三步走:检查标志 → 处理 → 清除标志。缺一不可。

我曾经遇到过一个 bug,ISR 里忘了清标志位,结果 CPU 一直卡在中断里出不来,主程序完全瘫痪。那次之后,我每次写完 ISR 都会反复确认标志位有没有清掉。

3.2.2 ISR 的编写铁律

  • 短小精悍:ISR 里只做最紧急的事。比如接收一个字节、设置一个标志位。复杂计算、延时、打印都放到主循环里做。
  • 避免阻塞:不要在 ISR 里用 while 等待、delay 延时、或者调用可能阻塞的函数(比如 printf)。
  • 保护共享数据:如果 ISR 和主循环共享变量,记得加 volatile 关键字,必要时关中断保护。
  • 不要嵌套太深:除非你很清楚自己在做什么,否则尽量保持中断优先级简单,避免多层嵌套导致栈溢出。

避坑指南:我曾经在一个电机控制项目里,ISR 里调用了 printf 打印调试信息。结果 printf 本身用了串口中断,形成了中断嵌套死锁。系统跑几秒就死机。后来改成用 DMA + 环形缓冲区,问题才解决。

3.2.3 ISR 的命名与声明

不同 MCU 的 ISR 命名规则不一样。以常见的 Cortex-M 内核为例:

MCU 系列 ISR 命名示例 说明
STM32 (HAL) HAL_GPIO_EXTI_Callback() 弱函数,用户重写
STM32 (标准库) USART1_IRQHandler() 直接写中断处理函数
AT32 TMR1_OVF_IRQHandler() 类似 STM32 风格
GD32 USART0_IRQHandler() 与 STM32 兼容

你想想看,如果函数名写错了,编译器不会报错,但中断向量表里就找不到你的 ISR。中断来了,CPU 跳到一个空地址,系统直接跑飞。这种 bug 最难查。

我的习惯:每次新建工程,我都会先打开启动文件(startup_xxx.s),把里面所有的中断向量名过一遍。然后在代码里用 __attribute__((weak)) 或者直接写空函数,确保每个中断都有默认处理。这样即使忘了写某个 ISR,系统也不会崩溃。

3.3 中断向量表与 ISR 的协作流程

为了让你更直观地理解整个过程,我画了一张流程图:

中断响应与向量表查找流程 中断响应与向量表查找流程 主程序执行 中断发生 CPU 保存现场(压栈) 查向量表,获取 ISR 地址 执行 ISR ISR 执行完毕后,CPU 恢复现场(出栈),继续执行主程序

这张图把整个流程串起来了。你注意看,从“中断发生”到“执行 ISR”,中间只经过“保存现场”和“查向量表”两步。这就是中断响应快的原因——硬件帮你做了大部分工作。

3.4 实战中的注意事项

  • 向量表重映射:有些场景(比如 Bootloader + App 架构)需要把向量表搬到 RAM 里。Cortex-M 系列可以通过 SCB->VTOR 寄存器设置偏移量。我做过一个远程升级项目,就是靠这个实现的。
  • 中断优先级分组:STM32 支持抢占优先级和子优先级。配置错了会导致低优先级中断抢了高优先级的事。我的建议是:统一用一种分组方式,别混用。
  • ISR 的栈使用:ISR 里尽量少用局部大数组。栈空间有限,一不小心就溢出了。我习惯在 ISR 里只设标志位,数据处理放主循环。

总结一下:中断向量表是硬件与软件之间的契约。你按规矩填表,CPU 按规矩跳转。ISR 写得好,系统稳如泰山;ISR 写得烂,系统随时翻车。