1. i.MX启动链全景

在i.MX系列处理器上,上电后的第一行代码并不来自你烧写的U-Boot,而是固化在芯片内部ROM里的一段只读程序。理解整条启动链的关键,不是记住每个阶段的名字,而是搞清楚每一阶段运行在什么存储介质上、由谁加载、把控制权交给谁、以及交接时留下了什么约定。本章先建立一张全景图,后续章节再逐段展开。

一、为什么i.MX需要多级启动

i.MX的启动链之所以分成多级,根源在于两个约束。第一,片内ROM容量极小(通常几十KB量级),只能实现最基础的硬件初始化和镜像搬运,无法容纳完整的引导逻辑。第二,启动介质种类繁多——eMMC、SD、QSPI NOR、NAND、串行下载(Serial Download)等,ROM Code必须用一套统一机制去适配它们。于是NXP采用"ROM Code负责最小引导,后续阶段逐级放大能力"的设计:ROM Code只做最必要的DDR初始化参数读取和镜像加载,把更大的灵活性交给SPL、U-Boot proper乃至操作系统。

这条链上存在一个容易被忽视的事实:每一级都是被上一级"加载"进内存后才执行的。ROM Code从启动介质读取SPL到片内RAM(OCRAM/TCM),SPL再把U-Boot proper搬到已初始化的DDR中运行。因此,任何一级的镜像格式、加载地址、校验方式出错,都会导致链在那一级断裂,而现象往往只是"没有任何输出"。

二、四个阶段的职责与交接边界

i.MX典型的启动链可划分为四个阶段,各自的运行位置和职责边界如下。

  • ROM Code(片内ROM):上电复位后首先执行。它根据eFUSE中的BOOT_CFG和GPIO引脚状态确定启动设备,初始化该设备控制器,读取镜像头部(IVT + Boot Data),校验DCD和镜像,然后按IVT指定的地址加载SPL并跳转。它不初始化DDR(除非DCD中包含DDR初始化命令)。
  • SPL / MLO(片内RAM):运行在OCRAM中,空间受限(通常约128KB可用)。核心任务是初始化DDR控制器、配置时钟,然后把U-Boot proper从启动介质读到DDR并跳转。它还会打印极简的调试信息(如"U-Boot SPL 2023.04")。
  • U-Boot proper(DDR):功能完整的引导程序,提供命令行、环境变量、设备驱动、网络、文件系统等。它最终加载内核镜像(Image/zImage)和设备树(DTB),跳转到内核入口。
  • 内核与根文件系统:严格说已不属于U-Boot启动链,但它是链的终点,决定了前面所有阶段是否"成功"。

阶段之间的交接边界可以用三个要素描述:交接时的运行地址、交接时已初始化的硬件、以及交接时传递的参数。ROM Code交给SPL时,DDR通常未初始化,参数极少;SPL交给U-Boot proper时,DDR已可用,且SPL会把启动设备信息、内存布局等通过全局数据结构(gd)或寄存器传递;U-Boot交给内核时,则通过bootargs、DTB和入口寄存器(如x0/r0/r2)传递。

i.MX 启动链四阶段全景 ① ROM Code 片内 ROM · 只读 读 BOOT_CFG / eFUSE 解析 IVT + DCD 加载 SPL → OCRAM ② SPL / MLO 片内 RAM · ~128KB 初始化 DDR 控制器 配置时钟 / PMIC 加载 U-Boot → DDR ③ U-Boot proper DDR · 完整功能 命令行 / 环境变量 设备驱动 / 网络 加载内核 + DTB ④ Kernel DDR · 操作系统 挂载根文件系统 启动 init 进程 交接 交接 交接 交接边界三要素 ROM → SPL 运行地址:OCRAM DDR:未初始化 SPL → U-Boot 运行地址:DDR DDR:已初始化 U-Boot → Kernel 参数:bootargs + DTB 入口:x0/r0/r2 启动介质与镜像载体 eFUSE / GPIO BOOT_CFG 选择 SD / eMMC 偏移 0x400 起 QSPI NOR 3B / 4B 地址 NAND / 串行下载 USB SDP 模式 介质选择

三、镜像格式:IVT与Boot Data

ROM Code并不认识"U-Boot"这个概念,它只认识一种固定格式的镜像头。在i.MX上,这个头由IVT(Image Vector Table)和Boot Data结构组成,通常位于镜像起始偏移处。以SD/eMMC启动为例,ROM Code会从设备偏移0x400(即1KB处)开始读取,因为前512字节留给MBR/分区表,后512字节留给其他用途。

IVT中几个关键字段决定了后续一切:entry指向SPL入口地址,boot_data指向Boot Data结构,dcd指向DCD(Device Configuration Data)——一段由ROM Code解释执行的寄存器配置脚本,常用于提前初始化DDR或配置引脚。Boot Data则记录镜像在内存中的加载地址和总长度。ROM Code读取这些字段后,把镜像搬运到load_addr,然后跳转到entry。

下面是一个典型的IVT布局示意(以i.MX6/7为例,地址为示例值):

偏移      字段          示例值        说明
0x00     header        0x402000D1    IVT 魔数 + 长度
0x04     entry         0x00910000    SPL 入口地址
0x08     reserved      0x00000000
0x0C     dcd           0x00910000    DCD 指针(可空)
0x10     boot_data     0x00910020    Boot Data 指针
0x14     self          0x00910000    IVT 自身地址
0x18     csf           0x00000000    HAB 签名指针
0x1C     reserved      0x00000000
0x20     start         0x00910000    镜像加载起始
0x24     length        0x00008000    镜像总长度
0x28     plugin        0x00000000    插件标志

注意entry和self的区别:self是IVT结构自身的物理地址,ROM Code用它来校验IVT是否被正确加载;entry是跳转目标。两者在简单场景下相同,但在需要重定位IVT时可能不同。

四、SPL到U-Boot proper的交接

SPL运行在片内RAM中,空间极其有限,因此它只做三件事:初始化DDR、把U-Boot proper从启动介质读到DDR、跳转。这里的关键参数是CONFIG_SPL_TEXT_BASE(SPL链接地址,必须落在OCRAM范围内)和CONFIG_SYS_TEXT_BASE(U-Boot proper链接地址,必须落在DDR范围内)。如果这两个地址配错,典型现象是SPL打印完版本号后卡死或复位。

SPL加载U-Boot proper时,会读取一个名为u-boot.img(或u-boot-dtb.img)的镜像。这个镜像同样带有头部(legacy image或FIT image),SPL根据头部中的加载地址和长度完成搬运。搬运完成后,SPL通过函数指针跳转到U-Boot proper的入口,同时把启动设备编号、内存大小等信息通过gd结构传递过去。

判断SPL是否成功交接的一个实用方法:在U-Boot proper的board_init_f早期加入串口打印。如果能看到该打印,说明SPL已完成DDR初始化和镜像搬运;如果只看到SPL版本号而无后续输出,问题通常出在DDR初始化参数或U-Boot镜像加载地址上。

五、U-Boot proper到内核的交接

U-Boot proper运行在DDR中,功能完整。它最终通过bootm、booti或bootz命令加载内核。以ARM64的booti为例,交接时:x0指向DTB物理地址,x1保留,x2保留,x3保留;内核入口地址由镜像头指定。同时,U-Boot把bootargs环境变量写入DTB的/chosen/bootargs节点,内核启动后从中读取命令行参数。

这一级的交接边界最"宽":U-Boot已经把串口、MMC、网络、USB等驱动全部初始化完毕,内核可以直接复用部分硬件状态(尽管内核通常会重新初始化)。因此,如果内核启动失败但U-Boot命令行可用,问题通常不在启动链的前三级,而在内核镜像格式、DTB匹配或bootargs配置上。

六、全景视角下的排障思路

把四个阶段和三个交接边界放在一起看,排障就有了清晰的定位逻辑:

  • 完全无输出:ROM Code阶段失败。检查BOOT_CFG引脚、eFUSE设置、启动介质是否可读、IVT魔数是否正确。
  • 有SPL版本号但无后续:SPL→U-Boot交接失败。检查DDR初始化参数(DCD或SPL中的DDR配置)、U-Boot镜像加载地址、镜像是否完整。
  • 有U-Boot命令行但内核不启动:U-Boot→内核交接失败。检查内核镜像格式(Image vs zImage)、DTB是否匹配、bootargs中的root=和console=是否正确。
  • 内核启动后挂载根文件系统失败:已超出U-Boot启动链范围,检查rootfs介质和内核驱动。