从寄存器到链接脚本:手把手拆解 STM32 启动文件与中断向量表重定向

对于有一定基础的嵌入式开发者而言,启动文件(startup_*.s)往往是一个“黑盒”——知道它重要,却很少深究。本文将带你从寄存器层面理解启动流程,并实战中断向量表重定向。

一、启动文件到底做了什么?

以 STM32F103 的 startup_stm32f103xb.s 为例,复位后硬件自动完成:

  1. 从地址 0x00000000(映射到 Flash 或 System memory)取出初始 MSP(主栈指针)。
  2. 0x00000004 取出复位向量(Reset_Handler 地址)。
  3. 跳转到 Reset_Handler 执行。

启动文件的核心任务:

  • 定义中断向量表(位于 Flash 起始处)。
  • 初始化栈指针(通过 __initial_sp 符号)。
  • 调用 SystemInit 配置时钟。
  • 调用 __main 完成数据段(.data)从 Flash 到 RAM 的拷贝、BSS 段清零,最后跳转到 main()

二、中断向量表结构

向量表是一个 4 字节对齐的数组,前两项固定为 MSP 和 Reset_Handler,后续为各外设中断服务函数地址。

// 典型向量表片段(C 语言表示,实际在汇编中定义)
extern uint32_t _estack;
void Reset_Handler(void);
void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));

__attribute__((section(".isr_vector")))
const uint32_t vector_table[] = {
    (uint32_t)&_estack,        // 0x00: 初始 MSP
    (uint32_t)Reset_Handler,   // 0x04: 复位向量
    (uint32_t)NMI_Handler,     // 0x08: NMI
    // ... 其他中断
};

链接脚本通过 .isr_vector 段将其放置在 Flash 起始地址。

三、为什么要重定向向量表?

常见场景:

  • Bootloader + App 架构:App 运行在偏移地址(如 0x08008000),需将向量表指向自身。
  • 动态修改中断服务函数:在 RAM 中重建向量表,实现运行时替换。
  • 多核/安全隔离:不同核心使用独立向量表。

STM32 通过 SCB->VTOR(向量表偏移寄存器,地址 0xE000ED08)控制向量表基址。复位后 VTOR 默认为 0,但若从 Flash 启动,实际基址为 0x08000000(取决于 BOOT 引脚映射)。

四、重定向实战:将向量表搬到 RAM

4.1 修改链接脚本

.ld 文件中定义 RAM 中的向量表段,并保留 Flash 中的原始表:

MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
  RAM (rwx)  : ORIGIN = 0x20000000, LENGTH = 64K
}

SECTIONS
{
  .isr_vector : {
    . = ALIGN(4);
    KEEP(*(.isr_vector))
    . = ALIGN(4);
  } >FLASH

  .ram_vector : {
    . = ALIGN(4);
    _ram_vector_start = .;
    KEEP(*(.ram_vector))
    . = ALIGN(4);
    _ram_vector_end = .;
  } >RAM AT> FLASH

  /* 其他段省略 */
}

4.2 定义 RAM 向量表并拷贝

在 C 代码中定义 RAM 向量表,并在 main() 前完成拷贝和 VTOR 设置:

// 定义 RAM 向量表(大小根据芯片中断数调整)
__attribute__((section(".ram_vector")))
const uint32_t ram_vector_table[64] = {
    (uint32_t)&_estack,
    (uint32_t)Reset_Handler,
    // 其余中断向量可后续动态填充
};

void relocate_vector_table(void)
{
    extern uint32_t _ram_vector_start, _ram_vector_end;
    uint32_t *src = (uint32_t *)&_ram_vector_start;
    uint32_t *dst = (uint32_t *)&_ram_vector_end;
    // 注意:此处仅为示意,实际需从 Flash 拷贝到 RAM
    // 更简单的方式:直接使用链接脚本的 AT> 加载地址
    // 但运行时需手动拷贝
    uint32_t size = (uint32_t)&_ram_vector_end - (uint32_t)&_ram_vector_start;
    memcpy((void *)0x20000000, (void *)&_ram_vector_start, size);

    // 设置 VTOR 指向 RAM 起始地址
    SCB->VTOR = 0x20000000;
    __DSB();
    __ISB();
}

注意:上述代码中 _ram_vector_start 是 RAM 中的地址,但链接脚本 AT> FLASH 表示其加载地址在 Flash。实际拷贝时需使用加载地址(LOADADDR)。更稳妥的做法:

extern uint32_t _sram_vector, _eram_vector, _sload_vector;
memcpy(&_sram_vector, &_sload_vector, (size_t)(&_eram_vector - &_sram_vector));

4.3 在启动文件中调用

修改 Reset_Handler,在 SystemInit 之后、__main 之前调用 relocate_vector_table()

五、关键注意事项

  • 对齐要求:VTOR 必须按向量表大小对齐(通常 128 字节或 512 字节),否则硬件可能取指错误。
  • 中断使能时机:重定向完成前不要使能任何中断,否则可能跳转到旧向量表。
  • 内存屏障:修改 VTOR 后需执行 __DSB()__ISB() 确保生效。
  • Bootloader 场景:App 的链接脚本需设置 FLASH ORIGIN = 0x08008000,并在 App 的 main() 开头设置 SCB->VTOR = 0x08008000
  • 调试影响:重定向后,调试器可能无法正确解析中断,需手动加载符号。

六、总结

启动文件与向量表是嵌入式系统的“第一行代码”。理解 MSP 初始化、向量表布局以及 VTOR 的作用,不仅能帮你解决 Bootloader 跳转异常、中断不响应等疑难杂症,更是迈向底层开发的必经之路。动手修改一次链接脚本,你将对“程序如何跑起来”有全新的认识。