1. 前言
書接上文(u-boot啟動流程分析(1)_平臺相關部分),本文介紹u-boot啟動流程中和具體版型(board)有關的部分,也即board_init_f/board_init_r所代表的、board有關初始化過程。該過程將持續u-boot的整個生命周期,直到main_loop(即傳說中的命令行)。
注1:由于u-boot后初始化過程,基本上涉及到了所有的軟件模塊,因此本文不能一一分析,基本原則就是撿看著順眼的、熟的下手了~。
2. Generic Board
我們在“u-boot啟動流程分析(1)_平臺相關部分”中,介紹過board_init_f接口,并在“X-003-UBOOT-基于Bubblegum-96平臺的u-boot移植說明”中,通過在SPL image中的board_init_f點亮了一個LED燈。
u-boot的基本策略,就是聲明一系列的API(如low_level_init、board_init_f、board_init_r等等),并在u-boot的核心邏輯中調用它們。平臺的移植和開發者,所需要做的,就是根據實際情況,實現它們。
與此同時,為了減少開發的工作量,u-boot為大部分API提供了通用實現(一般通過CONFIG配置項或者若定義去控制是否編譯)。以board_init_f和board_init_r兩個板級的初始化接口為例,u-boot分別在common/board_f.c和common/board_r.c兩個文件中提供了通用實現。查看common/Makefile可知:
ifndef CONFIG_SPL_BUILD
…
# boards
obj-$(CONFIG_SYS_GENERIC_BOARD) += board_f.o
obj-$(CONFIG_SYS_GENERIC_BOARD) += board_r.o
…
endif # !CONFIG_SPL_BUILD
這兩個通用接口是由CONFIG_SYS_GENERIC_BOARD配置項控制的,并且只會在u-boot image中被編譯。再通過arch/Kconfig中ARM平臺有關的配置可知:
config ARM
bool "ARM architecture"
select CREATE_ARCH_SYMLINK
select HAVE_PRIVATE_LIBGCC if !ARM64
select HAVE_GENERIC_BOARD
select SYS_GENERIC_BOARD
select SUPPORT_OF_CONTROL
ARM平臺自動使能了CONFIG_SYS_GENERIC_BOARD配置,因此u-boot image有關的板級啟動流程,是由Generic Board的代碼實現的,具體可參考后續的分析。
注2:由“u-boot啟動流程分析(1)_平臺相關部分”中的分析可知,SPL image的聲明周期,在自定義的board_init_f執行完成后,便結束了,因此本文將只針對u-boot image。
3. _main
接著“u-boot啟動流程分析(1)_平臺相關部分”的表述,我們從_main函數重新分析(只不過此時不再是SPL build,代碼不再貼出)。執行過程如下:
1)設置初始的堆棧
基址由CONFIG_SYS_INIT_SP_ADDR定義。
2)分配global data所需的空間
將堆棧16 bits對齊之后,調用board_init_f_alloc_reserve接口,從堆棧開始的地方,為u-boot的global data(struct global_data)分配空間。如下:
/* common/init/board_init.c */
ulong board_init_f_alloc_reserve(ulong top)
{
/* Reserve early malloc arena */
#if defined(CONFIG_SYS_MALLOC_F)
top -= CONFIG_SYS_MALLOC_F_LEN;
#endif
/* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
top = rounddown(top-sizeof(struct global_data), 16);
return top;
}
需要注意的是,如果定義了CONFIG_SYS_MALLOC_F_LEN,則會先預留出early malloc所需的空間。
3)初始化global data
global data的空間分配后,調用board_init_f_init_reserve,初始化global data。所謂的初始化,無非就是一些清零操作,不過有幾個地方需要注意:
1)如果不是ARM平臺(!CONFIG_ARM),則可以調用arch_setup_gd接口,進行arch級別的設置。當然,前提是,對應的arch應該實現這個接口。
2)如果定義了CONFIG_SYS_MALLOC_F,則會初始化gd->malloc_base。
4)執行前置的(front)初始化操作
調用board_init_f接口,執行前置的初始化操作,會再后面的章節詳細說明。
5)執行relocation操作,后面會詳細說明。
6)清除BBS段
7)執行后置的(rear)初始化操作
調用board_init_r接口,執行前置的初始化操作,會再后面的章節詳細說明。
4. global data介紹以及背后的思考
4.1 背景知識
要理解global data的意義,需要先理解如下的事實:
u-boot是一個bootloader,有些情況下,它可能位于系統的只讀存儲器(ROM或者flash)中,并從那里開始執行。
因此,這種情況下,在u-boot執行的前期(在將自己copy到可讀寫的存儲器之前),它所在的存儲空間,是不可寫的,這會有兩個問題:
1)堆棧無法使用,無法執行函數調用,也即C環境不可用。
2)沒有data段(或者正確初始化的data段)可用,不同函數或者代碼之間,無法通過全局變量的形式共享數據。
對于問題1,通常的解決方案是:
u-boot運行起來之后,在那些不需要執行任何初始化動作即可使用的、可讀寫的存儲區域,開辟一段堆棧(stack)空間。
一般來說,大部分的平臺(如很多ARM平臺),都有自己的SRAM,可用作堆棧空間。如果實在不行,也有可借用CPU的data cache的方法(不再過多說明)。
對于問題2,解決方案要稍微復雜一些:
首先,對于開發者來說,在u-boot被拷貝到可讀寫的RAM(這個動作稱作relocation)之前,永遠不要使用全局變量。
其次,在relocation之前,不同模塊之間,確實有通過全局變量的形式傳遞數據的需求。怎么辦?這就是global data需要解決的事情。
4.2 global data
為了在relocation前通過全局變量的形式傳遞數據,u-boot設計了一個巧妙的方法:
1)定義一個struct global_data類型的數據結構,里面保存了各色各樣需要傳遞的數據
該數據結構的具體內容,后面用到的時候再一個一個解釋,這里不再詳細介紹。具體可參考:include/asm-generic/global_data.h
2)堆棧配置好之后,在堆棧開始的位置,為struct global_data預留空間(可參考第3章中相關的說明),并將開始地址(就是一個struct global_data指針)保存在一個寄存器中,后續的傳遞,都是通過保存在寄存器中的指針實現
對arm64平臺來說,該指針保存在了X18寄存器中,如下:
/*https://github.com/wowotechX/u-boot/blob/x_integration/arch/arm/lib/crt0_64.S*/
bl????? board_init_f_alloc_reserve
mov???? sp, x0
/* set up gd here, outside any C code */
mov???? x18, x0
bl????? board_init_f_init_reserve
上面board_init_f_alloc_reserve的返回值(x0)就是global data的指針。
/* arch/arm/include/asm/global_data.h */
#ifdef __clang__
#define DECLARE_GLOBAL_DATA_PTR
#define gd????? get_gd()
static inline gd_t *get_gd(void)
{
gd_t *gd_ptr;
#ifdef CONFIG_ARM64
…
__asm__ volatile("mov %0, x18\n" : "=r" (gd_ptr));
#else
…
}
#else
#ifdef CONFIG_ARM64
#define DECLARE_GLOBAL_DATA_PTR???????? register volatile gd_t *gd asm ("x18")
#else
…
#endif
5. 前置的板級初始化操作
global data準備好之后,u-boot會執行前置的板級初始化動作,即board_init_f。所謂的前置的初始化動作,主要是relocation之前的初始化操作,也就是說:
執行board_init_f的時候,u-boot很有可能還在只讀的存儲器中。大家記住這一點就可以了!
注3:大家可能會覺得這里的f(front?)和r(rear?)的命名有點奇怪,我猜這個軟件工程師應該是車迷,是不是借用了前驅和后驅的概念?不得而知啊。
對于ARM等平臺來說,u-boot提供了一個通用的board_init_f接口,該接口使用u-boot慣用的設計思路:
u-boot將需要在board_init_f中初始化的內容,抽象為一系列API。這些API由u-boot聲明,由平臺的開發者根據實際情況實現。具體可參考本章后續的描述。
5.1 board_init_f
位于common/board_f.c中的board_init_f接口的實現非常簡單,如下(省略了一些無用代碼):
void board_init_f(ulong boot_flags)
{
…
gd->flags = boot_flags;
gd->have_console = 0;
if (initcall_run_list(init_sequence_f))
hang();
…
}
對global data進行簡單的初始化之后,調用位于init_sequence_f數組中的各種初始化API,進行各式各樣的初始化動作。后面將會簡單介紹一些和ARM平臺有關的、和平臺的移植工作有關的、比較重要的API。其它API,大家可以參考source code自行理解。
注4:下面紅色字體標注的API,是u-boot移植時有很大可能需要的API,大家留意就是。
5.2 fdtdec_setup
#ifdef CONFIG_OF_CONTROL
fdtdec_setup,
#endif
如果打開了CONFIG_OF_CONTROL,則調用fdtdec_setup,配置gd->fdt_blob指針(即device tree所在的存儲位置)。對ARM平臺來說,u-boot的Makefile會通過連接腳本,將dtb文件打包到u-boot image的“__dtb_dt_begin”位置處,因此不需要特別關心。
5.3 trace_early_init
#ifdef CONFIG_TRACE
trace_early_init,
#endif
由CONFIG_TRACE配置項控制,暫且不用關注,后面用到的時候再分析。
5.4 initf_malloc
如果定義了CONFIG_SYS_MALLOC_F_LEN,則調用initf_malloc,初始化malloc有關的global data,如gd->malloc_limit、gd->malloc_ptr。
5.5 arch_cpu_init
cpu級別的初始化操作,可以在需要的時候由CPU有關的code實現。
5.6 initf_dm
driver model有關的初始化操作。如果定義了CONFIG_DM,則調用dm_init_and_scan初始化并掃描系統所有的device。如果定義了CONFIG_TIMER_EARLY,調用dm_timer_init初始化driver model所需的timer。
5.7 board_early_init_f
#if defined(CONFIG_BOARD_EARLY_INIT_F)
board_early_init_f,
#endif
如果定義CONFIG_BOARD_EARLY_INIT_F,則調用board_early_init_f接口,執行板級的early初始化。平臺的開發者可以根據需要,實現board_early_init_f接口,以完成特定的功能。
5.8 timer_init
初始化系統的timer。
該接口應該由平臺或者板級的代碼實現,初始化成功后,u-boot會通過其它的API獲取當前的timestamp,后面用到的時候再詳細介紹。
5.9 get_clocks
獲取當前CPU和BUS的時鐘頻率,并保存在global data中:
gd->cpu_clk
gd->bus_clk
5.10 env_init
初始化環境變量有關的邏輯,不需要特別關注。
5.11 init_baud_rate
gd->baudrate = getenv_ulong("baudrate", 10, CONFIG_BAUDRATE);
獲取當前使用串口波特率,可以有兩個途徑(優先級從高到低):從"baudrate"中獲??;從CONFIG_BAUDRATE配置項獲取。
5.12 serial_init
初始化serial,包括u-boot serial core以及具體的serial driver。該函數執行后,系統的串口(特別是用于控制臺的)已經可用。
5.13 console_init_f
/* Called before relocation - use serial functions */
int console_init_f(void)
{
gd->have_console = 1;
#ifdef CONFIG_SILENT_CONSOLE
if (getenv("silent") != NULL)
gd->flags |= GD_FLG_SILENT;
#endif
print_pre_console_buffer(PRE_CONSOLE_FLUSHPOINT1_SERIAL);
return 0;
}
初始化系統的控制臺,之后串口輸出可用。大家可留意CONFIG_SILENT_CONSOLE配置項,如果使能,可以通過“silent”環境變量,控制u-boot的控制臺是否輸出。
5.14? fdtdec_prepare_fdt
#ifdef CONFIG_OF_CONTROL
fdtdec_prepare_fdt,
#endif
如果定義了CONFIG_OF_CONTROL,調用fdtdec_prepare_fdt接口,準備device tree有關的內容。后續device tree的分析文章會詳細介紹。
5.15 display_options/display_text_info/print_cpuinfo/show_board_info
通過控制臺,顯示一些信息,可用于debug。
5.16 misc_init_f
#if defined(CONFIG_MISC_INIT_F)
misc_init_f,
#endif
如果使能了CONFIG_MISC_INIT_F,則調用misc_init_f執行misc driver有關的初始化。
5.17 init_func_i2c
#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SYS_I2C)
init_func_i2c,
#endif
如果使能了CONFIG_HARD_I2C或者CONFIG_SYS_I2C,則調用init_func_i2c執行i2c driver有關的初始化。
5.18 init_func_spi
#if defined(CONFIG_HARD_SPI)
init_func_spi,
#endif
如果使能了CONFIG_HARD_SPI,則調用init_func_spi執行spi driver有關的初始化。
5.19 announce_dram_init
宣布我們要進行DDR的初始化動作了(其實就是一行打印)。
5.20 dram_init
#if defined(CONFIG_ARM) || defined(CONFIG_X86) || defined(CONFIG_NDS32) || \
defined(CONFIG_MICROBLAZE) || defined(CONFIG_AVR32)
dram_init,????????????? /* configure available RAM banks */
#endif
調用dram_init接口,初始化系統的DDR。dram_init應該由平臺相關的代碼實現。
如果DDR在SPL中已經初始化過了,則不需要重新初始化,只需要把DDR信息保存在global data中即可,例如:
gd->ram_size = …
5.21 testdram
#if defined(CONFIG_SYS_DRAM_TEST)
testdram,
#endif /* CONFIG_SYS_DRAM_TEST */
如果定義了CONFIG_SYS_DRAM_TEST,則會調用testdram執行DDR的測試操作。可以在開發階段打開,系統穩定后關閉。
5.22 DRAM空間的分配
DRAM初始化完成后,就可以著手規劃u-boot需要使用的部分,如下圖:
總結如下:
1)考慮到后續的kernel是在RAM的低端位置解壓縮并執行的,為了避免麻煩,u-boot將使用DRAM的頂端地址,即gd->ram_top所代表的位置。其中gd->ram_top是由setup_dest_addr函數配置的。
2)u-boot所使用的DRAM,主要分為三類:各種特殊功能所需的空間,如log buffer、MMU page table、LCD fb buffer、trace buffer、等等;u-boot的代碼段、數據段、BSS段所占用的空間(就是u-boot relocate之后的執行空間),由gd->relocaddr標示;堆棧空間,從gd->start_addr_sp處遞減。
3)特殊功能以及u-boot所需空間,是由reserve_xxx系列函數保留的,具體可參考source code,這里不再詳細分析。
4)reserve空間分配完畢后,堆棧緊隨其后,遞減即可。
5.23 setup_dram_config
調用dram_init_banksize接口(由具體的平臺代碼實現),初始化DDR的bank信息。
5.24 reloc_fdt
如果沒有定義CONFIG_OF_EMBED,則先將device tree拷貝到圖片1 new_fdt所在的位置,也就是device tree的relocation操作。
5.25 setup_reloc
計算relocation有關的信息,主要是 gd->reloc_off,計算公式如下:
gd->reloc_off = gd->relocaddr - CONFIG_SYS_TEXT_BASE;
其中CONFIG_SYS_TEXT_BASE是u-boot relocation之前在(只讀)memory的位置(也是編譯時指定的位置),gd->relocaddr是relocation之后的位置,因此gd->reloc_off代表u-boot relocation操作之后的偏移量,后面relocation時會用到。
同時,該函數順便把global data拷貝到了圖片1所示的“new global data”處,其實就是global data的relocation。
6. u-boot的relocation
前面講過,u-boot是有可能在只讀的memory中啟動的。簡單起見,u-boot假定所有的啟動都是這樣,因此u-boot的啟動邏輯,都是針對這種情況設計的。在這種情況下,基于如下考慮:
1)只讀memory中執行,代碼需要小心編寫(不能使用全局變量,等等)。
2)只讀memory執行速度通常比較慢。
u-boot需要在某一個時間點,將自己從“只讀memory”中,拷貝到可讀寫的memory(如SDRAM,后面統稱RAM,注意和SRAM區分,不要理解錯了)中繼續執行,這就是relocation(重定位)操作。
relocation的時間點,可以是“系統可讀寫memory始化完成之后“的任何時間點。根據u-boot當前的代碼邏輯,是在board_init_f執行完成之后,因為board_init_f中完成了很多relocation有關的準備動作,具體可參考第5章的描述。
u-boot relocation的代碼如下(以arm64為例):
/*https://github.com/wowotechX/u-boot/blob/x_integration/arch/arm/lib/crt0_64.S*/
ldr???? x0, [x18, #GD_START_ADDR_SP]??? /* x0 <- gd->start_addr_sp */
bic???? sp, x0, #0xf??? /* 16-byte alignment for ABI compliance */
ldr???? x18, [x18, #GD_BD]????????????? /* x18 <- gd->bd */
sub???? x18, x18, #GD_SIZE????????????? /* new GD is below bd */
adr???? lr, relocation_return
ldr???? x9, [x18, #GD_RELOC_OFF]??????? /* x9 <- gd->reloc_off */
add???? lr, lr, x9????? /* new return address after relocation */
ldr???? x0, [x18, #GD_RELOCADDR]??????? /* x0 <- gd->relocaddr */
b?????? relocate_code
relocation_return:
邏輯比較簡單:
1)從global data中取出relocation之后的堆?;?,16-byte對齊后,保存到sp中。
2)將新的global data的指針,保存在x18寄存器中。
3)計算relocation之后的執行地址(relocation_return處),計算的方法就是當前的relocation_return位置加上gd->reloc_off。
4)以relocation的目的地址(gd->relocaddr)為參數,調用relocate_code執行實際的relocation動作,就是將u-boot的代碼段、data段、bss段等數據,拷貝到新的位置(gd->relocaddr)。
7. 后置的板級初始化操作
relocate完成之后,真正的C運行環境才算建立了起來,接下來會執行“后置的板級初始化操作”,即board_init_r函數。board_init_r和board_init_f的設計思路基本一樣,也有一個很長的初始化序列----init_sequence_r,該序列中包含如下的初始化函數(邏輯比較簡單,這里不再涉及細節,權當列出index吧):
注5:老規矩,紅色字體標注的函數是比較重要的函數。
1)initr_trace,初始化并使能u-boot的tracing system,涉及的配置項有CONFIG_TRACE。
2)initr_reloc,設置relocation完成的標志。
3)initr_caches,使能dcache、icache等,涉及的配置項有CONFIG_ARM。
4)initr_malloc,malloc有關的初始化。
5)initr_dm,relocate之后,重新初始化DM,涉及的配置項有CONFIG_DM。
6)board_init,具體的板級初始化,需要由board代碼根據需要實現,涉及的配置項有CONFIG_ARM。
7)set_cpu_clk_info,Initialize clock framework,涉及的配置項有CONFIG_CLOCKS。
8)initr_serial,重新初始化串口(不太明白什么意思)。
9)initr_announce,宣布已經在RAM中執行,會打印relocate后的地址。
10)board_early_init_r,由板級代碼實現,涉及的配置項有CONFIG_BOARD_EARLY_INIT_R。
11)arch_early_init_r,由arch代碼實現,涉及的配置項有CONFIG_ARCH_EARLY_INIT_R。
12)power_init_board,板級的power init代碼,由板級代碼實現,例如hold住power。
13)initr_flash、initr_nand、initr_onenand、initr_mmc、initr_dataflash,各種flash設備的初始化。
14)initr_env,環境變量有關的初始化。
15)initr_secondary_cpu,初始化其它的CPU core。
16)stdio_add_devices,各種輸入輸出設備的初始化,如LCD driver等。
17)interrupt_init,中斷有關的初始化。
18)initr_enable_interrupts,使能系統的中斷,涉及的配置項有CONFIG_ARM(ARM平臺u-boot實在開中斷的情況下運行的)。
19)initr_status_led,狀態指示LED的初始化,涉及的配置項有CONFIG_STATUS_LED、STATUS_LED_BOOT。
20)initr_ethaddr,Ethernet的初始化,涉及的配置項有CONFIG_CMD_NET。
21)board_late_init,由板級代碼實現,涉及的配置項有CONFIG_BOARD_LATE_INIT。
22)等等…
23)run_main_loop/main_loop,執行到main_loop,開始命令行操作。