第2章:MCU启动流程详解
从复位向量到main函数,这条路我走过无数遍。说实话,很多工程师写了几年嵌入式代码,却从来没仔细看过芯片上电后到底干了什么。今天咱们就把这层窗户纸捅破。
2.1 复位向量——芯片的第一口气
MCU上电或复位后,CPU会去一个固定地址取指令。这个地址就是复位向量。以Cortex-M系列为例,复位向量位于0x00000000(或通过VTOR重映射)。
这里存的是什么?两个关键值:
- 初始栈指针(MSP)
- 复位异常入口地址
我刚开始做项目时,有一次把栈指针设错了,结果芯片一上电就跑飞。查了两天才发现是链接脚本里栈地址算偏了一个字节。嗯,从那以后我对复位向量格外敏感。
核心要点:复位向量不是代码,是数据。CPU硬件自动加载这两个值,然后跳转到复位异常处理函数。
2.2 启动文件——真正的幕后英雄
启动文件(startup_xxx.s)是汇编写的。很多人觉得汇编过时了,其实不然。启动阶段C环境还没建立,只能用汇编。
启动文件干了这几件事:
- 定义中断向量表
- 初始化堆栈指针
- 调用SystemInit()配置时钟
- 搬运.data段到RAM
- 清零.bss段
- 跳转到main()
你想想看,如果没有启动文件,你的全局变量初始值从哪来?全都会是随机值。我在一个低功耗项目里遇到过,某个全局变量没初始化,结果在休眠唤醒后值变了,导致逻辑错乱。查了半天才发现是启动文件里.bss段清零被优化掉了。
; 典型的Cortex-M启动文件片段
Reset_Handler PROC
EXPORT Reset_Handler
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
个人习惯:我一般会在SystemInit()里加个LED闪烁,这样一眼就能看出启动是否正常。调试初期特别管用。
2.3 堆栈初始化——别让局部变量没地方住
堆栈初始化看似简单,其实坑不少。栈(Stack)用于局部变量、函数调用、中断现场保存。堆(Heap)用于动态内存分配。
启动文件里会定义:
- 栈顶地址(Stack_TOP)
- 栈大小(Stack_Size)
- 堆大小(Heap_Size)
我见过最典型的错误是栈溢出。有个同事在中断里定义了一个大数组,结果栈空间不够,程序跑着跑着就死机了。而且这种问题很难复现,因为栈溢出往往在特定调用路径下才触发。
| 参数 | 典型值 | 说明 |
|---|---|---|
| Stack_Size | 0x400 (1KB) | 够用,但中断嵌套多时要加大 |
| Heap_Size | 0x200 (512B) | 不用malloc可以设为0 |
| Stack_TOP | RAM起始+RAM大小 | 栈向下生长,顶在RAM末尾 |
我曾经踩过的坑:低功耗模式下,如果栈指针指向了保留的RAM区域,唤醒后栈内容会被破坏。所以休眠前一定要确保栈指针在有效范围内。
2.4 从复位到main的完整路径
咱们把整个过程串起来:
- 上电复位,CPU从0x00000000加载MSP
- 从0x00000004加载PC,跳转到Reset_Handler
- Reset_Handler里调用SystemInit()配置时钟
- 搬运.data段(初始化全局变量)
- 清零.bss段(未初始化全局变量)
- 调用__main(C库初始化)
- 最终跳转到main()
说白了,main函数只是冰山一角。在它之前,芯片已经默默干了一大堆活。我建议你在调试时,可以在SystemInit()和main()入口各放一个断点,看看中间发生了什么。
避坑指南:如果你的MCU支持从多个启动源启动(比如从RAM或System Flash),一定要确认复位向量指向正确。我有个项目因为Bootloader和App的向量表冲突,导致跳转后直接HardFault。后来加了VTOR重映射才解决。
2.5 低功耗模式下的启动差异
低功耗模式(比如待机模式)唤醒后,芯片会经历一次完整的复位流程吗?不一定。
- 睡眠模式:CPU暂停,唤醒后从暂停处继续执行
- 停止模式:时钟关闭,唤醒后重新配置时钟
- 待机模式:相当于软复位,启动流程和上电类似
我做过一个电池供电的项目,用待机模式唤醒后,发现全局变量全被重新初始化了。因为待机模式会触发复位,启动文件重新搬运了.data段。解决方案是把需要保持的数据放在备份寄存器或RTC RAM里。
我的建议:在设计低功耗系统时,一定要搞清楚你的MCU在每种低功耗模式下,启动流程有什么不同。别等到产品量产了才发现唤醒后数据丢了。
好了,这一章的内容就到这。下一章咱们聊聊Bootloader的设计思路,看看怎么在启动流程里插入自己的代码。