2. 启动流程分析:MCU上电复位流程、向量表与复位向量、从Flash到RAM的代码搬运
MCU 上电后到底干了什么?很多人觉得就是「跑 main()」。其实没那么简单。从复位引脚拉高,到第一条用户指令执行,中间藏着不少门道。我调试 Bootloader 时,有七成的问题都出在这个阶段——要么向量表配错了,要么代码没搬完就跑飞了。
咱们一步步拆开看。
2.1 上电复位流程:从硬件到软件的接力
按下复位键,或者芯片刚上电,MCU 内部会走一套固定的流程。我把它分成三个阶段:
| 阶段 | 谁在干活 | 干了什么 | 典型耗时 |
|---|---|---|---|
| 1. 电源稳定 | POR / BOR 电路 | 检测 VDD 是否达到工作阈值 | 几十 μs ~ 几 ms |
| 2. 时钟就绪 | 内部 RC 振荡器 / PLL | 等待主时钟稳定输出 | 几百 μs ~ 几 ms |
| 3. 取复位向量 | CPU 内核 | 从向量表偏移 0x0000_0004 读取 PC 初值 | 几个时钟周期 |
前两个阶段是硬件自动完成的,你改不了。但第三个阶段——取复位向量——是软件介入的起点。这里有个坑:如果向量表地址配错了,CPU 会从错误的地方取 PC,直接跑飞。我在 STM32F4 上遇到过,链接脚本里把 VECT_TAB_OFFSET 设成了 0x200,结果复位后 PC 指向了 RAM 区,里面全是随机值……板子直接死掉。
关键点:复位后 CPU 固定从 0x0000_0000 取 MSP(主堆栈指针),从 0x0000_0004 取 PC。这是 Cortex-M 内核的硬连线行为,任何 MCU 都一样。
2.2 向量表与复位向量:CPU 的第一张地图
向量表说白了就是一张「中断服务函数地址表」。CPU 遇到异常或中断时,会查这张表,找到对应的处理函数跳过去。复位是优先级最高的异常,它的入口就是复位向量。
以 Cortex-M3/M4 为例,向量表布局长这样:
; 向量表示例(汇编)
__Vectors DCD __initial_sp ; 0x0000_0000: MSP 初值
DCD Reset_Handler ; 0x0000_0004: 复位向量
DCD NMI_Handler ; 0x0000_0008: NMI
DCD HardFault_Handler ; 0x0000_000C: 硬 fault
; ... 后面跟着其他异常和中断
注意看:向量表的第一项不是代码,是栈指针初值。CPU 复位后先加载 MSP,再跳转到 Reset_Handler。如果 MSP 给错了,一进中断就栈溢出,连 HardFault 都跑不进去。
我的习惯:在链接脚本里显式指定向量表起始地址,并在 startup 文件中放一个 .isr_vector 段。这样不管怎么优化,向量表都不会被挪走。
实际项目中,Bootloader 和 App 各有各的向量表。Bootloader 的向量表在 Flash 起始地址,App 的向量表在偏移地址(比如 0x0801_0000)。切换时需要通过 SCB->VTOR 寄存器告诉 CPU:「嘿,向量表搬家了」。我见过有人忘了改 VTOR,App 里一开中断就 HardFault——因为 CPU 还在查 Bootloader 的向量表。
2.3 从 Flash 到 RAM 的代码搬运:谁搬?搬什么?
代码为什么要搬?因为 Flash 慢,RAM 快。尤其是频繁调用的函数、中断服务程序,放在 Flash 里跑会拖慢系统。所以 Bootloader 和 App 启动时,通常会把 .text(代码段)和 .data(已初始化全局变量)从 Flash 搬到 RAM。
搬运工作由 Reset_Handler 里的启动代码完成。流程如下:
- 清零 .bss 段:把未初始化全局变量所在的 RAM 区域全部写 0。
- 搬运 .data 段:从 Flash 的
_sidata复制到 RAM 的_sdata,长度_edata - _sdata。 - 初始化堆栈:设置堆指针
__heap_start和栈限制__stack_limit。 - 调用 main():跳转到 C 世界的入口。
这段代码通常写在 startup_xxx.s 里,用汇编实现。我贴一段典型的搬运逻辑:
; .data 段搬运(汇编)
ldr r0, =_sidata ; Flash 源地址
ldr r1, =_sdata ; RAM 目标地址
ldr r2, =_edata ; RAM 结束地址
subs r2, r1 ; 计算长度
beq .L_loop_done ; 长度为 0 则跳过
.L_loop:
ldrb r3, [r0], #1 ; 从 Flash 读一个字节
strb r3, [r1], #1 ; 写入 RAM
subs r2, #1
bne .L_loop
.L_loop_done:
我曾经踩过的坑:某次把 _sidata 和 _sdata 在链接脚本里写反了,结果搬运时把 RAM 里的数据当源地址,Flash 里的数据当目标地址。一上电,Flash 内容被覆盖,Bootloader 直接变砖。从那以后,我每次都会在搬运前后加 CRC 校验。
2.4 整体流程可视化
下面这张图把上电到 main() 的完整路径画出来了。你可以对照着看,每一步对应哪个阶段。
2.5 避坑指南:启动阶段最容易翻车的三个点
- 向量表对齐:Cortex-M 要求向量表按 512 字节对齐(
0x200的倍数)。如果偏移地址没对齐,VTOR 写不进去,CPU 会忽略你的设置。我习惯在链接脚本里加ALIGN(512)强制对齐。 - 搬运长度别算错:链接脚本里
_sdata、_edata、_sidata这三个符号,任何一个写错都会导致搬运异常。建议编译后查看 map 文件,确认各段的起始和结束地址。 - 中断使能时机:在 .bss 清零和 .data 搬运完成之前,绝对不要开全局中断。否则中断一来,访问到的全局变量可能是垃圾值。Reset_Handler 里默认关中断,但如果你在 startup 里提前调了
__enable_irq()……嗯,后果自负。
一个小技巧:在搬运完成后、跳转到 main() 之前,加一段自检代码——检查 .data 段的前几个字节是否与 Flash 中的源数据一致。如果不对,直接进错误处理循环。这个习惯帮我抓到了三次链接脚本配置错误。