U-Boot-基礎概念與學習分享

Ma發表於2023-02-27

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-基礎概念與學習分享
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:#A44,stroke:#333,stroke-width:4px style arch fill:#A44,stroke:#333,stroke-width:4px style arm fill:#A43,stroke:#333,stroke-width:4px style mach-rockchip fill:#BBA,stroke:#333,stroke-width:4px style px30 fill:#444,stroke:#333,stroke-width:4px 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 的外設, 準備核心啟動引數以及載入執行核心等操作。
U-Boot-基礎概念與學習分享
Boot loader sequences, HouchengLin

根據以上描述, 以圖例形式表述 u-boot 的啟動流程應當如下所示。

flowchart LR p1(BootROM) p2(TPL, optional) p3(SPL) p4(ATF, optional) p5(U-Boot) p6(Kernel) p1 --> p2 --> p3 --> p4 --> p5 --> p6 p1 -.-> p3 p3 -.-> p5 p1 ==> p5

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 載入時實際的記憶體分佈情況。

U-Boot-基礎概念與學習分享
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) 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 架構

相關文章