目录
前言
本讲实验的目的是:分析一下 uboot 的启动流程,理清 uboot 是如何启动的。
在上讲实验里:U-Boot启动流程(上),我们已经介绍到了_main函数,在_main 函数里面调用了 board_init_f、 relocate_code、relocate_vectors 和 board_init_r 这 4 个函数。
board_init_f 函数
函数功能
_main 中会调用 board_init_f 函数, board_init_f 函数主要有两个工作:
①、初始化一系列外设,比如串口、定时器,或者打印一些消息等。
②、初始化 gd 的各个成员变量, uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。
- 初始化全局数据结构(
gd
) - 执行前期初始化序列(
init_sequence_f
):包括内存、时钟、串口等基础硬件初始化 - 为后续代码重定位做准备
函数源码
board_init_f 函数定义在文件 common/board_f.c 中:
void board_init_f(ulong boot_flags)
{
#ifdef CONFIG_SYS_GENERIC_GLOBAL_DATA
/*
* 对于某些架构,全局数据需要在调用本函数前初始化并保留。
* 其他架构需定义 CONFIG_SYS_GENERIC_GLOBAL_DATA,
* 在此使用栈空间临时存储全局数据,直到重定位完成。
*/
gd_t data; // 在栈上分配临时gd结构体
gd = &data; // 将全局指针gd指向该结构体
/*
* 在initcall_run_list的调试打印前清除全局数据,
* 否则调试输出可能获取错误的gd->have_console值。
*/
zero_global_data(); // 清零gd结构体(防止未初始化值干扰)
#endif
gd->flags = boot_flags; // 保存启动标志位(如冷启动/热启动)
gd->have_console = 0; // 标记控制台尚未初始化
/*
* 执行初始化序列 init_sequence_f 中的函数
* 若任何初始化失败(返回非零),则挂起系统
*/
if (initcall_run_list(init_sequence_f))
hang(); // 初始化失败时死循环
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
!defined(CONFIG_EFI_APP)
/*
* 对于非ARM、非Sandbox、非EFI的架构,此处应为不可达代码
* jump_to_copy() 通常不会返回(但某些架构可能缺少该函数)
*/
hang(); // 安全兜底
#endif
}
由上面代码中:通过函数 initcall_run_list 来运行初始化序列 init_sequence_f 里面的一些列函数。
init_sequence_f定义
去掉条件编译以后的 init_sequence_f 定义如下:
static init_fnc_t init_sequence_f[] = {
/* [1] 镜像长度计算 */
setup_mon_len, // 计算U-Boot镜像的完整长度(包括BSS段)
/* [2] 内存管理初始化 */
initf_malloc, // 初始化早期内存分配器(预重定位阶段的简单malloc)
initf_console_record, // 初始化控制台记录缓存(用于调试日志)
/* [3] CPU架构相关初始化 */
arch_cpu_init, // 基础CPU设置(如缓存、流水线)
initf_dm, // 初始化驱动模型(Driver Model)框架
arch_cpu_init_dm, // 架构扩展的驱动模型初始化(如时钟树配置)
/* [4] 计时与板级早期初始化 */
mark_bootstage, // 记录启动阶段(需依赖定时器,故在dm之后)
board_early_init_f, // 板级特定的早期硬件初始化(GPIO、电源等)
timer_init, // 初始化系统定时器(为后续延迟函数提供支持)
board_postclk_init, // 板级时钟后初始化(依赖timer_init)
get_clocks, // 获取CPU/总线时钟频率并存入gd->clock
/* [5] 环境与通信初始化 */
env_init, // 初始化环境变量存储(如SPI Flash中的环境分区)
init_baud_rate, // 解析环境变量设置串口波特率
serial_init, // 串口控制器初始化(输出调试信息)
console_init_f, // 控制台第一阶段初始化(无行缓冲)
/* [6] 信息展示与调试 */
display_options, // 打印U-Boot版本和启动参数
display_text_info, // 显示调试文本信息(如编译时间)
print_cpuinfo, // 输出CPU型号和时钟速度
show_board_info, // 打印板卡信息(厂商、型号等)
/* [7] 看门狗管理 */
INIT_FUNC_WATCHDOG_INIT, // 初始化硬件看门狗
INIT_FUNC_WATCHDOG_RESET,// 复位看门狗计时器(防止超时)
/* [8] 外设初始化 */
init_func_i2c, // I2C总线初始化(用于访问EEPROM/PMIC等)
announce_dram_init, // 打印"DRAM: "提示(调试用)
/* [9] DRAM内存初始化(最关键步骤) */
dram_init, // 初始化DDR控制器并检测内存大小
post_init_f, // 内存初始化后的硬件自检(可选)
testdram, // 简单内存测试(仅调试时启用)
/* [10] 内存布局规划 */
setup_dest_addr, // 计算重定位目标地址(通常为DRAM顶端)
reserve_round_4k, // 内存保留区对齐到4KB边界
reserve_mmu, // 保留MMU页表所需内存
reserve_trace, // 保留调试跟踪缓冲区
reserve_uboot, // 保留U-Boot重定位后的空间
reserve_malloc, // 保留堆内存区(malloc池)
reserve_board, // 保留板级特定数据区
setup_machine, // 设置机器ID(用于Linux启动)
reserve_global_data, // 保留全局数据区(gd)
reserve_fdt, // 保留设备树(DTB)空间
reserve_arch, // 保留架构特定数据区
reserve_stacks, // 保留栈空间
/* [11] 重定位准备 */
setup_dram_config, // 最终确定DRAM配置
show_dram_config, // 打印内存布局信息
display_new_sp, // 显示新栈指针地址(调试用)
reloc_fdt, // 处理设备树重定位(如调整地址指针)
setup_reloc, // 计算重定位偏移量并更新gd->reloc_off
NULL, // 终止标记
};
关键设计思想
1. 分层初始化
- 硬件依赖顺序:基础CPU → 时钟/定时器 → 内存 → 外设
- 调试优先:尽早初始化串口和控制台,确保后续初始化问题可调试。
2. 内存管理
两阶段处理:
- dram_init初始化物理内存控制器
- reserve_*规划逻辑内存布局(为U-Boot、内核、FDT保留区域)
3. 安全机制
- 看门狗:周期性复位防止初始化卡死
- 内存测试:testdram验证DDR稳定性(通常仅在开发阶段启用)
4. 跨平台支持
- 弱符号函数:如 board_early_init_f可由板级代码覆盖
- 条件编译:原代码通过宏控制不同架构的初始化项
典型问题定位
-
卡在
dram_init
:DDR配置错误(时序参数、电压设置) -
串口无输出:检查
init_baud_rate
和serial_init
的波特率匹配 -
重定位后崩溃:确认
reserve_uboot
和setup_reloc
计算的地址正确 -
看门狗复位:在耗时初始化(如DDR测试)中遗漏
INIT_FUNC_WATCHDOG_RESET
gd成员的关键作用
gd成员变量 | 直接操作函数 | 间接依赖函数 | 用途 |
---|---|---|---|
|
|
| 记录 U-Boot 镜像长度,决定重定位时拷贝大小 |
|
|
| 早期堆内存起始地址,供 |
|
|
| 当前堆内存分配指针 |
|
|
| 存储 CPU 主频,影响外设时钟分频 |
|
|
| 存储总线时钟频率,用于 I2C/SPI 等外设时钟配置 |
|
|
| 串口通信波特率,用于 |
|
|
| 系统内存总大小,决定内核加载地址和内存保留区 |
|
|
| 内存物理顶端地址,用于计算 U-Boot 重定位目标地址 |
|
|
| U-Boot 重定位的目标虚拟地址 |
|
|
| 重定位偏移量( |
|
|
| 标记控制台是否初始化完成(1=可用,0=不可用) |
|
|
| 环境变量存储区的物理地址(如 SPI Flash 中的偏移) |
|
|
| 启动标志位(如静默模式),控制启动过程中的信息输出行为 |
|
|
| 重定位后的新 |
|
|
| 设备树(DTB)的地址指针,用于内核启动时传递硬件配置信息 |
|
|
| 重定位后的栈顶地址(向下生长),用于设置 C 运行时环境的栈指针 |
|
|
| 板级信息结构体指针(含 MAC 地址、内存布局等),供内核或应用使用 |
内存初始化与重定位:
串口调试输出初始化:
内存分配
board_init_f 函数执行完成后,uboot 重定位后的偏移为 0X18747000,重定位后的新地址为0X9FF4700,新的 gd 首地址为 0X9EF44EB8,最终的 sp 为 0X9EF44E90。
relocate_code 函数
relocate_code
是 U-Boot 启动过程中将自身代码从加载地址(如 SRAM)复制到运行地址(如 DDR)的核心汇编函数,此函数定义在文件 arch/arm/lib/relocate.S 中。
函数功能
-
代码段拷贝:将 U-Boot 镜像从源地址复制到目标地址
-
重定位表修复:调整
.rel.dyn
段中的地址引用 -
缓存同步:确保指令缓存一致性(XScale 架构)
函数源码
ENTRY(relocate_code)
/* [1] 计算重定位偏移量 */
ldr r1, =__image_copy_start @ r1 = 源地址(U-Boot镜像起始位置0X87800000)
subs r4, r0, r1 @ r4 = 重定位偏移量(目标地址 - 源地址)
beq relocate_done @ 若偏移量为0(无需重定位),直接跳过
/* [2] 复制代码段到新地址 */
ldr r2, =__image_copy_end @ r2 = 源结束地址
copy_loop:
ldmia r1!, {r10-r11} @ 从源地址[r1]加载2个字到r10/r11
stmia r0!, {r10-r11} @ 存储到目标地址[r0]
cmp r1, r2 @ 检查是否到达源结束地址
blo copy_loop @ 循环直到复制完成
/* [3] 修复重定位表(.rel.dyn段) */
ldr r2, =__rel_dyn_start @ r2 = 重定位表起始地址
ldr r3, =__rel_dyn_end @ r3 = 重定位表结束地址
fixloop:
ldmia r2!, {r0-r1} @ r0 = 需修复的地址,r1 = 修复类型+符号索引
and r1, r1, #0xff @ 提取低8位(修复类型)
cmp r1, #23 @ 检查是否为相对地址修复(ARM_RELATIVE)
bne fixnext @ 若不是,跳过
/* [4] 相对地址修复 */
add r0, r0, r4 @ 计算修复后的地址(旧地址 + 偏移量)
ldr r1, [r0] @ 读取旧地址处的值
add r1, r1, r4 @ 调整该值(加上偏移量)
str r1, [r0] @ 写回修复后的值
fixnext:
cmp r2, r3 @ 检查是否处理完所有重定位项
blo fixloop @ 循环直到结束
relocate_done:
/* [5] XScale架构缓存处理 */
#ifdef __XSCALE__
mcr p15, 0, r0, c7, c7, 0 @ 无效化指令缓存
mcr p15, 0, r0, c7, c10, 4 @ 排空写缓冲区
#endif
/* [6] 函数返回 */
#ifdef __ARM_ARCH_4__
mov pc, lr @ ARMv4使用mov返回
#else
bx lr @ ARMv5+使用bx返回
#endif
ENDPROC(relocate_code)
U-Boot 重定位
U-Boot 通过 位置无关代码 + .rel.dyn
动态修复 解决重定位问题,核心思想是:
-
编译阶段:生成可重定位的二进制(
-pie
),记录地址依赖关系。 -
运行阶段:通过偏移量统一调整所有绝对地址引用,确保代码在任意位置正确运行。
-
间接访问:通过 Label 解耦代码和变量地址,使重定位只需修改数据(而非代码)。r2=__rel_dyn_start,也就是.rel.dyn 段的起始地址。
让我们详细分析一下这部分代码:
/* [3] 修复重定位表(.rel.dyn段) */
ldr r2, =__rel_dyn_start @ r2 = 重定位表起始地址
ldr r3, =__rel_dyn_end @ r3 = 重定位表结束地址
fixloop:
ldmia r2!, {r0-r1} @ r0 = 需修复的地址,r1 = 修复类型+符号索引
and r1, r1, #0xff @ 提取低8位(修复类型)
cmp r1, #23 @ 检查是否为相对地址修复(ARM_RELATIVE)
bne fixnext @ 若不是,跳过
/* [4] 相对地址修复 */
add r0, r0, r4 @ 计算修复后的地址(旧地址 + 偏移量)
ldr r1, [r0] @ 读取旧地址处的值
add r1, r1, r4 @ 调整该值(加上偏移量)
str r1, [r0] @ 写回修复后的值
fixnext:
cmp r2, r3 @ 检查是否处理完所有重定位项
blo fixloop @ 循环直到结束
- r2=__rel_dyn_start,也就是.rel.dyn 段的起始地址。
- r3=__rel_dyn_end,也就是.rel.dyn 段的终止地址。
- 从.rel.dyn 段起始地址开始,每次读取两个 4 字节的数据存放到 r0 和 r1 寄存器中, r0 存放低 4 字节的数据,也就是 Label 地址; r1 存放高 4 字节的数据,也就是 Label 标志。
- r1 中给的值与 0xff 进行与运算,其实就是取 r1 的低 8 位。
- 判断 r1 中的值是否等于 23(0X17)。
- 如果 r1 不等于 23 的话就说明不是描述 Label 的,执行函数 fixnext,否则的话继续执行下面的代码。
- r0 保存着 Label 值, r4 保存着重定位后的地址偏移, r0+r4 就得到了重定位后的Label 值。此时r0 保存着重定位后的 Label 值,相当于 0X87804198+0X18747000=0X9FF4B198。
- 读取重定位后 Label 所保存的变量地址,此时这个变量地址还是重定位前的(相当于 rel_a 重定位前的地址 0X8785DA50),将得到的值放到 r1 寄存器中。
- r1+r4 即 可 得 到 重 定 位 后 的 变 量 地 址 , 相 当 于 rel_a 重 定 位 后 的0X8785DA50+0X18747000=0X9FFA4A50。
- 重定位后的变量地址写入到重定位后的 Label 中,相等于设置地址 0X9FF4B198处的值为 0X9FFA4A50。
- 比较 r2 和 r3,查看.rel.dyn 段重定位是否完成。
- 如果 r2 和 r3 不相等,说明.rel.dyn 重定位还未完成,因此跳到 fixloop 继续重定位.rel.dyn 段。
relocate_vectors 函数
函数功能
relocate_vectors
负责将 U-Boot 的异常向量表(Exception Vectors)重定位到目标地址,确保中断和异常处理在重定位后仍能正常工作。
根据 CPU 架构不同,分为三种实现方式:
-
ARMv7-M(Cortex-M):直接设置 VTOR 寄存器。
-
支持 VBAR 的 ARMv7/ARMv8:使用 VBAR 寄存器重定向向量表。
-
传统 ARM(如 ARMv5/ARMv6):手动拷贝向量表到固定地址(0x00000000 或 0xFFFF0000)
函数源码
ENTRY(relocate_vectors)
#ifdef CONFIG_CPU_V7M
/* [1] ARMv7-M (Cortex-M) 处理 */
ldr r0, [r9, #GD_RELOCADDR] @ r0 = gd->relocaddr(目标地址)
ldr r1, =V7M_SCB_BASE @ r1 = SCB 基地址
str r0, [r1, V7M_SCB_VTOR] @ 设置 VTOR 寄存器(向量表偏移)
#else
#ifdef CONFIG_HAS_VBAR
/* [2] 支持 VBAR 的 ARMv7/ARMv8 处理 */
ldr r0, [r9, #GD_RELOCADDR] @ r0 = gd->relocaddr
mcr p15, 0, r0, c12, c0, 0 @ 设置 VBAR 寄存器
#else
/* [3] 传统 ARM(无 VBAR)处理 */
ldr r0, [r9, #GD_RELOCADDR] @ r0 = gd->relocaddr(向量表新地址)
mrc p15, 0, r2, c1, c0, 0 @ 读取 CP15 c1 寄存器(控制寄存器)
ands r2, r2, #(1 << 13) @ 检查 V 位(向量表位置)
ldreq r1, =0x00000000 @ 若 V=0,向量表位于 0x00000000
ldrne r1, =0xFFFF0000 @ 若 V=1,向量表位于 0xFFFF0000
/* 拷贝向量表(共 8 条指令,分两次批量拷贝) */
ldmia r0!, {r2-r8,r10} @ 从新地址加载 8 个字
stmia r1!, {r2-r8,r10} @ 存储到目标地址
ldmia r0!, {r2-r8,r10} @ 重复加载下一批
stmia r1!, {r2-r8,r10} @ 存储到目标地址
#endif
#endif
bx lr @ 函数返回
ENDPROC(relocate_vectors)
r0=gd->relocaddr,也就是重定位后 uboot 的首地址,向量表肯定是从这个地址开始存放的。
将 r0 的值写入到 CP15 的 VBAR 寄存器中,也就是将新的向量表首地址写入到寄存器 VBAR 中,设置向量表偏移。
board_init_r 函数
board_init_r 函数定义在文件 common/board_r.c中。
函数功能
board_init_r是 U-Boot 启动流程的 第二阶段初始化入口,负责在完成代码重定位后,初始化所有高级硬件模块(如存储、网络、显示等),并最终启动命令行或加载内核。其核心任务包括:
- 更新全局数据指针(gd)到重定位后的新地址。
- 手动重定位初始化函数表(某些架构需要)。
- 执行后期初始化序列(init_sequence_r)。
- 进入主循环(如命令行或自动启动)。
函数源码
void board_init_r(gd_t *new_gd, ulong dest_addr) {
/* [1] 特殊架构的MMU初始化(如AVR32) */
#ifdef CONFIG_AVR32
mmu_init_r(dest_addr); // AVR32需在此时初始化MMU
#endif
/* [2] 更新全局数据指针gd(非X86/ARM/ARM64架构需显式更新) */
#if !defined(CONFIG_X86) && !defined(CONFIG_ARM) && !defined(CONFIG_ARM64)
gd = new_gd; // 将gd指向重定位后的新地址
#endif
/* [3] 手动重定位初始化函数表(某些平台需调整函数指针) */
#ifdef CONFIG_NEEDS_MANUAL_RELOC
for (i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
init_sequence_r[i] += gd->reloc_off; // 函数地址 += 重定位偏移量
#endif
/* [4] 执行后期初始化序列 */
if (initcall_run_list(init_sequence_r))
hang(); // 若初始化失败,系统挂起
/* [5] 主循环(理论上不会执行到此处) */
hang(); // run_main_loop()通常不会返回
}
调用 initcall_run_list 函数来执行初始化序列 init_sequence_r, init_sequence_r 是一个函数集合, init_sequence_r 也定义在文件 common/board_r.c 中。
init_sequence_r 定义
init_fnc_t init_sequence_r[] = {
/* 调试支持类初始化 */
// 初始化调试跟踪系统,记录启动阶段耗时
initr_trace,
// 标记重定位完成,更新全局状态标志
initr_reloc,
/* 处理器与内存类初始化 */
// 启用指令/数据缓存(如MMU/Cache)
initr_caches,
// 调整全局数据(gd)中的指针到新地址
initr_reloc_global_data,
// 设置内存屏障,确保多核CPU初始化顺序
initr_barrier,
// 初始化完整的堆内存分配器
initr_malloc,
/* 控制台与调试类初始化 */
// 重定位控制台记录缓存(用于调试日志存储)
initr_console_record,
// 调整启动阶段标记(Bootstage)的存储地址
bootstage_relocate,
// 记录初始化流程各阶段时间戳
initr_bootstage,
/* 板级硬件初始化 */
// 初始化板级硬件(如芯片片选、GPIO配置)
board_init, /* Setup chipselects */
/* 输入输出设备初始化 */
// 初始化标准输入输出设备表
stdio_init_tables,
// 完全初始化串口设备
initr_serial,
// 打印系统启动横幅
initr_announce,
/* 看门狗保护(防止初始化超时) */
INIT_FUNC_WATCHDOG_RESET
INIT_FUNC_WATCHDOG_RESET
INIT_FUNC_WATCHDOG_RESET
/* 电源管理初始化 */
// 初始化电源管理芯片(如PMIC)
power_init_board,
/* 存储设备初始化 */
// 初始化NOR Flash控制器
initr_flash,
// 看门狗保护
INIT_FUNC_WATCHDOG_RESET
// 初始化NAND Flash控制器
initr_nand,
// 初始化MMC/SD控制器
initr_mmc,
/* 环境变量初始化 */
// 从存储设备加载环境变量到内存
initr_env,
// 看门狗保护
INIT_FUNC_WATCHDOG_RESET
/* 多核处理器初始化 */
// 启动次级CPU核(SMP系统)
initr_secondary_cpu,
// 看门狗保护
INIT_FUNC_WATCHDOG_RESET
/* 输入输出设备注册 */
// 注册所有标准设备(串口/USB键盘等)
stdio_add_devices,
// 初始化函数跳转表
initr_jumptable,
// 完全初始化控制台设备
console_init_r, /* fully init console as a device */
/* 中断系统初始化 */
// 看门狗保护
INIT_FUNC_WATCHDOG_RESET
// 配置中断控制器(如GIC)
interrupt_init,
// 全局启用中断
initr_enable_interrupts,
/* 网络配置初始化 */
// 从环境变量读取并配置MAC地址
initr_ethaddr,
/* 板级后期初始化 */
// 板卡特定的后期初始化
board_late_init,
/* 看门狗保护(高频复位段) */
INIT_FUNC_WATCHDOG_RESET
INIT_FUNC_WATCHDOG_RESET
INIT_FUNC_WATCHDOG_RESET
/* 网络设备初始化 */
// 初始化以太网/USB网络设备
initr_net,
// 看门狗保护
INIT_FUNC_WATCHDOG_RESET
/* 主流程入口 */
// 进入U-Boot主循环(永不返回)
run_main_loop,
};
让我们来分析一下其中的一些函数:
initr_mmc 函数,初始化 EMMC。
如果使用 EMMC 版本核心板的话就会初始化EMMC,串口输出如图:
console_init_r 函数 , 控制台初始化。
初始化完成以后此函数会调 用stdio_print_current_devices 函数来打印出当前的控制台设备,如图
initr_ethaddr 函数,初始化网络地址,也就是获取 MAC 地址。读取环境变量“ethaddr”的值。
board_late_init 函数,板子后续初始化,此函数定义在文件 mx6ull_alientek_emmc.c中。
如果环境变量存储在 EMMC 或者 SD 卡中的话此函数会调用 board_late_mmc_env_init 函数初始化 EMMC/SD。会切换到正在时候用的 emmc 设备,代码如图:
run_main_loop 函数
run_main_loop 函 数 定义 在 文件common/board_r.c 中。
函数功能
run_main_loop是 U-Boot 启动流程的 最终阶段,负责进入无限循环的主控制逻辑,根据系统配置执行以下操作:
- 交互模式:启动命令行界面(CLI),等待用户输入命令。
- 自动启动:执行预定义的启动脚本(如 bootcmd环境变量)。
- 沙盒模式(仅限 Sandbox):初始化虚拟环境测试框架。
还记得我们说过:uboot 启动以后会进入 3 秒倒计时,如果在 3 秒倒计时结束之前按下按下回车键,那么就会进入 uboot 的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动 Linux 内核。就是这个函数来实现的。
函数源码
static int run_main_loop(void)
{
/* [1] Sandbox 测试环境初始化(仅编译 Sandbox 时生效) */
#ifdef CONFIG_SANDBOX
sandbox_main_loop_init(); // 初始化沙盒测试环境(如虚拟文件系统、设备模拟)
#endif
/* [2] 无限循环执行主逻辑 */
for (;;) {
main_loop(); // 执行主循环(可能因自动启动失败返回,此时需重试)
}
/* [3] 理论上不可达的返回 */
return 0; // 保持编译器无警告(main_loop() 设计为永不返回)
}
死循环里只有一个main_loop 函数。
main_loop 函数
main_loop 函数定义在文件 common/main.c 里面。
main_loop是 U-Boot 初始化完成后进入的 主控制循环,负责处理以下核心逻辑:
- 启动前预处理(环境变量、固件更新)
- 自动启动流程(bootcmd执行)
- 命令行交互(用户输入处理)
- 版本管理与安全启动
void main_loop(void)
{
const char *s;
/* [1] 记录启动阶段标记(用于性能分析) */
bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
/* [2] 非通用板警告(兼容性提示) */
#ifndef CONFIG_SYS_GENERIC_BOARD
puts("Warning: Your board does not use generic board. Please read\n");
puts("doc/README.generic-board and take action. Boards not\n");
puts("upgraded by the late 2014 may break or be removed.\n");
#endif
/* [3] 设置版本变量(可选) */
#ifdef CONFIG_VERSION_VARIABLE
setenv("ver", version_string); /* 将U-Boot版本号存入环境变量 `ver` */
#endif
/* [4] 初始化命令行接口 */
cli_init(); /* 设置行编辑器、历史记录等 */
/* [5] 执行预启动命令 */
run_preboot_environment_command(); /* 执行环境变量中的 `preboot` 命令 */
/* [6] TFTP固件更新(可选) */
#if defined(CONFIG_UPDATE_TFTP)
update_tftp(0UL, NULL, NULL); /* 通过网络下载并更新固件 */
#endif
/* [7] 处理启动延迟与FDT控制台参数 */
s = bootdelay_process(); /* 解析 `bootdelay` 和环境变量 `bootcmd` */
if (cli_process_fdt(&s)) /* 从设备树获取控制台参数 */
cli_secure_boot_cmd(s); /* 安全启动验证(如启用) */
/* [8] 尝试自动启动 */
autoboot_command(s); /* 执行 `bootcmd` 中的命令(失败则返回) */
/* [9] 进入命令行交互模式(永不返回) */
cli_loop(); /* 处理用户输入命令 */
}
关键代码详解:
(1)启动阶段标记:bootstage_mark_name:记录当前进入主循环的时间点,用于分析启动耗时(需启用 CONFIG_BOOTSTAGE)。
(2)非通用板警告
- 背景:2014年后U-Boot要求板级代码适配通用框架(CONFIG_SYS_GENERIC_BOARD)。
- 作用:提示开发者更新旧板级代码,否则可能无法运行。
(3)版本管理:CONFIG_VERSION_VARIABLE:将 version_string(如 "U-Boot 2023.07")存入环境变量 ver,供脚本或用户查询。
(4)命令行初始化:cli_init():初始化行编辑功能(如退格键处理)、命令历史记录等。
(5)预启动命令:preboot环境变量:可在自动启动前执行特定命令(如网络配置、设备检测)。
(6)TFTP固件更新:CONFIG_UPDATE_TFTP:如果检测到固件更新标志(如按钮按下),从TFTP服务器下载新固件并写入Flash。
(7)启动延迟处理:bootdelay_process():
- 读取 bootdelay值(如 -1禁用自动启动,0立即启动)。
- 检查用户按键(如空格键中断自动启动)。
- 返回待执行的命令字符串(通常为 bootcmd内容)。
(8)自动启动:autoboot_command(s):执行 bootcmd中的命令序列(如 run load_kernel; bootm)。若执行失败且启用 CONFIG_BOOT_RETRY,则返回到 run_main_loop重试。
(9)命令行交互:cli_loop():
- 显示提示符(如 =>)。
- 解析并执行用户输入的命令(内置命令或可加载模块)。
- 永不返回(除非系统复位)。
cli_loop 函数
cli_loop函数定义在文件 common/cli.c 中。
函数功能
cli_loop是 U-Boot 命令行交互的核心函数,根据配置选择不同的解析器实现:
- HUSH 解析器(CONFIG_SYS_HUSH_PARSER):支持脚本、变量替换等高级功能。
- 简单解析器:基础命令执行,适用于资源受限环境。
函数源码
void cli_loop(void)
{
/* [1] HUSH 解析器模式(功能丰富) */
#ifdef CONFIG_SYS_HUSH_PARSER
parse_file_outer(); /* 进入HUSH解释器主循环(支持脚本/控制结构) */
/* 理论上不可达的代码(安全兜底) */
for (;;); // 死循环防止意外返回
#else
/* [2] 简单解析器模式(最小化实现) */
cli_simple_loop(); /* 基础行编辑与命令执行 */
#endif
}
可以看见,只会执行 parse_file_outer函数。
parse_file_outer函数
parse_file_outer是 HUSH Shell 解析器的 顶层入口函数,负责:
- 初始化输入流(文件/内存/控制台)
- 启动语法解析主循环
- 处理脚本或交互式命令的 多语句执行(分号分隔)
函数 parse_file_outer 定义在文件 common/cli_hush.c 中,去掉条件编译内容以后的函数内容如下:
int parse_file_outer(void)
{
int rcode; // 返回值:0=成功,非0=错误码
struct in_str input; // 输入流控制结构体
/* [1] 初始化输入流(默认指向控制台) */
setup_file_in_str(&input); // 设置input结构体的读取函数指针
/* [2] 启动语法解析主流程 */
rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);
/* [3] 理论上不可达的返回 */
return rcode; // 实际由parse_stream_outer的无限循环处理,此处为编译警告抑制
}
函数 parse_stream_outer,这个函数就是 hush shell 的命令解释器,负责接收命令行输入,然后解析并执行相应的命令。
parse_stream_outer函数
函数 parse_stream_outer 定义在文件 common/cli_hush.c中,精简版的函数内容如下:
static int parse_stream_outer(struct in_str *inp, int flag)
{
/* [1] 初始化解析上下文和临时缓冲区 */
struct p_context ctx; // 语法解析上下文(保存状态)
o_string temp = NULL_O_STRING; // 临时字符串缓冲区(存储部分解析结果)
int rcode; // 解析返回值
int code = 1; // 外层循环控制标志
do {
/* [2] 执行语法解析(核心处理) */
rcode = parse_stream(&temp, &ctx, inp,
flag & FLAG_CONT_ON_NEWLINE ? -1 : '\n'); // 换行符处理控制
/* [3] 成功解析后的命令执行 */
if (rcode != 1 && ctx.old_flag == 0) {
run_list(ctx.list_head); // 执行生成的命令链表
} else {
/* [4] 语法错误处理(如不完整输入) */
// 通常打印错误并保持解析状态
}
/* [5] 清理临时缓冲区 */
b_free(&temp);
/* [6] 循环条件判断 */
} while (rcode != -1 && // 非EOF
!(flag & FLAG_EXIT_FROM_LOOP) && // 未强制退出
(inp->peek != static_peek || b_peek(inp))); // 输入流未耗尽
return 0; // 实际不会返回(循环持续处理输入)
}
可以看见:调用 run_list 函数来执行解析出来的命令,而函数 run_list 经过一系列的函数调用,最终通过调用 cmd_process 函数来处理命令。
cmd_process 函数
函数源码
/**
* cmd_process - 核心命令处理函数
* @flag: 命令执行标志位(如CMD_FLAG_REPEAT)
* @argc: 参数个数(argv[0]是命令名)
* @argv: 参数数组(如["echo", "hello"])
*
* 返回值: 0=成功,非0=错误码(参考CMD_RET_xxx)
*/
int cmd_process(int flag, int argc, char *const argv[])
{
cmd_tbl_t *cmdtp; // 命令表项指针
/* [1] 参数合法性检查 */
if (argc < 1 || !argv || !argv[0])
return CMD_RET_USAGE; // 错误:空命令或无效参数
/* [2] 查找命令(支持哈希加速) */
cmdtp = find_cmd(argv[0]);
if (cmdtp) {
/* [3] 检查参数数量是否合法 */
if (argc > cmdtp->maxargs) {
printf("Usage:\n%s\n", cmdtp->usage);
return CMD_RET_USAGE;
}
#ifdef CONFIG_CMD_TEST
/* [4] 测试模式:打印命令信息(调试用) */
if (flag & CMD_FLAG_TEST) {
printf("Test cmd: %s\n", cmdtp->name);
return 0;
}
#endif
/* [5] 执行命令处理函数 */
return cmdtp->cmd(cmdtp, flag, argc, argv);
}
/* [6] 处理特殊格式(环境变量赋值) */
if (strchr(argv[0], '=')) {
return do_setenv(flag, argc, argv);
}
/* [7] 未知命令处理 */
printf("Unknown command '%s' - try 'help'\n", argv[0]);
return CMD_RET_FAILURE;
}
函数分析
cmd_tbl_t 类型的变量
typedef struct cmd_tbl_s {
char *name; // 命令名(如 "bootm")
int maxargs; // 最大参数数
int (*cmd)(...); // 处理函数指针
char *usage; // 用法说明
} cmd_tbl_t;
函数 find_cmd
调用函数 find_cmd 在命令表中找到指定的命令:
cmd_tbl_t *find_cmd(const char *cmd)
{
/* [1] 获取命令表的起始地址 */
cmd_tbl_t *start = ll_entry_start(cmd_tbl_t, cmd);
/* [2] 获取命令表的条目数量 */
const int len = ll_entry_count(cmd_tbl_t, cmd);
/* [3] 调用底层查找函数 */
return find_cmd_tbl(cmd, start, len);
}
参数 cmd 就是所查找的命令名字, uboot 中的命令表其实就是 cmd_tbl_t 结构体数组,通过函数 ll_entry_start 得到数组的第一个元素,也就是命令表起始地址。
通过函数 ll_entry_count得到数组长度,也就是命令表的长度。
最终通过函数 find_cmd_tbl 在命令表中找到所需的命令,每个命令都有一个 name 成员,所以将参数 cmd 与命令表中每个成员的 name 字段都对比一下,如果相等的话就说明找到了这个命令,找到以后就返回这个命令。
函数 cmd_call
找到命令以后调用函数 cmd_call 来执行具体的命令:
static int cmd_call(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[])
{
int result; // 命令执行结果
/* [1] 执行命令处理函数 */
result = (cmdtp->cmd)(cmdtp, flag, argc, argv);
/* [2] 错误处理与调试输出 */
if (result)
debug("Command failed, result=%d\n", result);
/* [3] 返回执行状态 */
return result;
}
调用 cmdtp 的 cmd 成员来处理具体的命令,返回值为命令的执行结果。
cmd_process 中会检测 cmd_tbl 的返回值,如果返回值为 CMD_RET_USAGE 的话就会调用cmd_usage 函数输出命令的用法,其实就是输出 cmd_tbl_t 的 usage 成员变量。
典型实现
int do_echo(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[])
{
for (int i = 1; i < argc; i++)
printf("%s%s", argv[i], i < argc-1 ? " " : "\n");
return 0;
}