MCU 启动流程详解:从复位向量到 main 函数
MCU 启动流程详解:从复位向量到 main 函数
很多嵌入式工程师写 C 代码很熟,但被问到”程序是怎么从复位运行到 main 函数的”时却答不上来。这段”幕后流程”虽然大部分由启动文件和链接脚本自动完成,但搞懂它对理解内存布局、排查启动崩溃、做 Bootloader 和 OTA 都至关重要。
本文以 Cortex-M 内核 + STM32 为例,把启动流程一步步拆开。
一、复位那一刻发生了什么
MCU 上电或复位后,程序计数器(PC)并不会直接跳到 main,而是先由硬件完成几件事:
- 从向量表的首地址(通常是 0x0000 0000 或可通过 VTOR 重映射)加载两个初始值;
- 第一个字是栈顶指针(MSP)的初始值,写入 SP;
- 第二个字是复位向量(Reset_Handler 的地址),加载到 PC。
这两个值就是启动的”原点”。Flash 的前两个 4 字节,一个决定栈在哪里,一个决定代码从哪里开始。
二、向量表:中断的”地址簿”
Cortex-M 的向量表是一张连续存放的地址表,前面是系统异常,后面是外设中断:
| 偏移 | 内容 |
|---|---|
| 0x00 | 初始栈顶指针(MSP) |
| 0x04 | Reset_Handler |
| 0x08 | NMI_Handler |
| 0x0C | HardFault_Handler |
| … | 其他系统异常 |
| 0x40 起 | 外设中断(IRQ) |
中断发生时,硬件会根据中断号自动从向量表取对应的处理函数地址。所以向量表的位置必须正确,这也是 Bootloader 跳转需要重定位向量表的原因。
三、启动文件做了什么
以 STM32 标准启动文件 startup_stm32f407xx.s 为例,核心流程如下:
/* Reset_Handler 的伪代码 */
void Reset_Handler(void)
{
/* 1. 把 .data 段从 Flash 复制到 RAM */
CopyDataSection();
/* 2. 把 .bss 段(未初始化全局变量)清零 */
ZeroBssSection();
/* 3. 调用系统初始化(时钟、FPU 等) */
SystemInit();
/* 4. 调用 C 运行时库初始化 */
__libc_init_array();
/* 5. 跳转到用户 main 函数 */
main();
}
这段逻辑对应汇编文件里的 Reset_Handler 标签,它会调用 SystemInit 初始化时钟,然后进入 __main(C 库启动代码),最终才轮到你的 main()。
四、.data 与 .bss:全局变量为什么能用
写程序时,全局变量的初值保存在哪里?答案分两种情况:
- 已初始化的全局变量:初值存在 Flash 的
.data段,运行时由启动代码复制到 RAM; - 未初始化的全局变量:在 RAM 的
.bss段,启动代码负责清零。
int g_init_val = 100; /* 初值 100 存在 Flash .data 段,运行时复制到 RAM */
int g_uninit_val; /* .bss 段,启动时清零 */
如果不理解这一点,遇到”全局变量初值不对””变量莫名清零”这类问题就很难解释。
五、链接脚本:内存地图的”图纸”
启动代码和链接脚本(.ld 文件)一一对应。链接脚本决定了各段放在 Flash 还是 RAM:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 192K
}
SECTIONS
{
.text : { *(.text) } > FLASH /* 代码 */
.rodata: { *(.rodata) } > FLASH /* 只读数据 */
.data : { *(.data) } > RAM AT> FLASH /* 已初始化变量 */
.bss : { *(.bss) } > RAM /* 未初始化变量 */
}
改链接脚本是做 Bootloader、分配自定义内存区域、固件加密等高级功能的必要技能。
六、常见启动问题排查
- 上电不跑:查复位电路、时钟配置(外部晶振没起振)、BOOT 引脚。
- HardFault:常见于栈溢出、数组越界写坏栈、外设时钟没开就访问寄存器。
- 进不了 main:可能是
SystemInit里卡住(等待时钟就绪超时),或链接脚本段配置错误。 - Bootloader 跳转失败:跳转前没重设 MSP、没关中断、没重定位向量表。
总结
从复位到 main,本质上是这样一条链:
硬件取复位向量 → 启动代码初始化数据段 → SystemInit 配时钟 → 跑 C 运行时 → 进入 main
搞懂这条链,你就真正理解了”单片机程序是怎么跑起来的”,也为 Bootloader、OTA、异常定位打下了地基。这是从”会用库”到”理解系统”的关键一跃。