天天看點

超詳細【Uboot驅動開發】(二)uboot啟動流程分析

作者:嵌入式藝術

#頭條創作挑戰賽#

超詳細【Uboot驅動開發】(二)uboot啟動流程分析

文章目錄

    • 2.1、程式執行流程圖
    • 2.2、u-boot.lds——Uboot的入口函數
    • 2.3、board_init_f——闆級前置初始化
    • 2.4、relocate_code重定向
      • 2.4.1 為什麼需要重定向呢?
      • 2.4.2 Uboot是如何重定向的?
      • 2.4.3 Uboot重定向作用
    • 2.5、board_init_r——闆級後置初始化
    • 2.6、main_loop——Uboot主循環
      • 2.6.1 bootdelay_process
      • 2.6.2 autoboot_command
      • 2.6.3 cli_loop
    • 2.7 參考文章:
上一篇文章:(一)uboot基礎了解 下一篇文章:(三)Uboot驅動模型

同大多數的Bootloader一樣,uboot的啟動過程也分為BL1、BL2兩個階段,分别對應着SPL和Uboot。

SPL(BL1階段):負責開發闆的基礎配置和裝置初始化,并且搬運Uboot到記憶體中,由彙編代碼和少量的C語言實作

Uboot(BL2階段):主要負責初始化外部裝置,引導Kernel啟動,由純C語言實作。

我們這篇文章,主要介紹Uboot(BL2階段)的啟動流程,BL1階段啟動流程的詳細分析,可以見我的後續文章。想要深入了解的,可以好好研究下!
超詳細【Uboot驅動開發】(二)uboot啟動流程分析

2.1、程式執行流程圖

我們先總體來看一下Uboot的執行步驟,這裡以EMMC作為啟動媒體,進行分析!

無論是哪種啟動媒體,基本流程都相似,我們這就往下看!

超詳細【Uboot驅動開發】(二)uboot啟動流程分析

打開圖檔,結合文檔、圖檔、代碼進行了解!

2.2、u-boot.lds——Uboot的入口函數

u-boot.lds:是uboot工程的連結腳本檔案,對于工程的編譯和連結有非常重要的作用,決定了uboot的組裝,并且u-boot.lds連結檔案中的ENTRY(_start)指定了uboot程式的入口位址。

如果不知道u-boot.lds放到在哪裡,可以通過find -name u-boot.lds查找,根目錄要進入到uboot的源碼的位置哦!

如果查找結果有很多,結合自己的闆子資訊,确定自己使用的u-boot.lds。

當然,準确的方法是檢視Makefile檔案,分析出來u-boot.lds所生成的位置。

在u-boot.lds的檔案中,可以看到.text段,存放的就是執行的文本段。截取部分代碼段如下:

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
 . = 0x00000000;				@起始位址
 . = ALIGN(4);					@四位元組對齊
 .text :			
 {	
  *(.__image_copy_start)		@映像檔案複制起始位址
  *(.vectors)					@異常向量表
  arch/arm/cpu/armv7/start.o (.text*)	@啟動函數
 }
 ......
}
           
  • ENTRY(_start):程式的入口函數,_start在arch/arm/lib/vectors.S中定義.globl _start
  • SECTIONS定義了段,包括text文本段、data資料段、bss段等。
  • __image_copy_start在System.map和u-boot.map中均有定義
  • arch/arm/cpu/armv7/start.o對應檔案arch/arm/cpu/armv7/start.S,該檔案中定義了main函數的入口。
Tip:上面隻進行大概分析,有彙編經驗的朋友,可以詳細進行分析!
超詳細【Uboot驅動開發】(二)uboot啟動流程分析

2.3、board_init_f——闆級前置初始化

跟随上文的程式執行流程圖,我們看board_init_f這個函數。其位于common/board_f.c。

void board_init_f(ulong boot_flags)
{
    gd->flags = boot_flags;
	gd->have_console = 0;

	if (initcall_run_list(init_sequence_f))
		hang();
}

static const init_fnc_t init_sequence_f[] = {
	setup_mon_len,
	...
	log_init,
	arch_cpu_init,		/* basic arch cpu dependent setup */
	env_init,		/* initialize environment */
	...       
	reloc_fdt,
	reloc_bootstage,
	reloc_bloblist,
	setup_reloc,
    ...
}
           

board_init_f(),其最核心的内容就是調用了init_sequence_f初始化序列,進行了一系列初始化的工作。

主要包括:序列槽、定時器、裝置樹、DM驅動模型等,另外還包括global_data結構體相關對象的變量。

詳細分析,可以看文末的參考文章[1]

我們需要注意的一點就是,在初始化隊列末尾,執行了幾個reloc_xxx的函數,這幾個函數實作了Uboot的重定向功能。

2.4、relocate_code重定向

重定向技術,可以說也算是Uboot的一個重點了,也就是将uboot自身鏡像拷貝到ddr上的另外一個位置的動作。

2.4.1 為什麼需要重定向呢?

一般需要重定向的條件如下:

  • uboot存儲在隻讀存儲器上,比如ROM、Nor flash,需要将代碼拷貝到DDR上,才能完整運作Uboot。
  • 為Kernel騰空間,Kernel一般會放在DDR的地段位址上,是以要把Uboot重定向到頂端位址,避免沖突。

2.4.2 Uboot是如何重定向的?

Uboot的重定向有如下幾個步驟:

  • 對relocate進行空間劃分
  • 計算uboot代碼空間到relocate的位置的偏移
  • relocate舊的global_data到新的global_data空間上
  • relocate Uboot
  • 修改relocate後的全局變量的label
  • relocate中斷向量表

運作大緻流程:

arch/arm/lib/crt0.S檔案内,主要實作了:

ENTRY(_main)
    bl  board_init_f
@@ 在board_init_f裡面實作了
@@                             (1)對relocate進行空間規劃
@@                             (2)計算uboot代碼空間到relocation的位置的偏移
@@                             (3)relocate舊的global_data到新的global_data的空間上

    ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
    ldr r9, [r9, #GD_BD]        /* r9 = gd->bd */
    sub r9, r9, #GD_SIZE        /* new GD is below bd */
@@ 把新的global_data位址放在r9寄存器中

    adr lr, here
    ldr r0, [r9, #GD_RELOC_OFF]     /* r0 = gd->reloc_off */
    add lr, lr, r0
@@ 計算傳回位址在新的uboot空間中的位址。b調用函數傳回之後,就跳到了新的uboot代碼空間中。

    ldr r0, [r9, #GD_RELOCADDR]     /* r0 = gd->relocaddr */
@@ 把uboot的新的位址空間放到r0寄存器中,作為relocate_code的參數
    b   relocate_code
@@ 跳轉到relocate_code中,在這裡面實作了
@@                                       (1)relocate舊的uboot代碼空間到新的空間上去
@@                                       (2)修改relocate之後全局變量的label
@@ 注意,由于上述已經把lr寄存器重定義到uboot新的代碼空間中了,是以傳回之後,就已經跳到了新的代碼空間了!!!!!!

    bl  relocate_vectors
@@ relocate中斷向量表
           
  • setup_reloc——重定向位址檢視(仿真有關)

在這裡我們說明一下board_init_f裡面的setup_reloc初始化函數

static int setup_reloc(void)
{
	if (gd->flags & GD_FLG_SKIP_RELOC) {
		debug("Skipping relocation due to flag\n");
		return 0;
	}

#ifdef CONFIG_SYS_TEXT_BASE
#ifdef ARM
	gd->reloc_off = gd->relocaddr - (unsigned long)__image_copy_start;
#elif defined(CONFIG_M68K)
	/*
	 * On all ColdFire arch cpu, monitor code starts always
	 * just after the default vector table location, so at 0x400
	 */
	gd->reloc_off = gd->relocaddr - (CONFIG_SYS_TEXT_BASE + 0x400);
#elif !defined(CONFIG_SANDBOX)
	gd->reloc_off = gd->relocaddr - CONFIG_SYS_TEXT_BASE;
#endif
#endif
	memcpy(gd->new_gd, (char *)gd, sizeof(gd_t));

	debug("Relocation Offset is: %08lx\n", gd->reloc_off);
	if (is_debug_open()) {
		printf("Relocating to %08lx, new gd at %08lx, sp at %08lx\n",
		      gd->relocaddr, (ulong)map_to_sysmem(gd->new_gd),
		      gd->start_addr_sp);
	}

	return 0;
}
           

由于,Uboot進行了重定向,是以按照正常的位址仿真的話,我們可能通路到錯誤的記憶體空間,通過setup_reloc的Relocating to %08lx列印,我們可以得到重定向後的位址,友善我們仿真。

Uboot的重定向也有相當大的一部分知識點,上面也僅僅是簡單介紹了relocate的基本步驟和流程,後續看大家需要,如果大家想了解,我再補上這一部分。

2.4.3 Uboot重定向作用

總之,Uboot重定向之後,把Uboot整體搬運到了高端記憶體區,為Kernel的加載提供空間,避免記憶體踐踏。

超詳細【Uboot驅動開發】(二)uboot啟動流程分析

2.5、board_init_r——闆級後置初始化

我們接着跟着流程圖往下看,重定向之後,Uboot運作于新的位址空間,接着我們執行board_init_r,主要作為Uboot運作的最後初始化步驟。

board_init_r這個函數,同樣位于common/board_f.c,主要用于初始化各類外設資訊

void board_init_r(gd_t *new_gd, ulong dest_addr)
{	
	if (initcall_run_list(init_sequence_r))
		hang();

	/* NOTREACHED - run_main_loop() does not return */
	hang();
}
static init_fnc_t init_sequence_r[] = {
	initr_reloc,
	initr_reloc_global_data,
    board_init,	/* Setup chipselects */
	initr_dm,
	initr_mmc,
	...
	run_main_loop
}
           

與board_init_f相同,同樣有一個init_sequence_r初始化清單,包括:initr_dmDM模型初始化,initr_mmcMMC驅動初始化,等等。

最終,uboot就運作到了run_main_loop,進而執行main_loop這個函數。

2.6、main_loop——Uboot主循環

該函數為Uboot的最終執行函數,無論是加載kernel還是uboot的指令行體系,均由此實作。
void main_loop(void)
{
	const char *s;

	bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");

	if (IS_ENABLED(CONFIG_VERSION_VARIABLE))
		env_set("ver", version_string);  /* set version variable */

	cli_init();

	if (IS_ENABLED(CONFIG_USE_PREBOOT))
		run_preboot_environment_command();

	if (IS_ENABLED(CONFIG_UPDATE_TFTP))
		update_tftp(0UL, NULL, NULL);

	s = bootdelay_process();
	if (cli_process_fdt(&s))
		cli_secure_boot_cmd(s);

	autoboot_command(s);

	cli_loop();
	panic("No CLI available");
}

           

env_set:設定環境變量,兩個參數分别為name和value

cli_init:用于初始化hash shell的一些變量

run_preboot_environment_command:執行預定義的環境變量的指令

bootdelay_process:加載延時處理,一般用于Uboot啟動後,有幾秒的倒計時,用于進入指令行模式。

cli_loop:指令行模式,主要作用于Uboot的指令行互動。

2.6.1 bootdelay_process

記得對照文章開始的執行流程圖哦!

詳細解釋标注于代碼中…

const char *bootdelay_process(void)
{
	char *s;
	int bootdelay;

	bootcount_inc();

	s = env_get("bootdelay");								//先判斷是否有bootdelay環境變量,如果沒有,就使用menuconfig中配置的CONFIG_BOOTDELAY時間
	bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;

	if (IS_ENABLED(CONFIG_OF_CONTROL))						//是否使用裝置樹進行配置
		bootdelay = fdtdec_get_config_int(gd->fdt_blob, "bootdelay",
						  bootdelay);

	debug("### main_loop entered: bootdelay=%d\n\n", bootdelay);

	if (IS_ENABLED(CONFIG_AUTOBOOT_MENU_SHOW))
		bootdelay = menu_show(bootdelay);
	bootretry_init_cmd_timeout();

#ifdef CONFIG_POST
	if (gd->flags & GD_FLG_POSTFAIL) {
		s = env_get("failbootcmd");
	} else
#endif /* CONFIG_POST */
	if (bootcount_error())
		s = env_get("altbootcmd");
	else
		s = env_get("bootcmd");								//擷取bootcmd環境變量,用于後續的指令執行

	if (IS_ENABLED(CONFIG_OF_CONTROL))
		process_fdt_options(gd->fdt_blob);
	stored_bootdelay = bootdelay;

	return s;
}
           

2.6.2 autoboot_command

詳細解釋标注于代碼中…

void autoboot_command(const char *s)
{
	debug("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");

	if (stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)) {
		bool lock;
		int prev;

		lock = IS_ENABLED(CONFIG_AUTOBOOT_KEYED) &&
			!IS_ENABLED(CONFIG_AUTOBOOT_KEYED_CTRLC);
		if (lock)
			prev = disable_ctrlc(1); /* disable Ctrl-C checking */

		run_command_list(s, -1, 0);

		if (lock)
			disable_ctrlc(prev);	/* restore Ctrl-C checking */
	}

	if (IS_ENABLED(CONFIG_USE_AUTOBOOT_MENUKEY) &&
	    menukey == AUTOBOOT_MENUKEY) {
		s = env_get("menucmd");
		if (s)
			run_command_list(s, -1, 0);
	}
}

           

我們看一下判斷條件stored_bootdelay != -1 && s && !abortboot(stored_bootdelay

  • stored_bootdelay:為環境變量的值,或者menuconfig設定的值
  • s:為環境變量bootcmd的值,為後續運作的指令
  • abortboot(stored_bootdelay):主要用于判斷是否有按鍵按下。如果按下,則不執行bootcmd指令,進入cli_loop 指令行模式;如果不按下,則執行bootcmd指令,跳轉到加載Linux啟動。

2.6.3 cli_loop

void cli_loop(void){	bootstage_mark(BOOTSTAGE_ID_ENTER_CLI_LOOP);#ifdef CONFIG_HUSH_PARSER	parse_file_outer();	/* This point is never reached */	for (;;);					//死循環#elif defined(CONFIG_CMDLINE)	cli_simple_loop();#else	printf("## U-Boot command line is disabled. Please enable CONFIG_CMDLINE\n");#endif /*CONFIG_HUSH_PARSER*/}
           

如上代碼,程式隻執行parse_file_outer來處理使用者的輸入、輸出資訊。

好啦,基本到這裡,我們已經對Uboot的啟動流程了然于胸了吧!

當然,更深層次的不建議去深入了解,有時間可以慢慢去研究。

大家有疑問,可以評論區交流…

超詳細【Uboot驅動開發】(二)uboot啟動流程分析

2.7 參考文章:

更多文章,可以關注我的公~号:【嵌入式藝術】哦,一起讨論嵌入式技術!

繼續閱讀