一.摘要
這篇文章主要對BootLoader(UBoot)的源碼進行了分析,并對UBoot的移植略作提及。? BootLoader的總目標是正確調用內核的執行,由于大部分的BoorLoader都依賴于CPU的體系結構。因此大部分的BootLoader都分為兩個步驟啟動。依賴于CPU體系結構(如設備初始化等)的代碼都放在stage1。而stage2一般使用C語言實現,能夠實現更加復雜的功能,代碼的可移植性也提高。
二.本文提綱
1. 摘要
2.? 本文提綱
3. UBoot啟動過程
4. Stage1(匯編語言實現)代碼分析
5. Stage2(C語言實現)代碼分析
6. UBoot移植過程中串口沒有顯示或者顯示亂碼的原因
7. 總結
三.UBoot啟動過程
UBoot其啟動過程主要可以分為兩個部分,Stage1和Stage2 。其中Stage1是用匯編語言實現的,主要完成硬件資源的初始化。而Stage2則是用C語言實現。主要完成內核程序的調用。這兩個部分的主要執行流程如下:
stage1包含以下步驟:
1. 硬件設備初始化
2. 為加載stage2準備RAM空間
3. 拷貝stage2的代碼到RAM空間
4. 設置好堆棧
5. 跳轉到stage2的C語言入口點
?
stage2一般包括以下步驟:
1. 初始化本階段要使用的硬件設備
2. 檢測系統內存映射
3. 將kernel映射和根文件系統映射從Flash讀到RAM空間中
4. 為內核設置啟動參數
5. 調用內核
四. Stage1(匯編語言實現)代碼分析
該階段主要是在cpu/arm920t/start.S文件中執行,這個匯編程序是U-Boot的入口程序,程序的開頭就是復位向量的代碼,主要的執行流程見下圖。
?
U-Boot啟動代碼流程圖
start.S代碼分析:
(1)主要實現復位向量,設置異常向量表。
_start:
b reset //復位向量
;
;設置異常向量表
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq //中斷向量
ldr pc, _fiq //中斷向量
(2)復位啟動子程序,將CPU設置到SVC模式
/* the actual reset code */
reset:
//復位啟動子程序
/* 設置CPU為SVC32模式 */
mrs r0,cpsr
bic r0,r0,#0x1f
;
;位清除,將某些位的值置0:r0 = r0 AND ( !0x1f)
orr r0,r0,#0xd3
;
;邏輯或,將r0與立即數進行邏輯或,放在r0中(第一個)
msr cpsr,r0
(3)關閉看門狗
/* 關閉看門狗 */
/* turn off the watchdog */
#if defined(CONFIG_S3C2400)
# define pWTCON 0x15300000
# define INTMSK 0x14400008 /* Interupt-Controller base addresses */
# define CLKDIVN 0x14800014 /* clock divisor register */
#elif defined(CONFIG_S3C2410)
# define pWTCON 0x53000000
# define INTMSK 0x4A000008 /* Interupt-Controller base addresses */
# define INTSUBMSK 0x4A00001C
# define CLKDIVN 0x4C000014 /* clock divisor register */
#endif
#if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410)
ldr r0, =pWTCON
mov
r1, #0x0
str
r1, [r0]
(4)禁止所有中斷,設置CPU頻率
/* 禁止所有中斷和設置CPU頻率 */
/*
* mask all IRQs by setting all bits
in
the INTMR - default
*/
mov
r1, #0xffffffff
ldr r0, =INTMSK
str
r1, [r0]
# if defined(CONFIG_S3C2410)
ldr r1, =0x3ff
ldr r0, =INTSUBMSK
str
r1, [r0]
# endif
/*
FCLK:HCLK:
PCLK =
1
:
2
:
4
*/
;
;FCLK用于CPU,HCLK用于AHB,PCLK用于APB
/* default FCLK is
120
MHz ! */
ldr r0, =CLKDIVN
;
;根據硬件手冊來設置CLKDIVN寄存器
mov
r1, #
3
;
;用戶手冊的推薦值
str
r1, [r0]
#endif /* CONFIG_S3C2400 || CONFIG_S3C2410 */
(5)系統重啟的時候執行的初始化代碼,而不是系統熱復位(從RAM中執行)的時候
/*
* we do sys-critical inits only at reboot,
*
not
when booting from
*/
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_crit
;
;跳轉去初始化CPU
#endif
;
;#ifdef CONFIG_INIT_CRITICAL 原文中的,估計是1.1.16版本的
;
; bl cpu_init_crit
;
;#endif
(6)CPU和RAM兩個關鍵的初始化子程序
函數一: /*? 初始化 CPU?*/
cpu_init_crit:
/*
* flush v4 I/D caches 設置CP15
*/
mov
r0, #
0
mcr p15,
0
, r0, c7, c7,
0
/* flush v3/v4 cache */
;
;使I/D cache失效:將寄存器r0的數據傳送到協處理器p15的c7中。C7寄存器位對應cp15中的cache控制寄存器
mcr p15,
0
, r0, c8, c7,
0
/* flush v4 TLB */
;
;使TLB操作寄存器失效:將r0數據送到cp15的c8、c7中。C8對應TLB操作寄存器
/*
* disable MMU stuff
and
caches 禁止MMU和caches
*/
mrc p15,
0
, r0, c1, c0,
0
;
;先把c1和c0寄存器的各位置0(r0 = 0)
bic r0, r0, #0x00002300 @ clear bits
13
,
9
:
8
(--V- --RS)
bic r0, r0, #0x00000087 @ clear bits
7
,
2
:
0
(B--- -CAM)
;
;這里我本來有個疑問:為什么要分開設置。因為arm匯編要求的立即數格式所決定的
orr r0, r0, #0x00000002 @ set bit
2
(??) (A) Align
;
;上一條已經設置bit1為0,這一條又設置為1??
orr r0, r0, #0x00001000 @ set bit
12
(I) I-Cache
mcr p15,
0
, r0, c1, c0,
0
;
;用上面(見下面)設定的r0的值設置c1??(cache類型寄存器)和c0(control字寄存器),以下為c0的位定義
;
;bit8: 0 = Disable System protection
;
;bit9: 0 = Disable ROM protection
;
;bit0: 0 = MMU disabled
;
;bit1: 0 = Fault checking disabled 禁止糾錯
;
;bit2: 0 = Data cache disabled
;
;bit7: 0 = Little-endian operation
;
;bit12: 1 = Instruction cache enabled
/* 配置內存區控制寄存器 ??有待分析,是1.
1
.4版本的
* before relocating, we have to setup RAM timing
* because memory timing is board-dependend, you will
* find a lowlevel_init.S
in
your board directory.
*/
mov
ip, lr
bl lowlevel_init
;
;位于board/smdk2410/lowlevel_init.S:用于完成芯片存儲器的初始化,執行完成后返回
mov
lr, ip
mov
pc, lr
函數二: /*? 把 U-Boot 重新定位到 RAM?*/
relocate:
adr r0, _start /* r0是代碼的當前位置 */
;
;adr偽指令,匯編器自動通過當前PC的值算出 如果執行到_start時PC的值,放到r0中:
當此段在flash中執行時r0 = _start =
0
;當此段在RAM中執行時_start = _TEXT_BASE(在board/smdk2410/config.mk中指定的值為0x33F80000,即u-boot在把代碼拷貝到RAM中去執行的代碼段的開始)
ldr r1, _TEXT_BASE /* 測試判斷是從Flash啟動,還是RAM */
;
;此句執行的結果r1始終是0x33FF80000,因為此值是又編譯器指定的(ads中設置,或-D設置編譯器參數)
cmp
r0, r1 /* 比較r0和r1,調試的時候不要執行重定位 */
beq stack_setup /* 如果r0等于r1,跳過重定位代碼 */
/* 準備重新定位代碼 */
;
;以上確定了復位啟動代碼是在flash中執行的(是系統重啟,而不是軟復位),就需要把代碼拷貝到RAM中去執行,以下為計算即將拷貝的代碼的長度
ldr r2, _armboot_start
;
;前面定義了,就是_start
ldr r3, _bss_start
;
;所謂bss段,就是未被初始化的靜態變量存放的地方,這個地址是如何的出來的?根據board/smsk2410/u-boot.lds內容?
sub
r2, r3, r2 /* r2 得到armboot的大小 */
add
r2, r0, r2 /* r2 得到要復制代碼的末尾地址 */
(7)重新定位代碼,循環拷貝啟動的代碼到RAM中
copy_loop:
ldmia {r3-r10} /*從源地址[r0]復制 */
;
;r0指向_start(=0)
stmia {r3-r10} /* 復制到目的地址[r1] */
;
;r1指向_TEXT_BASE(=0x33F80000)
cmp
r0, r2 /* 復制數據塊直到源數據末尾地址[r2] */
ble copy_loop
(8)初始化堆棧等
stack_setup:
ldr r0, _TEXT_BASE /* 上面是128 KiB重定位的u-boot */
sub
r0, r0, #CFG_MALLOC_LEN /* 向下是內存分配空間 */
sub
r0, r0, #CFG_GBL_DATA_SIZE /* 然后是bdinfo結構體地址空間 */
#ifdef CONFIG_USE_IRQ
sub
r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
#endif
;
;這些宏定義在/include/configs/smdk2410.h中:
#define CFG_MALLOC_LEN (CFG_ENV_SIZE +
128
*
1024
)
;
;64K+128K=0xC0
#define CFG_ENV_SIZE 0x10000 /* Total Size of Environment Sector
64k
*/
#define CONFIG_STACKSIZE (
128
*
1024
) /* regular stack
128k
*/
#define CFG_GBL_DATA_SIZE
128
/* size
in
bytes reserved for initial data */
用0x33F8000 – 0xC0 – 0x80得到_TEXT_BASE向下(低地址)的堆棧指針sp的起點地址
sub
sp, r0, #
12
/* 為abort-stack預留3個字 */
;
;得到最終sp指針初始值
clear_bss:
ldr r0, _bss_start /* 找到bss段起始地址 */
ldr r1, _bss_end /* bss段末尾地址 */
mov
r2, #0x00000000 /* 清零 */
clbss_l:
str
r2, [r0] /* bss段地址空間清零循環... */
add
r0, r0, #
4
cmp
r0, r1
bne clbss_l
(9)跳轉到start_armboot函數入口,_start_armboot字保存函數的入口指針
ldr pc, _start_armboot
_start_armboot:
.word start_armboot
;
;start_armboot函數在lib_arm/board.c中實現
?
五. Stage2(C語言實現)代碼分析
這個文件是bootloader的stage2部分,這個文件中的start_armboot函數是U-Boot執行的第一個C語言函數,主要完成系統的初始化工作,然后進入主循環,等待并處理用戶輸入的命令。
在編譯和鏈接BootLoader這樣的程序的時候,不能使用glibc庫中的任何支持函數,這就帶來了一個問題:從何處跳入Main函數,最直接的想法是直接把Main函數的起始地址作為整個Stage2執行映像的入口。但是這樣做有兩個缺點:
a: 無法通過Main函數傳遞參數
b: 無法處理Main函數返回的情況
一種更好的解決方案是利用trampoline(彈簧床)的概念:用匯編寫一段trampoline小程序,并將這段trampoline小程序作為Stage2可執行映像的入口點,然后就可以在trampoline小程序中用CPU跳轉指令跳入Main函數去執行,當Main函數執行結束以后CPU執行路徑顯然再次回到trampoline程序。其核心思想就是用這段trampoline程序作為Main函數的外部包裹。
(1). 初始化本階段要使用到的硬件設備,一般包括:
a:點亮LED,表示已經進入main函數執行(可選)
b: 至少一個串口,以便和終端用戶進行IO信息交換
c: 初始化定時器等
d: 輸出一些打印信息,程序名稱,版本號等
(2). 檢測系統的內存映射
所謂內存映射就是指在整個 4GB 物理地址空間中有哪些地址范圍被分配用來尋址系統的 RAM 單元。比如,在 SA-1100 CPU 中,從 0xC000,0000 開始的 512M 地址空間被用作系統的 RAM 地址空間,而在 Samsung S3C44B0X CPU 中,從 0x0c00,0000 到 0x1000,0000?之間的 64M 地址空間被用作系統的 RAM 地址空間。雖然 CPU 通常預留出一大段足夠的地址空間給系統 RAM,但是在搭建具體的嵌入式系統時卻不一定會實現 CPU 預留的全部 RAM?地址空間。也就是說,具體的嵌入式系統往往只把 CPU 預留的全部 RAM 地址空間中的一部分映射到 RAM 單元上,而讓剩下的那部分預留 RAM 地址空間處于未使用狀態。 ?由于上述這個事實,因此 Boot Loader 的 stage2 必須在它想干點什么 (比如,將存儲在 flash 上的內核映像讀到 RAM 空間中) 之前檢測整個系統的內存映射情況,也即它必須知道 CPU 預留的全部 RAM 地址空間中的哪些被真正映射到 RAM 地址單元,哪些是處于?"unused" 狀態的。
(3). 加載內核映像和根文件系統映像
a:規劃內存占用的布局:主要包括基地址和映像大小兩個方面。對于內核映像,一般將其拷貝到從(MEM_START+0x8000) 這個基地址開始的大約 1MB大小的內存范圍內(嵌入式 Linux 的內核一般都不操過 1MB)。為什么要把從 MEM_START到MEM_START+0x 8000 這段 32KB 大小的內存空出 來呢?這是因為 Linux 內核要在這段內存中放置一些全局數據結構,如:啟動參數和內核頁表等信息。而對于根文件系統映像,則一般將其拷貝到 MEM_START+0x0010,0000 開始的地方。如果用 Ramdisk 作為根文件系統映像,則其解壓后的大小一般是 1MB。
b:從Flash中拷貝映像
while
(count) {
*dest++ = *src++;
/*
they are all aligned with word boundary
*/
count
-=
4
;
/*
byte number
*/
};
(4). 設置內核的啟動參數
將內核映像拷貝到RAM中之后就可以啟動了,但是一般都需要先設定Linux內核的啟動參數。Linux2.4以后的內核都以標記列表(tagged list)的形式來傳遞啟動參數。啟動參數列表以標記ATAG_CORE開始,以標記ATAG_NONE結束。每個標記由標示被傳遞參數的tag_header結構以及隨后的參數數據結構來組成。數據結構tag和tag_header定義在Linux內核源碼的include/asm/setup.h頭文件中。在嵌入式Linux系統中,通常需要由BootLoader設定的參數有:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD。
比如,設置 ATAG_CORE 的代碼如下: ?
params = (struct tag *)BOOT_PARAMS; ? ? ? ??
params->hdr.tag = ATAG_CORE; ? ? ? ??
params->hdr.size = tag_size(tag_core); ? ? ? ??
params->u.core.flags = 0; ? ? ? ??
params->u.core.pagesize = 0; ? ? ? ??
params->u.core.rootdev = 0; ? ? ? ??
params = tag_next(params); ?
其中,BOOT_PARAMS 表示內核啟動參數在內存中的起始基地址,指針 params 是一個 struct?tag 類型的指針。宏 tag_next() 將以指向當前標記的指針為參數,計算緊臨當前標記的下一個標記的起始地址。注意,內核的根文件系統所在的設備 ID 就是在這里設置的。
(5). 調用內核
BootLoader調用內核的方法是直接跳轉到內核的第一條指令處,即直接跳到MEM_START+0x8000處。在跳轉的時候要滿足下面的條件:
a: CPU寄存器的設置
R0 = 0;
R1 = 機器類型ID,
b: CPU必須在SVC模式
c: Cache和MMU的設置:
MMU必須關閉
指令Cache可以打開也可以關閉
數據Cache必須關閉
說明:如果用 C 語言,可以像下列示例代碼這樣來調用內核: ?
void
(*theKernel)(
int
zero,
int
arch, u32 params_addr) = (
void
(*)(
int
,
int
,
u32))KERNEL_RAM_BASE;
……
theKernel(
0
, ARCH_NUMBER, (u32) kernel_params_start);
注意:theKernel()函數調用應該永遠不返回的。如果這個調用返回,則說明出錯。 ? ? ? ? ??
六. UBoot移植過程中串口沒有顯示或者顯示亂碼的原因
(1).?boot loader 對串口的初始化設置不正確。?
(2). 運行在 host 端的終端仿真程序對串口的設置不正確, 這包括:波特率、奇偶校驗、數據位和停止位等方面的設置。
關于BootLoader啟動時串口能輸出,但是啟動內核后不能正確顯示的原因:
(1). 內核編譯時缺少配置對串口驅動的支持,或配置正確的串口驅動
(2). BootLoader的串口配置和內核的不一致
(3). 內核沒有正確啟動
七.總結
U-Boot,全稱 Universal Boot Loader,是遵循GPL條款的 開放源碼 項目。從FADSROM、8xxROM、PPCBOOT逐步發展演化而來。其源碼目錄、編譯形式與 Linux內核 很相似,事實上,不少U-Boot源碼就是相應的Linux內核 源程序 的簡化,尤其是一些設備的驅動程序,這從U-Boot源碼的注釋中能體現這一點。
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

