u-boot 构建框架与启动分析

  • Board: rockchip-px30, armv8, Cortex-A35
  • U-Boot: rockchip-linux/u-boot, branch next-dev
  • Tools: VScode, Exuberant CTags

1. 前言

学习 u-boot 启动流程有些时日了, 虽然看了大量的文章以及在此期间仔细阅读源码, 但是仍感觉很多知识点掌握不深刻容易遗忘,不如在写博文的时候重溯整个流程, 也分享关于 u-boot 的学习方式。

2. 学习路线

一般来说芯片公司会提供相关的手册介绍各个组件, 这对了解特定型号的开发板是很有效的, 但不适合初学者进行系统的学习, 建立全局概念应当是第一位的。 学习 u-boot 应当对以下几个方面有所了解:

  • ARM Assembly, 推荐 Kyle BaldwinARM Assembly By Example, 我自己在 github 上也实现了相关的 lab。
  • Device Tree, The DeviceTree Specification 结合具体的 dts/dtsi 文件阅读学习。
  • Linker Script, GNU Binutils 官方的ld文档合具体的lds文件进行阅读学习, 结合 程序员的自我修养–链接装载与库 这本书建立相关概念。
  • Kconfig, Kconfig Language 同样用在了 u-boot 中需要对其有一定的了解。

如果对 DM(Driver Model) 有兴趣, 可以阅读 Linux Device Driver, Third Edition,但具备以上几块知识已经足够理解 u-boot 启动流程。 Das U-Boot 提供的文档以及 u-boot 源码中提供的 README 这类文档不需要仔细读完, 这些仅供学习参考, 但 elinux_talks 这部分资源也值得查阅。

3. U-Boot 框架与启动阶段

3.1 U-Boot 架构分析

u-boot 的开发者在开发文档中描述目录的层次结构, 但缺少更为宏观的概括。 以 rockchip-px30 为例, 其在 u-boot 中的文件可被划归为以下几类。以CPU,ARCH,Board 三级对文件进行划分可以帮助我们在配置新板时有更清晰的规划。 Quentin Schulz 在 2017 年的嵌入式 linux 欧洲会议上的演讲 Porting U-Boot and Linux on new ARM boards: a step-by-step guide 则具体介绍了详细的实施步骤。

CPU (armv8), ARCH (arm), Board(px30)

  • CPU 层级依赖文件
    • arch/arm/cpu/armv8/*c;*S;*lds
    • arch/arm/include/asm/armv8/*h
  • ARCH(arm) 层级依赖文件
    • arch/arm/lib/*c;*S;*lds
    • arch/arm/include/asm/*h;*S
  • Board 层级依赖文件
    • board/rockchip/evb_px30/*c
    • arch/arm/mach-rockchip/px30/*c
  • Board 层级配置文件
    • arch/arm/include/asm/arch-rockchip/*h;*S
    • include/rockchip/*h
    • include/px30_common.h;evb_px30.h
  • Board 层级非依赖文件
    • common(cmd, flash, env, usb, …), disk(partition)
    • drivers, fs, net, lib
U-Boot Hierarchy, HangX-Ma
U-Boot Hierarchy, HangX-Ma

u-boot 的初始化过程就是 CPU $\rightarrow$ ARCH $\rightarrow$ Board 的过程, 但并不严格划归, ARCH部分的通用代码会调用 Board 相关的接口。 在 wowo 的文章中提到曾经存在于 ARCH 和 Board 之间的 machine 层级由于最新的ARM64架构引入了 device tree 的缘故, 已经将 machine 概念删除了, 在当前 u-boot 中看到的 mach-xxx 的目录或文件就属于 machine 层级, 虽然 u-boot 还未更新相关的架构概念, 但在开发层面 u-boot 和 linux 内核几乎同时适用了 device tree, 这意味着 u-boot 也很可能在之后的更新中删除类似的 mach-xxx 文件。

3.1.1 举例——从 Kconfig 自底向上

Kconfig 中自底向上梳理整个编译框架, 假设我们使用的目标板是 rockchip-px30 系列的 evb-px30, 那么 board/rockchip/evb_px30 文件夹中定义了目标板的一些依赖代码, 在 include/configs/evb_px30.h 会有该目标板的配置信息, 类似的配置信息和编译是息息相关的需要格外留意。

从顶层的 board/rockchip/evb_px30/Kconfig 查看, 可以找到 TARGET_EVB_PX30 整个关键量以及定义的 BORADVENDOR 等编译相关的变量。

# board/rockchip/evb_px30/Kconfig
if TARGET_EVB_PX30

config SYS_BOARD
    default "evb_px30"

config SYS_VENDOR
    default "rockchip"

config SYS_CONFIG_NAME
    default "evb_px30"

config BOARD_SPECIFIC_OPTIONS # dummy
    def_bool y

endif

顺着前述所提及的关键量, 在 arch/arm/mach-rockchip/px30/Kconfig 中能找到引用信息(尤其是 source 了前述的 Kconfig 文件), 由于当前使用的就是 evb-px30 板, EVB_PX30bool 变量是 true。 可以看到该 Kconfig 文件在框架中属于亟待更新的 machine 层级, 所以在该部分可以看到 SYS_SOC 这个配置变量。 在该 Kconfig 文件中还覆盖定义了 SYS_MALLOC_F_LENSPL_SERIAL_SUPPORT

# arch/arm/mach-rockchip/px30/Kconfig
if ROCKCHIP_PX30

config TARGET_EVB_PX30
    bool "EVB_PX30"
    select BOARD_LATE_INIT

config SYS_SOC
    default "rockchip"

config SYS_MALLOC_F_LEN
    default 0x400

config SPL_SERIAL_SUPPORT
    default y

source "board/rockchip/evb_px30/Kconfig"

endif

在更上一级目录则看到更为通用的 Kconfig 文件会配置 ROCKCHIP_PX30 这个定义量。 可以看到在该目录下配置了 px30 系列使用默认配置。 我们再向上一级的查找 ARCH_ROCKCHIP 变量以其找到顶层的 xxx_defconfig 配置文件。

# arch/arm/mach-rockchip/Kconfig
if ARCH_ROCKCHIP

config ROCKCHIP_PX30
    bool "Support Rockchip PX30"
    select ARM64 if !ARM64_BOOT_AARCH32
    select GICV2
    select ARM_SMCCC
    select SUPPORT_SPL
    select SUPPORT_TPL
    select SPL if !ARM64_BOOT_AARCH32
    select TPL if !ARM64_BOOT_AARCH32
    select TPL_TINY_FRAMEWORK if TPL

    imply SPL_SEPARATE_BSS
    imply SPL_SERIAL_SUPPORT
    imply TPL_SERIAL_SUPPORT
    help
      The Rockchip PX30 is a ARM-based SoC with a quad-core Cortex-A35
      including NEON and GPU, Mali-400 graphics, several DDR3 options
      and video codec support. Peripherals include Gigabit Ethernet,
      USB2 host and OTG, SDIO, I2S, UART, SPI, I2C and PWMs.

if ROCKCHIP_PX30

config TPL_LDSCRIPT
    default "arch/arm/mach-rockchip/u-boot-tpl-v8.lds"

config TPL_TEXT_BASE
    default 0xff0e1000

config TPL_MAX_SIZE
    default 10240

config ROCKCHIP_RK3326
    bool "Support Rockchip RK3326 "
    help
      RK3326 can use most code from PX30, but at some situations we have
      to distinguish between RK3326 and PX30, so this macro gives help.
      It is usually selected in rk3326 board defconfig.
endif
...

在更上一级, 我们先找到了 arch/arm/Kconfig, ARCH 层级的默认配置。

# arch/arm/Kconfig
...
config ARCH_ROCKCHIP
    bool "Support Rockchip SoCs"
    select OF_CONTROL
    select BLK
    select DM
    select SPL_DM if SPL
    select SYS_MALLOC_F
    select SYS_THUMB_BUILD if !ARM64
    select SPL_SYS_MALLOC_SIMPLE if SPL
    imply DM_GPIO
    select DM_SERIAL
    select DM_SPI
    select DM_SPI_FLASH
    select DM_USB if USB
    select CMD_ROCKUSB if USB_GADGET_DOWNLOAD
    select ENABLE_ARM_SOC_BOOT0_HOOK
    select SYS_NS16550
    select SPI
    select DEBUG_UART_BOARD_INIT
    select PANIC_HANG
    imply DM_MMC
    imply DM_I2C
    imply DM_PWM
    imply DM_REGULATOR
    imply CMD_FASTBOOT
    imply FASTBOOT
    imply FAT_WRITE
    imply USB_FUNCTION_FASTBOOT
    imply USB_FUNCTION_ROCKUSB
    imply SPL_SYSRESET
    imply TPL_SYSRESET
    imply ADC
    imply SARADC_ROCKCHIP
...

最终我们能在 configs/evb-px30_defconfig(Target) 中找到用户自定义的基本宏信息, 另外一些信息则在前述提及的配置文件中。 例如 include/configs/px30_common.hinclude/configs/evb_px30.h, 以及 include/configs/rockchip-common.h。 我们自底向上, 特定的板级文件开始溯源, 找到了最终顶层的配置文件。 根据顶层的配置文件以及每个层级的配置文件可以梳理出编译特定板所需的功能。 另外, 在底层的 TARGET 的配置中可以看到诸如 SYS_xxx 的一系列配置, 这些配置会在更上层的 arch/Kconfig 中定义。 所以综上可以总结出如下配置关系图。

flowchart subgraph Target evb-px30_defconfig end subgraph arch subgraph arm subgraph mach-rockchip subgraph px30 Board/evb-px30 end end end end style Target fill:#FF6A6A,stroke:#363636,stroke-width:2px,color:#F5F5F5 style evb-px30_defconfig fill:#F5F5DC,color:#8B8378 style arch fill:#4A708B,stroke:#363636,stroke-width:2px,color:#F5F5F5 style arm fill:#838B8B,stroke:#363636,color:#F5F5F5 style mach-rockchip fill:#6CA6CD,stroke:#363636,color:#F5F5F5 style px30 fill:#4A708B,stroke:#363636,color:#F5F5F5 style Board/evb-px30 fill:#F5F5DC,color:#8B8378 Target -.-> arch

3.2 Boot Loader Stage

BLx(Boot Loader Stage) 指代 Boot Loader 的各个阶段, 具体的划分根据 u-boot 初始化时所在存储设备略有不同, 一般将 u-boot 启动划分为 4 个阶段, BL0, BL1, BL2, BL3。值得注意的是这与 ARM TrustZone 的划分非属同源, 在 ARM TrustZone 的划分中, u-boot 属于 BL33 Non-secure 部分。

  • BL0, SOC 生产厂家固化在 iROM(Internal ROM) 中的启动代码, 主要负责加载 BL1 的程序, 该部分被称作 Initial Program Loader (IPL) 或者 Primary Program Loader (PPL)。
  • BL1, 该部分被称为 SPL(Secondary Program Loader), 若 SPL 部分仍超过了 flash 存储限制, 首先会通过 TPL(Trinary Program Loader) 进行更简洁的初始化如 DDR 部分的初始化,以保证代码体积极小, 之后再从指定位置加载 SPL 继续执行初始化。
  • BL2, 该阶段 u-boot 运行程序重定位之前的部分, 主要负责一系列初始化操作以及构建 C 语言的运行环境, 最为关键的是将 u-boot 重定位至 DRAM/SDRAM 中继续执行 BL3 阶段的程序。
  • BL3, 在该阶段实质上加载了u-boot, 当然通过 ATF(Architecture Trusted Firmware) 加载也是可以的。 该阶段在负责初始化 SOC 的外设, 准备内核启动参数以及加载运行内核等操作。
Boot loader sequences, HouchengLin
Boot loader sequences, HouchengLin

根据以上描述, 以图例形式表述 u-boot 的启动流程应当如下所示。

flowchart LR p1(BootROM) p2(TPL, optional) p3(SPL) p4(ATF, optional) p5(U-Boot) p6(Kernel) style p1 fill:#4F94CD,stroke:#363636,stroke-width:2px,color:#F5F5F5 style p2 fill:#DCDCDC,stroke:#363636,color:#8B8B7A,color:#696969 style p3 fill:#DCDCDC,stroke:#363636,color:#8B8B7A,color:#696969 style p4 fill:#8B1A1A,stroke:#363636,color:#F5F5F5 style p5 fill:#4F94CD,stroke:#363636,stroke-width:2px,color:#F5F5F5 style p6 fill:#FF6A6A,stroke:#363636,stroke-width:2px,color:#F5F5F5 p1 --> p2 --> p3 --> p4 --> p5 --> p6 p1 -.-> p3 p3 -.-> p5 p1 ==> p5

3.3 BootFlow

Western Digital 在 2019 年 12 月的一份 An Introduction to RISC-V Boot Flow 报告中有这么两幅流程图阐述了 boot 的基本结构以及 ARM64 的 boot 流程, 非常清晰可供参阅。

Common boot flow, Western Digital
Common boot flow, Western Digital


Common boot flow in ARM64, Western Digital
Common boot flow in ARM64, Western Digital

4. 浅析 TPL

嵌入式的代码铁定有个名为 start.S 的入口汇编代码, 但在进行源码分析之前, 我比较喜欢阅读链接脚本以此获悉 u-boot 的构成以及分析启动过程中的一些工作。 在 arch/arm/cpu/armv8 目录下有两个 lds 文件, armv8 的 BootROM $\rightarrow$ u-boot 的引导使用 u-boot.lds 进行链接, 而在 u-boot 之前存在 SPL/TPL 阶段则会使用 u-boot-spl.ldsarch/arm/mach-rockchip/u-boot-tpl-v8.lds 进行链接。

在上述流程中提及 TPL 的存在, 这也是让我比较困惑的, TPL 如何 与 SPL 进行配合实现对 bootloader 的引导启动, 这一块内容值得深入探究。 不妨先从 TPL 的链接脚本入手, 厘清 TPL 阶段的相关逻辑。

这两篇文章关于 u-boot-spl.lds 有着不同详略的解析, 可以用以了解 u-boot 相关的链接脚本的 section 的基本功能以及了解链接脚本的基本概念, 这些内容已有前人做了充分的解析不再赘述。

4.1 TPL Configurations

根据前述配置, evb-px30 在启动时会经由 TPL 以及 SPL 引导 u-boot。 在 arch/arm/mach-rockchip/Kconfig 中可以看到与 TPL 相关的一些定义:TPLTPL_TINY_FRAMEWORKTPL_TINY_FRAMEWORKTPL_LDSCRIPTTPL_TEXT_BASETPL_MAX_SIZESUPPORT_TPL, 这些宏定义会影响后续编译的过程。

其中, CONFIG_TPL_BUILD 这个宏定义非常重要。 网上很多博客提及相关内容仅说明在定义 CONFIG_TPL 之后 CONFIG_TPL_BUILD 会自动定义, 但没有详细说明具体位置。 在 scripts/Makefile.autoconf 文件的 85-87 行可以看到几行规则, 实际上向 tpl/u-boot.cfg 传递了 CONFIG_SPL_BUILDCONFIG_TPL_BUILD 这两个量。 在其他任何 config.mkKconfigKbuild 这样的文件中都不会找到这两个量的定义。

# scripts/Makefile.autoconf
tpl/u-boot.cfg: include/config.h FORCE
    $(Q)mkdir -p $(dir $@)
    $(call cmd,u_boot_cfg,-DCONFIG_SPL_BUILD -DCONFIG_TPL_BUILD)

另外在顶层的 Makefile 文件我们可以找到这样一则规则, 是 script/Makefile.autoconf 的上一级引用。

# Makefile
u-boot.cfg spl/u-boot.cfg tpl/u-boot.cfg: include/config.h FORCE
    $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.autoconf $(@)

而当我们查看 script/Makefile.autoconf 所描述的功能时, 可以看到前述的 CONFIG_SPL_BUILDCONFIG_TPL_BUILD 以及其他生成的宏定义最终会被转移到 Kconfig 中以完成全局性的定义。

# This helper makefile is used for creating
#  - symbolic links (arch/$ARCH/include/asm/arch
#  - include/autoconf.mk, {spl,tpl}/include/autoconf.mk
#  - include/config.h
#
# When our migration to Kconfig is done
# (= When we move all CONFIGs from header files to Kconfig)
# this makefile can be deleted.

4.2 TPL Linker Script

BootROM 完成基本的初始化后首先会在 iRAM 中载入 TPL 段的运行代码。

OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")
OUTPUT_ARCH(aarch64)
ENTRY(_start)
SECTIONS
{
    . = 0x00000000;

    .text : {
        . = ALIGN(8);
        *(.__image_copy_start)
        CPUDIR/start.o (.text*)
        *(.text*)
    }

    .rodata : {
        . = ALIGN(8);
        *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*)))
    }

    .data : {
        . = ALIGN(8);
        *(.data*)
    }

    .u_boot_list : {
        . = ALIGN(8);
        KEEP(*(SORT(.u_boot_list*)));
    }

    .image_copy_end : {
        . = ALIGN(8);
        *(.__image_copy_end)
    }

    .end : {
        . = ALIGN(8);
        *(.__end)
    }

    _image_binary_end = .;

    .bss_start (NOLOAD) : {
        . = ALIGN(8);
        KEEP(*(.__bss_start));
    }

    .bss (NOLOAD) : {
        *(.bss*)
         . = ALIGN(8);
    }

    .bss_end (NOLOAD) : {
        KEEP(*(.__bss_end));
    }

    /DISCARD/ : { *(.dynsym) }
    /DISCARD/ : { *(.dynstr*) }
    /DISCARD/ : { *(.dynamic*) }
    /DISCARD/ : { *(.plt*) }
    /DISCARD/ : { *(.interp*) }
    /DISCARD/ : { *(.gnu*) }
}

#if defined(CONFIG_TPL_MAX_SIZE)
ASSERT(__image_copy_end - __image_copy_start < (CONFIG_TPL_MAX_SIZE), \
 "TPL image too big");
#endif

#if defined(CONFIG_TPL_BSS_MAX_SIZE)
ASSERT(__bss_end - __bss_start < (CONFIG_TPL_BSS_MAX_SIZE), \
 "TPL image BSS too big");
#endif

#if defined(CONFIG_TPL_MAX_FOOTPRINT)
ASSERT(__bss_end - _start < (CONFIG_TPL_MAX_FOOTPRINT), \
 "TPL image plus BSS too big");
#endif

ENTRY(_start) 实际上声明了程序的入口地址, 对 TPL 而言这是显而易见的, 因为 TPL 需要在该阶段获得程序的控制权完成一系列基本的初始化进程。 与其他 ld 文件不同的是, TPL 的链接脚本对 TPL 程序本身的大小有严格的控制。 在 machine 级的 arch/arm/mach-rockchip/Kconfig 中我们定义了 TPL_MAX_SIZE, 这使得我们可以检查 TPL image 的大小以满足 iRAM 的空间限制要求。一般来说,__image_copy_start__image_copy_end 这两个变量常用来辅助 u-boot 的重定位, 但在此处被赋予了新的功能。 另外可以看到 bss 段都被声明了 NOLOAD 属性, 这意味着 bss 段在 image 中并不占用任何空间, 但相关的地址信息会被保留用以在 u-boot加载时的一些数据初始化操作。 因而可以归纳得到 TPL 加载时实际的内存分布情况。

TPL Loading Memory, HangX-Ma
TPL Loading Memory, HangX-Ma

另外可以从 ld 文件中看到, 入口程序是 CPUDIR/start.o, CPUDIR 可以依据层级划分从各个较为顶层的 Makefile 文件中找到具体定义。 但根据架构分析中的概念, 不难得出此处的 CPUDIRarch/arm/armv8start.S 最终会定位到 _main 程序入口继续执行流程。(关于 start.S 的详细流程可以参考 ARMv8架构u-boot启动流程详细分析(二), 内核新视界 由于我们的编译是 AArch64 架构, 那么 C Runtime Environment 的建立也应当是 crt0_64.S, 可以在这个文件中看到, board_init_f_alloc_reserveboard_init_f_init_reserveboard_init_f_boot_flags 几个函数通过在栈顶预留内存来达到给 GD(Global Data) 开辟内存空间, 在 AArch64 架构中 GD 指针地址会被保留在 x18 寄存器中供全局使用, 之后跳转到 board_init_f。 这是一个分水岭, TPL, SPL 以及 u-boot 都会执行这个函数。

一般来说可以将 u-boot 的启动过程划分为两个阶段, 也就是前述的 BL2 和 BL3 的区分。 Pre-relocation(common/board_f.c), 此处的 f 表示程序执行所在的存储介质是 flash, 以及 After-relocation(common/board_r.c), 此处的 r 表示程序执行所在的存储介质是 RAM

我们知道 TPL 只完成一些很基本的初始化流程, 对于 TPL 而言实际上不存在重定位的需求, 所以关键就在 board_init_f 这个函数。

  • SPL: arch/arm/mach-rockchip/spl.c
  • TPL: arch/arm/mach-rockchip/tpl.c
  • U-Boot: common/board_f.c

在编译链接时, 编译组件就会对这几个文件进行区分, 以保证绑定正确的可执行文件。 在 arch/arm/mach-rockchip/Makefile 中就巧妙的在编译 TPL 文件时取消了 SPL 相关文件的生成, 而在编译 SPL 文件时则不受 TPL 的相关定义影响。

根据之前的宏定义梳理的在 TPL 阶段的 board_init_f 所做的工作如下, 时钟初始化, CPU 部分初始化, UART 串口初始化, SDRAM 初始化。 这些工作都完成之后会通过 arch/arm/mach-rockchip/Kconfig 默认定义的 TPL_ROCKCHIP_BACK_TO_BROM 宏引导的 back_to_bootrom 返回 BootROM 阶段再进行下一阶段的 SPL。

board_init_f
    rockchip_stimer_init
    arch_cpu_init
    debug_uart_init
    timer_init
    sdram_init
    back_to_bootrom
flowchart LR p1(BootROM) p2(TPL) p3(SPL) p4(U-Boot) p5(Kernel) style p1 fill:#4F94CD,stroke:#363636,stroke-width:2px,color:#F5F5F5 style p2 fill:#DCDCDC,stroke:#363636,color:#8B8B7A,color:#696969 style p3 fill:#DCDCDC,stroke:#363636,color:#8B8B7A,color:#696969 style p4 fill:#4F94CD,stroke:#363636,stroke-width:2px,color:#F5F5F5 style p5 fill:#FF6A6A,stroke:#363636,stroke-width:2px,color:#F5F5F5 p1 -.->|stage 1 'init'| p2 p2 -.->|stage 2 'back to rom'| p1 p1 -->|stage 3| p3 p3 --> p4 p4 --> p5

至于 SPL 的具体流程可以参考 TPL 的流程进行推导相关的资料也非常详细, 在参考部分的 U-Boot 部分已经列举了筛选过的较好的资料可供选读。

5. 总结

文章对 u-boot 学习路线进行了简单介绍, 并从 u-boot 构建框架着手解构 u-boot, 以 Kconfig 为索引文件自底向上分析框架。 除此之外还介绍了 Boot Loader 的几个基本流程, 对其中的 TPL 过程进行了剖析。后续会在此篇博文的基础上进行增改扩充基础概念部分, 而其他需要仔细剖析的部分则另建博文进行阐述。

6. 参考

6.1 U-Boot

6.2 ARM 参考手册

6.3 AArch64 架构