2、Bootloader深度解析与移植(上):U-Boot源码结构分析、启动流程详解、板级配置与设备树基础

好,咱们开始聊Bootloader。说实话,很多做嵌入式Linux的朋友,对U-Boot的态度就是「能用就行」。编译一下,烧进去,能启动内核就完事了。但真到了要移植新板子、或者排查启动异常的时候,才发现自己对U-Boot的理解其实很模糊。

我个人习惯,做系统裁剪之前,一定要先把Bootloader吃透。为什么?因为它是硬件和内核之间的桥梁。桥没搭好,后面全是坑。这一章,咱们就深入U-Boot内部,看看它的源码怎么组织、启动流程到底干了什么、板级配置怎么玩、设备树又是怎么回事。

2.1 U-Boot源码结构:别被目录吓到

第一次解压U-Boot源码,看到一堆目录,确实容易懵。我刚开始接触的时候,也是对着ls输出发愣。其实它的结构很清晰,咱们拆开看。

目录/文件 作用 我的经验
arch/ 架构相关代码,ARM、x86、RISC-V都在这里 移植ARM板子,90%的时间泡在arch/arm/里
board/ 板级特定代码,厂商或具体板子的目录 这里放的是「这块板子独有的东西」
common/ 通用功能,比如命令行、环境变量、HUSH解析器 别乱改,除非你知道后果
drivers/ 各种驱动:网卡、串口、MMC、NAND、USB 裁剪时,这里是大头,用不到的驱动全删掉
include/ 头文件,特别是configs/下的板级配置头文件 移植第一步,就是在这里新建一个头文件
dts/ 设备树源文件(Device Tree Source) 新板子必须有的东西,后面细说
tools/ 主机端工具,比如mkimage 打包内核镜像时常用

你看,核心就这几个。我个人习惯,拿到一个新板子,先看board/和include/configs/,这两个目录决定了板子能不能跑起来。

小技巧: 如果你要基于某款参考板做移植,先找到参考板的include/configs/头文件,复制一份,改名字。这是最稳妥的起步方式。

2.2 启动流程详解:从复位到命令行

U-Boot的启动流程,说白了就是一段「从汇编到C」的旅程。咱们以ARM64为例,捋一遍关键步骤。

  1. 复位向量(Reset Vector):CPU上电后,从0x00000000或0xFFFF0000开始执行。U-Boot在这里放了一条跳转指令,跳到_start。
  2. 汇编阶段(arch/arm/cpu/armv8/start.S):设置CPU为SVC模式、关中断、初始化异常向量表、设置栈指针。嗯,这里要注意,栈指针必须指向一段可用的RAM,否则后面的C代码一调用就崩。
  3. 低级别初始化(lowlevel_init):初始化时钟、DDR控制器。这一步很关键,DDR没配好,后面所有操作都是空中楼阁。我在项目中遇到过,DDR时序参数配错,板子死活跑不起来,最后用示波器一根一根量地址线才找到问题。
  4. 重定位(Relocation):U-Boot把自己从Flash或ROM复制到DDR中。为什么要这么做?因为DDR里运行速度快,而且可以修改代码段(比如打补丁)。
  5. 进入C世界(board_init_f -> board_init_r):初始化串口、定时器、I2C、网卡等外设。然后进入主循环,等待用户输入命令。

为什么会分成board_init_f和board_init_r?说白了,f阶段是在Flash里跑的,资源受限;r阶段是在DDR里跑的,可以放开手脚。这个设计很巧妙,我每次给别人讲的时候,都会强调这个分界线。

核心要点: 启动流程中,DDR初始化是第一个分水岭。DDR没起来,后面全是白搭。调试时,建议先在串口输出一个字符,确认CPU和串口活了,再搞DDR。

2.3 板级配置:从头文件到Kconfig

U-Boot的板级配置,经历过一次「文艺复兴」。早期版本全靠头文件宏定义,现在逐步迁移到Kconfig + 设备树。但很多老牌芯片厂商,还是喜欢用头文件方式。咱们两种都得会。

2.3.1 传统方式:include/configs/头文件

打开一个典型的板级配置头文件,你会看到一堆#define。比如:

/*  include/configs/myboard.h */
#define CONFIG_SYS_TEXT_BASE        0x80000000
#define CONFIG_NR_DRAM_BANKS        1
#define CONFIG_SYS_SDRAM_BASE       0x80000000
#define CONFIG_SYS_SDRAM_SIZE       0x20000000  /* 512 MB */
#define CONFIG_CONS_INDEX           1
#define CONFIG_BAUDRATE             115200
#define CONFIG_BOOTCOMMAND          "fatload mmc 0:1 0x82000000 kernel.img; bootm 0x82000000"

这些宏,定义了板子的内存布局、串口参数、启动命令。我刚开始移植时,最头疼的就是CONFIG_SYS_TEXT_BASE。这个值必须和链接脚本里的地址一致,否则U-Boot重定位时会跑飞。

避坑指南: 我曾经在移植一块i.MX6ULL板子时,CONFIG_SYS_TEXT_BASE设错了,结果U-Boot启动到一半就死机。查了两天,最后发现是链接地址和实际加载地址不匹配。记住:这个值必须等于你的U-Boot在DDR中的加载地址。

2.3.2 现代方式:Kconfig + 设备树

新版本的U-Boot,推荐用Kconfig来配置。你运行make menuconfig,选好芯片型号、板子型号、驱动选项,系统会自动生成.config文件。这种方式更直观,也更容易管理。

举个例子,选择ARM64架构的QEMU虚拟板:

$ make qemu_arm64_defconfig
$ make menuconfig
# 在这里可以开关各种功能
$ make -j4

我个人更喜欢Kconfig方式,因为不用记那么多宏名字。但说实话,很多工业级产品还在用老方式,因为稳定。你想想看,一个产品量产了好几年,谁会为了「配置方式更现代」去升级Bootloader?

2.4 设备树基础:让U-Boot认识你的硬件

设备树(Device Tree),是ARM Linux生态里绕不开的东西。它的作用,说白了就是「用数据结构描述硬件」。U-Boot从2014年开始,逐步引入设备树支持。现在,U-Boot本身也使用设备树来配置板级信息。

2.4.1 设备树文件结构

一个典型的设备树文件(.dts)长这样:

/*  arch/arm/dts/myboard.dts */
/dts-v1/;

#include "imx6ul.dtsi"

/ {
    model = "My Custom i.MX6UL Board";
    compatible = "mycompany,myboard", "fsl,imx6ul";

    chosen {
        stdout-path = &uart1;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB */
    };

    &uart1 {
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_uart1>;
        status = "okay";
    };
};

看到compatible属性了吗?它就是U-Boot和内核用来匹配驱动的「身份证」。U-Boot在启动时,会读取设备树,根据compatible找到对应的驱动初始化函数。

2.4.2 U-Boot中的设备树使用

U-Boot使用设备树,主要有两个场景:

  • U-Boot自身使用设备树:U-Boot编译时,会把设备树二进制(.dtb)附加到U-Boot镜像末尾。启动时,U-Boot解析这个.dtb,获取内存大小、串口地址、时钟频率等信息。
  • 传递给内核:U-Boot启动内核时,会把.dtb的地址通过寄存器(ARM64是x0)传递给内核。内核解析设备树,枚举所有硬件设备。

我在项目中遇到过一个问题:U-Boot里设备树的内存节点和实际硬件不一致,导致内核只识别了部分内存。排查方法很简单,在U-Boot命令行里输入fdt list /memory,看看reg属性对不对。

调试命令: U-Boot提供了一组fdt命令,可以查看和修改设备树。比如fdt addr $fdt_addr设置设备树地址,fdt list /列出根节点。这些命令在调试时非常有用。

2.5 本章知识结构图

下面这张图,把U-Boot源码结构、启动流程、板级配置和设备树的关系串起来了。你可以把它当作本章的「导航地图」。

U-Boot 知识结构图 U-Boot 深度解析知识结构 U-Boot 源码结构 arch/ board/ drivers/ include/configs/ dts/ 启动流程 复位向量 → _start lowlevel_init → DDR初始化 重定位 → board_init_r 板级配置 传统:头文件宏定义 现代:Kconfig + 设备树 设备树基础 .dts → .dtb → 二进制 U-Boot解析 → 内核传递 核心要点 • DDR初始化是启动分水岭 • TEXT_BASE必须与链接地址一致 • 设备树是U-Boot与内核的桥梁 调试建议 • 先用串口输出确认CPU存活 • fdt命令查看设备树内容 • 示波器量DDR时序参数

这张图里,我把四个核心模块放在同一层,用箭头表示它们之间的关系。源码结构是基础,启动流程是主线,板级配置是定制手段,设备树是数据载体。你移植一个新板子,其实就是在这四个模块里填内容。

2.6 本章小结

这一章,咱们把U-Boot的源码结构、启动流程、板级配置和设备树基础过了一遍。这些东西,是后续做系统裁剪和移植的「内功」。你想想看,如果连Bootloader怎么启动、配置在哪改、设备树怎么用都不清楚,后面裁剪内核、优化驱动,根本无从下手。