2. 启动流程分析:MCU上电启动流程、中断向量表重映射、启动模式选择

MCU 上电之后到底干了些什么?

很多工程师写了好几年代码,可能都没仔细想过这个问题。我刚开始做 Bootloader 的时候,也以为上电就是直接跑 main()。结果第一次调试 Boot 跳转应用就失败了,折腾了两天才发现——原来是中断向量表没重映射。

这一节,咱们就把 MCU 启动的底裤扒干净。你搞懂了这些,后面写 Bootloader 才能心里有底。

2.1 MCU 上电启动流程

MCU 上电后,硬件会自动完成一系列初始化动作。这个过程不需要你写一行代码,但你必须知道它做了什么。

核心流程(以 Cortex-M 内核为例):

  1. 上电复位:电源稳定后,复位信号释放,CPU 从地址 0x00000000 开始取指。
  2. 读取向量表:硬件自动从 0x00000000 加载栈顶指针(MSP),从 0x00000004 加载复位向量(Reset_Handler 地址)。
  3. 执行 Reset_Handler:这是系统启动的第一个软件入口。通常由启动文件(startup_xxx.s)提供。
  4. 系统初始化:在 Reset_Handler 中完成时钟配置、堆栈初始化、BSS 段清零、数据段拷贝等。
  5. 调用 main():最后跳转到 C 语言的 main 函数,进入你的应用逻辑。

嗯,这里要注意:不同 MCU 厂商的启动文件细节会有差异。比如 STM32 的启动文件里还会调用 SystemInit() 来配置时钟,而 NXP 的 LPC 系列可能把时钟初始化放在 main 里。我建议你拿到一个新平台,第一件事就是打开它的启动文件看一眼。

我的习惯:每次移植 Bootloader 到新芯片,我都会在 Reset_Handler 入口处加一个 GPIO 翻转。这样用示波器一量,就能确认芯片确实跑起来了。别小看这个动作,它能帮你快速区分「芯片没启动」和「软件跑飞了」。

2.2 中断向量表重映射

这是 Bootloader 开发中最容易踩坑的地方,没有之一。

什么叫向量表重映射?说白了,就是告诉 CPU:「嘿,别去 0x00000000 找中断处理函数了,去我指定的新地址找。」

为什么需要重映射?因为 Bootloader 和应用程序是两套独立的固件。Bootloader 在 0x08000000,应用程序在 0x08010000。如果应用程序的中断来了,CPU 还去 0x08000000 找向量表,那就会跑到 Bootloader 的中断处理函数里去——这显然不是你想要的结果。

我曾经踩过的坑:第一次做 Bootloader 跳转时,应用程序能跑起来,但一按按键就死机。查了两天,发现是中断向量表没重映射。按键触发了 EXTI 中断,CPU 去 Bootloader 的向量表里找处理函数,结果 Bootloader 根本没初始化外设,直接 HardFault 了。

Cortex-M 内核提供了向量表偏移寄存器(VTOR),专门用来干这个事。你只需要把 VTOR 的值改成应用程序的向量表基地址就行。

// 以 STM32F4 为例,应用程序起始地址为 0x08010000
#define APP_BASE_ADDR   0x08010000

void jump_to_app(void)
{
    // 1. 关闭全局中断
    __disable_irq();
    
    // 2. 重映射向量表到应用程序地址
    SCB->VTOR = APP_BASE_ADDR;
    
    // 3. 从应用程序向量表获取栈顶指针和复位向量
    uint32_t msp = *(uint32_t*)APP_BASE_ADDR;
    uint32_t reset_vector = *(uint32_t*)(APP_BASE_ADDR + 4);
    
    // 4. 设置主栈指针
    __set_MSP(msp);
    
    // 5. 跳转到应用程序复位向量
    void (*app_main)(void) = (void (*)(void))reset_vector;
    app_main();
    
    // 这里永远不会回来
}

这段代码看起来简单,但有几个细节你必须注意:

  • 关闭中断:跳转前必须关掉所有中断,否则跳转过程中断进来就乱套了。
  • VTOR 写入时机:必须在设置 MSP 之前写 VTOR,因为设置 MSP 后可能立即产生中断。
  • 对齐要求:VTOR 的值必须按 512 字节对齐(Cortex-M0/M0+ 是 256 字节)。

避坑指南:我曾经在某个项目里发现,跳转后应用程序的 SysTick 中断不工作。查了半天,发现是 Bootloader 里使能了 SysTick,跳转前没关掉。应用程序的 SysTick 初始化时发现定时器已经在跑,就没重新配置。所以我的建议是:跳转前把所有外设时钟都关了,让应用程序从头初始化。

2.3 启动模式选择

大多数 MCU 支持多种启动模式。以 STM32 为例,通过 BOOT0 和 BOOT1 引脚的电平组合,可以选择从主 Flash、系统存储器(内置 Bootloader)或 SRAM 启动。

BOOT0 BOOT1 启动区域 典型用途
0 X 主 Flash(0x08000000) 正常运行用户程序
1 0 系统存储器(0x1FFF0000) 内置 Bootloader,用于串口/USB 下载
1 1 SRAM(0x20000000) 调试或临时运行代码

你可能会问:这跟 Bootloader 有什么关系?关系大了。

我见过不少工程师,把 Bootloader 烧进主 Flash 后,发现程序跑不起来。一查,BOOT0 引脚被拉高了,芯片每次复位都进系统存储器的内置 Bootloader,根本不执行你的 Bootloader 代码。

另外,在开发 Bootloader 时,我建议你利用「从 SRAM 启动」模式来调试。把 Bootloader 代码下载到 SRAM 里运行,可以避免反复擦写 Flash,调试速度快得多。

启动模式选择的核心原则:

  • 量产产品:BOOT0 必须拉低,确保从主 Flash 启动。
  • 开发阶段:可以用跳线或按键切换 BOOT0,方便进入系统 Bootloader 进行固件升级。
  • 调试 Bootloader 本身:用 SRAM 启动模式,配合调试器单步执行。

2.4 启动流程全景图

下面这张图把整个启动流程串起来了。你对照着看,应该能更直观地理解每个环节。

MCU 上电启动流程全景图 ① 上电复位 ② 读取向量表(0x00000000) ③ 执行 Reset_Handler ④ 系统初始化 时钟配置 / BSS清零 / 数据段拷贝 ⑤ 跳转 main() Bootloader 场景 main() 中判断是否需要升级 需要 → 接收固件 → 写入 Flash 应用场景 main() 中初始化外设 进入 while(1) 主循环

2.5 常见故障与排查思路

启动流程这块出问题,现象通常很一致:芯片不跑、跑飞、或者跳转后死机。我总结了几种常见情况:

现象 可能原因 排查方法
上电后完全没反应 电源不稳、复位电路异常、BOOT 引脚配置错误 量电源、量复位引脚、检查 BOOT 电平
程序跑飞(进入 HardFault) 向量表地址不对、栈指针错误、中断未关闭就跳转 检查 VTOR 值、检查 MSP 设置、跳转前关中断
跳转后部分功能正常,中断不工作 向量表重映射后,外设中断使能状态未清理 跳转前关闭所有外设时钟和中断
下载程序后无法运行 Flash 起始地址与链接脚本不匹配 检查链接脚本的 FLASH 起始地址和大小

重要提醒:调试启动问题时,别一上来就怀疑硬件。先确认你的链接脚本(.ld 或 .icf)里定义的 Flash 起始地址和实际硬件是否一致。我见过有人把 STM32F103 的链接脚本里 Flash 地址写成 0x08000000,但芯片实际是从 0x08000000 映射到 0x00000000 的——这没问题。但如果你把应用程序的链接脚本里 Flash 地址写成 0x08010000,而 Bootloader 没做向量表重映射,那应用程序一进中断就完蛋。

嗯,启动流程这部分内容,说白了就是搞清楚「芯片上电后第一口饭从哪里吃」。你把这个搞明白了,后面写 Bootloader 的跳转逻辑、做固件升级、甚至调试 HardFault,都会顺手很多。