STM32 上电启动流程
STM32 上电启动流程:从向量表、MSP、PC 到 main()
一、STM32 上电后到底发生了什么?
STM32 上电或复位后,并不是直接从 main() 开始执行,而是先经过 Cortex-M 内核规定的启动流程。
最关键的一步是:
Cortex-M 在复位后,由硬件读取初始向量表中的前两项,自动初始化 MSP 和 PC。
整体流程可以概括为:
STM32 上电 / 复位
↓
Cortex-M 硬件启动
↓
读取初始向量表
↓
第 0 项 → MSP
第 1 项 → PC
↓
进入 Reset_Handler
↓
SystemInit()
↓
__main
↓
C 运行库初始化
↓
main()二、什么是中断向量表?
向量表本质上就是一张地址表。
典型的 STM32 启动文件中,会看到:
__Vectors
DCD __initial_sp
DCD Reset_Handler
DCD NMI_Handler
DCD HardFault_Handler
...它的含义不是“按照这些汇编指令顺序执行”。
因为:
DCD本身不是 CPU 执行的指令,而是在告诉汇编器:
在这里放一个 32 位数据。
最终烧录进 Flash 后,可能变成:
0x08000000 0x20000400
0x08000004 0x08000189
0x08000008 NMI_Handler 地址
0x0800000C HardFault_Handler 地址
...所以:
0x08000000
↓
初始 MSP 的值
0x08000004
↓
Reset_Handler 的地址三、__initial_sp 到底是什么?
启动文件中:
Stack_Size EQU 0x400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp这里的:
__initial_sp不是一条汇编指令,而是一个符号。
SPACE Stack_Size 会让链接器给栈预留空间。
例如最终可能得到:
SRAM
0x20000000
┌─────────────────┐
│ │
│ Stack │
│ 1 KB │
│ │
└─────────────────┘
0x20000400
↑
__initial_sp所以最终:
__initial_sp = 0x20000400然后:
DCD __initial_sp会让向量表第 0 项保存:
0x20000400因此 CPU 复位后读取这个值,就可以完成:
MSP = 0x20000400四、CPU 为什么会自动初始化 MSP 和 PC?
这里最重要的一个理解是:
这不是启动汇编代码执行出来的,而是 Cortex-M 硬件规定好的 Reset Sequence。
复位以后,CPU 会按照架构规定:
访问启动向量表
↓
读取第 0 个 32 位数据
↓
装入 MSP
↓
读取第 1 个 32 位数据
↓
装入 PC
↓
开始执行 PC 指向的程序可以粗略写成:
MSP ← [向量表地址 + 0]
PC ← [向量表地址 + 4]例如:
0x00000000 → 0x20000400
0x00000004 → 0x08000189那么硬件自动完成:
MSP = 0x20000400
PC = 0x08000189然后开始执行:
Reset_Handler五、为什么明明 Flash 在 0x08000000,CPU 却像是从 0x00000000 开始?
这里涉及 启动地址映射。
STM32 的 Main Flash 通常位于:
0x08000000但 Cortex-M 在复位时使用固定的启动地址空间。
STM32 会根据 BOOT 配置,将对应的启动区域映射到 0x00000000。
正常从 Main Flash 启动时,可以理解成:
0x00000000 ───────→ 0x08000000 Flash
0x00000004 ───────→ 0x08000004 Flash
0x00000008 ───────→ 0x08000008 Flash
...所以 CPU 在启动阶段访问:
0x00000000实际能够拿到 Flash:
0x08000000中的数据。
因此:
CPU访问 0x00000000
↓
STM32 地址映射
↓
Flash 0x08000000
↓
读取初始 MSP六、System Memory 启动又是什么?
STM32 并不只有 Main Flash 和 SRAM。
芯片内部还有一块特殊的 System Memory,里面放的是 ST 出厂预置的官方 Bootloader。
可以粗略理解为:
STM32
Main Flash
↓
你的程序
SRAM
↓
运行时数据、栈、堆
System Memory
↓
ST 官方 Bootloader当 BOOT 配置选择 System Memory 启动时,STM32 会将对应的启动区域映射到启动地址空间。
于是:
0x00000000
↓
System Memory
↓
ST 官方 Bootloader这样就可以通过 USART、USB、CAN 等接口下载程序,而不依赖用户自己写的 Bootloader。
七、System Memory 里的 Bootloader 有什么用?
它最大的作用就是:
提供一个不依赖用户程序的官方程序下载/更新入口。
例如:
STM32
↓
进入 System Memory
↓
ST 官方 Bootloader
↓
USART / USB / CAN
↓
接收新的程序
↓
擦除、写入 Main Flash
↓
重新运行用户程序所以它和自己写的 Bootloader 不一样。
ST 官方 Bootloader
↓
放在 System Memory
↓
ST 出厂提供而自己写的 Bootloader:
Main Flash
┌──────────────────┐
│ 自己的Bootloader │
├──────────────────┤
│ App │
└──────────────────┘前者主要提供官方下载通道,后者可以实现自己的启动逻辑、OTA、双分区升级等功能。
八、VTOR 到底是什么?
VTOR:
Vector Table Offset Register即:
向量表偏移寄存器。
它用于告诉 Cortex-M:
系统正常运行以后,中断和异常的向量表在哪里。
例如:
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;如果:
FLASH_BASE = 0x08000000
VECT_TAB_OFFSET = 0那么:
VTOR = 0x08000000以后发生:
USART 中断
TIM 中断
SysTick
PendSV
HardFault
...CPU 就能够根据 VTOR 指向的位置查找对应的异常/中断处理函数地址。
九、最容易混淆的地方:复位启动和 VTOR 不是一回事
非常容易产生这样的误解:
VTOR = 0
↓
CPU 去 0x00000000
↓
找到向量表这种因果关系并不准确。
正确理解应该是:
复位瞬间
Cortex-M 有自己规定好的启动机制:
复位
↓
硬件获取初始 MSP、PC
↓
进入 Reset_Handler这一步不是依靠 SystemInit() 里面设置的 VTOR 完成的。
进入 SystemInit() 后
才会执行:
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;这一步是在明确配置:
后续运行过程中,中断/异常向量表的位置。
因此:
复位启动
↓
硬件获取初始 MSP、PC
↓
Reset_Handler
↓
SystemInit
↓
设置 VTOR
↓
正常运行阶段的中断/异常按照 VTOR 查表十、Reset_Handler 做了什么?
启动文件中:
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP主要就做两件事情:
1. 调用 SystemInit()
2. 跳转到 __main也就是:
Reset_Handler
↓
SystemInit()
↓
__main十一、SystemInit() 做了什么?
典型的 STM32F429 SystemInit() 主要负责建立最基础的运行环境。
1. 使能 FPU
SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2));允许 Cortex-M4 使用硬件浮点单元。
2. 开启 HSI
RCC->CR |= 0x00000001;打开内部高速时钟 HSI。
3. 恢复 RCC 默认状态
RCC->CFGR = 0x00000000;将系统时钟相关配置恢复到默认状态。
4. 关闭 HSE、PLL 等
RCC->CR &= ...先把 HSE、PLL 等配置恢复到确定状态。
5. 恢复 PLL 默认配置
RCC->PLLCFGR = 0x24003010;让 PLL 配置回到复位默认状态。
6. 关闭 RCC 中断
RCC->CIR = 0x00000000;关闭 RCC 的相关中断。
7. 必要时初始化外部 RAM
SystemInit_ExtMemCtl();如果工程使用外部 SRAM / SDRAM,就在这里进行相关初始化。
8. 设置 VTOR
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;告诉 CPU 正常运行阶段的向量表位置。
十二、SystemInit() 并不等于最终时钟配置
这是另外一个容易混淆的地方。
SystemInit() 的主要目的是:
把 CPU、FPU、RCC、向量表等基础状态整理到一个确定的环境。
真正把:
HSE
↓
PLL
↓
SYSCLK
↓
HCLK
↓
PCLK1 / PCLK2配置成具体频率的代码,通常在:
SystemClock_Config();里面。
所以典型启动过程可以理解为:
Reset_Handler
↓
SystemInit()
↓
__main
↓
C 运行库初始化
↓
main()
↓
HAL_Init()
↓
SystemClock_Config()
↓
配置最终系统时钟十三、.data 和 .bss 初始化到底在哪里?
这是看启动文件时非常容易产生疑惑的一点。
很多资料会直接写:
Reset_Handler负责初始化.data、清零.bss。
但对于现在这份 Keil/ARM 启动文件来说,这种说法并不准确。
我的 Reset_Handler 实际上只有:
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0也就是说,它自己没有显式写出 .data 拷贝和 .bss 清零的汇编代码。
真正的流程是:
Reset_Handler
↓
SystemInit()
↓
__main
↓
ARM C 运行库启动代码
↓
初始化 .data
清零 .bss
其他 C 运行时初始化
↓
main().data 是什么?
例如:
int a = 10;
static int b = 20;这种带有初始值的全局/静态变量通常属于 .data。
程序运行时它们必须在 SRAM 中:
Flash
┌────────────────────────┐
│ .data 的初始值 10、20 │
└────────────────────────┘
↓ 启动时拷贝
SRAM
┌────────────────────────┐
│ .data │
│ a = 10 │
│ b = 20 │
└────────────────────────┘因为 SRAM 掉电后数据会消失,所以启动时必须从 Flash 把初始值重新拷贝到 RAM。
.bss 是什么?
例如:
int c;
static int d;这类未显式初始化的全局/静态变量通常属于 .bss。
C 语言规定它们启动时应为 0:
SRAM
.bss
┌──────────────────┐
│ c = 0 │
│ d = 0 │
│ ... │
└──────────────────┘因此 C 运行库启动时需要把 .bss 对应的内存区域清零。
为什么启动文件里看不到?
因为这些工作被封装到了:
IMPORT __main对应的 ARM C 运行库启动代码中。
所以:
__main
↓
C 运行时启动
↓
.data 拷贝
.bss 清零
其他运行库初始化
↓
最终进入 main()具体的内部函数名称和实现方式会随着 ARM Compiler 版本、链接配置等发生变化,但核心职责就是这些。
十四、__main 和 main() 完全不是一回事
这一点必须分清。
__main
↓
ARM C 运行库提供
↓
负责 C 运行时启动
↓
最终调用 main()
main()
↓
自己写的
↓
真正开始执行应用程序所以:
LDR R0, =__main
BX R0并不是“直接跳到用户的 main()”。
它是先把程序交给 C 运行库,让 C 运行库完成:
.data 初始化
.bss 清零
其他运行时初始化然后才真正进入:
int main(void)
{
...
}十五、最终把整个启动过程串起来
STM32 上电 / 复位
↓
Cortex-M 硬件启动序列
↓
找到初始启动向量表
↓
┌────────────────┴────────────────┐
↓ ↓
第 0 项:初始 MSP 第 1 项:Reset_Handler
↓ ↓
MSP PC
↓
Reset_Handler
↓
SystemInit()
↓
CPU/FPU/RCC 等基础初始化
↓
设置 VTOR
↓
__main
↓
C 运行库初始化
↓
┌────────────────┴──────────────┐
↓ ↓
.data 拷贝到 SRAM .bss 清零
└────────────────┬──────────────┘
↓
main()
↓
HAL_Init()
↓
SystemClock_Config()
↓
正式进入应用十六、最核心的几个结论
1. Cortex-M 复位后,硬件会读取初始向量表的前两项。
2. 第 0 项是初始 MSP,第 1 项是 Reset_Handler 地址,也就是初始 PC。
3.
DCD __initial_sp不是执行指令,而是在向量表中存放__initial_sp最终对应的地址。
4.
__initial_sp是链接阶段确定的符号,最终一般指向 SRAM 中的栈顶。
5.
0x00000000是 Cortex-M 的启动地址空间,STM32 会根据 BOOT 配置把对应的启动区域映射到这里。
6. VTOR 不是“复位时 CPU 找向量表的唯一依据”,而是用于指定正常运行期间异常/中断向量表的位置。
7.
Reset_Handler是真正开始执行启动汇编代码的地方。
8.
SystemInit()负责建立最基础的 CPU、FPU、RCC、向量表等运行环境。
9. 在我这份 Keil 启动文件中,
.data的拷贝和.bss的清零并没有直接写在Reset_Handler里,而是由__main进入的 ARM C 运行库启动代码完成。
10.
__main不是用户的main(),而是 C 运行时启动入口;它完成运行时初始化后,才进入真正的main()。
最终可以把 STM32 从上电到用户代码执行理解成四层:
第一层:Cortex-M 硬件
↓
读取初始向量表
初始化 MSP、PC
第二层:启动汇编
↓
Reset_Handler
调用 SystemInit
调用 __main
第三层:C 运行库
↓
.data 初始化
.bss 清零
其他运行时初始化
第四层:用户程序
↓
main()这四层就是理解 STM32 启动流程的核心。

