4、二级Bootloader(UBoot):UBoot的编译与配置、UBoot的启动流程、UBoot的命令行交互

好,我们接着聊。上一章我们把一级Bootloader(也就是ROM Code)讲透了,它把CPU从复位状态拉起来,完成了最基础的硬件初始化,然后跳转到下一站。这一站,就是UBoot。

UBoot,全称是Das U-Boot,江湖人称“万能引导加载程序”。说它万能,是因为它几乎支持所有主流架构——ARM、x86、RISC-V,当然也包括我们RMC系统用的RISC-V。我个人习惯把UBoot看作是“固件世界里的Linux”,因为它同样拥有庞大的驱动框架、文件系统支持和命令行环境。

说白了,UBoot就是二级Bootloader的典型代表。它的任务很明确:初始化DDR、加载内核镜像、传递启动参数,然后跳转到内核。但它的能力远不止这些——它还能通过网络烧写固件、调试硬件、甚至运行简单的脚本。嗯,这里要注意,UBoot的灵活性是一把双刃剑,配置不好反而会拖慢启动速度。

4.1 UBoot的编译与配置

编译UBoot,其实没那么玄乎。你想想看,它本质上就是一个嵌入式Linux内核风格的工程。配置方式也是经典的make menuconfig,或者更直接一点——用预设的defconfig文件。

我在项目中遇到过不少新手,上来就make,结果编译出一堆错误。为什么?因为没选对平台配置。RISC-V架构下,UBoot的配置通常长这样:

# 先清理
make distclean

# 选择RISC-V 64位平台配置
make qemu-riscv64_smode_defconfig

# 或者针对具体开发板
make my_riscv_board_defconfig

# 然后编译
make -j$(nproc)

这里有个关键点:配置决定了UBoot的行为。比如,你要不要支持FAT文件系统?要不要启用网络协议栈?DDR的时序参数是多少?这些都在配置阶段确定。

核心配置项(RISC-V平台)

  • CONFIG_RISCV_SMODE:运行在Supervisor模式,这是RISC-V Linux的标准启动方式
  • CONFIG_SYS_TEXT_BASE:UBoot的链接地址,通常放在DDR的起始位置偏移一段距离
  • CONFIG_BOOTCOMMAND:自动启动时执行的命令,比如加载内核
  • CONFIG_ENV_IS_IN_SPI_FLASH:环境变量保存在SPI Flash中

我曾经踩过一个坑:CONFIG_SYS_TEXT_BASE设置得和一级Bootloader的跳转地址不一致,结果UBoot加载后直接跑飞。排查了整整两天,最后发现是地址偏移算错了4KB。嗯,这种低级错误,犯过一次就再也不会忘了。

4.2 UBoot的启动流程

UBoot的启动流程,我习惯把它分成三个阶段:

  1. SPL(Secondary Program Loader)阶段:如果UBoot体积太大,ROM Code无法直接加载,就需要SPL作为过渡。SPL非常精简,只做最基本的初始化——设置时钟、初始化DDR、然后加载完整的UBoot到DDR中。
  2. UBoot主阶段:完整的UBoot被加载到DDR后,开始全面初始化——串口、网卡、存储设备、环境变量等。
  3. 启动内核:根据环境变量中的bootcmd,执行加载内核、设备树、ramdisk,最后跳转到内核入口。

你可能会问:为什么要有SPL?直接让ROM Code加载完整的UBoot不行吗?原因很简单——ROM Code通常只有几十KB的SRAM可用,而UBoot编译出来可能有几百KB甚至几MB。SPL就是那个“瘦身版”的UBoot,它只做一件事:把真正的UBoot请进DDR。

个人经验:在RISC-V平台上,SPL的调试非常痛苦,因为串口可能还没初始化。我习惯在SPL里加一个GPIO翻转的调试灯——看到灯闪了,说明SPL跑到了这里;没闪,说明死在前面了。这比看串口日志快得多。

下面是UBoot启动流程的简化示意图,我把它画成了SVG,方便你理解整个脉络:

UBoot启动流程 阶段1:SPL(Secondary Program Loader) 初始化时钟 → 初始化DDR → 从存储介质加载完整UBoot到DDR 阶段2:UBoot主阶段 串口初始化 → 网卡初始化 → 存储设备初始化 环境变量加载 → 命令行交互(等待用户输入) 阶段3:启动内核 执行bootcmd → 加载内核/设备树 → 跳转到内核入口 用户中断自动启动 进入命令行交互模式 图:UBoot三阶段启动流程及命令行交互分支

4.3 UBoot的命令行交互

UBoot的命令行,是我个人非常喜欢的一个功能。它本质上是一个微型Shell,支持变量、脚本、甚至条件判断。你想想看,在嵌入式开发中,能有一个交互式环境来调试硬件、测试驱动、烧写固件,这得省多少事。

常用的命令,我列个表:

命令 功能 示例
printenv 打印所有环境变量 printenv bootcmd
setenv 设置环境变量 setenv bootargs 'console=ttyS0,115200'
saveenv 保存环境变量到存储介质 saveenv
tftp 通过网络下载文件 tftp 0x80000000 kernel.bin
fatload 从FAT分区加载文件 fatload mmc 0:1 0x80000000 uImage
bootm 启动内核镜像 bootm 0x80000000
go 跳转到指定地址执行 go 0x80000000

这里有个实用技巧:环境变量是UBoot的灵魂。你可以通过setenv自定义启动参数,比如指定根文件系统在哪个分区、控制台波特率是多少、甚至写一个启动脚本。

避坑指南:我曾经在调试一块RISC-V开发板时,不小心把bootcmd设成了一个不存在的命令,结果UBoot启动后直接卡死,连命令行都进不去。后来怎么解决的?按住复位键的同时按任意键,强制UBoot进入命令行模式。嗯,这个“救命键”在UBoot里叫bootdelay,默认等待3秒,期间按任意键中断自动启动。

命令行交互还有一个杀手级应用——在线调试硬件。比如,你可以用md命令查看内存内容,用mw命令修改内存,用gpio命令控制GPIO引脚。我在项目中就经常用这些命令来验证硬件连接是否正确——比如写一个值到某个寄存器,然后用示波器量对应的引脚电平。这比写一个完整的驱动快得多。

最后说一句:UBoot的命令行虽然强大,但生产环境中通常不会让它停留太久。默认的bootdelay设为0,直接进入自动启动流程,这样能缩短开机时间。但开发阶段,我建议保留2-3秒的延迟,给自己留一个“救命”的机会。

本章小结:UBoot作为二级Bootloader,承担着从一级Bootloader手中接过控制权、初始化复杂外设、最终加载内核的重任。它的编译配置灵活但需谨慎,启动流程分为SPL、主阶段、内核启动三步,命令行交互则是开发调试的利器。掌握好UBoot,你就掌握了嵌入式系统启动流程中最关键的一环。


公众号:蓝海资料掘金营,微信deep3321