- 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 Baldwin 的 ARM 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 的初始化过程就是 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
整个关键量以及定义的 BORAD
, VENDOR
等编译相关的变量。
# 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_PX30
该 bool
变量是 true
。 可以看到该 Kconfig
文件在框架中属于亟待更新的 machine 层级, 所以在该部分可以看到 SYS_SOC
这个配置变量。 在该 Kconfig
文件中还覆盖定义了 SYS_MALLOC_F_LEN
和 SPL_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.h
, include/configs/evb_px30.h
, 以及 include/configs/rockchip-common.h
。 我们自底向上, 特定的板级文件开始溯源, 找到了最终顶层的配置文件。 根据顶层的配置文件以及每个层级的配置文件可以梳理出编译特定板所需的功能。 另外, 在底层的 TARGET 的配置中可以看到诸如 SYS_xxx
的一系列配置, 这些配置会在更上层的 arch/Kconfig
中定义。 所以综上可以总结出如下配置关系图。
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
根据以上描述, 以图例形式表述 u-boot 的启动流程应当如下所示。
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 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.lds
或 arch/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 相关的一些定义:TPL
, TPL_TINY_FRAMEWORK
, TPL_TINY_FRAMEWORK
, TPL_LDSCRIPT
, TPL_TEXT_BASE
, TPL_MAX_SIZE
, SUPPORT_TPL
, 这些宏定义会影响后续编译的过程。
其中, CONFIG_TPL_BUILD
这个宏定义非常重要。 网上很多博客提及相关内容仅说明在定义 CONFIG_TPL
之后 CONFIG_TPL_BUILD
会自动定义, 但没有详细说明具体位置。 在 scripts/Makefile.autoconf
文件的 85-87 行可以看到几行规则, 实际上向 tpl/u-boot.cfg
传递了 CONFIG_SPL_BUILD
, CONFIG_TPL_BUILD
这两个量。 在其他任何 config.mk
,Kconfig
, Kbuild
这样的文件中都不会找到这两个量的定义。
# 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_BUILD
, CONFIG_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
另外可以从 ld
文件中看到, 入口程序是 CPUDIR/start.o
, CPUDIR
可以依据层级划分从各个较为顶层的 Makefile
文件中找到具体定义。 但根据架构分析中的概念, 不难得出此处的 CPUDIR
是 arch/arm/armv8
。 start.S
最终会定位到 _main
程序入口继续执行流程。(关于 start.S
的详细流程可以参考 ARMv8架构u-boot启动流程详细分析(二), 内核新视界 由于我们的编译是 AArch64 架构, 那么 C Runtime Environment 的建立也应当是 crt0_64.S
, 可以在这个文件中看到, board_init_f_alloc_reserve
, board_init_f_init_reserve
, board_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
至于 SPL 的具体流程可以参考 TPL 的流程进行推导相关的资料也非常详细, 在参考部分的 U-Boot 部分已经列举了筛选过的较好的资料可供选读。
5. 总结
文章对 u-boot 学习路线进行了简单介绍, 并从 u-boot 构建框架着手解构 u-boot, 以 Kconfig 为索引文件自底向上分析框架。 除此之外还介绍了 Boot Loader 的几个基本流程, 对其中的 TPL 过程进行了剖析。后续会在此篇博文的基础上进行增改扩充基础概念部分, 而其他需要仔细剖析的部分则另建博文进行阐述。
6. 参考
6.1 U-Boot
- 从0移植uboot (二) _uboot启动流程分析, Abnor
- u-boot分析(文章类), wowo
- X-003-UBOOT-基于Bubblegum-96平台的u-boot移植说明, wowo
- u-boot启动流程, wowothink
- ARMv8架构u-boot启动流程详细分析(一), 内核新视界
- ARMv8架构u-boot启动流程详细分析(二), 内核新视界
- Armv8架构UBOOT 启动篇——SPL(start.S), Kernel_Nuts
- Armv8架构UBOOT 启动篇——SPL(u-boot-spl.lds链接脚本), Kernel_Nuts
- U-Boot - Bootloader for IoT Platform? [ELCE 2018], Alexey Brodkin, Synopsys
- U-boot startup sequence, HouchengLin
6.2 ARM 参考手册
- Arm® Instruction Set Reference Guide
- ARM Developer Suite Assembler Guide
- Arm Architecture Reference Manual for A-profile architecture
- Arm Cortex-A35 Processor Technical Reference Manual
- ARM ELF Specification