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为例,常见布局为:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| FLASH | 0x08000000 | 1MB | 向量表、代码、只读数据、.data初值 |
| RAM | 0x20000000 | 128KB | .data、.bss、堆、栈 |
| CCM RAM | 0x10000000 | 64KB | 仅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或时钟配置。理解上述关系后,定制链接脚本就不再是“抄一份能用的”,而是根据芯片实际RAM/Flash大小、是否需要CCM RAM、是否使用Bootloader来精确划分。建议每次修改后,用arm-none-eabi-objdump -h查看各段地址,并用arm-none-eabi-size确认Flash和RAM占用,确保.data的LMA落在Flash范围内、栈顶不越界。