2. 启动流程分析:MCU上电启动流程、中断向量表重映射、启动模式选择
MCU 上电之后到底干了些什么?
很多工程师写了好几年代码,可能都没仔细想过这个问题。我刚开始做 Bootloader 的时候,也以为上电就是直接跑 main()。结果第一次调试 Boot 跳转应用就失败了,折腾了两天才发现——原来是中断向量表没重映射。
这一节,咱们就把 MCU 启动的底裤扒干净。你搞懂了这些,后面写 Bootloader 才能心里有底。
2.1 MCU 上电启动流程
MCU 上电后,硬件会自动完成一系列初始化动作。这个过程不需要你写一行代码,但你必须知道它做了什么。
核心流程(以 Cortex-M 内核为例):
- 上电复位:电源稳定后,复位信号释放,CPU 从地址
0x00000000开始取指。 - 读取向量表:硬件自动从
0x00000000加载栈顶指针(MSP),从0x00000004加载复位向量(Reset_Handler 地址)。 - 执行 Reset_Handler:这是系统启动的第一个软件入口。通常由启动文件(startup_xxx.s)提供。
- 系统初始化:在 Reset_Handler 中完成时钟配置、堆栈初始化、BSS 段清零、数据段拷贝等。
- 调用 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 启动流程全景图
下面这张图把整个启动流程串起来了。你对照着看,应该能更直观地理解每个环节。
2.5 常见故障与排查思路
启动流程这块出问题,现象通常很一致:芯片不跑、跑飞、或者跳转后死机。我总结了几种常见情况:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 上电后完全没反应 | 电源不稳、复位电路异常、BOOT 引脚配置错误 | 量电源、量复位引脚、检查 BOOT 电平 |
| 程序跑飞(进入 HardFault) | 向量表地址不对、栈指针错误、中断未关闭就跳转 | 检查 VTOR 值、检查 MSP 设置、跳转前关中断 |
| 跳转后部分功能正常,中断不工作 | 向量表重映射后,外设中断使能状态未清理 | 跳转前关闭所有外设时钟和中断 |
| 下载程序后无法运行 | Flash 起始地址与链接脚本不匹配 | 检查链接脚本的 FLASH 起始地址和大小 |
重要提醒:调试启动问题时,别一上来就怀疑硬件。先确认你的链接脚本(.ld 或 .icf)里定义的 Flash 起始地址和实际硬件是否一致。我见过有人把 STM32F103 的链接脚本里 Flash 地址写成 0x08000000,但芯片实际是从 0x08000000 映射到 0x00000000 的——这没问题。但如果你把应用程序的链接脚本里 Flash 地址写成 0x08010000,而 Bootloader 没做向量表重映射,那应用程序一进中断就完蛋。
嗯,启动流程这部分内容,说白了就是搞清楚「芯片上电后第一口饭从哪里吃」。你把这个搞明白了,后面写 Bootloader 的跳转逻辑、做固件升级、甚至调试 HardFault,都会顺手很多。