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认识你的板子
拿到一块新板子,U-Boot默认不认识它。你得告诉U-Boot:你的CPU是什么、内存多大、Flash型号、串口用哪个。这些信息都放在板级配置里。
配置体系结构
U-Boot的配置分三层,我画个表格你就明白了:
| 层级 | 文件路径 | 作用 |
|---|---|---|
| 芯片级 | arch/arm/dts/ |
定义CPU、内存映射、时钟 |
| 板级 | board/<厂商>/<板名>/ |
板子特有的GPIO、电源、外设 |
| 配置头文件 | include/configs/<板名>.h |
宏定义:内存大小、环境变量分区 |
我建议你从一块参考板开始改。比如你的板子跟“mx6ull_14x14_evk”很像,那就复制它的配置目录,然后逐项修改。千万别从零写——U-Boot的配置宏有上千个,手写容易漏。
我的习惯做法:
- 在
board/下新建板子目录,复制参考板的Kconfig和Makefile - 修改
include/configs/下的头文件,重点改CONFIG_SYS_SDRAM_SIZE和CONFIG_SYS_LOAD_ADDR - 在
arch/arm/dts/下创建设备树文件,或者直接修改参考板的dts - 运行
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。我会这么做:
- 选参考板: V3s的参考板是
lichee_pi_zero,复制配置 - 改内存: 在
include/configs/sun8i.h中确认CONFIG_SYS_SDRAM_SIZE为0x4000000(64MB) - 改串口: 设备树中
uart0节点确认pinctrl配置正确 - 改Flash分区: 在
CONFIG_BOOTCOMMAND中指定从SPI Flash加载内核 - 编译测试:
make lichee_pi_zero_defconfig && make -j4 - 烧录验证: 用
sunxi-fel工具通过USB烧录,看串口输出
第一次启动时,串口如果能打印U-Boot版本号和Hit any key to stop autoboot,说明移植成功了八成。剩下的就是调外设——网卡、USB、LCD这些,一步步来。
调试小技巧: 在U-Boot命令行输入 bdinfo 可以查看板子信息,包括内存布局、时钟频率、MAC地址等。输入 printenv 查看环境变量。这两个命令是我调试时用得最多的。
U-Boot移植说难不难,说简单也不简单。核心就是三板斧:改配置、调设备树、烧录验证。把这套流程跑熟了,以后换任何芯片平台都能快速上手。