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中,负责加载操作系统

下面这张图把整个启动流程串起来了,我建议你多看几遍:

U-Boot启动流程全景图 阶段1:复位向量 CPU上电 → 跳转0x00000000 阶段2:SPL入口 start.S → board_init_f 阶段3:SPL初始化 时钟→串口→DDR→Flash 阶段4:加载proper 从Flash→DDR 阶段5:proper入口 board_init_r 阶段6:完整初始化 外设→环境变量→网络 阶段7:命令行提示符 等待用户输入 ⚠ 关键分界线 SPL运行在SRAM proper运行在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的十分之一。它要完成的任务很明确:

  1. 时钟初始化——让CPU跑在目标频率
  2. 串口初始化——不然你连调试信息都看不到
  3. DDR初始化——这是最关键的一步,DDR配错了系统直接死
  4. Flash/存储初始化——从NAND、eMMC或SD卡读取U-Boot proper
  5. 加载并跳转——把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 启动流程中的关键决策点

整个启动过程中,有几个地方是「分岔路口」:

  1. SPL是否启用?——有些芯片SRAM够大,可以不用SPL,直接跑U-Boot proper。但大部分SoC都需要SPL。
  2. 从哪个设备加载?——NAND、eMMC、SD卡、SPI Flash?不同设备驱动不同,加载地址也不同。
  3. 是否进入命令行?——生产环境下通常直接启动OS,开发时才需要命令行。

我个人习惯在移植初期,先把bootdelay设成-1(不自动启动),这样每次都能进命令行,方便调试。等所有功能都调通了,再改成正常值。

好了,U-Boot启动流程的概览就讲到这里。你可能会觉得内容有点多,但别急——后面每一章都会把今天提到的某个环节拆开细讲。先把这张全景图印在脑子里,后面学起来会轻松很多。