2. 启动流程分析:MCU上电启动流程、中断向量表与复位向量、从Flash到RAM的代码搬运

说实话,很多工程师写了几年嵌入式代码,却从来没仔细看过MCU上电后到底干了什么。我见过不少同事,程序跑飞了第一反应是查逻辑,结果折腾半天发现是启动文件配置错了。这一章,我们就来把MCU的“出生过程”彻底捋一遍。

2.1 MCU上电启动流程:从复位到main()

MCU上电后,不是直接跳进main()的。它得先完成一套固定的“初始化仪式”。这套流程,说白了就是芯片厂商帮你写好的启动代码(Startup Code)在干活。

我习惯把启动流程分成四个阶段:

  1. 硬件复位阶段:电源稳定后,复位控制器释放复位信号,CPU内核从复位向量表指定的地址开始取指。
  2. 启动文件执行阶段:执行芯片厂商提供的启动文件(比如STM32的startup_stm32xxxx.s),完成堆栈初始化、中断向量表复制、全局变量初始化等。
  3. C运行时环境初始化:设置堆指针、清零BSS段、调用构造函数(C++项目)。
  4. 跳转到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。这是更常见的做法。

在启动文件中,代码搬运通常由以下步骤完成:

  1. 定义Flash中代码段的起始地址和结束地址(由链接脚本提供)。
  2. 定义RAM中代码段的起始地址。
  3. 在启动文件中,用循环或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时对照着看。

MCU上电启动流程全景图 ① 上电复位 ② 读取向量表(0x0~0x4) ③ 跳转 Reset_Handler ④ 启动文件执行 堆栈初始化 / 代码搬运 / BSS清零 ⑤ 跳转 main() ⑥ 用户程序运行 关键寄存器变化 PC ← 0x00000004 内容 SP ← 0x00000000 内容 VTOR ← 向量表基址 Bootloader关注点 • 向量表重定位 • 跳转前关中断 • 栈指针恢复

2.5 启动流程中的关键寄存器

调试启动流程时,有几个寄存器你必须盯紧:

寄存器 作用 调试时怎么看
PC(程序计数器) 当前执行指令地址 上电后应等于复位向量值
SP(栈指针) 栈顶地址 上电后自动加载向量表首字
VTOR(向量表偏移寄存器) 中断向量表基址 Bootloader跳转前必须修改
SCB->AIRCR 中断优先级分组 跳转前后保持一致

我的调试习惯:在启动文件里加几个GPIO翻转点。上电后用示波器看GPIO波形,就能知道程序跑到哪一步了。这比看代码快多了。

2.6 常见启动问题排查

最后,我把这些年遇到的启动问题整理了一下。你调试时如果遇到类似现象,可以直接对照排查:

  • 上电后芯片无反应:检查复位电路、电源、晶振。用示波器看复位引脚波形。
  • 程序跑飞或HardFault:大概率是向量表配置错误,或者栈溢出。先检查VTOR寄存器。
  • 中断不响应:确认中断向量表已重定位,且中断使能寄存器已配置。
  • 全局变量值不对:检查启动文件中的代码搬运是否完成,BSS段是否清零。
  • Bootloader跳转后死机:跳转前没关中断,或者没恢复栈指针。跳转前记得关全局中断。