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的启动流程大致如下:

  1. 初始化串口、网络、存储等外设
  2. 读取环境变量(比如bootargs、bootcmd)
  3. 根据bootcmd执行启动命令
  4. 加载设备树(.dtb)和内核镜像(zImage/Image)到内存
  5. 跳转到内核入口

你想想看,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把设备树地址和内核镜像地址准备好后,通过一条指令跳转到内核入口。内核启动过程大致如下:

  1. 解压内核(如果是压缩镜像)
  2. 设置页表,开启MMU
  3. 解析设备树,初始化各个子系统
  4. 挂载根文件系统
  5. 启动init进程(通常是systemd或busybox)

这里有个细节:内核启动时,会从设备树中读取“chosen”节点,里面包含了bootargs和initrd的地址。所以U-Boot在加载设备树时,必须确保这个节点是完整的。

避坑指南:我曾经遇到一个奇怪的问题——内核启动到一半就卡住了,串口没有任何输出。后来发现是设备树中的内存大小(reg属性)写错了,内核访问了不存在的物理地址。所以,设备树里的内存配置一定要和硬件完全一致。

嗯,到这里,整个启动流程就走完了。从BootROM到Linux内核,每一步都有它的使命。你想想看,一个简单的“开机”动作,背后是这么多层软件的接力协作。下次你的板子起不来时,不妨按这个流程一步步排查,问题往往就出在某个环节的交接处。

总结一下:BootROM是硬件的“第一口奶”,SPL是瘦身搬运工,U-Boot是真正的指挥官,ATF是安全守护者,设备树是硬件身份证,内核是最终的执行者。这六个环节环环相扣,缺一不可。