4. 实现启动代码之向量表

任务目标

在汇编启动文件中定义ARM Cortex-M处理器所需的初始中断向量表,并正确设置初始栈指针(MSP)。向量表首字为栈顶地址,第二个字为复位处理函数入口,后续为各异常/中断服务函数入口。

实现方案

本案例以STM32F103(Cortex-M3)为例,使用GNU汇编语法编写startup.s。向量表通过汇编指令.word将各处理函数地址按固定顺序排列,其中第一个.word必须指向栈顶(由链接脚本中的_estack符号提供)。复位处理函数Reset_Handler中仅执行一条跳转指令,将控制权转交给C语言入口main,完成最小启动闭环。

关键完整代码

/* startup.s - 最小启动文件:向量表 + 初始栈指针 */
  .syntax unified
  .cpu cortex-m3
  .thumb

/* 栈顶地址由链接脚本定义,此处声明为外部符号 */
  .global _estack
/* 导出向量表与复位入口,供链接器使用 */
  .global g_pfnVectors
  .global Reset_Handler

/* 向量表必须放置在0x00000000或0x08000000起始处(由链接脚本决定) */
  .section .isr_vector, "a", %progbits
  .type g_pfnVectors, %object
  .size g_pfnVectors, .-g_pfnVectors

g_pfnVectors:
  .word _estack                  /* 初始栈指针(MSP) */
  .word Reset_Handler            /* 复位向量 */
  .word NMI_Handler              /* NMI */
  .word HardFault_Handler        /* 硬错误 */
  .word MemManage_Handler        /* 内存管理错误(可选) */
  .word BusFault_Handler         /* 总线错误(可选) */
  .word UsageFault_Handler       /* 用法错误(可选) */
  .word 0                        /* 保留 */
  .word 0                        /* 保留 */
  .word 0                        /* 保留 */
  .word 0                        /* 保留 */
  .word SVC_Handler              /* 系统服务调用 */
  .word DebugMon_Handler         /* 调试监视器(可选) */
  .word 0                        /* 保留 */
  .word PendSV_Handler           /* 可挂起系统服务 */
  .word SysTick_Handler          /* 系统滴答定时器 */

/* 默认异常处理函数:死循环(弱定义,可被C文件覆盖) */
  .section .text
  .thumb_func
  .weak NMI_Handler
  .weak HardFault_Handler
  .weak MemManage_Handler
  .weak BusFault_Handler
  .weak UsageFault_Handler
  .weak SVC_Handler
  .weak DebugMon_Handler
  .weak PendSV_Handler
  .weak SysTick_Handler

NMI_Handler:
HardFault_Handler:
MemManage_Handler:
BusFault_Handler:
UsageFault_Handler:
SVC_Handler:
DebugMon_Handler:
PendSV_Handler:
SysTick_Handler:
default_handler:
  b default_handler

/* 复位处理:跳转至C入口 */
  .thumb_func
  .type Reset_Handler, %function
Reset_Handler:
  ldr r0, =main
  bx r0
  .size Reset_Handler, .-Reset_Handler

/* 最小C入口(若工程无其他main定义) */
  .section .text.main
  .thumb_func
  .type main, %function
main:
  b main
  .size main, .-main

链接脚本关键配置

向量表必须位于存储区起始地址。以下为最小链接脚本片段,将.isr_vector段放置在FLASH起始处,并定义栈顶地址_estack:

/* minimal.ld */
ENTRY(Reset_Handler)
MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
  RAM (rwx)  : ORIGIN = 0x20000000, LENGTH = 20K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);  /* 栈顶 = RAM最高地址 */

SECTIONS
{
  .isr_vector :
  {
    KEEP(*(.isr_vector))
  } > FLASH

  .text :
  {
    *(.text*)
  } > FLASH
}

编译与验证

  1. 编译:使用arm-none-eabi工具链,执行以下命令生成ELF文件:
arm-none-eabi-as -mcpu=cortex-m3 -mthumb startup.s -o startup.o
arm-none-eabi-ld -T minimal.ld startup.o -o startup.elf
  1. 检查向量表内容:使用objdump验证首两个字是否正确:
arm-none-eabi-objdump -s -j .isr_vector startup.elf

预期输出(地址与具体值取决于链接地址):

08000000 20005000 08000009 0800000d 0800000d ...

其中0x20005000为栈顶地址(RAM 20K末尾),0x08000009为复位入口地址(Thumb模式最低位为1)。

  1. 判定通过条件:
  • 向量表首字等于_estack的值(即0x20005000);
  • 第二个字为Reset_Handler的地址且最低位为1(Thumb指令标志);
  • 后续异常入口地址均指向default_handler(因弱定义且未覆盖)。

常见问题

  • 栈指针未生效:确认链接脚本中_estack符号已定义且被向量表引用,否则首字为0,上电即跑飞。
  • 向量表位置错误:若使用Bootloader跳转,需确保应用向量表重定位(通过SCB->VTOR设置),本案例为裸机最小实现,向量表位于Flash起始。
  • Thumb位缺失:函数地址最低位必须为1,否则硬件将进入错误状态。汇编器通常自动处理,但手动计算地址时需注意。