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内核的配置方式一样。

配置流程

  1. 选择默认配置:make myboard_defconfig
  2. 微调配置:make menuconfig(图形界面)
  3. 编译: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时必画的。它把整个启动链路串起来了:

U-Boot启动流程图 U-Boot 启动流程 第一阶段:汇编代码(SPL) 设置异常向量表 初始化时钟/DDR 搬移U-Boot主体 第二阶段:C语言代码 board_init_f() 串口初始化 环境变量加载 驱动枚举 U-Boot 命令行 bootcmd → 启动内核 等待用户输入命令

这张图里,红色框是命令行入口。你想想看,如果串口没输出,你连红色框都到不了。所以移植的第一步,永远是让串口能打印东西。

3.6 移植后的验证清单

移植完别急着烧系统,先跑一遍这个清单:

检查项 验证方法 常见问题
串口输出 上电看打印信息 波特率不对、引脚复用错
DDR读写 md和mw命令 时序参数、ODT配置
存储设备 mmc dev或sf probe 分区表、驱动未使能
网络通信 ping 192.168.1.1 PHY地址、MAC地址
环境变量保存 saveenv后重启 存储分区偏移量错
内核引导 bootm或booti 设备树不匹配、内核地址错

我的习惯:每改一个配置,就编译烧录一次,验证一个功能。不要一次改完所有东西再验证,那样出了问题根本不知道是哪一步引起的。

U-Boot移植,说白了就是三板斧:选参考板、改设备树、调DDR。剩下的都是细节。但细节往往最要命。我见过有人卡在串口输出上整整一周,最后发现是晶振频率配错了。嗯,这种坑,踩过一次就记住了。