简介:面向嵌入式开发初学者与底层系统工程师的芯片启动流程学习文档,聚焦从上电复位到操作系统内核加载的完整链路,帮助读者理清 ROM Code、SPL、U-Boot 与 Kernel 四个阶段的职责划分与衔接关系。文档围绕 SD 卡启动场景展开,说明 ROM Code 如何完成 CPU 与系统初始化、判断启动方式并将内容读入 SRAM,SPL 如何先搬运前 70KB 程序再加载剩余内容至 DDR,以及 U-Boot 与 Kernel 在内存中的执行过程,同时涉及 UART、USB 下载模式与 ptset 测试模式等分支。资源包共 1 个 docx 文件,约 1.41MB,内容以流程梳理与阶段说明为主,适合对照理解启动各环节的依赖关系。已有 923 人学习,可作为梳理芯片 boot 机制、排查启动问题的参考材料。
1. 芯片启动链路:为什么上电后第一行代码不在你的固件里
按下电源键的那一瞬间,CPU 拿到的其实是一片空白——DDR 没初始化,时钟没配好,连栈指针都是垃圾值。可它偏偏能一路跑到操作系统,中间没有任何人插手。这条链路是 ROM code → SPL → u-boot → kernel,四个阶段接力,每一棒都只负责把下一棒搬进内存并交权。反直觉的地方在于:真正决定“能不能启动”的往往不是你的 u-boot 或 kernel,而是那段你永远改不了、烧在芯片里的 ROM code。它规定了从哪儿读、读多少、读到哪块 SRAM,后面所有阶段都得迁就它的约束。搞懂这条链路的底层机制,你才能在板子起不来的时候判断是搬运失败、内存没起来,还是交接参数错了,而不是对着串口一片空白干瞪眼。
2. ROM code:芯片里那段改不了的代码到底做了什么
ROM code 是芯片出厂时固化在片上 ROM 里的第一段指令,地址映射固定,内容不可修改。上电复位后,CPU 的 PC 被硬件强制指向这段代码的入口,此时它运行在最高特权级(常见实现是 reset 异常向量进入 handler 模式),手里没有任何可用的外部内存。它的全部工作可以概括成三件事:把 CPU 和最小系统拉起来、决定从哪个介质启动、把下一阶段搬进片内 SRAM。
2.1 复位向量与 CPU init:为什么第一步永远是配时钟和栈
复位那一刻,CPU 内部寄存器大多是未定义状态,PLL 还没锁定,总线时钟可能跑在最低频。ROM code 的第一段汇编通常做这几件事:屏蔽中断、设置临时栈指针(栈一般直接放在片内 SRAM 的高地址端)、配置 PLL 和分频把核心频率拉到可工作的档位、初始化看门狗(很多芯片在这里先喂狗或直接关狗,否则后面搬运稍慢就被复位)。这一步没有任何“业务逻辑”,全是让 CPU 具备执行 C 代码的条件。常见的实现思路是:汇编完成最小初始化后跳到 C 入口,后面的介质探测、搬运、校验都用 C 写,可维护性更好。
这里有个容易被忽略的细节:ROM code 阶段可用的 RAM 只有片内 SRAM(原文里叫 aon ram,即 always-on 域的小容量 SRAM),容量通常在几十 KB 到一两百 KB 量级。这个容量上限直接决定了后面 SPL 为什么必须“小”,也是整条链路设计的起点。
2.2 启动介质判定:SD、eMMC、UART、USB 是怎么选的
CPU 和系统初始化完成后,ROM code 进入启动介质选择。原文提到它先完成键盘、UART 等模块的初始化配置,然后判断用哪种方式 boot。工程上的常见做法是读一组启动选择引脚(strap pin)或熔丝位,得到一个 boot device 编码,再按优先级依次尝试:先试 SD/eMMC,失败则回落到 UART/USB 下载模式。这个“回落”机制是产线和救砖的关键——板子焊好后固件是空的,就是靠 UART/USB 下载模式把第一份 bootloader 灌进去。
以 SD 卡启动为例,ROM code 要做的动作链条是:配置 SD 控制器(时钟、总线宽度、电压)、发 CMD0/CMD8/ACMD41 完成卡初始化、读分区表或固定偏移、把 bootloader 的前若干 KB 读进 SRAM。原文说“读取 sd 卡的内容到 aon ram 中进行运行”,指的就是这一步。注意此时还没有 DDR,所有数据只能落在片内 SRAM,所以 ROM code 一次能搬的量被 SRAM 容量死死卡住。
2.3 搬运与交权:ROM code 怎么把控制权交给 SPL
搬运完成后,ROM code 通常还会做一次校验(checksum 或简单头部 magic 比对),确认搬进来的内容不是空的或损坏的,然后跳到 SPL 入口地址。交权时一般会通过寄存器传参,比如告诉下一阶段“我是从 SD 卡启动的”“boot 介质编号是多少”,SPL 和 u-boot 后续靠这些参数决定去哪里找 kernel。这段交接协议没有统一标准,各家芯片自定义,这也是换芯片平台时 bootloader 移植工作量大的根源之一。
提示:ROM code 的行为完全由芯片决定,调试时不要试图“改 ROM code”,而应通过启动引脚、熔丝位和串口打印反推它走到了哪一步。
3. SPL:为什么必须先搬 70KB 而不是一次搬完
SPL 存在的唯一理由,是 ROM code 能搬进 SRAM 的东西太小,而完整的 bootloader(u-boot)动辄几百 KB,塞不进去。原文写得很直白:如果 SD 卡里的 bootloader 比较大,无法一次性加载到片内 aon 的 sram,所以先搬运 SD 卡前 70KB 的程序过来运行,配置完毕后再搬运剩下的内容到 DDR。这 70KB 不是随便定的数字,它是“SRAM 可用容量减去栈和运行时开销”之后能安全容纳的上限,具体值因芯片而异。
3.1 SPL 的核心任务:把 DDR 点起来
SPL 运行在 SRAM 里,代码量被严格压缩,通常关掉大部分驱动,只保留串口(用于打印)和存储控制器。它最关键的工作是 DDR 初始化:配置 DDR 控制器的时序参数、做读写训练(training)、确认内存颗粒能稳定读写。DDR 没起来之前,大块内存不存在,kernel 无处安放;DDR 起来之后,SPL 才有能力把 u-boot 主体从 SD/eMMC 搬进 DDR。
下面是一段示意性的 SPL 主流程,帮助理解它的执行顺序(参数和函数名为示意,非某具体芯片源码):
/* SPL 入口:运行在片内 SRAM,栈也在 SRAM */ void spl_main(void) { board_init_f(); /* 时钟、串口、引脚复用等最小初始化 */ sd_ctrl_init(); /* 存储控制器初始化,准备读卡 */ ddr_init(); /* 关键一步:DDR 时序配置与训练 */ if (ddr_train_failed()) /* 训练失败则无法继续,通常直接复位或报错 */ panic("ddr init failed"); /* 从存储介质读取 u-boot 主体到 DDR 的链接地址 */ load_image_from_sd(UBOOT_LOAD_ADDR, UBOOT_MAX_SIZE); jump_to(UBOOT_LOAD_ADDR); /* 交权给 u-boot */ }这段代码里每个调用都对应一个硬约束:board_init_f必须在读卡前完成,否则 SD 控制器没时钟;ddr_init必须在搬运 u-boot 前完成,因为目标地址在 DDR;jump_to之前要保证 cache 和 MMU 状态符合 u-boot 的预期,否则跳过去第一条指令就翻车。原文说的“运行配置完毕后再搬运剩下的 sd 内容到 ddr”,对应的就是load_image_from_sd这一步。
3.2 70KB 边界怎么用:头部信息与分段加载
既然 SPL 只能占 SRAM 的一小块,工程上常见的做法是给 bootloader 镜像加一个头部,记录“总长度、加载地址、校验和”。SPL 先读头部,判断主体有多大,再决定分几次搬。原文的“前 70KB”可以理解为:ROM code 搬进 SRAM 的那部分本身就包含了 SPL 的完整可执行体,SPL 跑起来后再按头部信息把剩余部分搬到 DDR。这个分段设计让“小 SRAM 引导大固件”成为可能,代价是镜像格式和加载逻辑必须严格对齐,头部字段错一个字节,表现就是搬运到一半跑飞。
3.3 SPL 与 u-boot 的职责切分
很多人疑惑:既然 u-boot 也能初始化 DDR,为什么还要 SPL?原因是 u-boot 的代码体积和运行前提。u-boot 的链接地址通常在 DDR 里,它假设自己运行时 DDR 已经可用;如果让 ROM code 直接把 u-boot 搬进 SRAM,体积放不下。SPL 就是那个“体积够小、能进 SRAM、又能把 DDR 点起来”的中间层。职责切分的边界很清楚:SPL 负责“让大内存可用”,u-boot 负责“让系统可交互、可加载内核”。
4. u-boot 与 kernel:DDR 里的接力与参数传递
进入 u-boot 时,CPU 已经跑在 DDR 里,外设初始化可以放开手脚做。u-boot 阶段的任务从“活下去”变成“准备好环境并把内核拉起来”:初始化更多外设(网络、存储、显示)、提供命令行交互、按启动顺序尝试从各介质加载 kernel、准备设备树和启动参数,最后跳转到 kernel 入口。
4.1 从存储介质到 DDR:kernel 镜像是怎么被找到的
u-boot 加载 kernel 的常见路径是:读环境变量或启动脚本,确定 boot 介质和分区,从文件系统(ext4/FAT)或裸分区偏移读出 kernel 镜像(常见为压缩的 Image 或 zImage)和设备树(dtb),解压到 DDR 的约定地址。原文说“uboot/kernel 过程主要执行 ddr 内存里面的程序”,点明了这一阶段的运行位置。这里的关键机制是地址约定:kernel 的加载地址、dtb 的存放地址、启动参数的传递地址必须在 u-boot 配置和 kernel 配置之间对齐,错开就会表现为“内核解压后无输出”或“启动到一半挂死”。
4.2 启动参数与设备树:交接时到底传了什么
u-boot 跳转 kernel 前,通常通过寄存器传三个东西:机器码或设备树地址、启动参数区(bootargs)地址、以及可选的 initrd 地址。设备树描述硬件拓扑(内存大小、外设基地址、中断号),kernel 靠它知道自己在什么板子上跑。启动参数里则是根文件系统位置、控制台串口、内存预留等运行时配置。这套交接协议是 boot 链路里最容易出问题的一环:dtb 和 kernel 版本不匹配、bootargs 里根分区写错、内存节点大小和实际 DDR 不符,都会让系统在 kernel 早期就静默失败。
4.3 多启动模式的工程意义:UART/USB 下载与 SD/eMMC 启动
原文强调这套 boot 过程包含 USB/UART 下载模式、SD/eMMC boot 模式以及 ptset 测试模式,复杂度正来自这里。下载模式的价值在产线和研发:板子第一次上电、存储介质还是空的,就靠 UART/USB 把 bootloader 灌进去;测试模式(ptset)则是在不进正常启动流程的前提下,让 ROM code 停在某个测试状态,供工厂做硬件自检。多种模式共存意味着 ROM code 里有一张优先级表,工程上要保证“正常启动介质存在时不会被下载模式抢走”,否则会出现插着调试线就起不来的玄学现象。
5. 避坑与排查:启动链路起不来时先看哪一层
5.1 串口完全无输出:先怀疑 ROM code 没走到交权
现象:上电后串口一个字符都没有。原因通常不在 u-boot,而在更早:启动引脚电平不对导致 ROM code 选了下载模式、SD 卡分区或偏移不对导致读不到有效头部、或者 SRAM 里的 SPL 校验失败被 ROM code 放弃。解决顺序是先用示波器或万用表确认 strap 引脚,再用厂商提供的下载工具尝试 UART/USB 模式,能下载说明 ROM code 活着,问题在介质或镜像格式。
5.2 SPL 有打印但卡在 DDR 初始化
现象:串口打出 SPL 版本号后停住,或反复复位。原因是 DDR 时序参数与颗粒不匹配、训练失败。血泪经验是:换过内存颗粒的板子必须重新生成 DDR 配置,不能沿用旧参数。解决方法是打开 SPL 的 DDR 训练日志,逐项比对读写窗口,必要时降频验证。
5.3 u-boot 起来了但 kernel 无输出
现象:u-boot 命令行正常,boot之后串口安静。原因多为加载地址或设备树不匹配:kernel 被解压到错误地址、dtb 里的内存节点小于实际容量、bootargs 控制台参数写错。排查时先在 u-boot 里用md查看目标地址内容是否为有效镜像头,再确认 dtb 是否被正确加载。
5.4 换启动介质后行为不一致
现象:SD 卡能启动,eMMC 不能。原因是两种介质的初始化时序、分区偏移、控制器配置不同,ROM code 和 SPL 里对介质的假设没有同步修改。解决方法是把介质相关的偏移和初始化参数集中配置,逐介质验证,而不是复制一份配置改个名字。
5.5 插着调试线就启动失败
现象:不接调试器正常,接上就停在 ROM code。原因是某些调试接口会改变启动引脚电平或占用 UART,让 ROM code 误判为下载模式。这类问题没有后悔药,只能在硬件设计阶段把调试接口和启动 strap 隔离,软件侧无法补救。
6. 设计取舍:为什么启动链路要分成四段而不是一段
把启动拆成 ROM code、SPL、u-boot、kernel 四段,本质是在“片内 SRAM 太小”这个物理约束下做的分层妥协。ROM code 不可改,所以它必须足够简单、只做最小初始化并把控制权交出去;SPL 存在的代价是镜像格式和加载逻辑多了一层,收益是让大容量 DDR 固件成为可能;u-boot 承担交互和加载策略,把“启动什么”变成可配置;kernel 则专注运行时。每一层都在用“多一次搬运”换“更小的可信基”。
| 阶段 | 运行位置 | 核心职责 | 主要约束 |
|---|---|---|---|
| ROM code | 片内 ROM + SRAM | CPU/系统初始化、介质判定、搬 SPL | 不可修改,SRAM 容量小 |
| SPL | 片内 SRAM | DDR 初始化、搬 u-boot 主体 | 体积必须极小 |
| u-boot | DDR | 外设初始化、加载 kernel 与 dtb | 地址与参数约定 |
| kernel | DDR | 接管系统、进入用户空间 | 依赖前级交接正确 |
验证这条链路是否健康,我一般会按层打点:ROM code 阶段看启动引脚和下载模式是否可达,SPL 阶段看 DDR 训练日志,u-boot 阶段看镜像头和加载地址,kernel 阶段看早期串口输出和 dtb 解析。任何一层没有可观测输出,就先怀疑它上一层的交接,而不是在本层里瞎改。从那以后我每次拿到新板子,都强制先把这四层的观测点各验证一遍再动业务代码,省下的时间远比多花的调试时间多。希望帮到你。
本文还有配套的精品资源,点击获取