2. 嵌入式启动基础
大家好,我是你们的嵌入式系统讲师。今天我们来聊聊U-Boot的启动流程。说实话,我刚接触U-Boot那会儿,看着启动日志里密密麻麻的打印信息,心里也是发怵的。但后来我发现,只要把启动路径拆成几个关键阶段,这事儿就没那么玄乎了。
U-Boot从CPU上电复位到最终弹出命令行提示符,中间经历了好几个阶段。我习惯把它比作「接力赛」——每一棒都有明确的任务,交接棒的地方最容易出问题。咱们今天就把这条路径走一遍。
2.1 启动阶段划分:SPL与U-Boot proper
现代嵌入式系统里,U-Boot通常被拆成两个阶段:SPL(Secondary Program Loader)和U-Boot proper。为什么这么搞?说白了就是「空间不够用」。
我当年在给一款Cortex-A7芯片移植U-Boot时,芯片内部SRAM只有64KB。完整的U-Boot编译出来动不动就300KB+,根本塞不进去。所以必须有个「瘦身版」先跑起来,把DDR初始化好,再把完整的U-Boot从Flash搬到DDR里。
核心概念速览:
- SPL:精简版加载器,运行在SRAM中,负责最小系统初始化 + 加载U-Boot proper
- U-Boot proper:完整版Bootloader,运行在DDR中,负责加载操作系统
下面这张图把整个启动流程串起来了,我建议你多看几遍:
2.2 复位向量:一切从这里开始
CPU上电后,PC指针会指向一个固定地址——复位向量。ARM架构通常是0x00000000,有些芯片是0xFFFF0000。这个地址里放的就是第一条指令。
U-Boot的入口代码在arch/arm/cpu/armv7/start.S(以ARMv7为例)。我刚开始看这段汇编时,满屏的.globl和b reset看得我头晕。其实核心逻辑就几件事:
// 简化后的复位向量处理
reset:
/* 设置CPU为SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
/* 关闭中断 */
/* 设置栈指针 */
/* 跳转到板级初始化 */
bl board_init_f
我的经验:移植时最容易翻车的地方是「栈指针设置」。不同芯片的SRAM基地址不一样,栈顶算错了,SPL跑几步就崩。我建议你先把芯片手册里的内存映射图画出来,标清楚SRAM的起止地址。
2.3 SPL阶段:麻雀虽小五脏俱全
SPL的代码量大概只有U-Boot proper的十分之一。它要完成的任务很明确:
- 时钟初始化——让CPU跑在目标频率
- 串口初始化——不然你连调试信息都看不到
- DDR初始化——这是最关键的一步,DDR配错了系统直接死
- Flash/存储初始化——从NAND、eMMC或SD卡读取U-Boot proper
- 加载并跳转——把U-Boot proper搬到DDR,然后跳过去
这里有个细节很多人会忽略:SPL的链接地址。SPL是链接到SRAM地址空间的,而U-Boot proper是链接到DDR地址空间的。这两个地址不能重叠,否则跳转时数据就乱了。
| 对比项 | SPL | U-Boot proper |
|---|---|---|
| 运行位置 | 内部SRAM | 外部DDR |
| 代码大小 | 通常 < 64KB | 200KB ~ 500KB |
| 链接地址 | 由CONFIG_SPL_TEXT_BASE指定 | 由CONFIG_SYS_TEXT_BASE指定 |
| 主要任务 | 最小初始化 + 加载 | 完整初始化 + 启动OS |
| 调试难度 | 高(SRAM空间有限) | 中(DDR空间充裕) |
我曾经踩过的坑:有一次SPL加载U-Boot proper后跳转失败,查了两天才发现是DDR时序参数配错了。SPL初始化DDR时,参数是从芯片手册里抄的,但实际板子走线长度和参考设计不一样,导致DDR不稳定。从那以后,我每次移植都会先用DDR测试程序跑一遍,确认读写没问题再继续。
2.4 U-Boot proper阶段:全面初始化
U-Boot proper的入口函数是board_init_r(),它在common/board_r.c里。这个函数会调用一个长长的初始化函数列表:
// board_init_r 中的关键初始化序列(简化)
init_fnc_t init_sequence_r[] = {
serial_init, // 串口初始化
env_init, // 环境变量初始化
initr_net, // 网络初始化
initr_mmc, // MMC/SD卡初始化
initr_env, // 加载环境变量
initr_cmdline, // 命令行初始化
/* ... 还有几十个 ... */
NULL
};
这个列表的执行顺序很重要。比如网络初始化依赖环境变量里的IP地址,所以initr_env必须在initr_net之前。我见过有人把顺序搞反了,结果网络一直起不来。
2.5 命令行提示符:启动完成的标志
当所有初始化都完成后,U-Boot进入主循环main_loop()。它会检查:
- 有没有设置
bootdelay(倒计时秒数) - 有没有设置
bootcmd(自动启动命令) - 用户有没有按下任意键打断自动启动
如果倒计时结束用户没干预,就执行bootcmd里的命令——通常是加载并启动操作系统。如果用户按了键,就弹出命令行提示符,等你输入命令。
命令行提示符出现的条件:
- 所有硬件初始化成功
- 环境变量加载完成
- 主循环进入交互模式
如果卡在某个初始化函数里,命令行永远出不来。这时候就要靠串口打印信息定位了。
2.6 启动流程中的关键决策点
整个启动过程中,有几个地方是「分岔路口」:
- SPL是否启用?——有些芯片SRAM够大,可以不用SPL,直接跑U-Boot proper。但大部分SoC都需要SPL。
- 从哪个设备加载?——NAND、eMMC、SD卡、SPI Flash?不同设备驱动不同,加载地址也不同。
- 是否进入命令行?——生产环境下通常直接启动OS,开发时才需要命令行。
我个人习惯在移植初期,先把bootdelay设成-1(不自动启动),这样每次都能进命令行,方便调试。等所有功能都调通了,再改成正常值。
好了,U-Boot启动流程的概览就讲到这里。你可能会觉得内容有点多,但别急——后面每一章都会把今天提到的某个环节拆开细讲。先把这张全景图印在脑子里,后面学起来会轻松很多。