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 调试用,掉电丢失
我的经验:做 IAP 升级时,我习惯把 BOOT0 拉低(接 GND),这样上电默认从 Flash 启动。需要进入系统 Bootloader 时,通过软件触发或外部按键拉高 BOOT0 再复位。千万别在产品量产时把 BOOT0 悬空,干扰会导致启动异常,我吃过这个亏。

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 上。

注意:写 Bootloader 时,如果你把中断向量表重映射到 SRAM,别忘了修改 SYSCFG 寄存器。否则中断来了,CPU 还是去 Flash 找向量表,直接跑飞。我曾经调试一个 USB 中断不响应的 bug,查了两天才发现是向量表没重映射。

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 语言运行的基础。局部变量、函数调用、中断现场保存都靠它。启动文件里会做两件事:

  1. 设置栈顶指针 SP:从向量表第一个字加载,指向 SRAM 末尾
  2. 设置堆和栈的大小:在链接脚本中定义,启动文件调用初始化函数

看一段典型的启动文件代码:

; 栈配置
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
我建议:栈大小别设太小。RTOS 任务栈、中断嵌套、printf 都会吃栈。我一般给 0x1000(4KB)起步,如果用到文件系统或网络协议栈,至少 0x2000。堆的话,不用 malloc 可以设小点,甚至 0。

2.5 启动文件分析

启动文件(startup_xxx.s)是芯片上电后第一个执行的代码。它干了这几件事:

  1. 从向量表加载栈顶指针到 SP
  2. 跳转到 Reset_Handler
  3. 在 Reset_Handler 中:
    • 调用 SystemInit() 配置时钟、FPU、中断向量表偏移
    • 初始化 .data 段(从 Flash 拷贝到 SRAM)
    • 清零 .bss 段
    • 调用 C 库函数初始化堆(如果使用)
    • 最后跳转到 main()

下面这张图把整个流程串起来了:

STM32 启动流程 芯片复位 从 0x0000 0000 加载 SP 跳转 Reset_Handler SystemInit() 配置时钟 初始化 .data / 清零 .bss 跳转 main() 注意:Bootloader 模式下,应用程序的向量表需要偏移,否则中断无法响应

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。

避坑指南:做双区备份升级时,应用程序的链接脚本必须把 Flash 起始地址改成 APP 偏移地址。比如 APP 在 0x0801 0000,那 ORIGIN 就要改成 0x08010000,LENGTH 改成 48K(留 16K 给 Bootloader)。不改的话,编译出来的代码还是从 0x0800 0000 开始,直接覆盖 Bootloader。

2.7 实际项目中的启动配置

我记得有一次做远程升级项目,Bootloader 在 0x0800 0000,APP 在 0x0802 0000。结果 APP 死活跑不起来。查了半天,发现是 APP 的链接脚本没改,编译出来的中断向量表还在 0x0800 0000。Bootloader 跳转后,APP 的 SystemInit() 里也没设置 VTOR。中断一来,CPU 去 0x0800 0000 找向量表,找到的是 Bootloader 的,直接 HardFault。

解决方案很简单:

  1. APP 链接脚本:FLASH (rx) : ORIGIN = 0x08020000, LENGTH = 128K
  2. APP 的 SystemInit() 或 main() 开头:SCB->VTOR = 0x08020000;
  3. Bootloader 跳转前:关闭所有外设中断,复位 SysTick,确保干净跳转
总结一下:启动流程就是芯片从复位到 main() 的"开箱"过程。理解它,你才能写出可靠的 Bootloader 和应用程序。特别是向量表偏移和内存布局,这是 IAP 升级的基石。