imx6ull-系统移植篇8——U-Boot 启动流程(下)

目录

前言

board_init_f 函数

函数功能

函数源码

init_sequence_f定义

关键设计思想​​

典型问题定位

gd成员的关键作用

内存分配

relocate_code 函数

函数功能​

函数源码

U-Boot 重定位

relocate_vectors 函数

函数功能

函数源码

board_init_r 函数

函数功能

函数源码

init_sequence_r 定义

run_main_loop 函数

函数功能

函数源码

main_loop 函数

cli_loop 函数

函数功能

函数源码

parse_file_outer函数

parse_stream_outer函数

cmd_process 函数

函数源码

函数分析


前言

本讲实验的目的是:分析一下 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成员变量​​

​​直接操作函数​​

​​间接依赖函数​​

​用途​

mon_len

setup_mon_len

reserve_uboot

记录 U-Boot 镜像长度,决定重定位时拷贝大小

malloc_base

initf_malloc

malloc(早期)

早期堆内存起始地址,供 initf_malloc使用

malloc_ptr

initf_malloc

malloc/free(早期)

当前堆内存分配指针

cpu_clk

get_clocks

serial_init/timer_init

存储 CPU 主频,影响外设时钟分频

bus_clk

get_clocks

i2c_init

存储总线时钟频率,用于 I2C/SPI 等外设时钟配置

baudrate

init_baud_rate

serial_init

串口通信波特率,用于 serial_init

ram_size

dram_init

setup_dest_addr/reserve_*

系统内存总大小,决定内核加载地址和内存保留区

ram_top

dram_init

setup_dest_addr

内存物理顶端地址,用于计算 U-Boot 重定位目标地址

relocaddr

setup_dest_addr

relocate_code

U-Boot 重定位的目标虚拟地址

reloc_off

setup_reloc

relocate_vectors

重定位偏移量(新地址 - 旧地址),用于调整代码中的绝对地址引用

have_console

console_init_f

printf/debug

标记控制台是否初始化完成(1=可用,0=不可用)

env_addr

env_init

env_relocate

环境变量存储区的物理地址(如 SPI Flash 中的偏移)

flags

board_init_f

display_options

启动标志位(如静默模式),控制启动过程中的信息输出行为

new_gd

reserve_global_data

board_init_r

重定位后的新 gd结构指针

fdt_blob

reloc_fdt

fdtdec_*系列函数

设备树(DTB)的地址指针,用于内核启动时传递硬件配置信息

start_addr_sp

reserve_stacks

_main(汇编)

重定位后的栈顶地址(向下生长),用于设置 C 运行时环境的栈指针

bd

reserve_board

board_info

板级信息结构体指针(含 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                     @ 循环直到结束
  1. r2=__rel_dyn_start,也就是.rel.dyn 段的起始地址。
  2. r3=__rel_dyn_end,也就是.rel.dyn 段的终止地址。
  3. 从.rel.dyn 段起始地址开始,每次读取两个 4 字节的数据存放到 r0 和 r1 寄存器中, r0 存放低 4 字节的数据,也就是 Label 地址; r1 存放高 4 字节的数据,也就是 Label 标志。
  4. r1 中给的值与 0xff 进行与运算,其实就是取 r1 的低 8 位。
  5. 判断 r1 中的值是否等于 23(0X17)。
  6. 如果 r1 不等于 23 的话就说明不是描述 Label 的,执行函数 fixnext,否则的话继续执行下面的代码。
  7. r0 保存着 Label 值, r4 保存着重定位后的地址偏移, r0+r4 就得到了重定位后的Label 值。此时r0 保存着重定位后的 Label 值,相当于 0X87804198+0X18747000=0X9FF4B198。
  8. 读取重定位后 Label 所保存的变量地址,此时这个变量地址还是重定位前的(相当于 rel_a 重定位前的地址 0X8785DA50),将得到的值放到 r1 寄存器中。
  9. r1+r4 即 可 得 到 重 定 位 后 的 变 量 地 址 , 相 当 于 rel_a 重 定 位 后 的0X8785DA50+0X18747000=0X9FFA4A50。
  10. 重定位后的变量地址写入到重定位后的 Label 中,相等于设置地址 0X9FF4B198处的值为 0X9FFA4A50。
  11. 比较 r2 和 r3,查看.rel.dyn 段重定位是否完成。
  12. 如果 r2 和 r3 不相等,说明.rel.dyn 重定位还未完成,因此跳到 fixloop 继续重定位.rel.dyn 段。

relocate_vectors 函数

函数功能

relocate_vectors负责将 U-Boot 的异常向量表(Exception Vectors)重定位到目标地址,确保中断和异常处理在重定位后仍能正常工作。

根据 CPU 架构不同,分为三种实现方式:

  1. ​ARMv7-M(Cortex-M)​​:直接设置 VTOR 寄存器。

  2. ​支持 VBAR 的 ARMv7/ARMv8​​:使用 VBAR 寄存器重定向向量表。

  3. ​传统 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值