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 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 的啟動流程應當如下所示。
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