1. Bootloader 概述与 MCU 启动流程
各位同学好,我是老周。咱们这门课讲的是 Bootloader 移植、调优和调试,第一节课得先把地基打牢。说白了,Bootloader 就是 MCU 上电后第一个跑的程序,它负责把真正的应用程序(App)从某个地方搬到内存里,然后跳过去执行。
我刚开始做嵌入式那会儿,觉得 Bootloader 就是个“跳板”,没啥技术含量。后来踩了几个坑才发现——Bootloader 要是写不好,整个产品都起不来。你想想看,系统上电第一件事就是跑它,它要是挂了,后面全白搭。
1.1 什么是 Bootloader
Bootloader 是一段固化在 MCU 非易失性存储器(通常是 Flash)中的启动代码。它的核心职责就两条:
- 初始化硬件:把时钟、内存、外设等基础资源准备好
- 加载并跳转:从存储介质(Flash、SD 卡、UART、USB 等)读取 App 固件,放到 RAM 或 Flash 指定地址,然后跳转执行
关键认知:Bootloader 本身也是一个完整的 MCU 程序,有自己的中断向量表、堆栈和运行逻辑。它和 App 是“前后辈”关系——Bootloader 先跑,跑完了把控制权交给 App。
我在一个 OTA 项目中遇到过这样的情况:Bootloader 把 App 从外部 Flash 搬到内部 Flash 时,校验 CRC 通过了,但跳转后系统直接跑飞。查了两天才发现——跳转前没关全局中断,App 的中断向量表还没准备好,一个中断进来就崩了。嗯,这个坑后面我们会细讲。
1.2 MCU 的启动模式
大多数 ARM Cortex-M 系列 MCU(比如 STM32、GD32、NXP LPC 系列)都支持三种启动模式。启动模式由芯片的 BOOT0/BOOT1 引脚电平决定。
| 启动模式 | BOOT0 | BOOT1 | 启动地址 | 典型用途 |
|---|---|---|---|---|
| 主闪存 | 0 | 任意 | 0x0800 0000 | 正常运行用户程序 |
| 系统存储器 | 1 | 0 | 0x1FFF 0000 | 出厂 Bootloader(ISP 烧录) |
| SRAM | 1 | 1 | 0x2000 0000 | 调试/测试 |
我的习惯:开发阶段我会把 BOOT0 拉高,用系统存储器的 ISP 模式烧录 Bootloader。量产时 BOOT0 固定拉低,直接从主闪存启动。这样万一 Bootloader 刷坏了,还能通过 ISP 救回来。
为什么会设计三种模式?说白了就是为了“兜底”。主闪存模式是正常跑,系统存储器模式是芯片出厂时固化的一段 Bootloader(通常支持 UART/USB 下载),SRAM 模式主要用于调试——把程序直接下载到 RAM 里跑,不写 Flash,调试周期短很多。
1.3 启动文件(startup_xx.s)的作用
每个 MCU 工程里都有一个启动文件,名字通常是 startup_stm32f103xe.s 或 startup_gd32f303.s 这种格式。这个文件是 MCU 上电后执行的“第一行代码”,它用汇编写的,主要干这几件事:
- 定义中断向量表:把各个中断服务函数的地址按顺序排好
- 初始化堆栈指针:从向量表第一个字加载 SP 值
- 设置复位向量:从向量表第二个字加载 PC 值,跳转到 Reset_Handler
- 初始化数据段:把 .data 段从 Flash 拷贝到 RAM
- 清零 BSS 段:把 .bss 段全部清零
- 调用 SystemInit:配置系统时钟
- 跳转到 main:进入 C 语言世界
注意:启动文件是 MCU 厂商提供的,一般不需要修改。但做 Bootloader 时,你可能会需要自己写启动文件——因为 Bootloader 和 App 各自有独立的中断向量表,地址不能冲突。
我记得有一次帮客户移植 Bootloader,他们直接把 App 的启动文件拿过来用,结果 Bootloader 和 App 的中断向量表都放在 0x0800 0000,跳转后中断全乱套了。嗯,这个坑很经典,后面我们会专门讲怎么处理双向量表。
1.4 中断向量表与复位流程
Cortex-M 系列 MCU 的启动流程非常清晰。上电后,硬件自动做两件事:
- 从地址 0x0000 0000(映射到主闪存起始地址)读取栈顶地址,写入 MSP
- 从地址 0x0000 0004 读取复位向量地址,写入 PC
然后 CPU 就从复位向量指向的地址开始执行。这个流程是硬件固化的,你改不了。但你可以通过修改向量表的位置,让 CPU 跳到你指定的地方。
中断向量表的结构是这样的:
; 中断向量表示例(STM32F103)
__Vectors DCD __initial_sp ; 0x0000 0000: 栈顶地址
DCD Reset_Handler ; 0x0000 0004: 复位向量
DCD NMI_Handler ; 0x0000 0008: NMI
DCD HardFault_Handler ; 0x0000 000C: 硬错误
DCD MemManage_Handler ; 0x0000 0010: 内存管理
DCD BusFault_Handler ; 0x0000 0014: 总线错误
DCD UsageFault_Handler ; 0x0000 0018: 用法错误
; ... 后续中断按芯片手册排列
你想想看,如果 Bootloader 和 App 都放在主闪存里,Bootloader 的向量表在 0x0800 0000,App 的向量表在 0x0801 0000(偏移 64KB)。跳转到 App 之前,必须通过 SCB->VTOR 寄存器把向量表基址改成 0x0801 0000,否则中断来了 CPU 还是去 Bootloader 的向量表里找函数——那肯定跑飞。
核心要点:Bootloader 跳转到 App 前,必须完成三件事——关中断、改 VTOR、清流水线。少一步都可能出问题。
1.5 启动流程全景图
下面这张图把 MCU 从复位到 App 运行的完整流程画出来了。我建议你把它存下来,后面讲跳转逻辑时还会反复用到。
1.6 小结
这一章我们讲了 Bootloader 的基本概念、MCU 的三种启动模式、启动文件的作用,以及中断向量表和复位流程。这些都是后面做 Bootloader 移植的基础。
我个人觉得,理解启动流程最关键的一点是:MCU 上电后的一切行为都由向量表驱动。向量表放对了,系统就能跑;放错了,系统就崩。后面讲 Bootloader 跳转逻辑时,你会发现所有操作都是围绕“怎么安全地切换向量表”展开的。
课后建议:打开你的 MCU 工程,找到 startup_xx.s 文件,对照本章内容逐行看一遍。重点关注向量表定义、Reset_Handler 的实现、以及 SystemInit 的调用位置。看懂了启动文件,你就掌握了 MCU 的“命脉”。