3. Bootloader原理与U-Boot移植:启动流程、板级配置与烧录

Bootloader,说白了就是板子上电后第一个跑的程序。它负责把内核从Flash里搬出来,放到内存里,然后跳过去执行。我刚开始接触嵌入式时,总觉得Bootloader就是个“跳板”,后来踩了几次坑才明白——它其实是整个系统的基石。

U-Boot是目前最主流的Bootloader,支持ARM、RISC-V、x86等几乎所有架构。这一章,我们就把它彻底搞透。

3.1 U-Boot启动流程分析

U-Boot的启动分两个阶段。为什么分两段?因为第一阶段在SRAM里跑,空间小、功能弱;第二阶段才把代码搬到DDR里,这时候才能放开手脚。

核心要点:第一阶段(SPL)负责初始化最小硬件(时钟、DDR),第二阶段(U-Boot proper)负责加载内核、文件系统。

3.1.1 第一阶段:SPL(Secondary Program Loader)

SPL是U-Boot的“瘦身版”。它被烧录在ROM或Flash的固定位置,上电后由片内ROM加载到SRAM里执行。

SPL做的事儿很纯粹:

  • 设置CPU为SVC模式,关中断
  • 初始化时钟、串口、DDR控制器
  • 从Flash/MMC/NAND加载U-Boot proper到DDR
  • 跳转到U-Boot proper入口

我记得第一次移植U-Boot到一块新板子上,SPL死活跑不起来。后来用示波器量时钟引脚,发现PLL配置错了——分频系数写反了。嗯,这种低级错误,犯过一次就记住了。

3.1.2 第二阶段:U-Boot proper

U-Boot proper才是我们常说的“U-Boot”。它功能完整,支持命令行、网络、文件系统、环境变量等。

它的启动流程大致如下:

  1. board_init_f():初始化板级外设(GPIO、I2C、SPI等),设置内存布局
  2. relocate_code():把U-Boot自身从Flash拷贝到DDR的高地址
  3. board_init_r():重新初始化外设,建立堆栈,启动命令行
  4. main_loop():等待用户输入,或自动加载内核

我的习惯:调试阶段,我会在board_init_f()里加串口打印,确认板级初始化是否通过。如果串口没输出,问题大概率在SPL阶段。

3.2 板级配置文件的修改

U-Boot的板级支持,核心就是改配置文件。每个板子对应一个头文件,放在include/configs/目录下。

举个例子,我移植一块基于i.MX6ULL的板子,配置文件叫mx6ull_myboard.h。里面主要配置这几项:

配置项 说明 常见值
CONFIG_SYS_TEXT_BASE U-Boot在DDR中的加载地址 0x87800000
CONFIG_SYS_LOAD_ADDR 内核加载地址 0x82000000
CONFIG_BOOTCOMMAND 自动启动时执行的命令 "mmc read ...; bootz"
CONFIG_BOOTARGS 传给内核的命令行参数 "console=ttymxc0,115200"
CONFIG_SYS_CLK_FREQ 系统时钟频率 24000000

你想想看,这些配置如果有一个不对,板子就可能起不来。我曾经因为CONFIG_SYS_TEXT_BASE写错了地址,U-Boot加载到DDR后直接跑飞,串口输出一堆乱码。排查了半天才发现是地址冲突了。

3.2.1 设备树(Device Tree)的适配

现代U-Boot使用设备树来描述硬件。板级设备树文件放在arch/arm/dts/目录下。

你需要修改或新建一个.dts文件,至少包含:

  • CPU型号、主频
  • DDR大小、时序参数
  • 串口、网卡、MMC等外设的寄存器地址和中断号
  • GPIO复用配置(pinctrl)

避坑指南:我曾经在设备树里漏配了网卡的复位GPIO,导致U-Boot能启动,但网络死活不通。后来用mdio工具读PHY寄存器,发现PHY一直处于复位状态。加上GPIO配置后,问题解决。

3.3 U-Boot的编译与烧录

编译U-Boot,说白了就是三步:配置、编译、打包。

3.3.1 编译步骤

# 1. 清理
make distclean

# 2. 配置板级
make mx6ull_myboard_defconfig

# 3. 编译
make -j4

# 4. 生成SPL和U-Boot镜像
# 输出文件:SPL、u-boot.img、u-boot-dtb.img

编译完成后,你会得到几个关键文件:

  • SPL:第一阶段引导程序,烧录到偏移0x0
  • u-boot.img:第二阶段镜像,烧录到偏移0x200(或根据板子定义)
  • u-boot-dtb.img:包含设备树的完整镜像

3.3.2 烧录方式

烧录U-Boot,常见的有三种方式:

方式 工具 适用场景
SD卡烧录 dd命令 开发阶段,快速验证
NAND/NOR Flash烧录 JTAG或厂家工具 量产阶段
网络烧录(TFTP) U-Boot自带tftp命令 已有U-Boot,升级版本

我个人习惯用SD卡烧录,因为快。命令如下:

# 假设SD卡设备是/dev/sdb
sudo dd if=SPL of=/dev/sdb bs=1k seek=1
sudo dd if=u-boot.img of=/dev/sdb bs=1k seek=69
sync

注意seek偏移量,不同板子不一样。我见过有人把U-Boot烧到SD卡的分区表区域,结果卡在电脑上无法识别——嗯,分区表被覆盖了。

我的建议:烧录前先用hexdump查看SD卡前几个扇区的内容,确认没有重要数据。另外,烧录完成后,用md命令在U-Boot命令行里读一下内存,验证镜像是否加载正确。

3.4 U-Boot启动流程图

下面这张图,把整个启动流程串起来了。你对照着看,会更清楚。

U-Boot启动流程图 U-Boot启动流程图 上电复位 ROM加载SPL到SRAM SPL:初始化时钟、DDR 加载U-Boot proper到DDR U-Boot proper启动 board_init_f() relocate_code() main_loop() SPL阶段:约50KB代码 U-Boot阶段:约500KB代码

这张图里,从“上电复位”到“main_loop()”,一共7个关键节点。你移植时,每个节点都可能出问题。我的经验是:先确保SPL能跑通,再调试U-Boot proper。别想一口吃成胖子。

3.5 常见问题与调试技巧

最后,分享几个我实际项目中遇到的坑:

  • 串口无输出:检查波特率、引脚复用、时钟配置。我遇到过因为时钟树配错,串口波特率实际是115200但配置成9600的情况。
  • DDR初始化失败:用厂家提供的DDR训练工具,先确认DDR时序参数。别自己瞎猜,DDR时序差一点就起不来。
  • U-Boot加载内核后死机:检查内核加载地址和bootm命令的参数是否匹配。我曾经把内核加载到DDR的末尾,结果解压时覆盖了U-Boot自身。

重要提醒:调试U-Boot时,一定要保留一个能用的版本。我习惯在SD卡上同时放两个U-Boot镜像,一个稳定版,一个测试版。万一测试版刷坏了,还能从稳定版启动。

好了,U-Boot的启动流程、板级配置、编译烧录,就讲到这里。你动手移植时,记得对照着流程图一步步来。遇到问题别慌,先看串口输出,再看设备树,最后查配置——90%的问题都能解决。