4、向量表详解:异常与中断的入口地址映射

好,咱们今天聊聊向量表。这东西说白了,就是芯片上电后,CPU 第一眼要看的东西。你想想看,CPU 一复位,它怎么知道该从哪里开始执行?谁告诉它中断来了该跳到哪里去?答案就是向量表。

我个人习惯把向量表理解成一本「紧急情况联络簿」。CPU 遇到各种异常、中断,就像家里着火了、有人按门铃了,它得立刻翻这本联络簿,找到对应的处理电话(入口地址),然后打过去。这本联络簿就放在内存的最开头,地址 0x00000000 处。

4.1 向量表的结构:一张固定的地址映射表

在 ARM Cortex-M 系列处理器里,向量表的前 16 个是系统异常,从第 16 个开始是外部中断。每个表项占 4 个字节,存的是一个函数的地址。注意,不是指令,是地址。

我刚开始学的时候,老搞混一件事:以为向量表里存的是跳转指令。其实不是,它存的是处理函数的入口地址。CPU 拿到这个地址后,直接加载到 PC 寄存器里,就开始执行那个函数了。

偏移地址 异常/中断号 名称 描述
0x00 - MSP 初始值 主栈指针的初始值,复位后直接加载
0x04 1 Reset_Handler 复位异常入口,系统启动第一站
0x08 2 NMI_Handler 不可屏蔽中断,优先级最高
0x0C 3 HardFault_Handler 硬故障,系统出大问题了
0x10 4 MemManage_Handler 内存管理异常(MPU 相关)
0x14 5 BusFault_Handler 总线故障,访问了不该访问的地址
0x18 6 UsageFault_Handler 用法故障,指令执行出错
... ... ... ...
0x40 16 IRQ0_Handler 第一个外部中断
0x44 17 IRQ1_Handler 第二个外部中断

你看,第一个表项不是异常,而是栈指针的初始值。这是 ARM 设计的一个巧妙之处:复位后 CPU 先把栈指针设好,再跳转到 Reset_Handler。这样一上来就能用栈,不用你手动初始化。

核心要点:向量表第 0 项是 MSP 初始值,第 1 项才是复位入口。很多新手写链接脚本时,忘了把栈指针放在最前面,结果一上电就跑飞了。

4.2 向量表在启动文件中的实现

咱们直接看代码。这是典型的 Cortex-M 启动文件里向量表的写法:

__Vectors       DCD     __initial_sp          ; 0x00: 栈顶地址
                DCD     Reset_Handler         ; 0x04: 复位入口
                DCD     NMI_Handler           ; 0x08: NMI
                DCD     HardFault_Handler     ; 0x0C: 硬故障
                DCD     MemManage_Handler     ; 0x10: 内存管理
                DCD     BusFault_Handler      ; 0x14: 总线故障
                DCD     UsageFault_Handler    ; 0x18: 用法故障
                ; ... 其他系统异常 ...
                DCD     0                     ; 0x34: 保留
                DCD     0                     ; 0x38: 保留
                DCD     0                     ; 0x3C: 保留
                DCD     EXTI0_IRQHandler      ; 0x40: 外部中断0
                DCD     EXTI1_IRQHandler      ; 0x40: 外部中断1
                ; ... 更多中断 ...

这里每个 DCD 指令就是分配一个 4 字节的空间,并填入后面的符号地址。链接器在链接阶段,会把 Reset_HandlerHardFault_Handler 这些符号解析成实际的函数地址。

我曾经在一个项目里,因为链接脚本的段排列顺序写错了,导致向量表被放到了 0x08000000(Flash 起始地址),而不是 0x00000000。结果芯片一复位,从 0x00000000 读到的全是 0,直接 HardFault。查了两天才发现是这个问题。嗯,从那以后我每次都会检查 map 文件,确认向量表的位置对不对。

4.3 向量表的重定位:运行时搬移

大多数情况下,向量表放在 Flash 里就够了。但有些场景下,你需要把向量表搬到 RAM 里。比如做 IAP(在应用编程)时,你要从 Bootloader 跳转到 App,两个程序各有各的向量表。

ARM Cortex-M 提供了一个寄存器叫 VTOR(Vector Table Offset Register),专门用来干这个。你往 VTOR 里写一个新的基地址,CPU 就去那里找向量表了。

/* 将向量表重定位到 RAM 中的新地址 */
#define VECTOR_TABLE_RAM_BASE  0x20000000

void vector_table_relocate(void)
{
    /* 1. 先把 Flash 里的向量表复制到 RAM */
    memcpy((void*)VECTOR_TABLE_RAM_BASE, 
           (void*)SCB->VTOR, 
           sizeof(vector_table));
    
    /* 2. 设置 VTOR 寄存器,指向 RAM 中的新向量表 */
    SCB->VTOR = VECTOR_TABLE_RAM_BASE;
    
    /* 3. 确保内存屏障,让写操作生效 */
    __DSB();
}

这里有个坑,我提醒你一下:复制向量表时,一定要保证目标 RAM 地址是 256 字节对齐的。VTOR 寄存器要求低 9 位为 0,否则写进去会无效。我曾经在 STM32F4 上踩过这个坑,写进去后发现中断一个都进不去,调试了半天才发现是对齐问题。

注意:重定位向量表后,如果使能了中断,但新的向量表里对应的处理函数还没准备好,一旦中断触发,CPU 会跳到一个未初始化的地址,大概率触发 HardFault。所以重定位后,先别急着开中断,把处理函数都挂好再说。

4.4 异常优先级与向量表的关系

向量表只负责告诉 CPU「去哪里处理」,但不负责「谁先处理」。优先级是由另一个叫 NVIC(嵌套向量中断控制器)的硬件模块管理的。

不过,向量表的排列顺序其实隐含了默认优先级。在 Cortex-M 里,异常号越小,默认优先级越高。比如 Reset 是 -3(最高),NMI 是 -2,HardFault 是 -1。这些是固定的,你改不了。

外部中断的默认优先级呢?嗯,它们都一样,都是 0。如果你不主动设置优先级,所有外部中断的优先级相同,那就按异常号小的先响应。

我建议你在项目初期就把优先级分组和每个中断的优先级规划好。别等到调试时发现两个中断互相抢占,搞得系统不稳定,那时候再改就麻烦了。

4.5 向量表与中断延迟

向量表的设计直接影响中断延迟。CPU 响应中断时,需要做这几件事:

  1. 保存当前上下文(压栈)
  2. 从向量表读取中断处理函数地址
  3. 跳转到该地址执行

其中第二步,就是从向量表里查表。如果向量表在 Flash 里,Flash 的读取速度比 CPU 主频慢很多,尤其是带等待周期的 Flash。这就成了瓶颈。

所以有些高性能场景下,会把向量表搬到 RAM 里。RAM 的访问速度比 Flash 快得多,能减少几个时钟周期的中断延迟。虽然不多,但在实时性要求苛刻的系统里,这几个周期可能就是关键。

小技巧:如果你不想把整个向量表搬到 RAM,也可以只把常用的几个中断处理函数地址放到 RAM 里的一个跳转表中。但这样需要你自己维护这个跳转表,代码复杂度会高一些。我个人还是倾向于直接搬整个向量表,省心。

4.6 向量表在双核或多核系统中的处理

现在很多芯片都是多核的,比如 Cortex-M4 + Cortex-M0 的组合。每个核都有自己的向量表。它们各自独立,互不干扰。

但要注意的是,两个核可能共享同一片 Flash 或 RAM。你得确保它们的向量表地址不冲突。比如核0的向量表在 0x08000000,核1的向量表在 0x08010000。链接脚本里要分别指定。

我记得有个项目,两个核的向量表都放在了 0x08000000,结果核1复位后,读到的全是核0的向量表,直接跑到了核0的 Reset_Handler 里。两个核跑同一个函数,共享变量全乱套了。嗯,这种问题查起来特别头疼,因为现象是随机的。

4.7 向量表知识体系总览

下面这张图,我把向量表的核心知识点串起来了。你可以对照着看,心里有个整体框架。

向量表知识体系 向量表 (Vector Table) 1. 表结构 • 第0项: MSP初始值 • 第1项: Reset_Handler • 第2-15项: 系统异常 • 第16+: 外部中断 2. 启动文件实现 • DCD 指令分配表项 • 链接器解析符号地址 • 必须放在段首 3. 重定位 (VTOR) • 修改 VTOR 寄存器 • 需 256 字节对齐 • 用于 IAP 场景 4. 优先级映射 • 异常号越小优先级越高 • NVIC 管理外部中断优先级 • 默认优先级均为 0 5. 中断延迟 • Flash 读取慢,增加延迟 • RAM 中向量表可减少延迟 • 实时系统需关注 6. 多核处理 • 每个核独立向量表 • 地址不能冲突 • 链接脚本分别指定

这张图把向量表的六个核心方面都列出来了。你可以看到,它不只是一个简单的地址表,而是涉及启动流程、内存布局、中断响应性能等多个维度的设计。

好了,关于向量表的内容就讲到这里。记住一句话:向量表是 CPU 和中断处理函数之间的桥梁,这座桥搭不好,整个系统的异常和中断处理都会出问题。写启动文件时,多花点心思在向量表上,后面能省很多调试时间。