Bootloader原理与移植:U-Boot启动流程分析、U-Boot源码结构、板级配置与设备树修改、U-Boot烧写与调试

好,我们进入第二章。Bootloader,说白了就是嵌入式系统的"第一口饭"。CPU上电后,第一个跑的程序就是它。U-Boot是目前最主流的Bootloader,几乎支持所有主流架构。我这些年移植过的板子,十块里有八块用的都是U-Boot。

这一章,我会带你从启动流程、源码结构、板级配置、设备树修改,一直讲到烧写与调试。嗯,内容不少,但每一步都是实战中必须啃下来的硬骨头。

U-Boot启动流程分析

先搞清楚U-Boot是怎么跑起来的。我习惯把启动流程分成两个阶段:SPL(Secondary Program Loader)和U-Boot proper。为什么分两段?因为早期SRAM太小,装不下完整的U-Boot。

核心要点:SPL负责初始化最小硬件(时钟、DDR、串口),然后从存储介质加载U-Boot proper到DDR,跳转执行。

具体流程如下:

  1. CPU上电,从ROM执行固化代码——读取启动介质(SD卡、eMMC、NAND Flash等)的前几个扇区到SRAM。
  2. SPL阶段——初始化时钟、串口、DDR控制器。我在项目中遇到过SPL跑不起来的情况,最后发现是DDR时序参数配错了,板子直接"死"在DDR初始化。
  3. 加载U-Boot proper——SPL从存储介质读取U-Boot主镜像到DDR,然后跳转。
  4. U-Boot proper阶段——初始化更多外设(网卡、存储、USB等),解析环境变量,进入命令行或自动启动内核。

为什么会这样设计?你想想看,如果CPU内部SRAM只有64KB,而U-Boot proper有几百KB,不拆成两段根本放不下。

实战技巧:调试SPL时,可以在串口输出加打印信息,定位卡死位置。我习惯在SPL的board_init_f()和board_init_r()里各加一个puts()。

U-Boot源码结构

U-Boot源码目录看起来复杂,其实有规律可循。我刚开始看的时候也懵,后来总结出一套"按图索骥"的方法。

目录/文件 作用 我常用的场景
arch/ 架构相关代码(arm、riscv、x86等) 修改start.S、lowlevel_init.S
board/ 板级初始化代码 添加新板子时,复制参考板目录修改
configs/ 板级默认配置文件(xxx_defconfig) make xxx_defconfig 生成.config
include/configs/ 板级头文件配置(旧版方式) 定义宏如CONFIG_SYS_TEXT_BASE
dts/ 设备树源文件(.dts、.dtsi) 修改硬件描述,如DDR容量、外设地址
drivers/ 驱动代码(串口、网卡、MMC等) 移植新网卡驱动时重点看
cmd/ 命令行命令实现 添加自定义命令时参考
common/ 通用功能(环境变量、启动逻辑) 修改bootcmd时涉及

我个人习惯,拿到一个新板子,先看board/和configs/目录,找到最接近的参考板。然后对比dts/里的设备树,确认硬件差异。这样能快速定位需要修改的文件。

避坑指南:我曾经在移植时直接复制了另一个厂商的板级目录,结果编译出来的U-Boot在串口初始化就挂了。后来发现是GPIO复用配置不同。所以,复制参考板后,一定要逐行检查board.c里的引脚初始化。

板级配置与设备树修改

板级配置,说白了就是告诉U-Boot你的板子长什么样。现在主流做法是通过设备树(Device Tree)来描述硬件。U-Boot从2016年开始全面转向设备树,我建议你也用这种方式。

修改设备树的典型步骤:

  1. 找到对应.dts文件——在dts/目录下,按厂商和板子名称查找。
  2. 修改内存节点——设置DDR容量和起始地址。我遇到过客户给的DDR容量和实际焊接的不一致,导致U-Boot只能检测到一半内存。
  3. 修改串口节点——确认UART的基地址和时钟频率,否则串口输出乱码或没输出。
  4. 修改网卡节点——配置PHY地址、复位GPIO、时钟等。
  5. 修改存储节点——SD卡、eMMC、NAND Flash的分区和时序参数。

举个例子,修改DDR容量:

// 在.dts文件中
memory@80000000 {
    device_type = "memory";
    reg = <0x0 0x80000000 0x0 0x40000000>;  // 1GB DDR,起始0x80000000
};

嗯,这里要注意:reg属性的第三个值是容量,单位是字节。0x40000000就是1GB。如果板子只有512MB,就改成0x20000000。

个人经验:修改设备树后,一定要用make dtbs重新编译,然后检查生成的.dtb文件大小是否合理。我曾经改错了语法,编译没报错,但生成的dtb只有几十字节,加载后U-Boot直接崩溃。

U-Boot烧写与调试

烧写U-Boot,不同平台方法不同。我以最常用的SD卡启动为例:

  1. 编译U-Boot——make xxx_defconfig && make -j4,生成u-boot.bin或u-boot.img。
  2. 烧写到SD卡——sudo dd if=u-boot.bin of=/dev/sdb bs=1k seek=1。注意seek=1,跳过第一个1KB(分区表保留区)。
  3. 插入SD卡,上电启动——串口工具(如minicom、putty)看输出。

调试U-Boot,我常用的方法:

  • 串口打印——在关键函数加printf()或puts(),定位卡死位置。
  • LED闪烁——如果串口还没初始化,用GPIO控制LED闪烁,判断代码执行到哪一步。
  • JTAG调试——用OpenOCD + GDB,可以单步调试SPL和U-Boot proper。我习惯在board_init_f()设断点。
  • 保存环境变量——saveenv命令,避免每次重启都重新设置IP地址和启动参数。

避坑指南:我曾经在烧写U-Boot时,dd命令的seek参数写错了,写到了分区表位置,结果SD卡在电脑上无法识别。后来我养成了习惯:烧写前先用fdisk -l确认设备名,再用dd if=u-boot.bin of=/dev/sdb bs=1k seek=1 conv=fsync,加conv=fsync确保数据写回。

U-Boot启动流程与板级适配关系图

下面这张图,是我根据多年移植经验总结的。它把启动流程、源码目录、板级配置串在了一起。你移植时,可以对照这张图,一步步排查问题。

U-Boot启动流程与板级适配关系图 U-Boot启动流程与板级适配关系图 阶段一:ROM Code 阶段二:SPL (Secondary Program Loader) 初始化时钟、DDR、串口 从存储介质加载U-Boot proper 跳转到DDR执行 阶段三:U-Boot proper 初始化更多外设(网卡、USB等) 解析环境变量、设备树 进入命令行或启动内核 板级适配关键点 修改board/目录 → 修改dts/设备树 → 修改configs/默认配置 → 编译 → 烧写 → 调试 arch/ (CPU架构) board/ (板级代码) dts/ (设备树) configs/ (默认配置) 💡 移植时,按此图从ROM Code → SPL → U-Boot proper逐阶段验证,配合串口打印定位问题

这张图把整个流程串起来了。你移植时,可以按这个顺序:先确认SPL能不能跑,再看U-Boot proper能不能加载,最后看设备树解析和内核启动。每一步都有对应的源码目录和配置项。