3. 启动流程详解:BootROM、SPL、U-Boot、ATF、设备树、Linux内核启动
好,咱们今天聊点硬核的。一个SoC从按下电源键到Linux跑起来,中间到底经历了什么?很多人觉得不就是个“上电-加载内核”嘛,其实没那么简单。我当年刚接触ARM SoC时,也被这一连串的启动阶段搞得晕头转向。今天我就把这六个关键环节掰开揉碎了讲给你听。
3.1 BootROM:芯片的第一口奶
芯片上电后,CPU从复位向量开始执行。这时候内存还没初始化,代码只能放在芯片内部的一块只读存储器里——这就是BootROM。它通常只有几十KB,固化在芯片内部,用户改不了。
BootROM的任务很简单:初始化最基本的硬件(比如时钟、看门狗),然后根据启动引脚的电平状态,决定从哪儿加载下一级代码。常见的启动介质有:NAND Flash、eMMC、SD卡、SPI NOR Flash,甚至USB或UART。
关键点:BootROM里通常包含一个“下载模式”,比如通过USB或串口直接下载代码到SRAM中执行。这在开发阶段非常有用,我经常用它来烧写最初的SPL。
嗯,这里要注意:BootROM的代码质量直接决定了芯片能否正常启动。我在一个项目中遇到过,某款芯片的BootROM在读取eMMC时有个时序bug,导致10%的板子启动失败。最后只能通过修改PCB走线来规避,那叫一个折腾。
3.2 SPL:瘦身版的引导加载器
BootROM加载完SPL(Secondary Program Loader)后,就把控制权交给它了。SPL是U-Boot的精简版,体积通常控制在几十KB以内,因为BootROM能加载的大小有限。
SPL的主要工作:
- 初始化DDR内存控制器
- 初始化时钟和PLL
- 从存储介质加载U-Boot到DDR中
- 跳转到U-Boot执行
说白了,SPL就是个“搬运工”。它自己运行在芯片内部的SRAM里,等DDR初始化好了,就把U-Boot从Flash搬到DDR里,然后跳过去。
个人经验:调试SPL时,我习惯在关键位置加GPIO翻转,用示波器看波形来判断执行到哪里了。串口打印在SPL阶段往往不可靠,因为时钟和波特率可能还没配对。
3.3 U-Boot:真正的引导指挥官
U-Boot加载到DDR后,才算真正进入“操作系统”的世界。它提供了完整的命令行交互、网络协议栈、文件系统支持,还能加载设备树和内核镜像。
U-Boot的启动流程大致如下:
- 初始化串口、网络、存储等外设
- 读取环境变量(比如bootargs、bootcmd)
- 根据bootcmd执行启动命令
- 加载设备树(.dtb)和内核镜像(zImage/Image)到内存
- 跳转到内核入口
你想想看,U-Boot就像一个“迷你操作系统”,它有自己的驱动框架、内存管理、命令行解析器。我见过有人把U-Boot玩出花来——用它做固件升级、网络启动、甚至跑简单的测试程序。
避坑指南:我曾经在U-Boot的环境变量里写错了bootargs,导致内核启动时找不到根文件系统。排查了整整两天才发现是root=参数拼错了。所以,每次修改环境变量后,记得用saveenv保存,然后用printenv确认一遍。
3.4 ATF:安全世界的守护者
ATF(ARM Trusted Firmware)是ARM架构中负责安全启动和异常级别切换的固件。它运行在EL3(异常级别3),是系统中权限最高的软件。
ATF的主要职责:
- 初始化安全世界(Secure World)的硬件
- 提供安全监控调用(SMC)接口
- 管理电源状态(比如CPU的开启和关闭)
- 在U-Boot和内核之间传递控制权
为什么需要ATF?因为U-Boot运行在EL2或EL1,它没有权限配置系统级的电源管理或安全相关的寄存器。ATF作为EL3的固件,充当了“中间人”的角色。
关键点:ATF的启动顺序通常是:BootROM -> SPL -> ATF(BL31) -> U-Boot(BL33) -> Linux内核。ATF在U-Boot之前启动,初始化安全环境,然后才把控制权交给U-Boot。
我记得第一次接触ATF时,被它的多级镜像搞懵了:BL1、BL2、BL31、BL32、BL33……每个都有不同的作用。后来我画了个流程图才理清楚。简单说,BL1是BootROM的一部分,BL2是平台初始化代码,BL31是运行时固件,BL33就是U-Boot。
3.5 设备树:硬件描述的“身份证”
设备树(Device Tree)是一种描述硬件信息的数据结构。它用文本文件(.dts)描述CPU、内存、外设、中断、时钟等硬件资源,然后编译成二进制文件(.dtb)供内核使用。
为什么需要设备树?因为ARM生态太碎片化了。以前每个平台都要修改内核源码来适配硬件,有了设备树后,内核和硬件配置就解耦了。同一份内核镜像,搭配不同的设备树,就能跑在不同的板子上。
一个简单的设备树节点示例:
/ {
model = "My ARM Board";
compatible = "mycompany,myboard";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
uart0: serial@1c090000 {
compatible = "ns16550";
reg = <0x1c090000 0x1000>;
interrupts = <0 37 4>;
clock-frequency = <24000000>;
};
};
个人习惯:我写设备树时,喜欢先用一个“骨架”文件包含所有可能的节点,然后根据实际硬件裁剪。这样既不会遗漏,也方便后续扩展。另外,编译设备树时记得用dtc工具加上-I dts -O dtb参数。
3.6 Linux内核启动:最后的冲刺
U-Boot把设备树地址和内核镜像地址准备好后,通过一条指令跳转到内核入口。内核启动过程大致如下:
- 解压内核(如果是压缩镜像)
- 设置页表,开启MMU
- 解析设备树,初始化各个子系统
- 挂载根文件系统
- 启动init进程(通常是systemd或busybox)
这里有个细节:内核启动时,会从设备树中读取“chosen”节点,里面包含了bootargs和initrd的地址。所以U-Boot在加载设备树时,必须确保这个节点是完整的。
避坑指南:我曾经遇到一个奇怪的问题——内核启动到一半就卡住了,串口没有任何输出。后来发现是设备树中的内存大小(reg属性)写错了,内核访问了不存在的物理地址。所以,设备树里的内存配置一定要和硬件完全一致。
嗯,到这里,整个启动流程就走完了。从BootROM到Linux内核,每一步都有它的使命。你想想看,一个简单的“开机”动作,背后是这么多层软件的接力协作。下次你的板子起不来时,不妨按这个流程一步步排查,问题往往就出在某个环节的交接处。
总结一下:BootROM是硬件的“第一口奶”,SPL是瘦身搬运工,U-Boot是真正的指挥官,ATF是安全守护者,设备树是硬件身份证,内核是最终的执行者。这六个环节环环相扣,缺一不可。