2、启动流程概览:从复位到Main函数的完整旅程、Boot ROM与Bootloader的分工、启动模式选择
好,咱们直接进入正题。芯片上电之后,到底发生了什么?
很多刚入行的朋友会以为,CPU一通电就直接跳进main函数了。其实没那么简单。从复位到main,中间有一段「无人区」,这段路走不好,整个系统都起不来。我当年第一次调板子,就是卡在这一步,灯死活不亮,查了三天才发现是Boot ROM里读到的启动向量是错的。
今天我就带你走一遍这段路。咱们把它拆成三个部分:
- 复位后CPU的第一口气——Boot ROM
- 真正的搬运工——Bootloader
- 怎么告诉芯片从哪启动——启动模式选择
2.1 从复位到第一条指令:CPU在干什么?
芯片复位那一刻,CPU内部所有寄存器都被强制设成默认值。PC指针(程序计数器)会被硬件拉到某个固定地址。这个地址,就是Boot ROM的入口。
举个例子,ARM Cortex-M系列复位后从0x00000000取向量表,Cortex-A系列则从0x00000000或0xFFFF0000开始。不同架构不一样,但逻辑是一样的——硬件强制跳到一个固定的ROM区域。
这个ROM是芯片出厂时固化好的,你改不了。它里面放的是芯片厂商写好的第一段代码。这段代码的任务很简单:
- 初始化最基本的硬件(时钟、堆栈、内存控制器)
- 检测启动模式引脚的电平
- 从外部存储介质(Nor Flash、NAND、SD卡等)把下一段代码搬进SRAM或DDR
- 跳转到搬进来的代码
嗯,这里要注意:Boot ROM本身很小,通常只有几KB到几十KB。它不可能把整个操作系统都搬进去,它只负责把Bootloader搬进来。
核心理解:Boot ROM是芯片厂商写死的,Bootloader是开发者自己写的。Boot ROM负责「启动Bootloader」,Bootloader负责「启动操作系统」。
2.2 Boot ROM与Bootloader的分工
说白了,这就是一个接力赛。Boot ROM跑第一棒,Bootloader跑第二棒,操作系统跑第三棒。
Boot ROM 干的事:
- 初始化CPU最基本的状态(比如设置栈指针SP)
- 配置系统时钟(至少让CPU能跑起来)
- 读取启动模式选择引脚(后面会细说)
- 从指定设备读取固定大小的数据到内部SRAM
- 验证数据完整性(有些芯片会做CRC校验)
- 跳转到SRAM中的Bootloader入口
Bootloader 干的事:
- 初始化DDR/SDRAM控制器(让大内存可用)
- 初始化存储设备驱动(比如NAND控制器、SD卡接口)
- 从存储设备读取操作系统镜像到DDR
- 设置启动参数(比如传给内核的设备树、命令行参数)
- 跳转到操作系统入口
我在项目中遇到过一种情况:某款芯片的Boot ROM只支持从SPI Nor Flash加载最多64KB数据。但我们的Bootloader编译出来有120KB。怎么办?只能分两段——第一段Bootloader(叫SPL或Preloader)先被加载,它再初始化DDR,然后把完整的Bootloader从Flash搬进DDR。
个人经验:如果你用的是U-Boot,它的SPL(Secondary Program Loader)就是干这个的。SPL由Boot ROM加载,SPL再加载完整的U-Boot。这个分层设计非常实用,我建议你在选型时优先考虑支持这种方案的芯片。
2.3 启动模式选择:芯片怎么知道从哪启动?
芯片上电时,Boot ROM需要知道「我去哪里找Bootloader」。这个信息通常通过芯片的硬件引脚来设定。
常见的启动模式有这几种:
| 启动模式 | 存储介质 | 特点 | 典型场景 |
|---|---|---|---|
| Nor Flash启动 | Nor Flash(并行或SPI) | 支持XIP(原地执行),速度快,容量小 | 小系统、实时性要求高的场景 |
| NAND Flash启动 | NAND Flash | 容量大,价格低,但需要ECC和坏块管理 | 嵌入式Linux、大容量存储 |
| SD卡/eMMC启动 | SD卡或eMMC | 方便调试和量产,速度中等 | 开发板、消费电子 |
| UART/USB启动 | 串口或USB | 用于工厂烧录或紧急恢复 | 空片烧录、变砖恢复 |
芯片怎么知道用哪种模式?靠的是几个专用引脚(比如BOOT0、BOOT1)。芯片复位时,Boot ROM会去采样这些引脚的电平,然后决定从哪个设备读取数据。
举个例子,STM32的BOOT0拉高、BOOT1拉低,就从系统存储器(内置Bootloader)启动。两个都拉低,就从用户Flash启动。这个配置在硬件设计阶段就要定好,PCB打样回来再改就麻烦了。
避坑指南:我曾经吃过一次亏——某款芯片的启动模式引脚内部有弱上拉,但我外部接了下拉电阻,结果电平不确定,芯片每次复位后启动设备都不一样。后来查手册才发现,芯片采样是在复位释放后的第一个时钟周期,外部电容还没充放电完成。解决方案是加一个RC延时,或者直接用GPIO控制启动选择。
2.4 启动流程的完整时间线
咱们把整个过程串起来,画一条时间线:
- 复位:CPU所有寄存器复位,PC指向Boot ROM入口
- Boot ROM执行:初始化基本硬件,采样启动模式引脚
- 加载Bootloader:从Nor Flash/NAND/SD卡读取Bootloader到SRAM
- 跳转到Bootloader:CPU开始执行Bootloader代码
- Bootloader初始化:设置DDR、时钟、存储驱动
- 加载操作系统:从存储设备读取OS镜像到DDR
- 跳转到OS入口:CPU开始执行操作系统,最终调用main函数
整个过程,从复位到main,快的几十毫秒,慢的几秒。如果你做的是汽车电子或工业控制,启动时间是个硬指标。我记得有个项目要求冷启动必须在500ms内完成,我们不得不把Bootloader的代码精简到极致,连printf都去掉了。
2.5 一个简单的启动代码示例
下面是一个极简的Boot ROM伪代码,帮你理解它的逻辑:
// 伪代码:Boot ROM 启动流程
void boot_rom_main(void) {
// 1. 设置栈指针
set_stack_pointer(INTERNAL_SRAM_TOP);
// 2. 初始化基本时钟
pll_init();
clock_gate_enable(ALL);
// 3. 读取启动模式
uint32_t boot_mode = read_boot_pins();
// 4. 根据模式选择加载设备
switch(boot_mode) {
case BOOT_FROM_NOR:
nor_flash_init();
nor_flash_read(BOOTLOADER_ADDR, SRAM_ADDR, BOOTLOADER_SIZE);
break;
case BOOT_FROM_NAND:
nand_init();
nand_read_with_ecc(BOOTLOADER_ADDR, SRAM_ADDR, BOOTLOADER_SIZE);
break;
case BOOT_FROM_SD:
sd_init();
sd_read_blocks(BOOTLOADER_SECTOR, SRAM_ADDR, BOOTLOADER_SECTORS);
break;
case BOOT_FROM_UART:
uart_init(115200);
uart_read_bytes(SRAM_ADDR, BOOTLOADER_SIZE);
break;
}
// 5. 验证数据(可选)
if (crc_check(SRAM_ADDR, BOOTLOADER_SIZE) != EXPECTED_CRC) {
enter_error_loop(); // 死循环,等待看门狗复位
}
// 6. 跳转到Bootloader
jump_to(SRAM_ADDR);
}
你看,Boot ROM的逻辑其实不复杂。但它的每一行代码都经过芯片厂商的反复验证,因为一旦芯片流片回来,这段代码就改不了了。
2.6 小结
这一章咱们把启动流程的骨架搭起来了。你记住三句话就行:
- Boot ROM是芯片自带的,负责把Bootloader搬进内存
- Bootloader是你自己写的,负责把操作系统搬进内存
- 启动模式由硬件引脚决定,设计阶段就要想清楚
下一章,咱们会深入Boot ROM的内部,看看它到底是怎么跟Flash打交道的。尤其是NAND Flash的坏块管理和ECC校验,这里面的坑可不少。到时候我会分享一个我当年调NAND驱动时遇到的「幽灵坏块」问题,保证让你印象深刻。