2. Bootloader基础
大家好,我是你们的Bootloader讲师。今天咱们聊一个特别基础、但又特别容易被忽视的话题——嵌入式系统从上电到操作系统启动,中间到底经历了什么?
很多同学一上来就盯着U-Boot源码看,结果越看越懵。为什么?因为你不知道ROM Code在干嘛,不知道SPL和TPL到底解决了什么问题。说白了,你缺一张完整的地图。
这一章,我就带你把这整条链路走一遍。我会结合我这些年踩过的坑,把每个阶段该干什么、不该干什么,都给你讲清楚。
一、上电瞬间:CPU在干什么?
按下电源键的那一刻,CPU内部发生了什么?
我刚开始做嵌入式时,以为CPU上电后直接就去执行Flash里的程序了。后来被一个老工程师骂了一顿——「你连复位向量都不看,调什么Bootloader?」
实际上,CPU上电后做的第一件事,是去固定的地址取第一条指令。这个地址由芯片设计时硬编码决定,你改不了。
核心要点:
- ARM Cortex-A系列:通常从0x00000000或0xFFFF0000开始
- ARM Cortex-M系列:从0x00000000取栈指针,0x00000004取复位向量
- RISC-V:从0x80000000或芯片自定义地址开始
- x86:从0xFFFFFFF0(复位向量)开始
这里有个坑,我当年就栽过。有一次做i.MX6ULL的项目,板子死活起不来。查了三天,最后发现是启动介质选择引脚的电平没拉对,CPU根本没去读Flash,一直在ROM Code里打转。
注意:很多SoC在上电后会先执行芯片内部固化的ROM Code,而不是直接执行你的Bootloader。ROM Code负责初始化最基本的硬件(时钟、DDR控制器等),然后根据启动引脚的电平状态,决定从哪个介质加载下一级程序。
二、ROM Code:芯片出厂自带的「迷你Bootloader」
ROM Code是什么?它是芯片出厂时固化在ROM里的一段小程序。你没法改它,也没法擦除它。
它的任务很简单:
- 初始化最基本的硬件——比如PLL、时钟树、DDR控制器
- 检测启动引脚——决定从NAND、eMMC、SD卡还是UART启动
- 加载下一级程序——把SPL或第一级Bootloader从存储介质搬到SRAM或DDR里
- 跳转执行——把控制权交给加载进来的程序
我见过一个团队,ROM Code阶段就出了问题。他们用的芯片ROM Code默认DDR频率是400MHz,但板子上的DDR颗粒只能跑333MHz。结果ROM Code初始化DDR后,SPL一加载就崩溃。最后只能改硬件,把DDR颗粒换成能跑400MHz的型号。
经验之谈:ROM Code的代码质量通常很高,毕竟是芯片原厂写的。但它有个致命问题——不灵活。一旦芯片流片出来,ROM Code就定死了。所以ROM Code只做最基础的事,复杂的初始化留给后面的SPL去做。
三、SPL与TPL:为什么需要两级加载?
好,现在ROM Code把控制权交给了SPL。SPL全称是Secondary Program Loader,中文叫「二级程序加载器」。
你可能会问:为什么不能一步到位,直接加载完整的Bootloader?
原因很简单——SRAM太小了。
我举个例子。TI的AM335x芯片,内部SRAM只有64KB。U-Boot完整版编译出来动辄几百KB,根本放不下。所以必须分两步:
- SPL:体积小(通常几十KB),放在SRAM里运行。它的任务就是初始化DDR,然后把完整的Bootloader从Flash搬到DDR里。
- 完整的Bootloader(比如U-Boot):体积大,放在DDR里运行。它负责加载操作系统内核。
那TPL又是什么?TPL是Tertiary Program Loader,三级程序加载器。有些芯片的SRAM实在太小,连SPL都放不下,怎么办?那就再加一级:
| 阶段 | 运行位置 | 典型大小 | 主要任务 |
|---|---|---|---|
| ROM Code | 芯片内部ROM | 4KB - 32KB | 初始化时钟、检测启动介质、加载SPL |
| TPL | SRAM | 8KB - 16KB | 初始化DDR控制器(如果SRAM太小,SPL放不下) |
| SPL | SRAM 或 DDR | 32KB - 128KB | 初始化DDR(如果TPL没做)、加载完整Bootloader |
| 完整Bootloader | DDR | 200KB - 1MB+ | 加载操作系统内核、设备树、ramdisk |
你看,这个层级关系就像剥洋葱。每一层只做最少的事,然后把控制权交给下一层。
四、完整启动链路:一张图看懂
说了这么多,不如直接画张图。下面这张SVG图,把整个启动链路串起来了:
五、各阶段的内存布局
搞清楚了流程,咱们再看看内存是怎么分配的。这个特别重要——我见过太多人因为内存布局搞错,导致SPL覆盖了ROM Code的数据,或者Bootloader把内核加载地址给占了。
以典型的ARM Cortex-A7芯片为例:
内存地址空间(示意):
0x00000000 - 0x0000FFFF (64KB) SRAM
├─ 0x00000000 - 0x00000FFF (4KB) ROM Code使用的栈和全局变量
├─ 0x00001000 - 0x00002FFF (8KB) SPL代码段
├─ 0x00003000 - 0x00003FFF (4KB) SPL数据段/BSS
└─ 0x00004000 - 0x0000FFFF (48KB) 空闲(可用于临时缓冲区)
0x80000000 - 0x8FFFFFFF (256MB) DDR
├─ 0x80000000 - 0x8001FFFF (128KB) 完整Bootloader(由SPL加载)
├─ 0x82000000 - 0x8200FFFF (64KB) 设备树(DTB)
├─ 0x83000000 - 0x83FFFFFF (16MB) 内核映像(zImage/Image)
└─ 0x88000000 - 0x8FFFFFFF (128MB) ramdisk/根文件系统
避坑指南:我曾经在一个项目里,SPL把Bootloader加载到了0x80000000,但Bootloader自己又把内核加载到了0x80000000。结果内核一加载,就把Bootloader的代码覆盖了。虽然Bootloader已经完成了它的使命,但如果你还想在跳转内核前做点收尾工作(比如关闭MMU、刷新cache),那就全完了。
所以我的习惯是:给每个阶段留足安全间距。Bootloader放在0x80000000,内核放在0x82000000,中间留2MB的缓冲。
六、实际项目中的启动时间分配
最后,咱们聊聊启动时间。很多同学做启动优化时,一上来就盯着内核优化,却忽略了前面的阶段。
我给大家看一组真实数据(来自一个i.MX6ULL的项目):
| 阶段 | 耗时 | 占比 | 优化空间 |
|---|---|---|---|
| ROM Code | ~50ms | 5% | 几乎无(固化在芯片里) |
| SPL | ~150ms | 15% | 大(DDR初始化、时钟配置可优化) |
| 完整Bootloader | ~300ms | 30% | 大(驱动初始化、环境变量加载) |
| 内核启动 | ~500ms | 50% | 中(驱动、文件系统) |
你看,SPL和Bootloader加起来占了将近一半的时间。很多人只优化内核,却忽略了前面这两个阶段。其实SPL的DDR初始化、Bootloader的驱动加载,都有很大的优化空间。
我有个客户,产品要求启动时间小于1秒。他们一开始只优化内核,折腾了两个月,从500ms降到了400ms。后来我帮他们看了SPL和Bootloader,把DDR初始化参数调优、去掉不必要的驱动、用快速启动模式,SPL从150ms降到了60ms,Bootloader从300ms降到了120ms。加起来省了270ms,比优化内核的效果好多了。
我的建议:做启动优化时,先拿示波器或逻辑分析仪把各阶段的耗时量出来。哪个阶段占比最大,就先优化哪个。不要凭感觉,要凭数据。