(二)U-boot在開發板上移植過程詳解--bootloader架構分析
本例中採用的同樣是前邊一貫的實驗板,這裡就不對板子資源做進一步介紹了。
我們知道,bootloader是系統上電後最初載入執行的程式碼。它提供了處理器上電覆位後最開始需要執行的初始化程式碼。在PC機上載入程式一般由BIOS開始執行,然後讀取硬碟中位於MBR(Main Boot Record,主引導記錄)中的Bootloader(例如LILO或GRUB),並進一步引導作業系統的啟動。然而在嵌入式系統中通常沒有像BIOS那樣的韌體程式,因此整個系統的載入啟動就完全由bootloader來完成。它主要的功能是載入與引導核心映像。
一個嵌入式的儲存裝置通過通常包括四個分割槽,第一分割槽存放的當然是u-boot,第二個分割槽存放著u-boot要傳給系統核心的引數,第三個分割槽是系統核心(kernel),第四個分割槽則是根檔案系統。如下圖所示:
圖一 固態儲存裝置的典型空間分配結構
第一部分:Bootloader啟動模式
Bootloader的啟動過程可以是單階段的,也可以是多階段的。多階段的bootloader比單階段的bootloader提供更為複雜的功能。以及更好的移植性,比如U-boot。
第一階段:
Bootloader執行最基本的硬體初始化操作。如關閉中斷,關閉看門狗以避免處理器被複位,以及關閉MMU功能,關閉處理器快取(資料快取一定要關閉,指令快取可以開啟),設定系統時鐘,初始化記憶體等。這一階段程式碼通常由彙編程式碼編寫,為了執行下一階段的C語言程式還必須設定好堆疊。如果是從NAND Flash啟動,則必須通過NAND Flash控制器將bootloader程式碼複製到記憶體。
第二階段:
這一階段一般用C語言編寫,大致分為一下幾步:
1)初始化各種硬體裝置,比如設定處理器正常工作的時鐘頻率,初始化串列埠等。
2)檢測系統記憶體,主要是確定系統記憶體容量以及其地址空間資訊。
3)將核心映像檔案載入到記憶體。
4)準備核心引導引數。
5)跳轉到核心的第一條指令處,開始執行核心初始化程式碼,控制權轉移到核心程式碼,bootload的使命結束。
第二部分:Bootloader的操作模式
一般的bootload而都包含兩種不同的操作模式:啟動載入模式和下載模式
啟動載入模式:這種模式也稱自主模式,即bootloader從目標機上的某個固體儲存裝置上將作業系統載入到記憶體中執行,這個過程沒有使用者的介入。這種模式是正
常的工作模式,最終的產品釋出時必須工作在這種模式下。
下載模式:在這種模式下,目標機上的bootloader將通過串列埠,網路連線或者其他通訊手段從主機下載檔案,比如下載核心映像或根檔案系統映像等。從主機下載的檔案通常被儲存在目標機的記憶體中,然後再寫入到目標機上的Flash等固態儲存裝置中。這種工作模式通常在第一次安裝核心與跟檔案系統時使用。或者在系統更新時使用。進行嵌入式系統除錯時一般也讓bootloader工作在這一模式下。
第三部分:Arm bootloader的特點
要實現一個通用的bootloader是一件不可能的事情,但是還是可以根據Arm的體系結構,從理論上總結出一些Arm平臺上bootloader的共性,這些共性只能侷限於bootloader的基本功能。
對於一個運行於Arm平臺的系統來說,bootloader作為引導與載入核心映像的工具需要提供一下幾個功能:
1)bootloader必須能夠初始化記憶體。
2)雖然系統的啟動並不一定依賴串列埠,但一般來說bootloader應該初始化至少一個串列埠,通過它與主機進行通訊,以便進行開發,除錯和維護工作。
3)這是linux核心所要求的,如果不給出核心引數,則核心就會使用其預設引數。
4)一般來說,核心映像必須在記憶體執行,所以必須從其他非易失儲存介質上覆制到記憶體。
5)讓執行流程跳轉到核心映像的入口。
啟動核心時,系統必須處於指定的狀態,包括處理器模式,MMU和快取的設定,暫存器的設定等方面。
處理器模式)處理器應處於SVC模式,在這種特權模式下,核心才能執行所有的指令。中斷必須關閉。在異常向量表尚未初始化的情況下,如果發生中斷,將導致系統崩潰。一般來說,bootloader本身也沒有必要支援中斷的實現,這屬於核心的管理範圍。
MMU和快取設定)MMU必須關閉。啟動MMU進入保護模式是核心的工作。而bootloader本身工作在真實模式下,所有對地址的操作使用的都是實體地址,不存在虛擬地址。資料快取必須關閉,bootloader的主要功能是裝載核心映像,映像資料必須真實寫回記憶體中,不能僅放在處理器的快取中,所以資料快取必須關閉。指令快取可以開啟,一般情況下,推薦將指令快取也關閉。
暫存器設定)暫存器R0的值應為0,R1的值表示機器型別,R2的值則是引導引數列表在記憶體中的起始地址。這三個暫存器是在最後啟動核心時需要設定的。
第四部分:U-boot原始碼分析
在實際使用中,U-boot被固話在CPU的上電/復位啟動地址處(通常在非易失儲存器中)。每當嵌入式裝置上電/復位時,CPU總是從啟動地址(U-boot)處啟動。U-bo
ot啟動後,首先初始化各種硬體裝置,如CPU,快取,儲存器,MMU,匯流排控制器,各種I/O介面等,然後從遠端主機或者本地非易失儲存裝置中裝載可執行檔案或作業系統
,為整個嵌入式系統準備執行環境。要使用U-boot,最初必須使用某種硬體支援的方式將U-boot映像寫入非易失儲存器中。比如我這裡板子上沒有任何的bootloader
可以使用JTAG介面將U-boot映像寫入Flash的開始處。
U-boot採用了一種高度模組化的程式設計方式,不同功能類別的程式碼分別放在不同的目錄中,幾個U-boot常用到的目錄分析如下所示:
1 board)這個目錄中存放了所有U-boot支援的目標板的子目錄。在這個目錄中一般是針對特定目標板的初始化和操作程式碼。
2 cpu)這個目錄中存放了U-boot支援的所有CPU型別。
3 common)這個目錄中存放獨立於處理器體系架構的通用程式碼,包括U-boot的一些公共命令的實現。一般來說,其中以cmd_*.c命令的檔案就是相對命令的實
現。比如cmd_bootm.c就是對命令bootm的實現。
4 drivers)這個目錄中存放的是各種外設介面的驅動程式。
5 fs)這個目錄中存放了U-boot支援的檔案系統。
6 include)這個目錄是存放各種CPU及目標板的標頭檔案和配置檔案的公共目錄,其中的configs目錄存放了各種目標板的配置標頭檔案。針對不同的板子,裡邊的配置
檔案要根據實際情況進行修改。
7 lib_XXX)這個目錄存放XXX體系架構的處理器的相關支援。
8 net)這個目錄用於存放與網路功能相關的檔案。
下節,就要開始對U-boot原始碼中的關鍵功能的實現進行分析,主要是從一下幾個方面:
1)使用匯編語言編寫的第一階段程式碼
2)第二階段程式碼命令的實現
3)第二階段作業系統引導機制的實現
------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------
前邊,我們說了,一般的bootloader都分為兩個階段。我在講U-boot實現原始碼分析時,也是按照這連個階段來分析,如果對這兩個階段不清楚,請看前邊的部落格。好了,開始今天的主題:U-boot在開發板上移植過程詳解(2)---U-boot實現原始碼分析(start.S分析)
第一階段:
1)一些基本的硬體初始化工作
u-boot對應的第一階段程式碼放在cpu/arm920t/start.S檔案中,入口程式碼如下:
.globl _start
;global宣告一個符號可被其它檔案引用,相當於聲明瞭一個全域性變數,.globl與.global相同 ;.word偽操作用於分配一段字記憶體單元(分配的單元都是字對齊的),並用偽操作中的expr初始化。 _undefined_instruction: .word undefined_instruction ;就是在當前地址,即_undefined_instruction 處存放 undefined_instruction _software_interrupt: .word software_interrupt |
這部分就是異常向量表。當系統上電或復位後,將執行第一條指令,即跳轉到標籤為reset的程式碼處執行,具體如下:
reset:
;設定CPU為SVC32管理模式 #if defined(CONFIG_S3C2400)
;關閉看門狗 #if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410) mov r1, #0xffffffff
;遮蔽所有中斷 #ifndef CONFIG_SKIP_LOWLEVEL_INIT |
上面的程式碼將CPU設為管理模式,關閉看門狗,遮蔽中斷並設定中斷,最後呼叫cpu_init_crit函式進行cpu的初始化,程式碼如下:
cpu_init_crit:
;清除指令和資料快取 mcr p15, 0, r0, c8, c7, 0 ;mcr指令用於將ARM處理器暫存器的資料傳送到協處理器暫存器中,若協處理器不能成功完成操作,則 ;產生未定義指令異常。其中協處理器操作碼1和協處理器操作碼2為協處理器將要執行的操作,目的暫存器 ;為ARM處理器的暫存器,源暫存器1和源暫存器2均為協處理器的暫存器。 mrc p15, 0, r0, c1, c0, 0
;mrc 協處理器暫存器到ARM處理器暫存器的資料傳送指令 mov ip, lr
;設定SDRAM控制器,與具體的目標板相關 |
在這個函式中做了一下工作:清除指令與資料快取,禁用MMU與資料指令快取,最後呼叫lowlevel_init函式設定SDRAM控制器。該函式的實現與具體的目標板有關的。
2)準備RAM空間
所謂準備RAM空間,就是初始化記憶體晶片,使它可用。 在board/smdk2410/lowlevel.init.S就是這個作用,要注意這時的程式碼,資料都儲存在NOR Flash上,記憶體中還沒有,所以讀取資料時要變換地址,如下:
_TEXT_BASE: .globl lowlevel_init ;現在起三行進行地址變化,因為這時候記憶體中還沒有資料,不能使用連線程式時確定的地址來讀取資料 mov pc, lr .ltorg SMRDATA:
;13個暫存器的值 .word … … |
這裡做完以後,就要將整個U-boot的程式碼都複製到SDRAM中,這些又都在start.S中實現,如下:
relocate:
;將u-boot複製到RAM中 ldr r2, _armboot_start
;_armboot_start在前邊已經定義,是第一條指令的執行地址 copy_loop: |
接下來,就要設定棧,棧的設定靈活性很大,只要讓sp暫存器指向一段沒有使用的記憶體即可。
stack_setup: |
3)跳轉到第二階段程式碼的C入口點
在跳轉之前,還要清除BSS段(初始值為0,無初始值的全域性變數,靜態變數放在BSS段),程式碼如下:
clear_bss: clbss_l:str r2, [r0]
;往BSS段中寫入0值 |
現在,c函式的執行環境已經完全準備好了,通過如下命令直接跳轉(這之後,程式才在記憶體中執行),它將呼叫lib_arm/board.c中的start_armboot函式(這是一個C語言函式),這是第二階段的入口點:
ldr pc, _start_armboot _start_armboot: .word start_armboot |
在第二階段程式碼中,將進行更多的初始化工作,如對各種裝置和介面的初始化,串列埠終端的初始化等。如果沒有設定自動執行,則最終將進入一個迴圈,在迴圈內讀取使用者輸入的命令並執行,這些會在下一節詳細介紹。
----------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------- U-boot的第二階段和bootloader所完成的功能基本上是一致的,只是順序上有點差別。另外,u-boot在啟動核心之前可以讓使用者決定是否進入下載模式,即進入u-boot的控制介面。第二階段是從lib_arm/board.c中的start_armboot函式開始的。移植u-boot的主要工作在於對硬體的初始化,驅動。這裡就重點按照硬體的操作上。
(1)初始化本階段要用到的硬體裝置
這裡最重要的是設定系統時鐘,初始化串列埠,只要這兩個設定好了,就可以從串列埠看到列印資訊。board_init函式設定MPLL、改變系統時鐘,它是開發板相關的函式,在board/samsung/smdk2440/smdk2440.c中實現。值得注意的是board_init函式還儲存了機器型別ID,這將在呼叫核心的時候傳遞給核心。
串列埠的初始化函式主要是serial_init,它設定UART控制器,是CPU的相關函式,在cpu/arm920t/s3c2440/serial.c中實現。
(2)檢測系統記憶體對映
對於特定的開發板,器記憶體的分佈是明確的,所以可以直接設定。board/smdk2410/smdk2410.c中的dram_init函式指定了本開發板的記憶體起始地址為0x300
00000,大小為0x40000000.程式碼如下:
int dram_init(void) { //這兩個值都定義在include/configs/smdk2440.h中 gd->bd->bi_dram[0] . start = PHYS_SDRAM_1; //即0x30000000 gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE; //即0x04000000 return 0; } |
這些設定的引數,將在後面向核心傳遞引數時用到。
(3)U-boot命令實現
我們已經知道,即使是核心的啟動,也是通過U-boot命令來實現的。u-boot中的每個命令都通過U-BOOT-CMD巨集(在include/command.h)來定義,格式如下:
U_BOOT_CMD(name, maxargs, repeatable, command, “usage”, "help”) 各項引數說明如下: name:命令的名字,注意,它不是一個字串(不要用雙引號括起來) maxargs:最大的引數個數 repeatable:命令是否可重複,可重複是指執行一個命令後,下次敲回車即可再次執行 command:對應的函式指標,型別為(*cmd)(struct cmd_tbl_s *, int, int, char *[]) usage:簡短的使用說明,這是個字串 |
下面以bootm命令來說明,它有如下定義:
U_BOOT_CMD( bootm, CFG_MAXARGS, 1, do_bootm, "string1”, "string2" ); |
利用U_BOOT_CMD的巨集展開後的命令如下
cmd_tbl_t __u_boot_cmd_boot __attribute__ ((unused, section(".u_boot_cmd"))) = { "bootm", CFG_MAXARGS, 1, do_bootm, "string1", “string2”}; |
對於每個使用U_BOOT_CMD巨集來定義的命令,其實都是在".u_boot_cmd"段中定義一個cmd_tbl_t結構,如下:
struct cmd_tbl_s { char *name; //命名名稱 int maxargs; //最大引數個數 int repeatable; //是否允許自動重複 int (*cmd)(struct cmd_tbl_s *, int, int, char *[]); //實現函式 char *usage; //幫助資訊(短) char *help; //幫助資訊(長) }; typedef struct cmd_tbl_s cmd_tbl_t; |
在u-boot的連結指令碼board/smdk2410/u-boot.lds中有如下定義:
__u_boot_cmd_start = .; .u_boot_cmd : { *(.u_boot_cmd) } __u_boot_cmd_end = .; |
在程式中就是根據命令的名字在記憶體段__u_boot_cmd_start~__u_boot_cmd_end找到它的cmd_tbl_t結構,然後呼叫它的函式(請參考common/comm
and.c中的find_cmd函式)。
(4)引導核心的實現
U-boot也是通過標記列表向核心傳遞引數的。ARM Linux核心對bootloader的引導功能有一定要求,在執行核心程式碼前必須設定下列條件:
& 對CPU暫存器的設定為R0=0, R1=機器型別ID,R2=引導引數列表的地址
& 必須禁止中斷(IRQ與FIQ)
& CPU必須處於SVC模式
& MMU必須關閉
& 資料快取必須關閉
現在linux雖然支援兩種格式的引導引數,這裡主要介紹最常用的新的方式---即上面所說的標記列表的,這種方式靈活,且對引數的描述更細緻。
標籤列表的每個標籤由標籤頭和標籤體組成。標籤頭說明這個標籤的大小(單位是整數不是位元組)以及這個標籤的型別。型別是由核心定義好的一個數字。標籤頭用一個結構體struct tag_header表示,如下:
struct tag_header{
u32 size;
u32 tag;
};
|
在標籤頭之後,根據標籤的型別,所需的標籤體也是不同的。標籤列表的結束由一個特殊的標籤型別ATAG_NONE標誌,它沒有標籤體。
比較重要的兩個標籤型別是ATAG_MEM(設定記憶體資訊)和ATAG_CMDLINE(用來傳遞命令列引數,即U-boot的bootargs變數的內容),這裡列出來,需要的請大家檢視google。下面給出一些小細節:
&u-boot原始碼中給出了一些設定標籤列表的原始碼,放在檔案lib_arm/armlinux.c中,方法是先定義一個全域性變數static struct tag *params,其中這個結構體的型別是一個將所有標籤型別組合在一起的結構體,如下所示:
struct tag { struct tag_header hdr; union { struct tag_corecore; struct tag_mem32mem; struct tag_videotextvideotext; struct tag_ramdiskramdisk; struct tag_initrdinitrd; struct tag_serialnrserialnr; struct tag_revisionrevision; struct tag_videolfbvideolfb; struct tag_cmdlinecmdline; struct tag_acornacorn; struct tag_memclkmemclk; } u; }; |
所有標籤的頭格式都是相同的,只是標籤體不同,因此用聯合體的方式將它們組合在一起。下面就是設定起始標籤的函式程式碼:
static void setup_start_tag (bd_t *bd) { params = (struct tag *) bd->bi_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); } |
在這個函式中,首先將變數params的值設為標籤列表的開始地址,然後逐個設定標籤中的成員,最後params變數的值將指向下一個標籤應該設定的地址。其中,tag_size是一個巨集,用來得到標籤的大小。最後,用於設定標籤列表結束的函式如下:
static void setup_end_tag (bd_t *bd) { params->hdr.tag = ATAG_NONE; params->hdr.size = 0; } |
對於ARM架構的CPU,都是通過lib_arm/armlinux.c中的do_bootm_linxu函式來啟動核心的,方法如下:
首先,獲得核心映像的入口地址:
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep); |
這樣theKernel就指向核心存放的地址(對於ARM架構的CPU,通常是0x30008000),這裡的hdr指向核心U-boot映像頭部資料的指標,而hdr->ih_ep就是核心的入口地址,最後用下述程式碼呼叫核心:
theKernel (0, bd->bi_arch_number, bd->bi_boot_params); |
這裡的bd->bi_arch_number就是前面board_init函式設定的機器型別ID, bd->bi_boot_params就是標記列表的開始地址。根據ATPCS呼叫約定,上述函式的三個函式分別放在暫存器R0,R1,R2中,這樣就實現了核心要求的入口條件。
講到這裡,有關的U-boot的關鍵原始碼分析分析部分就完成了,下次開始就來U-boot移植的實踐操作篇。
原文地址:http://blog.csdn.net/hare_lee/article/details/6944487