Bootloader原理与U-Boot移植:启动流程、板级配置与设备树

嵌入式Linux系统启动,Bootloader是第一关。说白了,它就是上电后第一个跑起来的程序,负责把内核从Flash里搬到内存,然后跳过去执行。U-Boot是目前最主流的Bootloader,支持几百种开发板,咱们今天就把它吃透。

U-Boot启动流程:从汇编到C语言

U-Boot启动分两个阶段,我习惯叫它“两步走”。第一阶段用汇编,第二阶段用C语言。为什么这么设计?因为刚上电时,C语言运行环境还没准备好——栈指针没设、BSS段没清、内存控制器可能都没初始化。汇编先干这些脏活累活。

第一阶段(SPL / 汇编阶段)

  • 设置CPU为SVC模式(超级管理员模式)
  • 关闭中断、MMU、Cache
  • 初始化时钟、串口、内存控制器
  • 将第二阶段代码从Flash拷贝到内存
  • 跳转到第二阶段入口

第二阶段用C语言写,逻辑就清晰多了。它会初始化更多外设、解析环境变量、加载设备树、最后启动内核。我当年调试一块i.MX6ULL的板子,卡在第一阶段的内存初始化上整整两天——后来发现是DDR时序参数配错了。

U-Boot 启动流程 上电 → CPU从ROM启动 SPL(汇编):初始化时钟/内存/串口 拷贝U-Boot主程序到DDR 第二阶段(C语言):初始化外设 解析环境变量 / 加载设备树 启动内核(bootm / bootz) ⚠ 第一阶段出错 → 串口无输出

板级配置修改:让U-Boot认识你的板子

拿到一块新板子,U-Boot默认不认识它。你得告诉U-Boot:你的CPU是什么、内存多大、Flash型号、串口用哪个。这些信息都放在板级配置里。

配置体系结构

U-Boot的配置分三层,我画个表格你就明白了:

层级 文件路径 作用
芯片级 arch/arm/dts/ 定义CPU、内存映射、时钟
板级 board/<厂商>/<板名>/ 板子特有的GPIO、电源、外设
配置头文件 include/configs/<板名>.h 宏定义:内存大小、环境变量分区

我建议你从一块参考板开始改。比如你的板子跟“mx6ull_14x14_evk”很像,那就复制它的配置目录,然后逐项修改。千万别从零写——U-Boot的配置宏有上千个,手写容易漏。

我的习惯做法:

  1. 在 board/ 下新建板子目录,复制参考板的 Kconfig 和 Makefile
  2. 修改 include/configs/ 下的头文件,重点改 CONFIG_SYS_SDRAM_SIZE 和 CONFIG_SYS_LOAD_ADDR
  3. 在 arch/arm/dts/ 下创建设备树文件,或者直接修改参考板的dts
  4. 运行 make menuconfig 做图形化配置

关键配置项速查

宏定义 说明 常见坑
CONFIG_SYS_TEXT_BASE U-Boot在内存中的加载地址 跟链接脚本不一致会死机
CONFIG_BOOTCOMMAND 自动启动时执行的命令 写错路径内核起不来
CONFIG_ENV_OFFSET 环境变量在Flash中的偏移 跟分区表冲突会覆盖数据
CONFIG_SYS_MMC_ENV_DEV 环境变量存放的MMC设备号 多MMC设备时容易搞混

我曾经踩过的坑: 修改 CONFIG_SYS_TEXT_BASE 后忘记同步更新链接脚本 u-boot.lds,结果U-Boot跳转后直接跑飞。串口最后一行输出停在“Starting kernel ...”,然后就没然后了。排查了三个小时才发现是地址不匹配。

设备树编译与烧录:打通硬件描述

设备树(Device Tree)是Linux内核用来描述硬件信息的“配置文件”。U-Boot负责把设备树传给内核。你想想看,如果没有设备树,内核就得硬编码所有板子的硬件信息——那得维护多少份代码?

设备树编译流程

设备树源文件(.dts)经过编译生成二进制文件(.dtb)。U-Boot和内核都认识.dtb格式。编译命令很简单:

# 编译单个设备树
dtc -I dts -O dtb -o myboard.dtb myboard.dts

# 在U-Boot源码树中编译
make myboard.dtb

# 查看设备树内容
dtc -I dtb -O dts myboard.dtb

我习惯在U-Boot源码里直接编译,因为U-Boot的scripts/dtc/目录下自带dtc工具,版本匹配,不会出现兼容问题。

设备树在U-Boot中的加载位置

U-Boot启动内核时,需要告诉内核三个东西:内核镜像地址、设备树地址、ramdisk地址(可选)。典型命令如下:

# 从MMC加载内核和设备树到内存
fatload mmc 0:1 0x12000000 zImage
fatload mmc 0:1 0x13000000 myboard.dtb

# 启动内核
bootz 0x12000000 - 0x13000000

这里0x12000000和0x13000000是内存地址,不能重叠,也不能跟U-Boot自身占用的地址冲突。我一般把内核放在0x12000000,设备树放在0x13000000,中间留够空间。

设备树加载地址选择原则:

  • 避开U-Boot自身占用的内存区域(通常从0x17800000开始)
  • 设备树地址必须4字节对齐
  • 内核和设备树之间至少留1MB间隔,防止解压时覆盖
  • 不要超过板子的物理内存上限

烧录U-Boot到Flash

U-Boot编译后会生成多个镜像文件,烧录哪个取决于你的启动介质:

文件 说明 烧录位置
u-boot.bin 原始二进制镜像 SPI Flash偏移0x0
u-boot.imx i.MX系列专用格式(含IVT头) SD卡偏移0x400
u-boot-spl.bin SPL(第一阶段) NAND Flash起始块
u-boot.itb FIT格式(含多个镜像) 按FIT配置烧录

烧录命令因平台而异。以i.MX6ULL为例,烧录到SD卡:

# 确认SD卡设备号
ls /dev/sd*

# 烧录U-Boot到SD卡(注意:不是分区,是整卡偏移)
sudo dd if=u-boot.imx of=/dev/sdb bs=1k seek=1 conv=fsync

# 烧录设备树到SD卡分区
sudo mount /dev/sdb1 /mnt
sudo cp myboard.dtb /mnt/
sudo umount /mnt

千万注意: dd 命令的 of= 参数写的是整卡设备(如 /dev/sdb),不是分区(如 /dev/sdb1)。写错会破坏分区表,整张卡的数据全丢。我当年手滑过一次,板子直接变砖,后来用JTAG才救回来。

实战:从零移植U-Boot到一块新板子

假设你手头有一块基于Allwinner V3s的板子,内存64MB,SPI Flash 16MB,串口在UART0。我会这么做:

  1. 选参考板: V3s的参考板是lichee_pi_zero,复制配置
  2. 改内存: 在include/configs/sun8i.h中确认CONFIG_SYS_SDRAM_SIZE为0x4000000(64MB)
  3. 改串口: 设备树中uart0节点确认pinctrl配置正确
  4. 改Flash分区: 在CONFIG_BOOTCOMMAND中指定从SPI Flash加载内核
  5. 编译测试: make lichee_pi_zero_defconfig && make -j4
  6. 烧录验证: 用sunxi-fel工具通过USB烧录,看串口输出

第一次启动时,串口如果能打印U-Boot版本号和Hit any key to stop autoboot,说明移植成功了八成。剩下的就是调外设——网卡、USB、LCD这些,一步步来。

调试小技巧: 在U-Boot命令行输入 bdinfo 可以查看板子信息,包括内存布局、时钟频率、MAC地址等。输入 printenv 查看环境变量。这两个命令是我调试时用得最多的。

U-Boot移植说难不难,说简单也不简单。核心就是三板斧:改配置、调设备树、烧录验证。把这套流程跑熟了,以后换任何芯片平台都能快速上手。