2. 启动流程分析:MCU上电启动流程、中断向量表与复位向量、从Flash到RAM的代码搬运
说实话,很多工程师写了几年嵌入式代码,却从来没仔细看过MCU上电后到底干了什么。我见过不少同事,程序跑飞了第一反应是查逻辑,结果折腾半天发现是启动文件配置错了。这一章,我们就来把MCU的“出生过程”彻底捋一遍。
2.1 MCU上电启动流程:从复位到main()
MCU上电后,不是直接跳进main()的。它得先完成一套固定的“初始化仪式”。这套流程,说白了就是芯片厂商帮你写好的启动代码(Startup Code)在干活。
我习惯把启动流程分成四个阶段:
- 硬件复位阶段:电源稳定后,复位控制器释放复位信号,CPU内核从复位向量表指定的地址开始取指。
- 启动文件执行阶段:执行芯片厂商提供的启动文件(比如STM32的startup_stm32xxxx.s),完成堆栈初始化、中断向量表复制、全局变量初始化等。
- C运行时环境初始化:设置堆指针、清零BSS段、调用构造函数(C++项目)。
- 跳转到main():最后调用__main或_start,进入用户程序。
关键认知:从复位到main(),这中间的所有动作都是芯片厂商帮你安排好的。但如果你要做Bootloader,就必须自己接管这个过程。
我在做第一个Bootloader项目时,就犯过一个低级错误——我以为只要把程序烧到Flash里就能跑。结果上电后芯片直接死机。后来查了半天,发现是中断向量表的位置没改。嗯,这个坑后面会细说。
2.2 中断向量表与复位向量
中断向量表(IVT, Interrupt Vector Table)是MCU启动的“导航地图”。它告诉CPU:发生某个中断时,该去哪里执行代码。
对于ARM Cortex-M系列,中断向量表的前两个条目是固定的:
| 偏移地址 | 内容 | 说明 |
|---|---|---|
| 0x00000000 | 栈顶指针(MSP初始值) | 上电后CPU自动加载到主堆栈指针 |
| 0x00000004 | 复位向量(Reset_Handler地址) | 上电后CPU跳转的第一条指令地址 |
| 0x00000008 | NMI中断入口 | 不可屏蔽中断 |
| 0x0000000C | HardFault中断入口 | 硬件错误处理 |
| ... | 其他外设中断 | 按芯片手册排列 |
你想想看,CPU上电后第一件事是什么?它从0x00000000读取栈顶指针,然后从0x00000004读取复位向量地址,接着跳过去执行。这就是整个启动的“第一推动力”。
个人习惯:我每次做新板子,都会先读一下0x00000000和0x00000004这两个地址的内容,确认Flash烧录正确。用调试器看一眼,心里踏实。
这里有个容易踩的坑:中断向量表的位置不是固定的。默认情况下,Cortex-M芯片从Flash起始地址(通常是0x08000000或0x00000000)读取向量表。但如果你做了Bootloader,用户程序的向量表就得偏移到新的位置。
我曾经遇到过这样一个问题:Bootloader跳转到用户程序后,一开中断就HardFault。查了两天才发现,用户程序的向量表还是指向Flash起始地址,而那里放的是Bootloader的向量表。用户程序的中断请求来了,CPU跑到Bootloader的向量表里找中断入口,自然就崩了。
解决办法很简单:在用户程序启动时,通过修改SCB->VTOR寄存器,把向量表基址重定位到用户程序的起始地址。比如:
/* 将向量表重定位到用户程序起始地址 0x08010000 */
SCB->VTOR = 0x08010000;
2.3 从Flash到RAM的代码搬运
为什么要把代码从Flash搬到RAM?直接原地执行不行吗?
答案是:速度。Flash的读取速度通常比CPU主频慢很多。以STM32F4为例,CPU主频168MHz,而Flash的读取速度往往只有几十MHz。如果代码直接在Flash里跑,CPU就得等Flash,这叫“等待周期”(Wait State)。
我做过一个电机控制项目,PWM中断服务函数放在Flash里执行,结果中断响应时间长了将近一倍。后来把中断函数搬到RAM里,问题立刻解决。
代码搬运通常分两种情况:
- 全部搬运:把整个程序从Flash复制到RAM,然后在RAM里执行。适合对实时性要求极高的场景。
- 部分搬运:只把关键函数(如中断服务函数、时间敏感函数)搬到RAM。这是更常见的做法。
在启动文件中,代码搬运通常由以下步骤完成:
- 定义Flash中代码段的起始地址和结束地址(由链接脚本提供)。
- 定义RAM中代码段的起始地址。
- 在启动文件中,用循环或DMA将数据从Flash复制到RAM。
看一个典型的启动文件搬运代码(简化版):
; 从Flash复制数据段到RAM
ldr r0, =_sidata ; Flash中数据段的起始地址
ldr r1, =_sdata ; RAM中数据段的起始地址
ldr r2, =_edata ; RAM中数据段的结束地址
copy_loop:
cmp r1, r2
itt lt
ldrlt r3, [r0], #4
strlt r3, [r1], #4
blt copy_loop
这里_sidata、_sdata、_edata这些符号,都是在链接脚本(.ld文件)里定义的。它们告诉启动文件:从哪里搬、搬到哪里、搬多少。
避坑指南:链接脚本里的地址定义必须和实际硬件地址一致。我见过有人把_sdata定义到未初始化的RAM区域,结果程序一跑就乱掉。每次改链接脚本,我都会用map文件核对一遍地址。
2.4 启动流程全景图
下面这张图,把整个启动流程串起来了。我建议你把它存下来,调试Bootloader时对照着看。
2.5 启动流程中的关键寄存器
调试启动流程时,有几个寄存器你必须盯紧:
| 寄存器 | 作用 | 调试时怎么看 |
|---|---|---|
| PC(程序计数器) | 当前执行指令地址 | 上电后应等于复位向量值 |
| SP(栈指针) | 栈顶地址 | 上电后自动加载向量表首字 |
| VTOR(向量表偏移寄存器) | 中断向量表基址 | Bootloader跳转前必须修改 |
| SCB->AIRCR | 中断优先级分组 | 跳转前后保持一致 |
我的调试习惯:在启动文件里加几个GPIO翻转点。上电后用示波器看GPIO波形,就能知道程序跑到哪一步了。这比看代码快多了。
2.6 常见启动问题排查
最后,我把这些年遇到的启动问题整理了一下。你调试时如果遇到类似现象,可以直接对照排查:
- 上电后芯片无反应:检查复位电路、电源、晶振。用示波器看复位引脚波形。
- 程序跑飞或HardFault:大概率是向量表配置错误,或者栈溢出。先检查VTOR寄存器。
- 中断不响应:确认中断向量表已重定位,且中断使能寄存器已配置。
- 全局变量值不对:检查启动文件中的代码搬运是否完成,BSS段是否清零。
- Bootloader跳转后死机:跳转前没关中断,或者没恢复栈指针。跳转前记得关全局中断。