2. 启动流程全景:从复位向量到操作系统启动的完整链路
做嵌入式启动这么多年,我经常被问到同一个问题:“芯片上电后到底发生了什么?”
很多人觉得Bootloader就是个黑盒子,按个复位键,系统就起来了。其实不然。从复位向量到操作系统启动,中间是一条完整的链路。每个环节都有它的使命,也都有它的坑。
今天我就带你把这整条链路走一遍。咱们不绕弯子,直接看核心。
2.1 启动阶段的划分:四步走
现代嵌入式系统的启动,一般分成四个阶段。我习惯把它们叫做“四步走”:
| 阶段 | 载体 | 主要任务 | 典型大小 |
|---|---|---|---|
| ROM Code | 芯片内部ROM | 初始化时钟、检测启动介质、加载SPL | 几KB ~ 几十KB |
| SPL | 片上SRAM | 初始化DDR、加载TPL或U-Boot proper | 16KB ~ 64KB |
| TPL | DDR | 更复杂的硬件初始化、加载U-Boot proper | 32KB ~ 128KB |
| U-Boot proper | DDR | 完整硬件初始化、加载操作系统 | 几百KB ~ 几MB |
你可能会问:“为什么非要分这么多阶段?直接一个Bootloader搞定不行吗?”
嗯,这个问题我当年也问过。答案很简单:芯片上电时,内部SRAM太小了。比如有些芯片只有64KB的SRAM,而U-Boot proper动辄几百KB,根本放不下。所以必须分阶段,像剥洋葱一样,一层层把硬件初始化好,把内存空间撑大,最后才能加载完整的Bootloader。
核心理解:启动分阶段的本质原因,是芯片内部存储资源有限。每一阶段都在为下一阶段“铺路”——初始化更复杂的硬件、提供更大的内存空间。
2.2 第一阶段:ROM Code——芯片出厂就有的“看门人”
ROM Code是芯片出厂时固化在内部ROM里的代码。你改不了它,也删不掉它。它就像芯片的“看门人”,负责在复位后做第一件事。
ROM Code一般做这几件事:
- 初始化最基本的硬件:比如CPU的异常向量表、堆栈指针、最基本的时钟。
- 检测启动介质:它会按照芯片厂商预设的顺序,去检查NAND Flash、eMMC、SD卡、UART等介质上有没有可启动的镜像。
- 加载SPL:找到启动介质后,把SPL(Secondary Program Loader)加载到内部SRAM中,然后跳转过去执行。
我记得有一次调试一块新板子,死活起不来。查了半天,发现是ROM Code在检测启动介质时,因为某个GPIO的上拉电阻没焊,导致它误判了启动源。嗯,这种问题最头疼,因为ROM Code你改不了,只能改硬件。
避坑指南:我曾经在一个项目里,因为启动介质的时序参数和ROM Code的默认配置不匹配,导致SPL加载失败。后来我养成了一个习惯:拿到新芯片,第一件事就是仔细看芯片手册里关于ROM Code启动介质检测的时序要求。别想当然,芯片厂商的默认配置不一定适合你的板子。
2.3 第二阶段:SPL——在SRAM里“螺蛳壳里做道场”
SPL是ROM Code加载到SRAM里的第一个用户代码。它的大小通常被限制在16KB到64KB之间。你想想看,这点空间,连个完整的U-Boot都放不下,但SPL却要完成一个关键任务:初始化DDR。
为什么初始化DDR这么重要?因为DDR是系统的主内存,只有DDR起来了,才能加载更大的程序(比如U-Boot proper)。
SPL的主要工作:
- 初始化DDR控制器:配置时序、频率、电压等参数。
- 初始化存储控制器:比如eMMC控制器、NAND控制器,这样才能从存储介质里读取数据。
- 加载TPL或U-Boot proper:从存储介质里把下一阶段的代码读到DDR里,然后跳转。
我个人习惯在SPL阶段加一个串口打印,输出“SPL started”和“SPL jumping to next stage”。别小看这两行打印,调试启动故障时,它们能帮你快速定位问题出在哪个阶段。
小技巧:SPL的代码量非常有限,所以尽量别在SPL里做复杂操作。我见过有人在SPL里做文件系统解析,结果SPL体积超标,直接塞不进SRAM。记住:SPL的使命就是“初始化DDR + 加载下一阶段”,别贪多。
2.4 第三阶段:TPL——可选的“中间人”
TPL(Tertiary Program Loader)不是所有平台都有。它是在SPL和U-Boot proper之间的一个可选阶段。什么时候需要TPL?
- DDR初始化特别复杂:比如某些DDR需要训练、校准,SPL那点空间搞不定。
- 需要更灵活的启动策略:比如支持多种启动介质、多种镜像格式。
TPL运行在DDR里,所以它的空间比SPL宽裕多了。它一般负责:
- 更完整的硬件初始化(比如PMIC、时钟树)。
- 从存储介质加载U-Boot proper。
- 做一些安全检查或镜像校验。
说实话,我接触的项目里,用TPL的不多。大部分平台都是SPL直接加载U-Boot proper。但如果你在做一些高端SoC(比如TI的某些AM系列),可能会遇到TPL。遇到也别慌,它的逻辑和SPL类似,只是跑在DDR里,空间更大。
2.5 第四阶段:U-Boot proper——真正的“Bootloader”
到了U-Boot proper,才算是进入了我们通常理解的Bootloader世界。它运行在DDR里,有充足的内存空间,可以做各种复杂的事情。
U-Boot proper的主要任务:
- 完整的硬件初始化:网卡、USB、显示、文件系统等。
- 加载操作系统:从存储介质(eMMC、NAND、网络、USB等)读取内核镜像和设备树,然后跳转到内核入口。
- 提供交互式命令行:方便开发人员调试、修改启动参数。
我个人觉得,U-Boot proper是整个启动链路里最“好玩”的阶段。因为你可以在这里做很多定制:比如从网络加载内核、从USB启动、甚至做一个简单的图形界面。当然,也是问题最多的地方。
关键点:U-Boot proper加载操作系统时,会做两件最重要的事:设置启动参数(bootargs)和传递设备树(DTB)。这两个东西错了,内核肯定起不来。我调试过很多启动故障,最后发现都是bootargs里某个参数写错了,或者DTB地址传错了。
2.6 启动流程全景图
说了这么多,不如一张图来得直观。下面这张图展示了从复位向量到操作系统启动的完整链路:
从这张图可以看得很清楚:启动是一个层层递进的过程。每一阶段都在为下一阶段做准备,直到最后操作系统接管系统控制权。
2.7 启动故障定位的“第一性原理”
了解了启动流程全景,你就能理解启动故障定位的“第一性原理”了:先确定系统卡在哪个阶段,然后针对那个阶段去排查。
我总结了一个简单的排查思路:
- 看串口输出:如果串口没有任何输出,问题大概率在ROM Code或SPL阶段。检查硬件连接、启动介质、时钟配置。
- 有SPL输出,但卡在DDR初始化:检查DDR的时序配置、电压、硬件连接。这是最常见的故障点。
- SPL成功,但U-Boot proper起不来:检查存储介质读取是否正常、镜像是否损坏、加载地址是否正确。
- U-Boot proper起来了,但内核起不来:检查bootargs、设备树、内核镜像格式。
我的习惯:每移植一个新平台,我都会在SPL和U-Boot proper的入口处加一个GPIO翻转。这样即使串口没输出,用示波器量一下GPIO电平,也能知道系统跑到了哪个阶段。这招帮我省了不少调试时间。