4. STM32启动流程与链接脚本

STM32上电或复位后,CPU并不会直接跳到main()。它先做一件非常“硬件”的事:从Flash起始地址取出前两个字,第一个字装入MSP(主堆栈指针),第二个字装入PC(程序计数器),随后从复位向量指向的地址开始执行。这一机制决定了启动文件、向量表和链接脚本必须严格配合,否则程序可能在进入main之前就跑飞。

启动文件:复位后的第一段代码

以STM32F407的startup_stm32f407xx.s为例,它通常包含以下关键部分:

  • 栈与堆定义:Stack_Size、Heap_Size通过EQU定义,并在.stack、.heap段中分配空间。
  • 向量表:g_pfnVectors是一个只读数据数组,第0项为初始MSP,第1项为Reset_Handler,之后依次是NMI、HardFault、SysTick、外设中断等。
  • Reset_Handler:调用SystemInit()配置时钟,再调用__main(ARM C库入口),由它完成.data段从Flash到SRAM的复制、.bss段清零,最后跳转到用户的main()。

需要特别注意的是,__main不是用户写的main,而是编译器运行时库提供的入口。它负责“散装加载”(scatter loading)语义下的初始化工作。如果链接脚本中.data的加载地址(LMA)和运行地址(VMA)设置错误,__main复制数据时就会读到错误内容,表现为全局变量初值异常。

向量表重定位:为什么需要、怎么做

默认情况下,向量表位于Flash的0x08000000。但在以下场景中需要重定位:

  • 使用Bootloader,应用程序被放到0x08008000等偏移地址;
  • 希望将向量表搬到SRAM以获得更低的中断延迟;
  • 实现双区升级或动态切换固件。

STM32通过SCB->VTOR寄存器指定向量表基址。典型做法是在SystemInit()中或main()开头写入:

SCB->VTOR = 0x08008000;
__DSB();
__ISB();

同时,链接脚本必须把向量表放到该地址,并保证Reset_Handler等入口地址与VTOR一致。若使用HAL库,还需在system_stm32f4xx.c中修改VECT_TAB_OFFSET,否则SystemInit()会覆盖你的设置。

内存布局与链接脚本定制

链接脚本(.ld)描述Flash和SRAM的起止地址、各段如何放置。以STM32F407为例,常见布局为:

区域起始地址大小用途
FLASH0x080000001MB向量表、代码、只读数据、.data初值
RAM0x20000000128KB.data、.bss、堆、栈
CCM RAM0x1000000064KB仅CPU可访问,适合栈或高速数据

一个精简的链接脚本片段如下:

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

SECTIONS
{
  .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
  .text : { *(.text*) } > FLASH
  .rodata : { *(.rodata*) } > FLASH
  _sidata = LOADADDR(.data);
  .data : { _sdata = .; *(.data*); _edata = .; } > RAM AT> FLASH
  .bss : { _sbss = .; *(.bss*); _ebss = .; } > RAM
  ._user_heap_stack : { . = . + 0x400; . = . + 0x800; } > RAM
}

其中AT> FLASH表示.data的运行地址在RAM,但加载地址在Flash;_sidata就是复制源地址。启动代码会依据_sidata、_sdata、_edata完成搬运。

堆栈设置与常见陷阱

栈由MSP管理,启动文件中Stack_Size决定其大小。若使用RTOS,每个任务还有自己的栈(PSP)。堆用于malloc,Heap_Size为0时调用malloc会失败。常见问题包括:

  • 栈溢出:局部大数组、深递归导致HardFault,可通过填充魔数或查看MSP值定位;
  • 堆栈重叠:链接脚本中._user_heap_stack未预留足够空间,堆向上增长与栈向下增长相撞;
  • VTOR未对齐:向量表基址必须按异常表大小对齐(通常128字节或256字节)。
调试启动问题时,可在Reset_Handler开头点亮一个GPIO,用示波器观察从复位到main的耗时;若GPIO不翻转,说明卡在SystemInit或时钟配置。
STM32启动流程与内存映射关系 FLASH 0x08000000 向量表 .isr_vector MSP | Reset_Handler | ... .text 代码段 Reset_Handler / main .rodata 只读数据 常量、字符串 .data 初值副本 _sidata 加载地址 中断服务程序 SysTick_Handler USART1_IRQHandler SRAM 0x20000000 .data 运行区 _sdata → _edata .bss 零初始化 _sbss → _ebss 堆 Heap 向上增长 ↑ 栈 Stack (MSP) 向下增长 ↓ CCM RAM 可选 0x10000000 仅CPU访问 取MSP/PC __main 复制 .data Flash → SRAM 清零 .bss SCB->VTOR = 向量表基址 重定位

理解上述关系后,定制链接脚本就不再是“抄一份能用的”,而是根据芯片实际RAM/Flash大小、是否需要CCM RAM、是否使用Bootloader来精确划分。建议每次修改后,用arm-none-eabi-objdump -h查看各段地址,并用arm-none-eabi-size确认Flash和RAM占用,确保.data的LMA落在Flash范围内、栈顶不越界。