你有没有想过:按下电源开关到main()开始执行,中间发生了什么?
很多人写了几年STM32,从来没打开过startup文件。但面试一问"启动流程"就懵了。
这篇从上电一步步讲到main(),帮你搞懂那段"看不见的代码"。
一、为什么需要了解启动流程?
1.1 看得见的问题
- 程序跑飞,HardFault,不知道哪里错了
- 全局变量初始值不对
- printf不工作
- 自定义 bootloader 跳转后崩溃
这些问题可能都和启动流程有关。
1.2 面试必问
“STM32从上电到进入main(),经历了哪些步骤?”——这是中高级嵌入式岗位的高频面试题。
二、启动流程全景图
1. 上电/复位
↓
2. CPU从0x08000000(Flash起始)读栈指针(MSP)
> **MSP是什么?** MSP(Main Stack Pointer,主栈指针)指向栈的顶部。栈是一段RAM空间,用于函数调用时保存局部变量和返回地址。CPU复位时第一件事就是读MSP,因为后面执行代码可能就要调用函数,需要知道栈在哪。
> **什么是"段"?** 编译器把不同类型的变量分到不同的内存区域,这些区域叫"段"。.data段存有初始值的全局变量,.bss段存没初始值的全局变量(自动清零)。
↓
3. CPU从0x08000004读复位函数地址(Reset_Handler)
↓
4. 执行Reset_Handler(启动文件里的汇编代码)
↓
5. 初始化.data段(已初始化全局变量从Flash拷到RAM)
↓
6. 清零.bss段(未初始化全局变量)
↓
7. 调用SystemInit()(配置时钟)
↓
8. 调用__main(C库初始化)
↓
9. 调用main()(你的程序入口)
三、逐步详解
3.1 第1步:读栈指针
CPU复位后,硬件自动做两件事:
Flash地址 内容
0x08000000 → 栈顶地址(如0x20005000,RAM末尾)
0x08000004 → Reset_Handler函数地址
0x08000008 → NMI_Handler
0x0800000C → HardFault_Handler
… (中断向量表)
这两步是硬件固定行为,不可更改。所以向量表必须在Flash起始地址。
3.2 第2步:执行Reset_Handler
启动文件(startup_stm32f103xb.s)里的汇编代码:
Reset_Handler:
LDR R0, =__initial_sp ; 设置栈指针
MSR MSP, R0
BL SystemInit ; 调用SystemInit()配置时钟
BL __main ; 调用C库初始化,最终跳到main()
3.3 第3步:初始化.data段
.data段存已初始化的全局变量,但它们的初始值存在Flash里,运行时要拷到RAM。
/* 全局变量初始值存在Flash,运行时拷到RAM */
int counter = 100; /* .data段 */
const int version = 1; /* .rodata段(只读,留Flash) */
启动代码做的事:
从Flash的_data_load地址 → 拷贝到 → RAM的_data_run地址
长度 = .data段大小
3.4 第4步:清零.bss段
.bss段存未初始化或初始化为0的全局变量,启动时统一清零。
int flag; /* .bss段,自动清零 */
int buf[256]; /* .bss段,自动清零 */
static int count = 0; /* .bss段 */
启动代码做的事:
把RAM的.bss区域全部填0
为什么不存Flash:.bss全是0,存Flash浪费空间。只在RAM里清零即可。
3.5 第5步:SystemInit()
/* system_stm32f1xx.c */
void SystemInit(void)
{
/* 配置中断向量表地址 */
SCB->VTOR = FLASH_BASE; /* SCB=System Control Block(系统控制块),VTOR=Vector Table Offset Register(向量表偏移寄存器),这行设置中断向量表的位置 */
/* CubeMX会在这里调用SystemClock_Config()配置PLL */
}
3.6 第6步:__main → main()
__main是C库的初始化函数,做:
- 初始化堆(用于malloc)
- 调用C++构造函数(如果有)
- 最终调用你的main()
四、内存分布
Flash (0x08000000):
┌──────────────┐
│ 向量表 │ ← 中断向量,固定在最前面
├──────────────┤
│ .text │ ← 代码
├──────────────┤
│ .rodata │ ← const常量、字符串
├──────────────┤
│ .data初始值 │ ← 全局变量的初始值
└──────────────┘
RAM (0x20000000):
┌──────────────┐
│ .data │ ← 已初始化全局变量(从Flash拷来)
├──────────────┤
│ .bss │ ← 未初始化全局变量(清零)
├──────────────┤
│ Heap │ ← 堆(malloc用,向下生长)
├──────────────┤
│ │
│ Stack │ ← 栈(向下生长,MSP指向栈顶)
└──────────────┘
五、启动文件里的关键信息
5.1 栈和堆大小
Stack_Size EQU 0x00000400 ; 栈1KB
Heap_Size EQU 0x00000200 ; 堆512B
注意:这里设的栈是MSP(主栈),中断也用这个栈。FreeRTOS的任务栈是另外分配的。
5.2 向量表
__Vectors:
DCD __initial_sp ; 栈顶地址
DCD Reset_Handler ; 复位
DCD NMI_Handler ; NMI
DCD HardFault_Handler ; 硬件错误
…
DCD USART1_IRQHandler ; USART1中断
…
函数名必须和这里一致,否则中断进不去。
5.3 弱符号(Weak)
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B . ; 死循环
ENDP
[WEAK]表示弱定义:如果你没写NMI_Handler函数,用这个默认的(死循环);如果你写了,用你的。
这就是为什么HAL回调能用:HAL库里用__weak声明,你可以重写覆盖。
六、常见踩坑
坑1:全局变量初始值不对
原因:.data段没正确拷贝(可能是修改了启动文件或链接脚本)。
检查:用调试器看全局变量的值是否等于定义时的初始值。
坑2:栈溢出
原因:启动文件里Stack_Size太小,函数调用层级深时溢出。
解决:调大Stack_Size(如从1KB改为4KB)。
坑3:HardFault_Handler死循环
默认HardFault_Handler是B .(死循环)。调试时可以改成打印故障信息:
void HardFault_Handler(void)
{
printf("HardFault! PC=0x%08X LR=0x%08X\\n",
*(uint32_t*)(__get_MSP() + 24), /* PC */
*(uint32_t*)(__get_MSP() + 20)); /* LR */
while (1);
}
坑4:Bootloader跳转后崩溃
从bootloader跳转到APP前,必须:
/* Bootloader跳转到APP */
typedef void (*func_t)(void);
uint32_t app_addr = 0x08008000;
func_t app_reset = (func_t)(*(uint32_t *)(app_addr + 4));
__disable_irq(); /* 关中断 */
__set_MSP(*(uint32_t *)app_addr); /* 设置MSP */
app_reset(); /* 跳转 */
七、总结
| 1. 硬件 | 从Flash起始读MSP和Reset_Handler地址 |
| 2. Reset_Handler | 汇编初始化,设置栈 |
| 3. .data初始化 | 从Flash拷贝已初始化全局变量到RAM |
| 4. .bss清零 | 未初始化全局变量清零 |
| 5. SystemInit | 配置时钟和向量表 |
| 6. __main | C库初始化,调用main() |
一句话总结:启动流程的核心是"硬件读向量表→汇编初始化→拷贝.data→清零.bss→配置时钟→进入main()",理解了这6步,启动流程就通了。
下一篇预告:《HAL库 vs LL库 vs 寄存器:到底用哪个?》
如果这篇文章对你有帮助,点赞 + 收藏 + 关注,这是我持续更新的动力!
有问题欢迎评论区交流,我会逐条回复。
作者:嵌入式阿蔡