4. 复位向量表:向量表结构、栈指针与复位向量、向量表重定位

好,咱们进入第四章。说实话,复位向量表这玩意儿,是MCU上电后第一个跟硬件打交道的“数据结构”。你写的代码能不能跑起来,全看它摆得对不对。我见过不少新手,甚至有些老手,在向量表上栽过跟头——要么栈指针没设对,要么重定位后程序直接飞了。

今天咱们就把这个“第一道关卡”彻底讲透。

4.1 向量表的基本结构

先看一张总图,把向量表的全貌印在脑子里。

ARM Cortex-M 向量表结构(默认起始地址 0x00000000) 地址偏移 内容 说明 0x0000_0000 MSP 初始值 上电后 SP 直接加载此值 必须指向有效 RAM 区域 0x0000_0004 Reset_Handler PC 上电后跳转到此地址 注意:LSB 必须为 1(Thumb 模式) 0x0000_0008 NMI / HardFault ... 系统异常:NMI、HardFault、MemManage 等 0x0000_0040 IRQ0 ~ IRQn 外设中断向量,按中断号顺序排列 每个向量占 4 字节,共 16 个系统异常 + 外设中断 向量表总大小 = (中断数 + 16) × 4 字节 ⚠ 关键约束 向量表必须 256 字节对齐(即地址低 8 位为 0)

这张图你得记住。Cortex-M 系列上电后,硬件自动从 0x00000000 加载栈指针(MSP),从 0x00000004 加载复位向量作为 PC 初始值。这两步是硬连线行为,你改不了。

4.2 栈指针与复位向量

咱们拆开细说。

4.2.1 栈指针(MSP 初始值)

上电瞬间,CPU 还没跑任何代码,它得先有个栈才能调用函数。硬件直接从向量表第 0 项把值灌进主栈指针(MSP)。

这个值必须指向 RAM 区域。我见过有人把链接脚本写错了,栈指针指到了 Flash 地址——结果一压栈就 HardFault,连 main 都进不去。

栈指针设置要点:

  • 必须指向 RAM 的合法地址,通常放在 RAM 末尾(栈向下生长)
  • 建议 8 字节对齐,某些内核有对齐要求
  • 不要指向未初始化的 RAM 区域(比如某些 bootloader 占用的区域)

看一个典型的链接脚本片段,你就明白了:

/* 链接脚本示例 */
_estack = ORIGIN(RAM) + LENGTH(RAM);  /* RAM 末尾 */

SECTIONS
{
    .isr_vector :
    {
        . = ALIGN(256);
        KEEP(*(.isr_vector))  /* 向量表段 */
        . = ALIGN(4);
    } > FLASH

    /* ... 其他段 ... */
}

启动文件里,向量表第一项就是 _estack:

__Vectors:
    .word _estack               /* 栈指针 */
    .word Reset_Handler         /* 复位向量 */
    .word NMI_Handler           /* NMI */
    .word HardFault_Handler     /* HardFault */
    /* ... 其他向量 ... */

4.2.2 复位向量(Reset_Handler)

复位向量是 CPU 上电后执行的第一条指令地址。注意,Cortex-M 是 Thumb 模式,地址的 LSB 必须为 1。硬件取向量值时,会自动把 LSB 清零作为实际跳转地址,但向量表里必须写奇数地址。

我曾经踩过的坑:

有一次我手写启动文件,复位向量忘了加 1,写成 .word Reset_Handler 而不是 .word Reset_Handler + 1。结果芯片上电后直接跑飞,调试器都连不上。查了半天才发现是 LSB 没置位。从那以后,我每次写完启动文件都会用 hex 工具看一眼向量表的前 8 个字节。

正确的写法:

.word Reset_Handler + 1   /* 确保 LSB = 1 */

Reset_Handler 本身要做什么?至少这几步:

  1. 初始化 .data 段(从 Flash 拷贝到 RAM)
  2. 清零 .bss 段
  3. 设置系统时钟
  4. 调用 main()

嗯,这些是 C 运行时环境的标配。不过车载 MCU 里,有些安全相关的项目会要求 Reset_Handler 先做自检,再进 main。

4.3 向量表重定位

默认向量表在 0x00000000,但实际项目中很少这么用。为什么?

  • Bootloader 场景:Bootloader 占 0x00000000,应用程序得挪到别的地址
  • OTA 升级:A/B 分区切换时,向量表位置跟着变
  • RAM 中运行:某些低功耗模式或安全场景,需要把向量表搬到 RAM

Cortex-M 提供了 向量表偏移寄存器(VTOR),位于系统控制块(SCB)中,地址是 0xE000ED08。你往这个寄存器写新地址,硬件就从新位置取向量了。

我个人习惯:

在 Bootloader 跳转到应用程序之前,一定会做两件事:一是把 VTOR 改成应用分区的基址,二是重新设置 MSP。顺序不能反——先设 VTOR,再设 MSP,最后跳转。否则中断一来就乱套。

代码实现很简单:

/* 设置向量表偏移 */
#define SCB_VTOR      (*(volatile uint32_t *)0xE000ED08)
#define APP_BASE      0x08010000  /* 应用分区起始地址 */

void jump_to_app(void)
{
    uint32_t msp_value;

    /* 1. 设置 VTOR */
    SCB_VTOR = APP_BASE;

    /* 2. 从新向量表取 MSP */
    msp_value = *(volatile uint32_t *)APP_BASE;
    __set_MSP(msp_value);

    /* 3. 从新向量表取 PC 并跳转 */
    void (*app_entry)(void) = (void (*)(void))
        (*(volatile uint32_t *)(APP_BASE + 4));
    app_entry();
}

这里有个细节:__set_MSP() 是内联函数,直接写 MSP 寄存器。跳转前要关掉全局中断,等应用初始化完再开。

4.3.1 重定位的约束条件

约束项 要求 说明
对齐 256 字节对齐 VTOR 的低 8 位被硬件忽略,自动对齐
地址范围 必须在 Code 或 RAM 区域 不能指向外设地址空间
中断使能 重定位前建议关中断 防止重定位过程中断响应出错
双核场景 每个核有自己的 VTOR 如 Cortex-M4 + M0+,需分别设置

4.4 实战中的向量表设计

车载 MCU 的向量表设计,跟普通嵌入式项目比,多了几个硬性要求:

  • 安全冗余:有些项目要求向量表有 CRC 校验,防止 Flash 损坏导致向量错误
  • Bootloader 保护:Bootloader 的向量表通常写保护,防止应用误擦
  • 中断延迟:重定位后的向量表如果放在 RAM,访问速度比 Flash 快,但 RAM 掉电丢失

避坑指南:

我曾经在一个项目里,把向量表重定位到了 RAM,但忘了在跳转前把向量表从 Flash 拷贝过去。结果中断一来,RAM 里全是 0xFFFFFFFF,PC 直接飞到非法地址。嗯,从那以后我写了个宏,每次重定位前自动检查目标地址是否已初始化。

最后,给你一个检查清单,每次做向量表相关开发时对照一下:

  1. 向量表是否 256 字节对齐?
  2. 栈指针是否指向 RAM 有效区域?
  3. 复位向量的 LSB 是否为 1?
  4. VTOR 设置是否在关中断环境下执行?
  5. 跳转前是否重新设置了 MSP?
  6. 如果向量表在 RAM,是否已完成拷贝?

这六条,我每次做 Bootloader 或分区切换时都会过一遍。少一条,就可能出大问题。