超詳細分析Bootloader(Uboot)到內核的啓動流程(萬字長文!)

工科生一枚,熱衷於底層技術開發,有強烈的好奇心,感興趣內容包括單片機,嵌入式Linux,Uboot等,歡迎學習交流!
愛好跑步(減肥~~),打籃球(菜雞一枚~~),睡覺。
歡迎加入嵌入式與Linux學習交流羣,羣號1061551626(備註CSDN)。羣內主要分享嵌入式學習經驗,問題答疑,各種視頻,電子書等。

Bootloader啓動流程分析

  Bootloader的啓動過程可以分爲單階段、多階段兩種。通常多階段的 Bootloader能提供更爲複雜的功能以及更好的可移植性。從固態存儲設備上啓動的 Bootloader大多都是兩階段的啓動過程。第一階段使用匯編來實現,它完成一些依賴於CPU體系結構的初始化,並調用第二階段的代碼;第二階段則通常使用C語言來實現,這樣可以實現更復雜的功能,而且代碼會有更好的可讀性和可移植性。
  一般而言,這兩個階段完成的功能可以如下分類:

Bootloader第一階段的功能

硬件設備初始化

  首先需要設置時鐘,設置MPLL,具體參見下面的FCLK HCLK PCLK 部分。接着設置CLKDIVN地址爲0x4C000014,寫入0x05,表示設置分頻係數爲FCLK:HCLK:PCLK=1:4:8。接着,關閉看門狗,關中斷,啓動ICACHE,關閉DCACHE和TLB,關閉MMU(ICACHE爲指令緩存,可以不關閉,指令直接操作的硬件,實際的物理地址。但是DCACHE就必須要關閉,此時MMU沒有使能,虛擬地址映射不成功,sdram無法訪問,DCACHE無數據)。start.s具體代碼如下:

	/* 設置時鐘 */
		ldr r0, =0x4c000014
		//	mov r1, #0x03;			
		mov r1, #0x05;			  // FCLK:HCLK:PCLK=1:4:8
		str r1, [r0]
	
		/* 如果HDIVN非0,CPU的總線模式應該從“fast bus mode”變爲“asynchronous bus mode” */
		mrc p15, 0, r1, c1, c0, 0		/* 讀出控制寄存器 */ 
		orr r1, r1, #0xc0000000 		/* 設置爲“asynchronous bus mode” */
		mcr p15, 0, r1, c1, c0, 0		/* 寫入控制寄存器 */
	
		/* MPLLCON = S3C2440_MPLL_200MHZ */
		ldr r0, =0x4c000004
		ldr r1, =S3C2440_MPLL_400MHZ
		str r1, [r0]
		/* 啓動ICACHE */
		mrc p15, 0, r0, c1, c0, 0	@ read control reg
		orr r0, r0, #(1<<12)
		mcr p15, 0, r0, c1, c0, 0	@ write it back

  這裏具體講下是如何設置FCLK HCLK PCLK
  FCLK又稱爲內核時鐘,是提供給ARM920T 的時鐘。
  HCLK又稱爲總線時鐘,是提供給用於存儲器控制器,中斷控制器,LCD 控制器,DMA 和 USB 主機模塊的 AHB總線(advanced high-performance bus)的時鐘。
  PCLK又稱爲I/O接口時鐘,是提供給用於外設如WDT,IIS,I2C,PWM 定時器,MMC/SD 接口,ADC,UART,GPIO,RTC 和SPI的 APB (advanced peripherals bus)總線的時鐘。
  S3C2440 FLCK值爲400MHz,HCLK值爲100MHz、PCLK值爲50MHz。那麼這些值通過什麼方法計算出來呢?S3C2440上的時鐘源是12MHz,如果想讓CPU工作在更高頻率上,就需要通過PLL(鎖相環)來提高主頻。S3C2440上的PLL有兩種,一種是MPLL,它是用來產生FCLK、HCLK、PCLK的高頻工作時鐘;還有一種是UPLL,用來爲USB提供工作頻率。S3C2440時鐘體系如下
在這裏插入圖片描述
在這裏插入圖片描述
  從時序圖中,我們可以看到,上電之後,如果什麼都不設置,FCLK和晶振的頻率相等。當設置PLL後,CPU並不是馬上就使用設置好的高頻時鐘,而是有一段鎖定時間,在這段時間裏,CPU停止運行,等12MHz變成高頻時鐘穩定以後,整個系統再重新運行。
  開啓MPLL的過程:
  1、設置LOCKTIME變頻鎖定時間
  2、設置FCLK與晶振輸入頻率(Fin)的倍數
  3、設置FCLK,HCLK,PCLK三者之間的比例
  從手冊上可以看到,LOCKTIME的默認時間是0xFFFFFFFF,控制方法如圖:
  (剛設置好PLL時,系統認爲這是PLL還沒穩定,所有這時不用PLL的時鐘,而用外部晶振做時鐘,將PLL鎖住,過了LOCKTIME後認爲PLL已經穩定了,才使用PLL給系統提供時鐘)
在這裏插入圖片描述
  FCLK與Fin的倍數通過MPLLCON寄存器設置,三者之間有以下關係:
  MPLL(FCLK) = (2 * m * Fin)/(p*2^s)
  其中:m = MDIV + 8, p = PDIV + 2, s = SDIV
  PLL配置寄存器如圖:
在這裏插入圖片描述
  當設置完MPLL之後,就會自動進入LockTime變頻鎖定期間,LockTime之後,MPLL輸出穩定時鐘頻率。
   FCLK、HCLK、PCLK的設置比例如圖:
在這裏插入圖片描述
在這裏插入圖片描述
  如果HDIV設置爲非0,CPU的總線模式要進行改變,默認情況下FCLK = HCLK,CPU工作在fast bus mode快速總線模式下,HDIV設置爲非0後, FCLK與HCLK不再相等,要將CPU改爲asynchronous bus mod異步總線模式,可以通過下面的嵌入彙編代碼實現:

__asm__(
    "mrc    p15, 0, r1, c1, c0, 0\n"        /* 讀出控制寄存器 */ 
    "orr    r1, r1, #0xc0000000\n"          /* 設置爲“asynchronous bus mode” */
    "mcr    p15, 0, r1, c1, c0, 0\n"        /* 寫入控制寄存器 */
    );

爲加載 Bootloader的第二階段代碼準備RAM空間(初始化內存空間)

  lowlevel_init中設置相應BANK地址,主要用來設置SDRAM。內存是被映射在了0x30000000-0x40000000的位置,即bank6與bank7。那麼在內存時序設置的時候,主要關心的,就是bank6與bank7。當然,bank0也是需要關注的,因爲它是啓動時,啓動程序存放的位置。但是bank0是由OM[1:0],即板子上的那幾個小開關中的兩個來控制的,所以這裏程序上是不用管它的。

SMRDATA:
		.long 0x22011110	 //BWSCON
		.long 0x00000700	 //BANKCON0
		.long 0x00000700	 //BANKCON1
		.long 0x00000700	 //BANKCON2
		.long 0x00000700	 //BANKCON3  
		.long 0x00000740	 //BANKCON4
		.long 0x00000700	 //BANKCON5
		.long 0x00018005	 //BANKCON6
		.long 0x00018005	 //BANKCON7
		.long 0x008C04F4	 // REFRESH
		.long 0x000000B1	 //BANKSIZE
		.long 0x00000030	 //MRSRB6
		.long 0x00000030	 //MRSRB7

  接下來設置棧地址指向NAND,準備初始化NANDFLASH。

	ldr	sp, =(CONFIG_SYS_INIT_SP_ADDR)//等於0x30000f80
	bic	sp, sp, #7 /* 8-byte alignment for ABI compliance */

	bl nand_init_ll

  初始化NANDFLASH,其中包括設置時序NFCONF,(參考芯片手冊和2440手冊設置nandflsh的啓動時序)。TACLS表示的建立所用的時間,TWRPH0表示nWE寫控制信號的持續時間,TWRPH1表示數據生效所用的時間,什麼時候可以讀數據。 最後就是使能NFCONT NAND Flash控制器,初始化ECC, 禁止片選。到這裏,NANDFLASH的初始化就完成了。下面就可以進行重定位了。

void nand_init_ll(void)
{
#define TACLS   0
#define TWRPH0  1
#define TWRPH1  0
	/* 設置時序 */
	NFCONF = (TACLS<<12)|(TWRPH0<<8)|(TWRPH1<<4);
	/* 使能NAND Flash控制器, 初始化ECC, 禁止片選 */
	NFCONT = (1<<4)|(1<<1)|(1<<0);	
}

複製 Bootloader的第二階段代碼到SDRAM空間中(重定位)

  首先判斷是NOR啓動還是NAND啓動,如果是NAND啓動就直接拷貝數據。拷貝代碼之前,要傳遞給拷貝函數三個參數,源,目的,長度。讀取NAND的話要參考芯片手冊的NAND讀取數據的時序,選中NAND,發出讀命令,發出地址,發出讀命令,判斷狀態,讀取數據,取消選中等。

	bl copy_code_to_sdram
	bl clear_bss                         //清除bss段(參考自制uboot章節)
void copy_code_to_sdram(unsigned char *src, unsigned char *dest, unsigned int len)
{	
	int i = 0;
	
	/* 如果是NOR啓動 */
	if (isBootFromNorFlash())
	{
		while (i < len)
		{
			dest[i] = src[i];
			i++;
		}
	}
	else
	{
		//nand_init();
		nand_read_ll((unsigned int)src, dest, len);
	}
}

void clear_bss(void)
{
	extern int __bss_start, __bss_end__;
	int *p = &__bss_start;
	
	for (; p < &__bss_end__; p++)
		*p = 0;
}

  最後要清除bss。bss段不佔用空間,都是未初始化的全局變量或者已經初始化爲零的變量,本來就是零,直接清零就好。不清零的話未初始化的變量可能會存在未知的數值。

設置好棧

  設置棧跳轉到SDRAM執行。

ldr pc,=call_board_init_f            //絕對跳轉,跳到SDRAM上執行

跳轉到第二階段代碼的C入口點

  跳轉到SDRAM執行剩下的程序。

call_board_init_f:
.globl base_sp
base_sp:
	.long 0

	ldr	r0,=0x00000000
	bl	board_init_f
	
	/*unsigned int id 的值存在r0中,正好給board_init_r使用*/
	ldr r1, =_TEXT_BASE
	/*重新設置棧到之前的位置 指向原來addr_sp += 128;*/
	ldr sp,base_sp  
	/*調用第二階段代碼*/
	bl	board_init_r

Bootloader第二階段的功能。

初始化本階段要使用到的硬件設備

  爲了方便開發,至少要初始化一個串口以便程序員與 Bootloader進行交互。

檢測系統內存映射( memory map)

  所謂檢測內存映射,就是確定板上使用了多少內存、它們的地址空間是什麼。由於嵌入式開發中 Bootloader多是針對某類板子進行編寫,所以可以根據板子的情況直接設置,不需要考慮可以適用於各類情況的複雜算法。

將內核映象和根文件系統映象從 Flash上讀到SDRAM空間中

  Flash上的內核映象有可能是經過壓縮的,在讀到SDRAM之後,還需要進行解壓。當然,對於有自解壓功能的內核,不需要 Bootloader來解壓。將根文件系統映象複製到SDRAM中,這不是必需的。這取決於是什麼類型的根文件系統以及內核訪問它的方法。 
  將內核存放在適當的位置後,直接跳到它的入口點即可調用內核。調用內核之前,下列條件要滿足。
  (1)CPU寄存器的設置。
  R0=0。
  R1=機器類型ID;對於ARM結構的CPU,其機器類型ID可以參見 linux/arch/arm tools/ mach-types
  R2=啓動參數標記列表在RAM中起始基地址。
  (2)CPU工作模式。
  必須禁止中斷(IRQs和FIQs)
  CPU必須爲SVC模式
  (3) Cache和MMU的設置。
  MMU必須關閉
  指令 Cache可以打開也可以關閉
  數據 Cache必須關閉

爲內核設置啓動參數

  Bootloader與內核的交互是單向的, Bootloader將各類參數傳給內核。由於它們不能同時行,傳遞辦法只有一個:Bootloader將參數放在某個約定的地方之後,再啓動內核,內核啓動後從這個地方獲得參數。

  除了約定好參數存放的地址外,還要規定參數的結構。Linu2.4x以後的內核都期望以標記列表( tagged list)的形式來傳遞啓動參數。標記,就是一種數據結構;標記列表,就是挨着存放的多個標記。標記列表以標記 ATAG CORE開始,以標記 ATAG NONE結束。

  標記的數據結構爲tag,它由一個 tag header結構和一個聯合( union)組成。 tag_ header結構表小標記的類型及長度,比如是表示內存還是表示命令行參數等。對於不同類型的標記使用不同的聯合( union),比如表示內存時使用 tag_mem32,表示命令行時使用 tag cmdline。

  bootloader與內核約定的參數地址,設置內存的起始地址和大小,指定根文件系統在那個分區,系統啓動後執行的第一個程序linuxrc,控制檯ttySAC0等。

調用內核

uboot啓動內核詳解

  下面我們來展開說下uboot具體是如何調用內核的,引導內核啓動的。

uboot與Linux內核之間的參數傳遞

  我們知道,uboot啓動後已經完成了基本的硬件初始化(如:內存、串口等),接下來它的主要任務就是加載Linux內核到開發板的內存,然後跳轉到Linux內核所在的地址運行。

  具體是如何跳轉呢?做法很簡單,直接修改PC寄存器的值爲Linux內核所在的地址,這樣CPU就會從Linux內核所在的地址去取指令,從而執行內核代碼。

  在前面我們已經知道,在跳轉到內核以前,uboot需要做好以下三件事情:

  (1)CPU寄存器的設置。
  R0=0。
  R1=機器類型ID;對於ARM結構的CPU,其機器類型ID可以參見 linux/arch/arm tools/ mach-types
  R2=啓動參數標記列表在RAM中起始基地址。
  (2)CPU工作模式。
  必須禁止中斷(IRQs和FIQs)
  CPU必須爲SVC模式
  (3) Cache和MMU的設置。
  MMU必須關閉
  指令 Cache可以打開也可以關閉
  數據 Cache必須關閉

  其中上面第一步CPU寄存器的設置中,就是通過R0,R1,R2三個參數給內核傳遞參數的。(ATPCS規則可以參考

爲什麼要給內核傳遞參數呢?

  在此之前,uboot已經完成了硬件的初始化,可以說已經”適應了“這塊開發板。然而,內核並不是對於所有的開發板都能完美適配的(如果適配了,可想而知這個內核有多龐大,又或者有新技術發明了,可以完美的適配各種開發板),此時對於開發板的環境一無所知。所以,要想啓動Linux內核,uboot必須要給內核傳遞一些必要的信息來告訴內核當前所處的環境。

如何給內核傳遞參數?

  因此,uboot就把機器ID通過R1傳遞給內核,Linux內核運行的時候首先就從R1中讀取機器ID來判斷是否支持當前機器。這個機器ID實際上就是開發板CPU的ID,每個廠家生產出一款CPU的時候都會給它指定一個唯一的ID,大家可以到uboot源碼的arch\arm\include\asm\mach-type.h文件中去查看。
在這裏插入圖片描述
  R2存放的是塊內存的基地址,這塊內存中存放的是uboot給Linux內核的其他參數。這些參數有內存的起始地址、內存大小、Linux內核啓動後掛載文件系統的方式等信息。很明顯,參數有多個,不同的參數有不同的內容,爲了讓Linux內核能精確的解析出這些參數,雙方在傳遞參數的時候要求參數在存放的時猴需要技照雙方規定的格式存放。

  除了約定好參數存放的地址外,還要規定參數的結構。Linux2.4.x以後的內核都期望以標記列表(tagged list)的形式來傳遞啓動參數。標記,就是一種數據結構;標記列表,就是挨着存放的多個標記。標記列表以標記ATAG_CORE開始,以標記ATAG_NONE結束。

  標記的數據結構爲tag,它由一個tag_header結構和一個聯合(union)組成。tag_header結構表示標記的類型及長度,比如是表示內存還是表示命令行參數等。對於不同類型的標記使用不同的聯合(union),比如表示內存時使用tag_ mem32,表示命令行時使用 tag_cmdline。具體代碼見arch\arm\include\asm\setup.h。
在這裏插入圖片描述
  從上面可以看出,struct tag結構體由struct tag_header+聯合體u構成,結構體struct tag_header用來描述每個tag的頭部信息,如tag的類型,tag大小。聯合體u用來描述每個傳遞給Linux內核的參數信息。
  下面以傳遞內存標記、傳遞命令行參數爲例來說明參數的傳遞。
(1)設置開始標記ATAG_CORE

	tag->hdr.tag  = ATAG_CORE;
	tag->hdr.size = tag_size(tag_core);
	tag->u.core.flags = params->u1.s.flags & FLAG_READONLY;
	tag->u.core.pagesize = params->u1.s.page_size;
	tag->u.core.rootdev = params->u1.s.rootdev;

	tag = tag_next(tag);

  涉及到的結構體定義如下

struct tag_header {
	__u32 size;
	__u32 tag;
};

/* The list must start with an ATAG_CORE node */
#define ATAG_CORE	0x54410001

struct tag_core {
	__u32 flags;		/* bit 0 = read-only */
	__u32 pagesize;
	__u32 rootdev;
};

  其中tag_next,tag_size定義如下,指向當前標記的結尾

#define tag_next(t)	((struct tag *)((u32 *)(t) + (t)->hdr.size))
#define tag_size(type)	((sizeof(struct tag_header) + sizeof(struct type)) >> 2)

  (2)設置內存標記

	t->hdr.tag = ATAG_MEM;
	t->hdr.size = tag_size(tag_mem32);
	t->u.mem.start = CFG_GLOBAL_RAM_BASE;
	t->u.mem.size = CFG_GLOBAL_RAM_SIZE;

	t = tag_next(t);

  相關結構體定義如下

#define ATAG_MEM	0x54410002

struct tag_mem32 {
	__u32	size;
	__u32	start;	/* physical start address */
};

  (3)設置命令行參數標記

命令行參數是一個字符串,一般用它來告訴內核掛載根文件系統的方式。由uboot的bootargs環境變量提供,它的內容有如下兩種格式

root=nfs nfsroot=202.193.61.237:/work/nfs_root/first_fs ip=202.193.61.196 init=/linuxrc console=ttySAC0,115200
root=/dev/mtdblock2 ip=202.193.61.196 init=/linuxrc console=ttySAC0,115200
名稱 含義
root 告訴Linux內核掛載根文件系統的方式,nfs表示以NFS服務的方式掛載根文件系統,/dev/mtdblock2表示根文件系統在MTD設置的第二個分區上。
nfsroot 告訴Linux內核,以NFS方式掛載根文件系統時,根文件系統所在主機的P地址和路徑
ip 告訴Linux內核,啓動後它的p地址
init 告訴Linux內核,啓動的第一個應用程序是根目錄下的linuxrc程序
console 告訴Linux區內核,控制檯爲ttySAC0,波特率爲115200
tag = tag_next(tag);
tag->hdr.tag = ATAG_CMDLINE;
tag->hdr.size = (strlen(params->commandline) + 3 +
		 sizeof(struct tag_header)) >> 2;
strcpy(tag->u.cmdline.cmdline, params->commandline);

tag = tag_next(tag);

  相關結構體定義如下

/* command line: \0 terminated string */
#define ATAG_CMDLINE	0x54410003

struct tag_cmdline {
	char	cmdline[1];	/* this is the minimum size */
};

  (4)設置結束標記

	tag->hdr.tag = ATAG_NONE;
	tag->hdr.size = 0;

  我們明白了運行Linux區內核的時候,uboot需要給內核的傳遞的參數,接下來我們就來看看如何從uboot.中跳到Linux內核。

uboot跳轉到Linux內核

  在uboot中可以使用go和bootm來跳轉到內核,這兩個命令的區別如下:

  (1)go命令僅僅修改pc的值到指定地址

  格式:go addr

  (2)bootm命令是uboot專門用來啓動uImage格式的Linux內核,它在修改pc的值到指定地址之前,會設置傳遞給Linux內核的參數,用法如下:

  格式:bootm addr

內核鏡像格式vmlinuz和zImage和uImage

  (1)uboot經過編譯直接生成的elf格式的可執行程序是u-boot,這個程序類似於windows下的exe格式,在操作系統下是可以直接執行的。但是這種格式不能用來燒錄下載。我們用來燒錄下載的是u-boot.bin,這個東西是由u-boot使用arm-linux-objcopy工具進行加工(主要目的是去掉一些無用的)得到的。這個u-boot.bin就叫鏡像(image),鏡像就是用來燒錄到iNand中執行的。

  (2)linux內核經過編譯後也會生成一個elf格式的可執行程序,叫vmlinux或vmlinuz,這個就是原始的未經任何處理加工的原版內核elf文件;嵌入式系統部署時燒錄的一般不是這個vmlinuz/vmlinux,而是要用objcopy工具去製作成燒錄鏡像格式(就是u-boot.bin這種,但是內核沒有.bin後綴),經過製作加工成燒錄鏡像的文件就叫Image(製作把78M大的精簡成了7.5M,因此這個製作燒錄鏡像主要目的就是縮減大小,節省磁盤)。

  (3)原則上Image就可以直接被燒錄到Flash上進行啓動執行(類似於u-boot.bin),但是實際上並不是這麼簡單。實際上linux的作者們覺得Image還是太大了所以對Image進行了壓縮,並且在image壓縮後的文件的前端附加了一部分解壓縮代碼。構成了一個壓縮格式的鏡像就叫zImage。(因爲當年Image大小剛好比一張軟盤(軟盤有2種,1.2M的和1.44MB兩種)大,爲了節省1張軟盤的錢於是乎設計了這種壓縮Image成zImage的技術)。

  (4)uboot爲了啓動linux內核,還發明瞭一種內核格式叫uImage。uImage是由zImage加工得到的,uboot中有一個工具,可以將zImage加工生成uImage。注意:uImage不關linux內核的事,linux內核只管生成zImage即可,然後uboot中的mkimage工具再去由zImage加工生成uImage來給uboot啓動。這個加工過程其實就是在zImage前面加上64字節的uImage的頭信息即可。

  (5)原則上uboot啓動時應該給他uImage格式的內核鏡像,但是實際上uboot中也可以支持zImage,是否支持就看x210_sd.h中是否定義了LINUX_ZIMAGE_MAGIC這個宏。所以大家可以看出:有些uboot是支持zImage啓動的,有些則不支持。但是所有的uboot肯定都支持uImage啓動。

  (6)如果直接在kernel底下去make uImage會提供mkimage command not found。解決方案是去uboot/tools下cp mkimage /usr/local/bin/,複製mkimage工具到系統目錄下。再去make uImage即可。

  通過上面的介紹我們瞭解了內核鏡像的各種格式,如果通過uboot啓動內核,Linux必須爲uImage格式。

uboot中bootm命令實現

  bootm命令在uboot源碼common/cmd_bootm.c中實現,它的功能如下:

  (1)讀取uImage頭部,把內核拷貝到合適的地方。

  (2)把參數給內核準備好。

  (3)引導內核。

  當我們使用我們在uboot使用bootm命令後,bootm命令會從uImage頭中讀取信息後,發現是Linux內核,就會調用do_bootm_linux()函數,函數的具體實現bootm.c中

int do_bootm_linux(int flag, int argc, char *argv[], bootm_headers_t *images)
{
	/* No need for those on ARM */
	if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
		return -1;

	if (flag & BOOTM_STATE_OS_PREP) {
		boot_prep_linux(images);
		return 0;
	}

	if (flag & BOOTM_STATE_OS_GO) {
		boot_jump_linux(images);
		return 0;
	}

	boot_prep_linux(images);
	boot_jump_linux(images);
	return 0;
}

  do_bootm_linux 函數最終會 跳轉執行 boot_prep_linux 和 boot_jump_linux 函數,首先分析 boot_prep_linux 函數(位於 bootm.c 文件中):

static void boot_prep_linux(bootm_headers_t *images)
{
    char *commandline = getenv("bootargs");      //從環境變量中獲取 bootargs 的值

  。。。。。。。
        setup_board_tags(&params);      
        setup_end_tag(gd->bd);    //將 tag 參數保存在指定位置
    } else {
        printf("FDT and ATAGS support not compiled in - hanging\n");
        hang();
    }
    do_nonsec_virt_switch();
}

   從代碼可以看出來,boot_prep_linux,主要功能是將 tag 參數保存到指定位置,比如 bootargs 環境變量 tag,串口 tag,接下來分析 boot_jump_linux 函數(位於 bootm.c 文件中):

static void boot_jump_linux(bootm_headers_t *images, int flag)
{
    unsigned long machid = gd->bd->bi_arch_number;      //獲取機器id (在 board/samsung/jz2440/jz2440.c 中設置,爲 MACH_TYPE_SMDK2410(193))
    char *s;
    void (*kernel_entry)(int zero, int arch, uint params);
    unsigned long r2;
    int fake = (flag & BOOTM_STATE_OS_FAKE_GO);

    kernel_entry = (void (*)(int, int, uint))images->ep;    //獲取 kernel的入口地址,此處應爲 30000000

    s = getenv("machid");        //從環境變量裏獲取機器id (本例中還未在環境變量裏設置過機器 id)
    if (s) {            //判斷環境變量裏是否設置機器id
        strict_strtoul(s, 16, &machid);    //如果設置則用環境變量裏的機器id
        printf("Using machid 0x%lx from environment\n", machid);
    }

    debug("## Transferring control to Linux (at address %08lx)" \
        "...\n", (ulong) kernel_entry);
    bootstage_mark(BOOTSTAGE_ID_RUN_OS);
    announce_and_cleanup(fake);

    if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
        r2 = (unsigned long)images->ft_addr;
    else
        r2 = gd->bd->bi_boot_params;    //獲取 tag參數地址,gd->bd->bi_boot_params在 setup_start_tag 函數裏設置
if (!fake) kernel_entry(0, machid, r2); }  //進入內核

  通過分析可以看出,最終進入內核的函數爲 :

kernel_entry(0, machid, r2)

  到這裏bootm就成功給內核傳遞了參數,並跳轉到了內核。關於go命令的實現可以自己參考內核,在cmd_boot.c文件中,所不同的是,go命令實現的時候沒有設置參數,只是簡單的跳轉執行。如果想要使用go來跳轉到Linux內核,我們需要做簡單的修改,有興趣的可以自己研究下,這裏就不展開講了。
  至此,uboot就啓動了內核。啓動內核後就是掛載根文件系統了,下篇將具體介紹是如何掛載根文件系統的。
在這裏插入圖片描述
本文參考:<嵌入式Linux應用開發完全手冊>
https://www.cnblogs.com/PengfeiSong/p/6407903.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章