海思uboot啟動流程詳細分析(三)【轉】


1. 前言

書接上文(u-boot啟動流程分析(二)_平台相關部分),本文介紹u-boot啟動流程中和具體版型(board)有關的部分,也即board_init_f/board_init_r所代表的、board有關初始化過程。該過程將持續u-boot的整個生命周期,直到main_loop(即傳說中的命令行)。

注1:由於u-boot后初始化過程,基本上涉及到了所有的軟件模塊,因此本文不能一一分析,基本原則就是撿看着順眼的、熟的下手了~。

2. Generic Board

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可知:

# boards
obj-y += board_f.o
obj-y += board_r.o

可以看到這兩個文件都會調用到;

3. _main

_main函數位於crt0_64.S中:

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 背景知識

我們看看board_init_r函數:

在uboot的common文件下有一個board_r.c文件:

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寄存器中,如下:

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聲明,由平台的開發者根據實際情況實現。具體可參考本章后續的描述。

void board_init_r(gd_t *new_gd, ulong dest_addr)
{
#ifdef CONFIG_NEEDS_MANUAL_RELOC
	int i;
#endif

#ifdef CONFIG_AVR32
	mmu_init_r(dest_addr);
#endif

#if !defined(CONFIG_X86) && !defined(CONFIG_ARM) && !defined(CONFIG_ARM64)
	gd = new_gd;
#endif

#ifdef CONFIG_NEEDS_MANUAL_RELOC
	for (i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
		init_sequence_r[i] += gd->reloc_off;
#endif

	if (initcall_run_list(init_sequence_r))
		hang();

	/* NOTREACHED - run_main_loop() does not return */
	hang();
}

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自行理解。

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需要使用的部分,如下圖:
image

總結如下:

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,開始命令行操作。

轉發請注明出處。蝸窩科技,www.wowotech.net。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM