電子產(chǎn)業(yè)一站式賦能平臺(tái)

PCB聯(lián)盟網(wǎng)

搜索
查看: 1237|回復(fù): 0
收起左側(cè)

自學(xué)嵌入式視頻教程_四個(gè)方面來(lái)討論嵌入式系統(tǒng)的Boot Loader

[復(fù)制鏈接]

2607

主題

2607

帖子

7472

積分

高級(jí)會(huì)員

Rank: 5Rank: 5

積分
7472
跳轉(zhuǎn)到指定樓層
樓主
發(fā)表于 2020-7-22 10:57:29 | 只看該作者 回帖獎(jiǎng)勵(lì) |倒序?yàn)g覽 |閱讀模式
自學(xué)嵌入式視頻教程_四個(gè)方面來(lái)討論嵌入式系統(tǒng)的Boot Loader,   

一個(gè)嵌入式 Linux 系統(tǒng)從軟件的角度看通?梢苑譃樗膫(gè)層次:

1. 引導(dǎo)加載程序。包括固化在固件(firmware)中的 boot 代碼(可選),和 Boot Loader 兩大部分。

2. Linux 內(nèi)核。特定于嵌入式板子的定制內(nèi)核以及內(nèi)核的啟動(dòng)參數(shù)。

3. 文件系統(tǒng)。包括根文件系統(tǒng)和建立于 Flash 內(nèi)存設(shè)備之上文件系統(tǒng)。通常用 ram disk 來(lái)作為 root fs。

4. 用戶應(yīng)用程序。特定于用戶的應(yīng)用程序。有時(shí)在用戶應(yīng)用程序和內(nèi)核層之間可能還會(huì)包括一個(gè)嵌入式圖形用戶界面。常用的嵌入式 GUI 有:MicroWindows 和 MiniGUI 懂。

引導(dǎo)加載程序是系統(tǒng)加電后運(yùn)行的第一段軟件代碼;貞浺幌 PC 的體系結(jié)構(gòu)我們可以知道,PC 機(jī)中的引導(dǎo)加載程序由 BIOS(其本質(zhì)就是一段固件程序)和位于硬盤(pán) MBR 中的 OS Boot Loader(比如,LILO 和 GRUB 等)一起組成。

BIOS 在完成硬件檢測(cè)和資源分配后,將硬盤(pán) MBR 中的 Boot Loader 讀到系統(tǒng)的 RAM 中,然后將控制權(quán)交給 OS Boot Loader。Boot Loader 的主要運(yùn)行任務(wù)就是將內(nèi)核映象從硬盤(pán)上讀到 RAM 中,然后跳轉(zhuǎn)到內(nèi)核的入口點(diǎn)去運(yùn)行,也即開(kāi)始啟動(dòng)操作系統(tǒng)。

而在嵌入式系統(tǒng)中,通常并沒(méi)有像 BIOS 那樣的固件程序(注,有的嵌入式 CPU 也會(huì)內(nèi)嵌一段短小的啟動(dòng)程序),因此整個(gè)系統(tǒng)的加載啟動(dòng)任務(wù)就完全由 Boot Loader 來(lái)完成。比如在一個(gè)基于 ARM7TDMI core 的嵌入式系統(tǒng)中,系統(tǒng)在上電或復(fù)位時(shí)通常都從地址 0x00000000 處開(kāi)始執(zhí)行,而在這個(gè)地址處安排的通常就是系統(tǒng)的 Boot Loader 程序。

本文將從 Boot Loader 的概念、Boot Loader 的主要任務(wù)、Boot Loader 的框架結(jié)構(gòu)以及 Boot Loader 的安裝等四個(gè)方面來(lái)討論嵌入式系統(tǒng)的 Boot Loader。

  

Boot Loader 的概念

簡(jiǎn)單地說(shuō),Boot Loader 就是在操作系統(tǒng)內(nèi)核運(yùn)行之前運(yùn)行的一段小程序。通過(guò)這段小程序,我們可以初始化硬件設(shè)備、建立內(nèi)存空間的映射圖,從而將系統(tǒng)的軟硬件環(huán)境帶到一個(gè)合適的狀態(tài),以便為最終調(diào)用操作系統(tǒng)內(nèi)核準(zhǔn)備好正確的環(huán)境。

通常,Boot Loader 是嚴(yán)重地依賴于硬件而實(shí)現(xiàn)的,特別是在嵌入式世界。因此,在嵌入式世界里建立一個(gè)通用的 Boot Loader 幾乎是不可能的。盡管如此,我們?nèi)匀豢梢詫?duì) Boot Loader 歸納出一些通用的概念來(lái),以指導(dǎo)用戶特定的 Boot Loader 設(shè)計(jì)與實(shí)現(xiàn)。

1. Boot Loader 所支持的 CPU 和嵌入式板每種不同的 CPU 體系結(jié)構(gòu)都有不同的 Boot Loader。有些 Boot Loader 也支持多種體系結(jié)構(gòu)的 CPU,比如 U-Boot 就同時(shí)支持 ARM 體系結(jié)構(gòu)和MIPS 體系結(jié)構(gòu)。除了依賴于 CPU 的體系結(jié)構(gòu)外,Boot Loader 實(shí)際上也依賴于具體的嵌入式板級(jí)設(shè)備的配置。這也就是說(shuō),對(duì)于兩塊不同的嵌入式板而言,即使它們是基于同一種 CPU 而構(gòu)建的,要想讓運(yùn)行在一塊板子上的 Boot Loader 程序也能運(yùn)行在另一塊板子上,通常也都需要修改 Boot Loader 的源程序。

2. Boot Loader 的安裝媒介(Installation Medium)系統(tǒng)加電或復(fù)位后,所有的 CPU 通常都從某個(gè)由 CPU 制造商預(yù)先安排的地址上取指令。比如,基于 ARM7TDMI core 的 CPU 在復(fù)位時(shí)通常都從地址 0x00000000 取它的第一條指令。而基于 CPU 構(gòu)建的嵌入式系統(tǒng)通常都有某種類(lèi)型的固態(tài)存儲(chǔ)設(shè)備(比如:ROM、EEPROM 或 FLASH 等)被映射到這個(gè)預(yù)先安排的地址上。因此在系統(tǒng)加電后,CPU 將首先執(zhí)行 Boot Loader 程序。

下圖1就是一個(gè)同時(shí)裝有 Boot Loader、內(nèi)核的啟動(dòng)參數(shù)、內(nèi)核映像和根文件系統(tǒng)映像的固態(tài)存儲(chǔ)設(shè)備的典型空間分配結(jié)構(gòu)圖。

圖1 固態(tài)存儲(chǔ)設(shè)備的典型空間分配結(jié)構(gòu)

  



4. Boot Loader 的啟動(dòng)過(guò)程是單階段(Single Stage)還是多階段(Multi-Stage)通常多階段的 Boot Loader 能提供更為復(fù)雜的功能,以及更好的可移植性。從固態(tài)存儲(chǔ)設(shè)備上啟動(dòng)的 Boot Loader 大多都是 2 階段的啟動(dòng)過(guò)程,也即啟動(dòng)過(guò)程可以分為 stage 1 和 stage 2 兩部分。而至于在 stage 1 和 stage 2 具體完成哪些任務(wù)將在下面討論。

5. Boot Loader 的操作模式 (Operation Mode)大多數(shù) Boot Loader 都包含兩種不同的操作模式:“啟動(dòng)加載”模式和“下載”模式,這種區(qū)別僅對(duì)于開(kāi)發(fā)人員才有意義。但從最終用戶的角度看,Boot Loader 的作用就是用來(lái)加載操作系統(tǒng),而并不存在所謂的啟動(dòng)加載模式與下載工作模式的區(qū)別。

啟動(dòng)加載(Boot loading)模式:這種模式也稱(chēng)為“自主”(Autonomous)模式。也即 Boot Loader 從目標(biāo)機(jī)上的某個(gè)固態(tài)存儲(chǔ)設(shè)備上將操作系統(tǒng)加載到 RAM 中運(yùn)行,整個(gè)過(guò)程并沒(méi)有用戶的介入。這種模式是 Boot Loader 的正常工作模式,因此在嵌入式產(chǎn)品發(fā)布的時(shí)侯,Boot Loader 顯然必須工作在這種模式下。

下載(Downloading)模式:在這種模式下,目標(biāo)機(jī)上的 Boot Loader 將通過(guò)串口連接或網(wǎng)絡(luò)連接等通信手段從主機(jī)(Host)下載文件,比如:下載內(nèi)核映像和根文件系統(tǒng)映像等。從主機(jī)下載的文件通常首先被 Boot Loader 保存到目標(biāo)機(jī)的 RAM 中,然后再被 Boot Loader 寫(xiě)到目標(biāo)機(jī)上的FLASH 類(lèi)固態(tài)存儲(chǔ)設(shè)備中。

Boot Loader 的這種模式通常在第一次安裝內(nèi)核與根文件系統(tǒng)時(shí)被使用;此外,以后的系統(tǒng)更新也會(huì)使用 Boot Loader 的這種工作模式。工作于這種模式下的 Boot Loader 通常都會(huì)向它的終端用戶提供一個(gè)簡(jiǎn)單的命令行接口。

像 Blob 或 U-Boot 等這樣功能強(qiáng)大的 Boot Loader 通常同時(shí)支持這兩種工作模式,而且允許用戶在這兩種工作模式之間進(jìn)行切換。比如,Blob 在啟動(dòng)時(shí)處于正常的啟動(dòng)加載模式,但是它會(huì)延時(shí) 10 秒等待終端用戶按下任意鍵而將 blob 切換到下載模式。如果在 10 秒內(nèi)沒(méi)有用戶按鍵,則 blob 繼續(xù)啟動(dòng) Linux 內(nèi)核。

6. BootLoader 與主機(jī)之間進(jìn)行文件傳輸所用的通信設(shè)備及協(xié)議最常見(jiàn)的情況就是,目標(biāo)機(jī)上的 Boot Loader 通過(guò)串口與主機(jī)之間進(jìn)行文件傳輸,傳輸協(xié)議通常是 xmodem/ymodem/zmodem 協(xié)議中的一種。但是,串口傳輸?shù)乃俣仁怯邢薜,因此通過(guò)以太網(wǎng)連接并借助 TFTP 協(xié)議來(lái)下載文件是個(gè)更好的選擇。

此外,在論及這個(gè)話題時(shí),主機(jī)方所用的軟件也要考慮。比如,在通過(guò)以太網(wǎng)連接和 TFTP 協(xié)議來(lái)下載文件時(shí),主機(jī)方必須有一個(gè)軟件用來(lái)的提供 TFTP 服務(wù)。

在討論了 BootLoader 的上述概念后,下面我們來(lái)具體看看 BootLoader 的應(yīng)該完成哪些任務(wù)。

  

Boot Loader 的主要任務(wù)與典型結(jié)構(gòu)框架

在繼續(xù)本節(jié)的討論之前,首先我們做一個(gè)假定,那就是:假定內(nèi)核映像與根文件系統(tǒng)映像都被加載到 RAM 中運(yùn)行。之所以提出這樣一個(gè)假設(shè)前提是因?yàn)椋谇度胧较到y(tǒng)中內(nèi)核映像與根文件系統(tǒng)映像也可以直接在 ROM 或 Flash 這樣的固態(tài)存儲(chǔ)設(shè)備中直接運(yùn)行。但這種做法無(wú)疑是以運(yùn)行速度的犧牲為代價(jià)的。

從操作系統(tǒng)的角度看,Boot Loader 的總目標(biāo)就是正確地調(diào)用內(nèi)核來(lái)執(zhí)行。

另外,由于 Boot Loader 的實(shí)現(xiàn)依賴于 CPU 的體系結(jié)構(gòu),因此大多數(shù) Boot Loader 都分為 stage1 和 stage2 兩大部分。依賴于 CPU 體系結(jié)構(gòu)的代碼,比如設(shè)備初始化代碼等,通常都放在 stage1 中,而且通常都用匯編語(yǔ)言來(lái)實(shí)現(xiàn),以達(dá)到短小精悍的目的。而 stage2 則通常用C語(yǔ)言來(lái)實(shí)現(xiàn),這樣可以實(shí)現(xiàn)給復(fù)雜的功能,而且代碼會(huì)具有更好的可讀性和可移植性。

Boot Loader 的 stage1 通常包括以下步驟(以執(zhí)行的先后順序):

· 硬件設(shè)備初始化。

· 為加載 Boot Loader 的 stage2 準(zhǔn)備 RAM 空間。· 拷貝 Boot Loader 的 stage2 到 RAM 空間中! 設(shè)置好堆棧。

· 跳轉(zhuǎn)到 stage2 的 C 入口點(diǎn)。

Boot Loader 的 stage2 通常包括以下步驟(以執(zhí)行的先后順序):· 初始化本階段要使用到的硬件設(shè)備。· 檢測(cè)系統(tǒng)內(nèi)存映射(memory map)! 將 kernel 映像和根文件系統(tǒng)映像從 flash 上讀到 RAM 空間中! 為內(nèi)核設(shè)置啟動(dòng)參數(shù)。· 調(diào)用內(nèi)核。

  

Boot Loader 的 stage1

基本的硬件初始化

這是 Boot Loader 一開(kāi)始就執(zhí)行的操作,其目的是為 stage2 的執(zhí)行以及隨后的 kernel 的執(zhí)行準(zhǔn)備好一些基本的硬件環(huán)境。它通常包括以下步驟(以執(zhí)行的先后順序):

1. 屏蔽所有的中斷。為中斷提供服務(wù)通常是 OS 設(shè)備驅(qū)動(dòng)程序的責(zé)任,因此在 Boot Loader 的執(zhí)行全過(guò)程中可以不必響應(yīng)任何中斷。中斷屏蔽可以通過(guò)寫(xiě) CPU 的中斷屏蔽寄存器或狀態(tài)寄存器(比如 ARM 的 CPSR 寄存器)來(lái)完成。

2. 設(shè)置 CPU 的速度和時(shí)鐘頻率。

3. RAM 初始化。包括正確地設(shè)置系統(tǒng)的內(nèi)存控制器的功能寄存器以及各內(nèi)存庫(kù)控制寄存器等。

4. 初始化 LED。典型地,通過(guò) GPIO 來(lái)驅(qū)動(dòng) LED,其目的是表明系統(tǒng)的狀態(tài)是 OK 還是 Error。如果板子上沒(méi)有 LED,那么也可以通過(guò)初始化 UART 向串口打印 Boot Loader 的 Logo 字符信息來(lái)完成這一點(diǎn)。

5. 關(guān)閉 CPU 內(nèi)部指令/數(shù)據(jù) cache。

為加載 stage2 準(zhǔn)備 RAM 空間

為了獲得更快的執(zhí)行速度,通常把 stage2 加載到 RAM 空間中來(lái)執(zhí)行,因此必須為加載 Boot Loader 的 stage2 準(zhǔn)備好一段可用的 RAM 空間范圍。

由于 stage2 通常是 C 語(yǔ)言執(zhí)行代碼,因此在考慮空間大小時(shí),除了 stage2 可執(zhí)行映象的大小外,還必須把堆?臻g也考慮進(jìn)來(lái)。此外,空間大小最好是 memory page 大小(通常是 4KB)的倍數(shù)。一般而言,1M 的 RAM 空間已經(jīng)足夠了。具體的地址范圍可以任意安排,比如 blob 就將它的 stage2 可執(zhí)行映像安排到從系統(tǒng) RAM 起始地址 0xc0200000 開(kāi)始的 1M 空間內(nèi)執(zhí)行。但是,將 stage2 安排到整個(gè) RAM 空間的最頂 1MB(也即(RamEnd-1MB) - RamEnd)是一種值得推薦的方法。

為了后面的敘述方便,這里把所安排的 RAM 空間范圍的大小記為:stage2_size(字節(jié)),把起始地址和終止地址分別記為:stage2_start 和 stage2_end(這兩個(gè)地址均以 4 字節(jié)邊界對(duì)齊)。因此:stage2_end=stage2_start+stage2_size 另外,還必須確保所安排的地址范圍的的確確是可讀寫(xiě)的 RAM 空間,因此,必須對(duì)你所安排的地址范圍進(jìn)行測(cè)試。具體的測(cè)試方法可以采用類(lèi)似于 blob 的方法,也即:以 memory page 為被測(cè)試單位,測(cè)試每個(gè) memory page 開(kāi)始的兩個(gè)字是否是可讀寫(xiě)的。為了后面敘述的方便,我們記這個(gè)檢測(cè)算法為:test_mempage,其具體步驟如下:

1. 先保存 memory page 一開(kāi)始兩個(gè)字的內(nèi)容。2. 向這兩個(gè)字中寫(xiě)入任意的數(shù)字。比如:向第一個(gè)字寫(xiě)入 0x55,第 2 個(gè)字寫(xiě)入 0xaa。3. 然后,立即將這兩個(gè)字的內(nèi)容讀回。顯然,我們讀到的內(nèi)容應(yīng)該分別是 0x55 和 0xaa。如果不是,則說(shuō)明這個(gè) memory page 所占據(jù)的地址范圍不是一段有效的 RAM 空間。4. 再向這兩個(gè)字中寫(xiě)入任意的數(shù)字。比如:向第一個(gè)字寫(xiě)入 0xaa,第 2 個(gè)字中寫(xiě)入 0x55。5. 然后,立即將這兩個(gè)字的內(nèi)容立即讀回。顯然,我們讀到的內(nèi)容應(yīng)該分別是 0xaa 和 0x55。如果不是,則說(shuō)明這個(gè) memory page 所占據(jù)的地址范圍不是一段有效的 RAM 空間。6. 恢復(fù)這兩個(gè)字的原始內(nèi)容。測(cè)試完畢。為了得到一段干凈的 RAM 空間范圍,我們也可以將所安排的 RAM 空間范圍進(jìn)行清零操作。

拷貝 stage2 到 RAM 中

拷貝時(shí)要確定兩點(diǎn):(1) stage2 的可執(zhí)行映象在固態(tài)存儲(chǔ)設(shè)備的存放起始地址和終止地址;(2) RAM 空間的起始地址。

設(shè)置堆棧指針 sp

堆棧指針的設(shè)置是為了執(zhí)行 C 語(yǔ)言代碼作好準(zhǔn)備。通常我們可以把 sp 的值設(shè)置為(stage2_end-4),也即在 3.1.2 節(jié)所安排的那個(gè) 1MB 的 RAM 空間的最頂端(堆棧向下生長(zhǎng))。

此外,在設(shè)置堆棧指針 sp 之前,也可以關(guān)閉 led 燈,以提示用戶我們準(zhǔn)備跳轉(zhuǎn)到 stage2。

經(jīng)過(guò)上述這些執(zhí)行步驟后,系統(tǒng)的物理內(nèi)存布局應(yīng)該如下圖2所示。

跳轉(zhuǎn)到 stage2 的 C 入口點(diǎn)

在上述一切都就緒后,就可以跳轉(zhuǎn)到 Boot Loader 的 stage2 去執(zhí)行了。比如,在 ARM 系統(tǒng)中,這可以通過(guò)修改 PC 寄存器為合適的地址來(lái)實(shí)現(xiàn)。

圖2 bootloader 的 stage2 可執(zhí)行映象剛被拷貝到 RAM 空間時(shí)的系統(tǒng)內(nèi)存布局

  

  

Boot Loader 的 stage2

正如前面所說(shuō),stage2 的代碼通常用 C 語(yǔ)言來(lái)實(shí)現(xiàn),以便于實(shí)現(xiàn)更復(fù)雜的功能和取得更好的代碼可讀性和可移植性。但是與普通 C 語(yǔ)言應(yīng)用程序不同的是,在編譯和鏈接 boot loader 這樣的程序時(shí),我們不能使用 glibc 庫(kù)中的任何支持函數(shù)。其原因是顯而易見(jiàn)的。這就給我們帶來(lái)一個(gè)問(wèn)題,那就是從那里跳轉(zhuǎn)進(jìn) main() 函數(shù)呢?直接把 main() 函數(shù)的起始地址作為整個(gè) stage2 執(zhí)行映像的入口點(diǎn)或許是最直接的想法。但是這樣做有兩個(gè)缺點(diǎn):1)無(wú)法通過(guò)main() 函數(shù)傳遞函數(shù)參數(shù);2)無(wú)法處理 main() 函數(shù)返回的情況。一種更為巧妙的方法是利用 trampoline(彈簧床)的概念。也即,用匯編語(yǔ)言寫(xiě)一段trampoline 小程序,并將這段 trampoline 小程序來(lái)作為 stage2 可執(zhí)行映象的執(zhí)行入口點(diǎn)。然后我們可以在 trampoline 匯編小程序中用 CPU 跳轉(zhuǎn)指令跳入 main() 函數(shù)中去執(zhí)行;而當(dāng) main() 函數(shù)返回時(shí),CPU 執(zhí)行路徑顯然再次回到我們的 trampoline 程序。簡(jiǎn)而言之,這種方法的思想就是:用這段 trampoline 小程序來(lái)作為 main() 函數(shù)的外部包裹(external wrapper)。

下面給出一個(gè)簡(jiǎn)單的 trampoline 程序示例(來(lái)自blob):

.text.globl _trampoline_trampoline:bl main/* if main ever returns we just call it again */b _trampoline

可以看出,當(dāng) main() 函數(shù)返回后,我們又用一條跳轉(zhuǎn)指令重新執(zhí)行 trampoline 程序――當(dāng)然也就重新執(zhí)行 main() 函數(shù),這也就是 trampoline(彈簧床)一詞的意思所在。

初始化本階段要使用到的硬件設(shè)備

這通常包括:(1)初始化至少一個(gè)串口,以便和終端用戶進(jìn)行 I/O 輸出信息;(2)初始化計(jì)時(shí)器等。在初始化這些設(shè)備之前,也可以重新把 LED 燈點(diǎn)亮,以表明我們已經(jīng)進(jìn)入 main() 函數(shù)執(zhí)行。設(shè)備初始化完成后,可以輸出一些打印信息,程序名字字符串、版本號(hào)等。

檢測(cè)系統(tǒng)的內(nèi)存映射(memory map)

所謂內(nèi)存映射就是指在整個(gè) 4GB 物理地址空間中有哪些地址范圍被分配用來(lái)尋址系統(tǒng)的 RAM 單元。比如,在 SA-1100 CPU 中,從 0xC000,0000 開(kāi)始的 512M 地址空間被用作系統(tǒng)的 RAM 地址空間,而在 Samsung S3C44B0X CPU 中,從 0x0c00,0000 到 0x1000,0000 之間的 64M 地址空間被用作系統(tǒng)的 RAM 地址空間。雖然 CPU 通常預(yù)留出一大段足夠的地址空間給系統(tǒng) RAM,但是在搭建具體的嵌入式系統(tǒng)時(shí)卻不一定會(huì)實(shí)現(xiàn) CPU 預(yù)留的全部 RAM 地址空間。也就是說(shuō),具體的嵌入式系統(tǒng)往往只把 CPU 預(yù)留的全部 RAM 地址空間中的一部分映射到 RAM 單元上,而讓剩下的那部分預(yù)留 RAM 地址空間處于未使用狀態(tài)。 由于上述這個(gè)事實(shí),因此 Boot Loader 的 stage2 必須在它想干點(diǎn)什么 (比如,將存儲(chǔ)在 flash 上的內(nèi)核映像讀到 RAM 空間中) 之前檢測(cè)整個(gè)系統(tǒng)的內(nèi)存映射情況,也即它必須知道 CPU 預(yù)留的全部 RAM 地址空間中的哪些被真正映射到 RAM 地址單元,哪些是處于 “unused” 狀態(tài)的。

(1) 內(nèi)存映射的描述

可以用如下數(shù)據(jù)結(jié)構(gòu)來(lái)描述 RAM 地址空間中的一段連續(xù)(continuous)的地址范圍:

typedef struct memory_area_struct {u32 start; /* the base address of the memory region */u32 size; /* the byte number of the memory region */int used;} memory_area_t;

這段 RAM 地址空間中的連續(xù)地址范圍可以處于兩種狀態(tài)之一:(1)used=1,則說(shuō)明這段連續(xù)的地址范圍已被實(shí)現(xiàn),也即真正地被映射到 RAM 單元上。

(2)used=0,則說(shuō)明這段連續(xù)的地址范圍并未被系統(tǒng)所實(shí)現(xiàn),而是處于未使用狀態(tài)。

基于上述 memory_area_t 數(shù)據(jù)結(jié)構(gòu),整個(gè) CPU 預(yù)留的 RAM 地址空間可以用一個(gè) memory_area_t 類(lèi)型的數(shù)組來(lái)表示,如下所示:

memory_area_t memory_map[NUM_MEM_AREAS] = {[0 。。. (NUM_MEM_AREAS - 1)] = {.start = 0,.size = 0,.used = 0},};

(2) 內(nèi)存映射的檢測(cè)

下面我們給出一個(gè)可用來(lái)檢測(cè)整個(gè) RAM 地址空間內(nèi)存映射情況的簡(jiǎn)單而有效的算法:

/* 數(shù)組初始化 */for(i = 0; i 《 NUM_MEM_AREAS; i++)memory_map[i].used = 0;/* first write a 0 to all memory locations */for(addr = MEM_START; addr 《 MEM_END; addr += PAGE_SIZE)* (u32 *)addr = 0;for(i = 0, addr = MEM_START; addr 《 MEM_END; addr += PAGE_SIZE) { /* * 檢測(cè)從基地址 MEM_START+i*PAGE_SIZE 開(kāi)始,大小為* PAGE_SIZE 的地址空間是否是有效的RAM地址空間。 */ 調(diào)用3.1.2節(jié)中的算法test_mempage(); if ( current memory page isnot a valid ram page) {/* no RAM here */if(memory_map[i].used )i++;continue;}

/* * 當(dāng)前頁(yè)已經(jīng)是一個(gè)被映射到 RAM 的有效地址范圍 * 但是還要看看當(dāng)前頁(yè)是否只是 4GB 地址空間中某個(gè)地址頁(yè)的別名? */if(* (u32 *)addr != 0) { /* alias? *//* 這個(gè)內(nèi)存頁(yè)是 4GB 地址空間中某個(gè)地址頁(yè)的別名 */if ( memory_map[i].used )i++;continue;}

/* * 當(dāng)前頁(yè)已經(jīng)是一個(gè)被映射到 RAM 的有效地址范圍 * 而且它也不是 4GB 地址空間中某個(gè)地址頁(yè)的別名。 */if (memory_map[i].used == 0) {memory_map[i].start = addr;memory_map[i].size = PAGE_SIZE;memory_map[i].used = 1;} else {memory_map[i].size += PAGE_SIZE;}} /* end of for (…) */

在用上述算法檢測(cè)完系統(tǒng)的內(nèi)存映射情況后,Boot Loader 也可以將內(nèi)存映射的詳細(xì)信息打印到串口。

加載內(nèi)核映像和根文件系統(tǒng)映像

(1) 規(guī)劃內(nèi)存占用的布局

這里包括兩個(gè)方面:(1)內(nèi)核映像所占用的內(nèi)存范圍;(2)根文件系統(tǒng)所占用的內(nèi)存范圍。在規(guī)劃內(nèi)存占用的布局時(shí),主要考慮基地址和映像的大小兩個(gè)方面。

對(duì)于內(nèi)核映像,一般將其拷貝到從(MEM_START+0x8000) 這個(gè)基地址開(kāi)始的大約1MB大小的內(nèi)存范圍內(nèi)(嵌入式 Linux 的內(nèi)核一般都不操過(guò) 1MB)。為什么要把從 MEM_START 到 MEM_START+0x8000 這段 32KB 大小的內(nèi)存空出來(lái)呢?這是因?yàn)?Linux 內(nèi)核要在這段內(nèi)存中放置一些全局?jǐn)?shù)據(jù)結(jié)構(gòu),如:?jiǎn)?dòng)參數(shù)和內(nèi)核頁(yè)表等信息。

而對(duì)于根文件系統(tǒng)映像,則一般將其拷貝到 MEM_START+0x0010,0000 開(kāi)始的地方。如果用 Ramdisk 作為根文件系統(tǒng)映像,則其解壓后的大小一般是1MB。

(2)從 Flash 上拷貝

由于像 ARM 這樣的嵌入式 CPU 通常都是在統(tǒng)一的內(nèi)存地址空間中尋址 Flash 等固態(tài)存儲(chǔ)設(shè)備的,因此從 Flash 上讀取數(shù)據(jù)與從 RAM 單元中讀取數(shù)據(jù)并沒(méi)有什么不同。用一個(gè)簡(jiǎn)單的循環(huán)就可以完成從 Flash 設(shè)備上拷貝映像的工作:

while(c++ount) {*dest++ = *src++; /* they are all aligned with word boundary */count -= 4; /* byte number */};

設(shè)置內(nèi)核的啟動(dòng)參數(shù)

應(yīng)該說(shuō),在將內(nèi)核映像和根文件系統(tǒng)映像拷貝到 RAM 空間中后,就可以準(zhǔn)備啟動(dòng) Linux 內(nèi)核了。但是在調(diào)用內(nèi)核之前,應(yīng)該作一步準(zhǔn)備工作,即:設(shè)置 Linux 內(nèi)核的啟動(dòng)參數(shù)。

Linux 2.4.x 以后的內(nèi)核都期望以標(biāo)記列表(tagged list)的形式來(lái)傳遞啟動(dòng)參數(shù)。啟動(dòng)參數(shù)標(biāo)記列表以標(biāo)記 ATAG_CORE 開(kāi)始,以標(biāo)記 ATAG_NONE 結(jié)束。每個(gè)標(biāo)記由標(biāo)識(shí)被傳遞參數(shù)的 tag_header 結(jié)構(gòu)以及隨后的參數(shù)值數(shù)據(jù)結(jié)構(gòu)來(lái)組成。數(shù)據(jù)結(jié)構(gòu) tag 和 tag_header 定義在 Linux 內(nèi)核源碼的include/asm/setup.h 頭文件中:

/* The list ends with an ATAG_NONE node. */#define ATAG_NONE 0x00000000struct tag_header {u32 size; /* 注意,這里size是字?jǐn)?shù)為單位的 */u32 tag;};……struct tag {struct tag_header hdr;union {struct tag_core core;struct tag_mem32 mem;struct tag_videotext videotext;struct tag_ramdisk ramdisk;struct tag_initrd initrd;struct tag_serialnr serialnr;struct tag_revision revision;struct tag_videolfb videolfb;struct tag_cmdline cmdline;/* * Acorn specific */struct tag_acorn acorn;/* * DC21285 specific */struct tag_memclk memclk;} u;};

在嵌入式 Linux 系統(tǒng)中,通常需要由 Boot Loader 設(shè)置的常見(jiàn)啟動(dòng)參數(shù)有:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。

比如,設(shè)置 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 表示內(nèi)核啟動(dòng)參數(shù)在內(nèi)存中的起始基地址,指針 params 是一個(gè) struct tag 類(lèi)型的指針。宏 tag_next() 將以指向當(dāng)前標(biāo)記的指針為參數(shù),計(jì)算緊臨當(dāng)前標(biāo)記的下一個(gè)標(biāo)記的起始地址。注意,內(nèi)核的根文件系統(tǒng)所在的設(shè)備ID就是在這里設(shè)置的。

下面是設(shè)置內(nèi)存映射情況的示例代碼:

for(i = 0; i 《 NUM_MEM_AREAS; i++) {if(memory_map[i].used) {params-》hdr.tag = ATAG_MEM;params-》hdr.size = tag_size(tag_mem32);params-》u.mem.start = memory_map[i].start;params-》u.mem.size = memory_map[i].size;

params = tag_next(params);}}

可以看出,在 memory_map[]數(shù)組中,每一個(gè)有效的內(nèi)存段都對(duì)應(yīng)一個(gè) ATAG_MEM 參數(shù)標(biāo)記。

Linux 內(nèi)核在啟動(dòng)時(shí)可以以命令行參數(shù)的形式來(lái)接收信息,利用這一點(diǎn)我們可以向內(nèi)核提供那些內(nèi)核不能自己檢測(cè)的硬件參數(shù)信息,或者重載(override)內(nèi)核自己檢測(cè)到的信息。比如,我們用這樣一個(gè)命令行參數(shù)字符串“console=ttyS0,115200n8”來(lái)通知內(nèi)核以 ttyS0 作為控制臺(tái),且串口采用 “115200bps、無(wú)奇偶校驗(yàn)、8位數(shù)據(jù)位”這樣的設(shè)置。下面是一段設(shè)置調(diào)用內(nèi)核命令行參數(shù)字符串的示例代碼:

char *p;/* eat leading white space */for(p = commandline; *p == ‘ ’; p++);/* skip non-existent command lines so the kernel will still * use its default command line. */if(*p == ‘’)return;params-》hdr.tag = ATAG_CMDLINE;params-》hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) 》》 2;strcpy(params-》u.cmdline.cmdline, p);params = tag_next(params);

請(qǐng)注意在上述代碼中,設(shè)置 tag_header 的大小時(shí),必須包括字符串的終止符‘’,此外還要將字節(jié)數(shù)向上圓整4個(gè)字節(jié),因?yàn)?tag_header 結(jié)構(gòu)中的size 成員表示的是字?jǐn)?shù)。

下面是設(shè)置 ATAG_INITRD 的示例代碼,它告訴內(nèi)核在 RAM 中的什么地方可以找到 initrd 映象(壓縮格式)以及它的大。

params-》hdr.tag = ATAG_INITRD2;params-》hdr.size = tag_size(tag_initrd);

params-》u.initrd.start = RAMDISK_RAM_BASE;params-》u.initrd.size = INITRD_LEN;

params = tag_next(params);

下面是設(shè)置 ATAG_RAMDISK 的示例代碼,它告訴內(nèi)核解壓后的 Ramdisk 有多大(單位是KB):

params-》hdr.tag = ATAG_RAMDISK;params-》hdr.size = tag_size(tag_ramdisk);

params-》u.ramdisk.start = 0;params-》u.ramdisk.size = RAMDISK_SIZE; /* 請(qǐng)注意,單位是KB */params-》u.ramdisk.flags = 1; /* automatically load ramdisk */

params = tag_next(params);

最后,設(shè)置 ATAG_NONE 標(biāo)記,結(jié)束整個(gè)啟動(dòng)參數(shù)列表:

static void setup_end_tag(void){params-》hdr.tag = ATAG_NONE;params-》hdr.size = 0;}

調(diào)用內(nèi)核

Boot Loader 調(diào)用 Linux 內(nèi)核的方法是直接跳轉(zhuǎn)到內(nèi)核的第一條指令處,也即直接跳轉(zhuǎn)到 MEM_START+0x8000 地址處。在跳轉(zhuǎn)時(shí),下列條件要滿足:

卡片的背景色需要調(diào)出布局工具欄來(lái)設(shè)置1. CPU 寄存器的設(shè)置:· R0=0;· R1=機(jī)器類(lèi)型 ID;關(guān)于 Machine Type Number,可以參見(jiàn) linux/arch/arm/tools/mach-types! R2=啟動(dòng)參數(shù)標(biāo)記列表在 RAM 中起始基地址;

2. CPU 模式:· 必須禁止中斷(IRQs和FIQs);· CPU 必須 SVC 模式;

3. Cache 和 MMU 的設(shè)置:· MMU 必須關(guān)閉;· 指令 Cache 可以打開(kāi)也可以關(guān)閉;· 數(shù)據(jù) Cache 必須關(guān)閉;如果用 C 語(yǔ)言,可以像下列示例代碼這樣來(lái)調(diào)用內(nèi)核:每個(gè)卡片都可以嵌套插入圖片或其它模板

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()函數(shù)調(diào)用應(yīng)該永遠(yuǎn)不返回的。如果這個(gè)調(diào)用返回,則說(shuō)明出錯(cuò)。

  

關(guān)于串口終端

在 boot loader 程序的設(shè)計(jì)與實(shí)現(xiàn)中,沒(méi)有什么能夠比從串口終端正確地收到打印信息能更令人激動(dòng)了。此外,向串口終端打印信息也是一個(gè)非常重要而又有效的調(diào)試手段。但是,我們經(jīng)常會(huì)碰到串口終端顯示亂碼或根本沒(méi)有顯示的問(wèn)題。造成這個(gè)問(wèn)題主要有兩種原因:(1) boot loader 對(duì)串口的初始化設(shè)置不正確。(2) 運(yùn)行在 host 端的終端仿真程序?qū)Υ诘脑O(shè)置不正確,這包括:波特率、奇偶校驗(yàn)、數(shù)據(jù)位和停止位等方面的設(shè)置。

此外,有時(shí)也會(huì)碰到這樣的問(wèn)題,那就是:在 boot loader 的運(yùn)行過(guò)程中我們可以正確地向串口終端輸出信息,但當(dāng) boot loader 啟動(dòng)內(nèi)核后卻無(wú)法看到內(nèi)核的啟動(dòng)輸出信息。對(duì)這一問(wèn)題的原因可以從以下幾個(gè)方面來(lái)考慮:(1) 首先請(qǐng)確認(rèn)你的內(nèi)核在編譯時(shí)配置了對(duì)串口終端的支持,并配置了正確的串口驅(qū)動(dòng)程序。(2) 你的 boot loader 對(duì)串口的初始化設(shè)置可能會(huì)和內(nèi)核對(duì)串口的初始化設(shè)置不一致。此外,對(duì)于諸如 s3c44b0x 這樣的 CPU,CPU 時(shí)鐘頻率的設(shè)置也會(huì)影響串口,因此如果 boot loader 和內(nèi)核對(duì)其 CPU 時(shí)鐘頻率的設(shè)置不一致,也會(huì)使串口終端無(wú)法正確顯示信息。(3) 最后,還要確認(rèn) boot loader 所用的內(nèi)核基地址必須和內(nèi)核映像在編譯時(shí)所用的運(yùn)行基地址一致,尤其是對(duì)于 uClinux 而言。假設(shè)你的內(nèi)核映像在編譯時(shí)用的基地址是 0xc0008000,但你的 boot loader 卻將它加載到 0xc0010000 處去執(zhí)行,那么內(nèi)核映像當(dāng)然不能正確地執(zhí)行了。

  

結(jié)束語(yǔ)

Boot Loader 的設(shè)計(jì)與實(shí)現(xiàn)是一個(gè)非常復(fù)雜的過(guò)程。如果不能從串口收到那激動(dòng)人心的“uncompressing linux.。。.。。.。。.。。.。。.。。 done, booting the kernel……”內(nèi)核啟動(dòng)信息,恐怕誰(shuí)也不能說(shuō):“嗨,我的 boot loader 已經(jīng)成功地轉(zhuǎn)起來(lái)了!”。

-END-

發(fā)表回復(fù)

本版積分規(guī)則

關(guān)閉

站長(zhǎng)推薦上一條 /1 下一條


聯(lián)系客服 關(guān)注微信 下載APP 返回頂部 返回列表