3. Bootloader原理与移植:U-Boot启动流程分析、U-Boot源码结构、U-Boot的配置与编译、U-Boot的移植实战
Bootloader,说白了就是嵌入式系统的「第一口奶」。CPU上电后,它得先跑起来,把硬件初始化好,再把操作系统内核请进内存。U-Boot是目前用得最广的Bootloader,没有之一。我这些年经手的项目,从ARM9到Cortex-A72,几乎都在跟它打交道。
3.1 U-Boot启动流程分析
U-Boot的启动分两个阶段。为什么分两段?因为早期的CPU片上SRAM太小,装不下完整的U-Boot,只能先跑一小段代码把DDR初始化好,再把主体代码搬过去。现在虽然硬件强了,但这个架构保留了下来。
第一阶段:汇编阶段(SPL / TPL)
这一阶段在CPU的复位向量处开始,通常是汇编代码。核心任务就三件:
- 设置异常向量表——CPU复位后第一条指令从这里取
- 初始化关键硬件——时钟、看门狗、DDR控制器
- 搬移U-Boot主体——从Flash/NAND/SD卡复制到DDR
我记得第一次调SPL时,板子死活不跑DDR。查了半天,发现是时序参数配错了。嗯,DDR初始化这块,寄存器值差一个数,系统就起不来。
第二阶段:C语言阶段
进入DDR后,U-Boot开始执行C代码。入口函数是board_init_f(),然后是board_init_r()。这里会做:
- 串口初始化——让你能看到打印信息
- 环境变量加载——从哪里启动、传什么参数
- 设备驱动枚举——网卡、MMC、USB等
- 启动内核——根据bootcmd变量执行
关键点:U-Boot的启动流程可以用一个命令打断——在串口打印倒计时时按任意键,就能进入命令行。这是调试阶段最常用的手段。
3.2 U-Boot源码结构
U-Boot的源码目录,初看挺吓人,其实有规律。我习惯把它分成几块:
| 目录 | 作用 | 我常用的子目录 |
|---|---|---|
arch/ |
CPU架构相关代码 | arm/、arm/cpu/armv8/ |
board/ |
板级初始化代码 | board/mycompany/myboard/ |
drivers/ |
外设驱动 | serial/、mmc/、net/ |
include/ |
头文件和配置文件 | configs/、configs/myboard.h |
common/ |
通用功能 | 命令行解析、环境变量 |
cmd/ |
命令实现 | bootm.c、sf.c |
移植时,你主要改的是board/和include/configs/。架构代码基本不动,除非你要支持一颗新CPU。
3.3 U-Boot的配置与编译
U-Boot的配置系统,早期是make *_config,现在统一用Kconfig了。跟Linux内核的配置方式一样。
配置流程
- 选择默认配置:
make myboard_defconfig - 微调配置:
make menuconfig(图形界面) - 编译:
make -j4
举个例子,我最近移植的一块板子,用的是i.MX8M Mini:
# 先看看有哪些现成的配置
ls configs/ | grep imx8mm
# 选一个最接近的
make imx8mm_evk_defconfig
# 打开配置界面,调整DDR频率和串口
make menuconfig
# 编译
export CROSS_COMPILE=aarch64-linux-gnu-
make -j8
小技巧:编译时加上V=1能看到详细编译命令,排查头文件路径问题很有用。
关键配置文件
移植时,你主要关注两个文件:
include/configs/myboard.h——板级配置宏,定义DDR大小、Flash地址、环境变量位置等board/mycompany/myboard/Kconfig——板级Kconfig,让menuconfig能识别你的板子
3.4 U-Boot的移植实战
移植U-Boot,说白了就是让U-Boot在你的板子上跑起来。我总结了一套步骤,这些年一直在用:
第一步:选一个参考板
找一颗同系列CPU的官方评估板。比如你用i.MX8M Mini,就参考imx8mm_evk。复制一份:
cp -r board/freescale/imx8mm_evk board/mycompany/myboard
cp configs/imx8mm_evk_defconfig configs/myboard_defconfig
第二步:改设备树
U-Boot现在也用设备树了。你得根据你的硬件原理图,修改:
- DDR时序参数——这个最坑,配错了直接死机
- 串口引脚——UART的GPIO复用
- 网卡PHY地址——MDIO总线上的地址
- 存储设备——eMMC还是NAND,分区怎么分
第三步:调DDR
我曾经在一个项目上卡了三天,就是因为DDR的ODT配置不对。板子能跑,但跑几分钟就挂。后来用U-Boot的md命令反复读写DDR,才发现某些地址位会出错。
避坑指南:DDR参数不要直接抄参考板。同样的CPU,不同的PCB走线长度,时序参数可能差10%。最好用DDR厂商提供的计算工具重新算一遍。
第四步:验证串口输出
串口是调试的生命线。移植的第一步,就是让U-Boot在串口上打印东西。如果串口没输出,检查:
- 波特率对不对(通常是115200)
- 引脚复用配置
- 时钟频率——串口时钟源对不对
第五步:烧录与启动
编译出来的u-boot.bin,烧到启动介质里。常见方式:
- SD卡:
dd if=u-boot.bin of=/dev/sdb bs=1024 seek=1 - eMMC:通过下载模式烧录
- SPI Flash:用烧录器或JTAG
烧完后上电,看到U-Boot的倒计时提示,恭喜你,移植成功了。
3.5 U-Boot启动流程图
下面这张图,是我每次给团队新人讲U-Boot时必画的。它把整个启动链路串起来了:
这张图里,红色框是命令行入口。你想想看,如果串口没输出,你连红色框都到不了。所以移植的第一步,永远是让串口能打印东西。
3.6 移植后的验证清单
移植完别急着烧系统,先跑一遍这个清单:
| 检查项 | 验证方法 | 常见问题 |
|---|---|---|
| 串口输出 | 上电看打印信息 | 波特率不对、引脚复用错 |
| DDR读写 | md和mw命令 |
时序参数、ODT配置 |
| 存储设备 | mmc dev或sf probe |
分区表、驱动未使能 |
| 网络通信 | ping 192.168.1.1 |
PHY地址、MAC地址 |
| 环境变量保存 | saveenv后重启 |
存储分区偏移量错 |
| 内核引导 | bootm或booti |
设备树不匹配、内核地址错 |
我的习惯:每改一个配置,就编译烧录一次,验证一个功能。不要一次改完所有东西再验证,那样出了问题根本不知道是哪一步引起的。
U-Boot移植,说白了就是三板斧:选参考板、改设备树、调DDR。剩下的都是细节。但细节往往最要命。我见过有人卡在串口输出上整整一周,最后发现是晶振频率配错了。嗯,这种坑,踩过一次就记住了。