U-Boot 深度解析:从原理到实践 - 详解
U-Boot 深度解析:从原理到实践
1 U-Boot 概述与背景介绍
U-Boot(Universal Boot Loader)是嵌入式系统领域中最流行、功能最完善的 BootLoader 软件之一. 作为嵌入式 Linux 系统的"引导者", U-Boot 负责在操作系统运行之前完成关键的硬件初始化和系统引导工作. 其名称"Universal"体现了它的设计目标——支持多种架构、多种平台和多种操作系统
从历史角度看, U-Boot 起源于 2000 年的 PPCBoot 项目, 由德国 DENX 软件工程中心的 Wolfgang Denk 主导开发. 它后来融合了 8xxROM 和 FADSROM 的功能, 逐步发展成为今天支持 ARM、PowerPC、MIPS、x86、RISC-V 等多种处理器架构的"通用"BootLoader. 根据 2023 年嵌入式开源峰会上的数据, U-Boot 项目每年有约 6000 次提交, 拥有约 300 万行 C 代码, 支持超过 21 种 RISC-V 开发板以及大量的 ARM 平台
在嵌入式系统中, U-Boot 的地位相当于 PC 系统中的 BIOS 或 UEFI. 如同 PC 启动时首先运行 BIOS 一样, 嵌入式系统上电后首先执行 U-Boot. U-Boot 负责初始化处理器、设置时钟、配置内存控制器、检测 DRAM, 然后初始化必要的硬件设备如串口、网络、存储设备等, 最后加载操作系统内核并将控制权转交给内核
为了更清晰地理解 U-Boot 在嵌入式系统中的地位, 下表对比了不同 BootLoader 的特点:
表:常见 BootLoader 对比
| BootLoader | 支持架构 | 主要特点 | 使用场景 |
|---|---|---|---|
| U-Boot | ARM, PowerPC, MIPS, x86, RISC-V 等 | 功能全面, 支持多种设备、文件系统和协议 | 嵌入式 Linux 系统, 工业控制, 网络设备 |
| GRUB | x86, x86_64 | 用户友好, 支持多系统引导 | PC 服务器, 桌面系统 |
| Coreboot | x86, ARM, RISC-V | 开源, 快速启动 | Chrome OS, 部分笔记本 |
| Barebox | ARM, x86, MIPS, RISC-V | 模块化设计, 启动速度快 | 汽车电子, 工业自动化 |
U-Boot 的设计遵循"通用性"原则, 其核心任务是"在任何设备上启动任何操作系统". 这一目标通过高度可移植的架构和丰富的功能集来实现. U-Boot 不仅支持 Linux, 还支持 VxWorks、QNX、Windows Embedded 等多种嵌入式操作系统, 展现了其真正的通用性
2 U-Boot 启动流程深入分析
U-Boot 的启动过程是一个精心设计的序列化过程, 涉及从底层硬件初始化到高级命令行交互的完整流程. 理解这一流程对于深入掌握 U-Boot 的工作原理至关重要
2.1 两阶段启动模型
U-Boot 采用经典的两阶段启动模型, 这种设计主要是为了平衡硬件初始化的特殊性和软件功能的可移植性. 第一阶段使用汇编语言编写, 直接与硬件交互, 完成最基础的初始化;第二阶段使用 C 语言编写, 实现更复杂的功能
Stage1:汇编阶段
第一阶段从处理器复位向量开始, 通常在 cpu/<arch>/start.S 文件中实现. 以 ARM 架构为例, 启动流程始于 _start 标签, 该标签在链接脚本中被指定为入口点. 这一阶段的主要任务包括:
- 设置异常向量表:处理器在启动时已知的异常(如复位、未定义指令、中断等)需要明确的处理函数入口点
- 切换到特权模式:将处理器置于 Supervisor (SVC) 模式, 确保对系统资源的完全访问权限
- 关闭看门狗:防止在初始化过程中看门狗定时器超时导致系统复位
- 关闭中断:在初始化完成前避免中断干扰
- 设置系统时钟:配置 CPU 和总线时钟, 为后续操作提供时序基础
- 初始化内存控制器:配置 DRAM 控制器, 为代码重定位做准备
- 设置栈指针:为运行 C 代码准备环境
- 代码重定位:将 U-Boot 自身从存储设备(如 NOR Flash)复制到 RAM 中
Stage2:C 语言阶段
第二阶段从跳转到 C 语言入口函数开始, 通常是 board_init_f() 和 board_init_r(). 这一阶段使用 C 语言实现, 提供了更好的可读性和可移植性, 主要任务包括:
- 完整的硬件初始化:初始化串口、网络、存储设备等外围硬件
- 环境变量初始化:设置或从持久化存储加载环境变量
- 设备树处理:解析和处理设备树 blob(如果使用设备树)
- 命令解析:进入主循环, 处理自动启动序列或命令行交互
2.2 启动流程详解
下图展示了 U-Boot 的完整启动流程:

启动过程中的关键函数调用顺序如下图所示, 这反映了 U-Boot 从最基本的硬件初始化到复杂功能执行的渐进过程:

2.3 代码重定位机制
代码重定位是 U-Boot 启动过程中的关键步骤. 在许多嵌入式系统中, U-Boot 最初从 ROM 或 Flash 运行, 这些存储介质通常访问速度较慢, 且不支持写操作(如 XIP). 重定位过程将 U-Boot 自身复制到 RAM 中, 从而提高执行速度并允许修改代码段
重定位的实现涉及以下几个关键点:
- 位置无关代码:第一阶段代码必须设计为位置无关码(PIC), 使其能够在加载到不同地址时正常执行
- 重定位偏移计算:通过比较链接时地址和运行时地址确定重定位的偏移量
- 符号表重定位:修正全局变量和函数指针的地址, 使其指向新的位置
以下是 ARM 架构中重定位的关键代码片段(基于 cpu/arm920t/start.S):
relocate:
adr r0, _start /* r0 存储当前代码位置 */
ldr r1, _TEXT_BASE /* r1 存储链接时指定地址 */
cmp r0, r1 /* 比较是否已经在RAM中 */
beq stack_setup /* 如果相等则跳过重定位 */
ldr r2, _armboot_start
ldr r3, _bss_start
sub r2, r3, r2 /* r2 得到代码段大小 */
add r2, r0, r2 /* r2 得到要复制代码的末尾地址 */
copy_loop:
ldmia r0!, {r3-r10} /* 从源地址[r0]复制8个字 */
stmia r1!, {r3-r10} /* 复制到目的地址[r1] */
cmp r0, r2 /* 检查是否到达末尾 */
ble copy_loop /* 循环复制 */
这段汇编代码实现了将 U-Boot 代码从当前位置(r0)复制到链接时指定的地址(r1)的过程. 循环每次复制 8 个字(32 字节), 以提高复制效率
2.4 特殊启动模式:SPL 和 TPL
对于内存受限或启动要求严格的系统, U-Boot 引入了 SPL(Secondary Program Loader)和 TPL(Tertiary Program Loader)的概念. SPL 是一个精简的 U-Boot 版本, 仅包含最基本的初始化功能, 用于加载完整版的 U-Boot
SPL 的使用场景包括:
- 内存限制:当芯片内 SRAM 容量不足以容纳完整 U-Boot 时, 先用 SPL 初始化外部 DRAM
- 安全启动:SPL 可以包含安全验证代码, 确保后续加载的固件经过授权
- 快速启动:在某些应用场景中, SPL 可以直接加载操作系统内核, 跳过完整 U-Boot
SPL 的编译通过 CONFIG_SPL_BUILD 定义区分, 只有预定位阶段在 SPL 构建上运行, 它执行最小化的初始化后加载完整的 U-Boot 映像
3 U-Boot 代码框架与核心模块
U-Boot 的代码框架经过多年发展, 形成了一套高度模块化、可移植性极强的架构. 理解这一框架对于深入掌握 U-Boot 和进行二次开发至关重要
3.1 目录结构分析
U-Boot 源代码采用按功能分层的目录结构, 主要目录及其职责如下:
- arch/:架构相关代码, 按处理器架构细分(arm, powerpc, mips, x86 等)
- board/:板级支持包, 按厂商和板卡型号细分
- common/:通用功能, 包括命令实现、环境变量处理等
- drivers/:设备驱动程序, 支持各种外设控制器
- doc/:文档, 使用 Sphinx 系统构建
- include/:头文件, 包含各类平台配置和通用定义
- lib/:通用库函数, 包括压缩算法、字符串操作等
- net/:网络协议栈, 支持 TFTP、NFS、HTTP 等
- fs/:文件系统支持, 包括 FAT、EXT4、JFFS2 等
- tools/:工具程序, 用于镜像处理、签名验证等
表:U-Boot 主要目录功能说明
| 目录 | 主要功能 | 典型文件 |
|---|---|---|
| arch/ | 处理器架构特定代码 | arch/arm/cpu/armv8/start.S |
| board/ | 板级特定初始化和配置 | board/freescale/imx8mp_evk/ |
| common/ | 通用命令和功能 | common/cmd_bootm.c |
| drivers/ | 外设驱动程序 | drivers/mmc/mmc.c |
| net/ | 网络协议实现 | net/tftp.c |
| fs/ | 文件系统支持 | fs/fat/fat.c |
| lib/ | 通用库函数 | lib/rsa/rsa-verify.c |
3.2 构建系统:Kconfig 和 Makefile
U-Boot 使用与 Linux 内核相似的构建系统, 基于 Kconfig 和 Makefile. 这提供了统一的配置接口和灵活的编译选项管理
Kconfig 系统:
- 提供层次化的配置菜单
- 处理配置项间的依赖关系
- 生成自动配置头文件(
include/config.h) - 支持板级定义默认配置(
defconfig)
构建过程:
- 选择默认配置:
make <board>_defconfig - 定制配置:
make menuconfig - 编译:
make CROSS_COMPILE=aarch64-linux-gnu-
近年来, U-Boot 项目清除了散布在头文件中的 #define 配置语句, 现在有一个文本文件描述了配置, 这意味着完全可以不需要任何跟特定 board 关联起来的 config.h 文件
3.3 模块依赖关系
U-Boot 的各个模块之间存在明确的依赖关系, 下图展示了主要模块之间的依赖关系:

这种模块化设计使得 U-Boot 具有良好的可扩展性和可维护性. 开发者可以针对新的硬件平台, 实现特定的架构支持和板级支持包, 而无需修改核心代码
4 核心数据结构与机制
U-Boot 的功能实现依赖于一系列精心设计的核心数据结构和机制. 理解这些数据结构对于深入分析 U-Boot 的工作原理和进行定制开发至关重要
4.1 全局数据变量
U-Boot 使用两个重要的全局数据结构来管理系统的运行时数据:gd_t(全局数据)和 bd_t(板级数据)
gd_t(全局数据):gd_t 结构存储 U-Boot 运行所需的全局数据, 定义在 include/asm-arm/global_data.h:
typedef struct global_data {
bd_t *bd; /* 板级数据指针 */
unsigned long flags; /* 状态标志位 */
unsigned long baudrate; /* 控制台波特率 */
unsigned long have_console; /* 控制台初始化标志 */
unsigned long reloc_off; /* 重定位偏移量 */
unsigned long env_addr; /* 环境变量地址 */
unsigned long env_valid; /* 环境变量CRC校验标志 */
unsigned long fb_base; /* 帧缓冲区基地址 */
#ifdef CONFIG_VFD
unsigned char vfd_type; /* 显示设备类型 */
#endif
void **jt; /* 跳转表 */
} gd_t;
bd_t(板级数据):bd_t 结构包含与特定硬件平台相关的参数, 定义在 include/asm-arm/u-boot.h:
typedef struct bd_info {
int bi_baudrate; /* 控制台波特率 */
unsigned long bi_ip_addr; /* IP地址 */
unsigned char bi_enetaddr[6]; /* MAC地址 */
struct environment_s *bi_env;
ulong bi_arch_number; /* 板卡唯一ID */
ulong bi_boot_params; /* 启动参数地址 */
struct {
ulong start;
ulong size;
} bi_dram[CONFIG_NR_DRAM_BANKS]; /* DRAM配置 */
#ifdef CONFIG_HAS_ETH1
unsigned char bi_enet1addr[6]; /* 第二以太网口MAC */
#endif
} bd_t;
这两个结构在内存中的布局如下图所示:

全局数据指针 gd 通过寄存器 r8(ARM架构)直接访问, 这样可以在任何函数中快速访问全局数据, 无需复杂的参数传递. 声明方式为:
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")
在需要使用 gd 指针的函数中, 只需声明 DECLARE_GLOBAL_DATA_PTR 即可直接访问全局数据
4.2 设备管理
U-Boot 使用设备模型来管理系统中的各种硬件设备. 设备模型提供了一致的接口来初始化和操作设备
设备结构体:device_t 结构定义在 include/devices.h 中:
typedef struct {
int flags; /* 设备标志:输入/输出/系统 */
int ext; /* 支持的扩展 */
char name[16]; /* 设备名称 */
/* 通用函数 */
int (*start) (void); /* 启动设备 */
int (*stop) (void); /* 停止设备 */
/* 输出函数 */
void (*putc) (const char c); /* 输出字符 */
void (*puts) (const char *s); /* 输出字符串 */
/* 输入函数 */
int (*tstc) (void); /* 测试是否有字符可用 */
int (*getc) (void); /* 获取字符 */
/* 其他函数 */
void *priv; /* 私有数据 */
} device_t;
U-Boot 维护一个标准 IO 设备数组 stdio_devices[], 用于管理控制台输入输出设备. 当调用 printf() 等标准 IO 函数时, 实际上是通过 stdio_devices 数组中对应设备的 IO 函数进行操作
4.3 命令系统
U-Boot 的命令系统提供了一种灵活的机制来扩展交互功能. 每个命令对应一个 cmd_tbl_t 结构:
struct cmd_tbl_s {
char *name; /* 命令名称 */
int maxargs; /* 最大参数数 */
int repeatable; /* 是否可重复执行 */
int (*cmd)(struct cmd_tbl_s *, int, int, char * const []); /* 命令函数 */
char *usage; /* 简短用法说明 */
#ifdef CONFIG_SYS_LONGHELP
char *help; /* 详细帮助信息 */
#endif
#ifdef CONFIG_AUTO_COMPLETE
int (*complete)(int argc, char * const argv[], char last_char, int maxv, char *cmdv[]);
#endif
};
typedef struct cmd_tbl_s cmd_tbl_t;
命令通过 U_BOOT_CMD 宏注册到系统中:
#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help) \
cmd_tbl_t __u_boot_cmd_##_name = { #_name, _maxargs, _rep, _cmd, _usage, _help }
这种设计使得添加新命令变得非常简单, 开发者只需定义命令处理函数并使用宏注册即可
4.4 环境变量系统
环境变量是 U-Boot 中用于存储配置参数的重要机制, 它使用简单的"名称-值"对形式存储数据. 环境变量通常存储在非易失性存储器中, 如 Flash、EEPROM 等
环境变量相关操作:
- printenv:打印环境变量
- setenv:设置环境变量
- saveenv:保存环境变量到持久存储
- getenv:获取环境变量值(代码内部使用)
常用的环境变量包括:
- bootdelay:自动启动延迟时间
- baudrate:串口控制台波特率
- bootcmd:自动启动时执行的命令
- bootargs:传递给内核的启动参数
- ipaddr:本地 IP 地址
- serverip:TFTP 服务器 IP 地址
4.5 核心数据结构关系
以下 Mermaid 类图展示了 U-Boot 核心数据结构之间的关系:

这些核心数据结构共同构成了 U-Boot 的运行时环境, 管理系统状态、硬件设备和用户交互. 理解它们之间的关系对于深入分析 U-Boot 的工作原理至关重要
5 设备树(Device Tree)机制
设备树(Device Tree)是现代 U-Boot 和 Linux 内核中用于描述硬件配置数据的重要机制. 它解决了传统嵌入式系统中硬件信息硬编码的问题, 提供了更灵活、更可维护的硬件描述方法
5.1 设备树基本概念
设备树是一种树形数据结构, 用于描述系统的硬件组成, 包括 CPU、内存、总线、外设等. 它采用人类可读的文本格式(.dts)编写, 然后编译成二进制格式(.dtb)供固件和内核使用
设备树的主要组成部分:
- 节点:表示一个硬件设备或容器
- 属性:描述节点的特征, 键值对形式
- 兼容性:指定设备与哪个驱动程序兼容
- 地址:描述设备在总线上的地址映射
以下是一个简单的设备树示例:
/dts-v1/;
/ {
model = "My Board";
compatible = "my-board", "vendor,board";
cpus {
cpu@0 {
compatible = "arm,cortex-a53";
device_type = "cpu";
reg = <0x0>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
uart0: serial@10000000 {
compatible = "ns16550a";
reg = <0x10000000 0x1000>;
clock-frequency = <1843200>;
status = "okay";
};
};
5.2 U-Boot 中的设备树使用
U-Boot 在 2011 年左右引入了设备树, 与 Linux 内核差不多同时. 设备树在 U-Boot 中的使用主要包括以下几个方面:
设备树传递过程:
U-Boot 接收来自前级引导程序(如 SPL)的设备树, 可能对其进行修改, 然后传递给内核. 整个过程如下图所示:

设备树操作命令:
U-Boot 提供了一系列命令用于操作设备树:
- fdt addr:设置设备树在内存中的地址
- fdt print:打印设备树节点和属性
- fdt set:修改设备树属性值
- fdt move:将设备树移动到新地址
- fdt resize:调整设备树大小
例如, 通过 U-Boot 命令行修改设备树中某个设备的状态:
=> fdt addr 0x83000000
=> fdt print /soc/usb@210000
usb@210000 {
compatible = "vendor,usb";
reg = <0x210000 0x1000>;
status = "disabled";
};
=> fdt set /soc/usb@210000 status "okay"
=> fdt print /soc/usb@210000
usb@210000 {
compatible = "vendor,usb";
reg = <0x210000 0x1000>;
status = "okay";
};
这种动态修改设备树的能力在调试和系统配置中非常有用
5.3 向内核传递参数
U-Boot 通过设备树向 Linux 内核传递参数, 这是通过特定的节点和属性实现的. 主要参数包括:
- chosen 节点:包含内核启动参数、初始内存磁盘地址等
- memory 节点:描述系统内存布局
- aliases 节点:为设备提供别名
U-Boot 中设置内核参数的代码通常在 lib/fdtdec.c 和相关文件中实现. 关键的设置过程包括:
int ft_setup(void *blob, bd_t *bd)
{
int node;
/* 设置 chosen 节点中的 bootargs */
node = fdt_path_offset(blob, "/chosen");
if (node < 0) {
node = fdt_add_subnode(blob, 0, "chosen");
if (node < 0)
return node;
}
fdt_setprop(blob, node, "bootargs",
bd->bi_bootargs, strlen(bd->bi_bootargs) + 1);
/* 设置内存节点 */
node = fdt_path_offset(blob, "/memory");
if (node < 0) {
node = fdt_add_subnode(blob, 0, "memory");
if (node < 0)
return node;
}
fdt_setprop_string(blob, node, "device_type", "memory");
fdt_setprop(blob, node, "reg", &bd->bi_dram[0].start,
sizeof(bd->bi_dram[0]) * CONFIG_NR_DRAM_BANKS);
return 0;
}
5.4 设备树与驱动绑定
U-Boot 使用与 Linux 内核相似的驱动模型, 设备树中的 compatible 属性用于将设备与驱动程序绑定. 当 U-Boot 初始化时, 它会扫描设备树中的节点, 并为每个节点查找匹配的驱动程序
驱动绑定过程:
- 解析设备树节点
- 查找与
compatible属性匹配的驱动程序 - 创建设备实例并初始化
- 将设备添加到设备列表
这种机制使得驱动程序可以独立于具体的硬件平台, 提高了代码的可重用性
6 工具命令与调试方法
熟练掌握 U-Boot 的工具命令和调试方法对于开发和排查问题至关重要. U-Boot 提供了丰富的命令和调试手段, 帮助开发者理解系统状态和诊断问题
6.1 常用命令分类
U-Boot 的命令可以分为多个功能类别, 下表列出了主要的命令分类和典型示例:
表:U-Boot 主要命令分类
| 类别 | 命令示例 | 功能描述 |
|---|---|---|
| 信息查询 | bdinfo, printenv, version | 显示系统信息、环境变量、版本信息 |
| 内存操作 | md, mm, mw, cmp | 显示、修改、比较内存内容 |
| 存储设备 | mmc, fatinfo, ext4load | 访问 MMC、FAT、EXT4 等存储设备 |
| 文件传输 | tftp, nfs, loadb | 通过网络或串口加载文件 |
| 环境变量 | setenv, saveenv | 设置和保存环境变量 |
| 引导控制 | boot, bootm, bootz | 引导操作系统镜像 |
| 设备树 | fdt, flatten | 设备树操作命令 |
| 测试诊断 | mtest, i2c, spi | 内存测试和总线诊断 |
6.2 调试手段与方法
U-Boot 提供了多种调试手段, 帮助开发者诊断启动问题和硬件故障
串口调试:
最基本的调试方法是通过串口输出信息. 确保配置正确的波特率和数据格式, 可以通过 printenv baudrate 查看当前设置
环境变量调试:
通过设置特定的环境变量可以启用调试输出:
=> setenv bootargs ${bootargs} earlyprintk console=ttyS0,115200
=> setenv verbosity 7
设备树调试:
U-Boot 提供了多种设备树相关的调试方法:
- dtdiff 工具:对比两个 dtb 文件的差异
- kernel device-tree base:查看
/sys/firmware/devicetree/base目录下的设备树信息 - U-Boot fdt command:使用
fdt命令动态修改设备树 - dtc 工具:将 dtb 文件反汇编为 dts 文本
代码调试技巧:
- 使用
debug()函数代替printf, 可以通过编译选项控制输出 - 在代码中插入
__asm__("bkpt #0");设置断点(ARM架构) - 使用
gd->flags和gd->have_console检查全局状态
6.3 调试命令示例
以下是一些实用的调试命令示例:
内存测试:
=> mtest 100000 200000
Pattern 00000000 Writing... Reading...
Pattern 55555555 Writing... Reading...
I2C 总线扫描:
=> i2c dev 0 # 选择 I2C 总线
=> i2c probe # 扫描总线上的设备
Valid chip addresses: 50 68
设备树查看:
=> fdt addr 0x83000000 # 设置设备树地址
=> fdt print /soc/i2c@210000 # 查看特定节点
=> fdt list /chosen # 查看 chosen 节点
网络调试:
=> setenv ipaddr 192.168.1.2
=> setenv serverip 192.168.1.1
=> tftp 0x81000000 uImage # 通过 TFTP 加载内核
表:常用调试方法对比
| 调试方法 | 适用场景 | 优点 | 局限性 |
|---|---|---|---|
| 串口输出 | 基本启动问题 | 简单直接, 无需额外工具 | 信息量有限, 可能影响时序 |
| 环境变量 | 配置参数调试 | 灵活, 可动态修改 | 需要系统基本正常运行 |
| 设备树操作 | 硬件配置问题 | 可实时修改硬件配置 | 需要设备树知识 |
| 网络调试 | 大文件传输 | 速度快, 便于远程调试 | 需要网络硬件正常工作 |
| 内存测试 | 硬件稳定性 | 检测内存硬件故障 | 测试期间系统不可用 |
6.4 高级调试功能
U-Boot 还提供了一些高级调试功能:
跟踪功能:
U-Boot 支持函数跟踪功能, 可以记录函数的进入和退出情况, 并将其存储在内存缓冲区中, 以便后续导出在另一个系统中进行分析. 跟踪功能在 U-Boot 中已经存在了一段时间了, 但最近进行了更新, 以使用 trace-cmd 接口
模拟器测试:
U-Boot 使用 QEMU 等模拟器进行测试, 因此可以在 Linux 工作站上模拟 SPI 闪存测试 U-Boot. 这样就可以编写各种无需硬件即可运行的测试;模拟器可以更改"硬件"以检查需要验证的内容
7 简单实例:添加自定义命令
为了更深入理解 U-Boot 的工作原理, 本节将通过一个具体的实例演示如何为 U-Boot 添加自定义命令. 这个示例将展示从命令定义到功能实现的完整过程
7.1 创建命令文件
首先, 我们在 U-Boot 源代码的 common/cmd_hello.c 文件中实现一个简单的 “hello” 命令:
#include <common.h>
#include <command.h>
static int do_hello(struct cmd_tbl_s *cmdtp, int flag, int argc, char * const argv[])
{
int i;
printf("Hello from U-Boot!\n");
/* 打印所有参数 */
printf("Arguments: %d\n", argc);
for (i = 0; i < argc; i++) {
printf(" argv[%d] = %s\n", i, argv[i]);
}
/* 访问全局数据 */
DECLARE_GLOBAL_DATA_PTR;
printf("U-Boot relocation offset: 0x%lx\n", gd->reloc_off);
printf("Board name: %s\n", gd->bd->bi_arch_number);
return 0;
}
U_BOOT_CMD(
hello, /* 命令名称 */
CONFIG_SYS_MAXARGS, /* 最大参数数 */
1, /* 是否可重复(输入空行重复执行)*/
do_hello, /* 命令函数 */
"print a hello message", /* 简短用法 */
"[args...]\n" /* 详细帮助 */
" - print hello message with arguments\n"
" and global data info"
);
这个简单的命令实现了以下功能:
- 打印欢迎信息
- 显示所有传入的参数
- 访问全局数据并显示重定位偏移和板卡信息
7.2 修改 Makefile 和 Kconfig
为了让构建系统包含我们的新命令, 需要修改相应的配置文件
在 common/Makefile 中添加编译条目:
obj-$(CONFIG_CMD_HELLO) += cmd_hello.o
在 common/Kconfig 中添加配置选项:
config CMD_HELLO
bool "hello command"
default y
help
This enables the 'hello' command which prints a
friendly greeting and demonstrates U-Boot command
implementation. The command shows arguments and
accesses global data structures.
7.3 配置和编译
完成代码添加后, 需要配置和编译 U-Boot:
# 进入U-Boot源码目录
cd u-boot
# 选择板级配置
make <board_name>_defconfig
# 启用hello命令(如果未默认启用)
make menuconfig
# 在 Command line interface 下找到 hello command 并启用
# 编译U-Boot
make CROSS_COMPILE=aarch64-linux-gnu-
7.4 测试命令
将编译得到的 U-Boot 镜像烧写到目标板并启动, 在 U-Boot 命令行中测试新命令:
=> hello arg1 arg2 arg3
Hello from U-Boot!
Arguments: 4
argv[0] = hello
argv[1] = arg1
argv[2] = arg2
argv[3] = arg3
U-Boot relocation offset: 0x0
Board name: 0x00000000
7.5 扩展功能:添加环境变量操作
为了展示更复杂的功能, 我们可以扩展 hello 命令, 使其能够读取和修改环境变量:
static int do_hello_enhanced(struct cmd_tbl_s *cmdtp, int flag, int argc, char * const argv[])
{
char *env_val;
printf("Hello from enhanced U-Boot command!\n");
if (argc > 1) {
/* 尝试读取环境变量 */
env_val = getenv(argv[1]);
if (env_val) {
printf("Environment %s = %s\n", argv[1], env_val);
} else {
printf("Environment %s not found\n", argv[1]);
}
}
if (argc > 2) {
/* 设置环境变量 */
printf("Setting environment %s to %s\n", argv[1], argv[2]);
setenv(argv[1], argv[2]);
}
return 0;
}
U_BOOT_CMD(
hello, CONFIG_SYS_MAXARGS, 1, do_hello_enhanced,
"enhanced hello command with env access",
"[name [value]]\n"
" - print hello and access environment variables\n"
" if name provided, show env value\n"
" if value provided, set env variable"
);
测试扩展后的命令:
=> hello bootcmd
Hello from enhanced U-Boot command!
Environment bootcmd = run distro_bootcmd
=> hello myvar myvalue
Hello from enhanced U-Boot command!
Environment myvar not found
Setting environment myvar to myvalue
=> hello myvar
Hello from enhanced U-Boot command!
Environment myvar = myvalue
这个实例演示了 U-Boot 命令的基本结构和常见操作, 包括参数处理、全局数据访问和环境变量操作. 通过这些基础, 可以开发出更复杂的自定义命令来满足特定需求
8 核心框架剖析与总结
通过前文对 U-Boot 各个方面的深入分析, 我们现在可以对其整体架构和设计哲学进行系统性的总结. U-Boot 作为一个成熟的开源 BootLoader, 其设计体现了嵌入式系统引导程序的典型特征和发展趋势
8.1 整体架构回顾
U-Boot 的核心架构可以概括为以下层次模型:

这种分层架构使得 U-Boot 具有良好的可扩展性和可维护性. 每一层都提供清晰的接口, 允许开发者针对特定需求进行定制
8.2 设计哲学与特点
U-Boot 的设计体现了以下几个核心原则:
1. 通用性与可移植性
U-Boot 的"通用"设计理念使其能够支持多种处理器架构和硬件平台. 通过清晰的抽象层和配置系统, 将通用代码与平台特定代码分离, 大大提高了代码的可重用性
2. 模块化与可配置性
U-Boot 采用高度模块化的设计, 每个功能组件都可以独立配置和编译. 通过 Kconfig 系统, 开发者可以精确控制包含哪些功能, 从而优化镜像大小和功能集
3. 渐进式初始化
U-Boot 的启动过程采用渐进式初始化策略, 从最基本的硬件初始化开始, 逐步扩展到复杂的外设和功能. 这种"由简到繁"的方法确保了启动过程的可靠性
4. 交互性与自动化并重
U-Boot 既提供了丰富的交互命令用于调试和开发, 又支持完整的自动化引导流程. 这种设计使其既能满足开发阶段的灵活性需求, 又能满足产品阶段的自动化需求
8.3 与现代固件标准的融合
近年来, U-Boot 不断适应现代固件标准的发展趋势:
UEFI 支持:
U-Boot 长期以来支持 UEFI, 并可以直接通过 UEFI 引导发行版, 如 Fedora 和 Ubuntu. 它还支持 UEFI Secure Boot, 并可以处理使用可信平台模块(TPM)对引导过程进行测量和验证
Verified Boot for Embedded (VBE):
VBE 是 UEFI 的替代方案, 基于现有的标准如 FIT 格式. 与 UEFI Secure Boot 中从引导镜像到 U-Boot 的一堆回调不同, VBE 提供了引导镜像需要的一切内容, 这是一种更简单的方法
设备树统一:
U-Boot 在 2011 年左右就引入了 devicetree, 与 Linux 差不多同时. 最终希望 U-Boot 可以从 Linux 获取其 device tree, 并且只要从 device tree 中明确了所需的硬件驱动程序, U-Boot 就可以直接使用它
8.4 性能与优化
U-Boot 在性能和代码优化方面也有持续改进:
链接时优化:
该项目添加了链接时优化(LTO). "它会让你的 board 的构建时间变长四倍, 但 size 会减小 5%. "这通常来说是有好处的, 所以大多数 Arm 板默认开启了 LTO
事件系统:
Events 功能提供了一种监视设备创建等相关事件的方法;这是 weak function 的一个替代方案. 一个 event 会被发布出来, 其他地方可以告知说对特定事件感兴趣, 然后在 event 发生时进行处理;所有这些监听事件的地方都可以使用工具轻松列出来, 所以有着更好的可见性
8.5 总结
U-Boot 作为嵌入式系统领域最流行的 BootLoader, 其成功源于其通用性、可移植性和丰富的功能集. 通过深入分析其启动流程、代码框架、核心数据结构和实际应用, 我们可以得出以下结论:
U-Boot 的两阶段启动模型有效地平衡了硬件初始化的特殊性和高级功能的可移植性需求
模块化的架构和清晰的代码组织使得 U-Boot 能够支持大量硬件平台, 同时保持核心代码的稳定性
设备树机制的使用解决了硬件描述与代码分离的问题, 提高了系统的可配置性和可维护性
丰富的命令集和调试手段为开发和故障诊断提供了强大支持
持续的项目演进使 U-Boot 能够适应新的硬件技术和软件标准.
浙公网安备 33010602011771号