第二章 嵌入式系统启动流程:从复位向量到Bootloader
各位同学,咱们今天聊点硬核的。嵌入式系统上电后到底发生了什么?说白了,就是一颗芯片从「死」到「活」的过程。我做了这么多年嵌入式,每次看到系统启动成功,心里还是会有种莫名的踏实感。
2.1 复位向量:一切从这里开始
芯片上电后,CPU会先干一件事——去一个固定的地址取指令。这个地址就叫复位向量。你想想看,CPU刚醒来,啥也不知道,总得有人告诉它「第一步该干啥」吧?
不同的架构,复位向量的位置不一样:
| 架构 | 复位向量地址 | 说明 |
|---|---|---|
| ARM Cortex-M | 0x00000000 | 向量表起始地址,包含栈指针和复位处理函数地址 |
| ARM Cortex-A | 0x00000000 或 0xFFFF0000 | 由硬件配置决定,通常从0x00000000开始 |
| x86 | 0xFFFFFFF0 | 靠近4GB地址空间顶部,跳转到BIOS |
| RISC-V | 0x80000000 或 0x00000000 | 取决于具体实现,常见于0x80000000 |
我在项目中遇到过一件挺坑的事。有一次用了一款国产MCU,手册上写复位向量在0x00000000,结果板子死活起不来。后来拿示波器一量,发现芯片内部默认映射到了0x20000000。嗯,从此以后我拿到新芯片,第一件事就是拿逻辑分析仪确认复位向量。
2.2 ROM Code:芯片出厂自带的「小管家」
很多现代SoC内部都有一小块ROM,出厂时写死了代码。这段代码叫ROM Code,也叫Boot ROM。它的任务很简单:初始化最基本的硬件,然后去找启动介质。
ROM Code一般会做这几件事:
- 初始化CPU时钟和PLL
- 配置基本的存储控制器(比如DDR控制器)
- 检测启动模式引脚的电平
- 根据启动模式去读取外部存储设备
- 把读取到的代码加载到SRAM或DDR中
- 跳转到加载的代码执行
说白了,ROM Code就是个「引路人」。它自己不会跑操作系统,但它知道怎么把操作系统加载进来。我个人习惯把ROM Code理解成「芯片的BIOS」,虽然不完全准确,但初学者这么想比较容易上手。
2.3 启动模式:芯片怎么知道自己该从哪启动?
芯片上电后,ROM Code会去读几个GPIO引脚的电平。这些引脚叫启动模式选择引脚(Boot Mode Pins)。不同的电平组合,对应不同的启动介质。
常见的启动模式有:
- NAND Flash启动:从NAND Flash读取前几KB的数据到内部SRAM
- eMMC启动:从eMMC设备的特定分区读取数据
- SD卡启动:从SD卡读取MBR或特定扇区
- UART/USB下载:等待主机通过串口或USB发送数据
- SPI NOR Flash启动:从SPI接口的NOR Flash读取数据
我记得有一次调试一块开发板,怎么都启动不了。查了半天,发现是启动模式引脚虚焊了。ROM Code读到的电平是浮空的,结果它以为要从一个不存在的设备启动。你想想看,这种问题查起来多折腾人。
核心要点:启动模式引脚的电平必须在芯片复位前稳定下来。很多芯片要求这些引脚在复位信号释放前至少保持几十微秒的稳定状态。设计PCB时,这些引脚上最好加个10kΩ的上拉或下拉电阻。
2.4 启动介质选择:NAND、eMMC、SD卡
选哪种存储介质来放Bootloader和内核,这问题我当年纠结了很久。每种介质都有自己的脾气。
NAND Flash
NAND Flash便宜,容量大,但有个毛病——天生带坏块。你买回来的NAND Flash,出厂时就可能有几个坏块。而且用着用着,坏块还会增多。
NAND Flash启动有个特点:ROM Code只能读取前几KB(通常是4KB或8KB)。这几KB里放的是一级Bootloader(SPL或MLO)。这个一级Bootloader要足够小,小到能塞进芯片内部的SRAM里。
一级Bootloader的任务是:初始化DDR控制器,然后把完整的Bootloader从NAND Flash的后面区域加载到DDR中。
eMMC
eMMC本质上就是NAND Flash加了一个控制器。这个控制器帮你管理坏块、做磨损均衡。对软件来说,eMMC就像一个普通的块设备,读写起来比NAND Flash省心多了。
eMMC启动时,ROM Code会去读eMMC的Boot分区(Boot Area Partition)。这个分区是专门为启动设计的,容量不大,但访问速度快。
我个人的建议是:如果成本允许,优先选eMMC。省下来的调试时间,比省的那几块钱值多了。
SD卡
SD卡启动在开发阶段特别方便。你想想看,编译完内核,往SD卡里一拷,插上板子就能跑。不用烧写,不用拆芯片。
SD卡启动的流程一般是:
- ROM Code读取SD卡的MBR(主引导记录)
- 从MBR中找到活动分区
- 读取活动分区的第一个扇区
- 执行这个扇区里的代码
不过SD卡有个问题——可靠性不如eMMC。插拔多了,金手指容易氧化。量产产品里很少用SD卡做启动介质,除非是树莓派那种定位的产品。
选型建议:
- 成本敏感、容量要求高 → NAND Flash + 坏块管理
- 可靠性优先、开发周期短 → eMMC
- 开发调试阶段、原型验证 → SD卡
2.5 从复位到Bootloader的完整流程
好了,我们把前面讲的串起来,看看完整的启动流程:
- 上电复位:芯片上电,复位信号释放
- CPU取复位向量:从固定地址读取复位向量,跳转到ROM Code入口
- ROM Code执行:初始化基本硬件,读取启动模式引脚
- 加载一级Bootloader:根据启动模式,从对应介质读取前几KB代码到SRAM
- 执行一级Bootloader:初始化DDR,加载完整的Bootloader到DDR
- 执行完整Bootloader:初始化更多硬件,加载内核到内存
- 跳转到内核:Bootloader把控制权交给内核
这个过程,说白了就是「小代码加载大代码」的接力赛。ROM Code是最小的,只能加载几KB。一级Bootloader大一点,能加载几百KB。完整的Bootloader更大,能加载几MB的内核。
嗯,这里要注意一点:每一步的代码都不能超过上一步能加载的大小限制。我见过有人把一级Bootloader编译出来200KB,结果芯片内部SRAM只有64KB。这种问题查起来特别郁闷,因为编译不报错,烧写也不报错,就是跑不起来。
好了,这一章的内容就到这。下一章我们会深入Bootloader的内部结构,看看U-Boot到底是怎么工作的。到时候我会拿一个实际项目里的U-Boot代码来拆解,保证让你看得明明白白。