2. MCU启动流程与存储映射:STM32启动模式、Flash与SRAM地址映射、中断向量表、堆栈初始化、启动文件分析
做嵌入式开发这么多年,我见过不少工程师上来就写应用代码,芯片怎么启动的完全不管。结果呢?Bootloader 跑不起来,中断进不去,堆栈溢出也不知道。说白了,你不懂启动流程,就没办法真正掌控这颗芯片。
这一章,咱们就把 STM32 从复位到 main() 的完整路径捋一遍。我会结合自己的项目经验,把那些容易踩的坑也指出来。
2.1 STM32 的三种启动模式
STM32 复位后,CPU 会先看 BOOT0 和 BOOT1 两个引脚的电平状态。这两个引脚决定了芯片从哪儿开始取第一条指令。
| BOOT1 | BOOT0 | 启动区域 | 说明 |
|---|---|---|---|
| X | 0 | 主 Flash | 正常启动模式,用户代码存放位置 |
| 0 | 1 | 系统存储器 | 内置 Bootloader,用于串口/USB/DFU 下载 |
| 1 | 1 | 内嵌 SRAM | 调试用,掉电丢失 |
2.2 Flash 与 SRAM 地址映射
STM32 采用统一编址,外设、Flash、SRAM 都映射到 4GB 的地址空间里。以 STM32F103 为例,常用的地址范围如下:
- Flash 起始地址:0x0800 0000,容量从 64KB 到 1MB 不等
- SRAM 起始地址:0x2000 0000,容量通常 20KB 到 192KB
- 系统存储器:0x1FFF F000,内置 Bootloader 所在
- 别名区:0x0000 0000,复位后映射到选中的启动区域
这里有个关键点:0x0000 0000 是个别名区。复位后,它根据 BOOT 引脚映射到 Flash、系统存储器或 SRAM。CPU 实际上是从 0x0000 0000 取向量表的,但物理数据在 0x0800 0000 上。
2.3 中断向量表
中断向量表是 MCU 中断系统的"电话本"。每个中断源对应一个入口地址,CPU 根据中断号查表跳转。
STM32 的向量表固定在 Flash 起始地址(0x0800 0000),前 4 字节存放栈顶指针,紧接着是复位向量,再往后是其他中断向量。
; startup_stm32f103xe.s 中的向量表片段
__Vectors DCD __initial_sp ; 0x0800 0000: 栈顶地址
DCD Reset_Handler ; 0x0800 0004: 复位入口
DCD NMI_Handler ; 0x0800 0008: NMI
DCD HardFault_Handler ; 0x0800 000C: 硬错误
DCD MemManage_Handler ; 0x0800 0010: 内存管理
DCD BusFault_Handler ; 0x0800 0014: 总线错误
DCD UsageFault_Handler ; 0x0800 0018: 用法错误
; ... 后续中断向量
你想想看,如果 Bootloader 在 0x0800 0000,应用程序在 0x0801 0000,那应用程序的中断向量表就得偏移。怎么做?设置 SCB->VTOR 寄存器。
SCB->VTOR = FLASH_BASE | APP_OFFSET;,其中 APP_OFFSET 是应用程序在 Flash 中的偏移量,比如 0x10000。
2.4 堆栈初始化
堆栈是 C 语言运行的基础。局部变量、函数调用、中断现场保存都靠它。启动文件里会做两件事:
- 设置栈顶指针 SP:从向量表第一个字加载,指向 SRAM 末尾
- 设置堆和栈的大小:在链接脚本中定义,启动文件调用初始化函数
看一段典型的启动文件代码:
; 栈配置
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; 堆配置
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
2.5 启动文件分析
启动文件(startup_xxx.s)是芯片上电后第一个执行的代码。它干了这几件事:
- 从向量表加载栈顶指针到 SP
- 跳转到 Reset_Handler
- 在 Reset_Handler 中:
- 调用 SystemInit() 配置时钟、FPU、中断向量表偏移
- 初始化 .data 段(从 Flash 拷贝到 SRAM)
- 清零 .bss 段
- 调用 C 库函数初始化堆(如果使用)
- 最后跳转到 main()
下面这张图把整个流程串起来了:
2.6 链接脚本与内存布局
启动文件负责执行,但内存怎么划分,是链接脚本(.ld 文件)说了算。以 STM32F103C8T6(64KB Flash,20KB SRAM)为例:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
.isr_vector : { *(.isr_vector) } > FLASH
.text : { *(.text*) } > FLASH
.rodata : { *(.rodata*) } > FLASH
.data : { *(.data*) } > RAM AT > FLASH
.bss : { *(.bss*) } > RAM
.heap : { *(.heap*) } > RAM
.stack : { *(.stack*) } > RAM
}
注意 .data 段有个 AT > FLASH,意思是它运行时在 RAM,但初始值存储在 Flash 里。启动文件负责在 Reset_Handler 中把这些初始值从 Flash 拷贝到 RAM。
2.7 实际项目中的启动配置
我记得有一次做远程升级项目,Bootloader 在 0x0800 0000,APP 在 0x0802 0000。结果 APP 死活跑不起来。查了半天,发现是 APP 的链接脚本没改,编译出来的中断向量表还在 0x0800 0000。Bootloader 跳转后,APP 的 SystemInit() 里也没设置 VTOR。中断一来,CPU 去 0x0800 0000 找向量表,找到的是 Bootloader 的,直接 HardFault。
解决方案很简单:
- APP 链接脚本:
FLASH (rx) : ORIGIN = 0x08020000, LENGTH = 128K - APP 的 SystemInit() 或 main() 开头:
SCB->VTOR = 0x08020000; - Bootloader 跳转前:关闭所有外设中断,复位 SysTick,确保干净跳转