6.1 Bootloader
對于計算機系統(tǒng)來說,從開機上電到操作系統(tǒng)啟動需要一個引導過程。嵌入式Linux系統(tǒng)同樣離不開引導程序,這個引導程序就叫作Bootloader。
6.1.1 Bootloader介紹
Bootloader是在操作系統(tǒng)運行之前執(zhí)行的一段小程序。通過這段小程序,我們可以初始化硬件設備、建立內(nèi)存空間的映射表,從而建立適當?shù)南到y(tǒng)軟硬件環(huán)境,為最終調(diào)用操作系統(tǒng)內(nèi)核做好準備。
對于嵌入式系統(tǒng),Bootloader是基于特定硬件平臺來實現(xiàn)的。因此,幾乎不可能為所有的嵌入式系統(tǒng)建立一個通用的Bootloader,不同的處理器架構(gòu)都有不同的Bootloader。Bootloader不但依賴于CPU的體系結(jié)構(gòu),而且依賴于嵌入式系統(tǒng)板級設備的配置。對于2塊不同的嵌入式板而言,即使它們使用同一種處理器,要想讓運行在一塊板子上的Bootloader程序也能運行在另一塊板子上,一般也都需要修改Bootloader的源程序。
反過來,大部分Bootloader仍然具有很多共性,某些Bootloader也能夠支持多種體系結(jié)構(gòu)的嵌入式系統(tǒng)。例如,U-Boot就同時支持PowerPC、ARM、MIPS和X86等體系結(jié)構(gòu),支持的板子有上百種。通常,它們都能夠自動從存儲介質(zhì)上啟動,都能夠引導操作系統(tǒng)啟動,并且大部分都可以支持串口和以太網(wǎng)接口。
本章將對各種Bootloader總結(jié)分類,分析它們的共同特點。以U-Boot為例,詳細討論Bootloader的設計與實現(xiàn)。
6.1.2 Bootloader的啟動
Linux系統(tǒng)是通過Bootloader引導啟動的。一上電,就要執(zhí)行Bootloader來初始化系統(tǒng)??梢酝ㄟ^第4章的Linux啟動過程框圖回顧一下。
系統(tǒng)加電或復位后,所有CPU都會從某個地址開始執(zhí)行,這是由處理器設計決定的。比如,X86的復位向量在高地址端,ARM處理器在復位時從地址0x00000000取第一條指令。嵌入式系統(tǒng)的開發(fā)板都要把板上ROM或Flash映射到這個地址。因此,必須把Bootloader程序存儲在相應的Flash位置。系統(tǒng)加電后,CPU將首先執(zhí)行它。
主機和目標機之間一般有串口可以連接,Bootloader軟件通常會通過串口來輸入輸出。例如:輸出出錯或者執(zhí)行結(jié)果信息到串口終端,從串口終端讀取用戶控制命令等。
Bootloader啟動過程通常是多階段的,這樣既能提供復雜的功能,又有很好的可移植性。例如:從Flash啟動的Bootloader多數(shù)是兩階段的啟動過程。從后面U-Boot的內(nèi)容可以詳細分析這個特性。
大多數(shù)Bootloader都包含2種不同的操作模式:本地加載模式和遠程下載模式。這2種操作模式的區(qū)別僅對于開發(fā)人員才有意義,也就是不同啟動方式的使用。從最終用戶的角度看,Bootloader的作用就是用來加載操作系統(tǒng),而并不存在所謂的本地加載模式與遠程下載模式的區(qū)別。
因為Bootloader的主要功能是引導操作系統(tǒng)啟動,所以我們詳細討論一下各種啟動方式的特點。
1.網(wǎng)絡啟動方式
這種方式開發(fā)板不需要配置較大的存儲介質(zhì),跟無盤工作站有點類似。但是使用這種啟動方式之前,需要把Bootloader安裝到板上的EPROM或者Flash中。Bootloader通過以太網(wǎng)接口遠程下載Linux內(nèi)核映像或者文件系統(tǒng)。第4章介紹的交叉開發(fā)環(huán)境就是以網(wǎng)絡啟動方式建立的。這種方式對于嵌入式系統(tǒng)開發(fā)來說非常重要。
使用這種方式也有前提條件,就是目標板有串口、以太網(wǎng)接口或者其他連接方式。串口一般可以作為控制臺,同時可以用來下載內(nèi)核影像和RAMDISK文件系統(tǒng)。串口通信傳輸速率過低,不適合用來掛接NFS文件系統(tǒng)。所以以太網(wǎng)接口成為通用的互連設備,一般的開發(fā)板都可以配置10M以太網(wǎng)接口。
對于PDA等手持設備來說,以太網(wǎng)的RJ-45接口顯得大了些,而USB接口,特別是USB的迷你接口,尺寸非常小。對于開發(fā)的嵌入式系統(tǒng),可以把USB接口虛擬成以太網(wǎng)接口來通訊。這種方式在開發(fā)主機和開發(fā)板兩端都需要驅(qū)動程序。
另外,還要在服務器上配置啟動相關(guān)網(wǎng)絡服務。Bootloader下載文件一般都使用TFTP網(wǎng)絡協(xié)議,還可以通過DHCP的方式動態(tài)配置IP地址。
DHCP/BOOTP服務為Bootloader分配IP地址,配置網(wǎng)絡參數(shù),然后才能夠支持網(wǎng)絡傳輸功能。如果Bootloader可以直接設置網(wǎng)絡參數(shù),就可以不使用DHCP。
TFTP服務為Bootloader客戶端提供文件下載功能,把內(nèi)核映像和其他文件放在/tftpboot目錄下。這樣Bootloader可以通過簡單的TFTP協(xié)議遠程下載內(nèi)核映像到內(nèi)存。如圖6.1所示。
圖6.1 網(wǎng)絡啟動示意圖
大部分引導程序都能夠支持網(wǎng)絡啟動方式。例如:BIOS的PXE(Preboot Execution Environment)功能就是網(wǎng)絡啟動方式;U-Boot也支持網(wǎng)絡啟動功能。
2.磁盤啟動方式
傳統(tǒng)的Linux系統(tǒng)運行在臺式機或者服務器上,這些計算機一般都使用BIOS引導,并且使用磁盤作為存儲介質(zhì)。如果進入BIOS設置菜單,可以探測處理器、內(nèi)存、硬盤等設備,可以設置BIOS從軟盤、光盤或者某塊硬盤啟動。也就是說,BIOS并不直接引導操作系統(tǒng)。那么在硬盤的主引導區(qū),還需要一個Bootloader。這個Bootloader可以從磁盤文件系統(tǒng)中把操作系統(tǒng)引導起來。
Linux傳統(tǒng)上是通過LILO(LInux LOader)引導的,后來又出現(xiàn)了GNU的軟件GRUB(GRand Unified Bootloader)。這2種Bootloader廣泛應用在X86的Linux系統(tǒng)上。你的開發(fā)主機可能就使用了其中一種,熟悉它們有助于配置多種系統(tǒng)引導功能。
LILO軟件工程是由Werner Almesberger創(chuàng)建,專門為引導Linux開發(fā)的?,F(xiàn)在LILO的維護者是John Coffman,最新版本下載站點:http://lilo.go.。LILO有詳細的文檔,例如LILO套件中附帶使用手冊和參考手冊。此外,還可以在LDP的“LILO mini-HOWTO”中找到LILO的使用指南。
GRUB是GNU計劃的主要bootloader。GRUB最初是由Erich Boleyn為GNU Mach操作系統(tǒng)撰寫的引導程序。后來有Gordon Matzigkeit和Okuji Yoshinori接替Erich的工作,繼續(xù)維護和開發(fā)GRUB。GRUB的網(wǎng)站http://www./software/grub/上有對套件使用的說明文件,叫作《GRUB manual》。GRUB能夠使用TFTP和BOOTP或者DHCP通過網(wǎng)絡啟動,這種功能對于系統(tǒng)開發(fā)過程很有用。
除了傳統(tǒng)的Linux系統(tǒng)上的引導程序以外,還有其他一些引導程序,也可以支持磁盤引導啟動。例如:LoadLin可以從DOS下啟動Linux;還有ROLO、LinuxBIOS,U-Boot也支持這種功能。
3.Flash啟動方式
大多數(shù)嵌入式系統(tǒng)上都使用Flash存儲介質(zhì)。Flash有很多類型,包括NOR Flash、NAND Flash和其他半導體盤。其中,NOR Flash(也就是線性Flash)使用最為普遍。
NOR Flash可以支持隨機訪問,所以代碼是可以直接在Flash上執(zhí)行的。Bootloader一般是存儲在Flash芯片上的。另外,Linux內(nèi)核映像和RAMDISK也可以存儲在Flash上。通常需要把Flash分區(qū)使用,每個區(qū)的大小應該是Flash擦除塊大小的整數(shù)倍。圖6.2是Bootloader和內(nèi)核映像以及文件系統(tǒng)的分區(qū)表。
圖6.2 Flash存儲示意圖
Bootloader一般放在Flash的底端或者頂端,這要根據(jù)處理器的復位向量設置。要使Bootloader的入口位于處理器上電執(zhí)行第一條指令的位置。
接下來分配參數(shù)區(qū),這里可以作為Bootloader的參數(shù)保存區(qū)域。
再下來內(nèi)核映像區(qū)。Bootloader引導Linux內(nèi)核,就是要從這個地方把內(nèi)核映像解壓到RAM中去,然后跳轉(zhuǎn)到內(nèi)核映像入口執(zhí)行。
然后是文件系統(tǒng)區(qū)。如果使用Ramdisk文件系統(tǒng),則需要Bootloader把它解壓到RAM中。如果使用JFFS2文件系統(tǒng),將直接掛接為根文件系統(tǒng)。這兩種文件系統(tǒng)將在第12章詳細講解。
最后還可以分出一些數(shù)據(jù)區(qū),這要根據(jù)實際需要和Flash大小來考慮了。
這些分區(qū)是開發(fā)者定義的,Bootloader一般直接讀寫對應的偏移地址。到了Linux內(nèi)核空間,可以配置成MTD設備來訪問Flash分區(qū)。但是,有的Bootloader也支持分區(qū)的功能,例如:Redboot可以創(chuàng)建Flash分區(qū)表,并且內(nèi)核MTD驅(qū)動可以解析出redboot的分區(qū)表。
除了NOR Flash,還有NAND Flash、Compact Flash、DiskOnChip等。這些Flash具有芯片價格低,存儲容量大的特點。但是這些芯片一般通過專用控制器的I/O方式來訪問,不能隨機訪問,因此引導方式跟NOR Flash也不同。在這些芯片上,需要配置專用的引導程序。通常,這種引導程序起始的一段代碼就把整個引導程序復制到RAM中運行,從而實現(xiàn)自舉啟動,這跟從磁盤上啟動有些相似。
6.1.3 Bootloader的種類
嵌入式系統(tǒng)世界已經(jīng)有各種各樣的Bootloader,種類劃分也有多種方式。除了按照處理器體系結(jié)構(gòu)不同劃分以外,還有功能復雜程度的不同。
首先區(qū)分一下“Bootloader”和“Monitor”的概念。嚴格來說,“Bootloader”只是引導設備并且執(zhí)行主程序的固件;而“Monitor”還提供了更多的命令行接口,可以進行調(diào)試、讀寫內(nèi)存、燒寫Flash、配置環(huán)境變量等?!?/SPAN>Monitor”在嵌入式系統(tǒng)開發(fā)過程中可以提供很好的調(diào)試功能,開發(fā)完成以后,就完全設置成了一個“Bootloader”。所以,習慣上大家把它們統(tǒng)稱為Bootloader。
表6.1列出了Linux的開放源碼引導程序及其支持的體系結(jié)構(gòu)。表中給出了X86 ARM PowerPC體系結(jié)構(gòu)的常用引導程序,并且注明了每一種引導程序是不是“Monitor”。
表6.1 開放源碼的Linux 引導程序
Bootloader |
Monitor |
描 述 |
x86 |
ARM |
PowerPC |
LILO |
否 |
Linux磁盤引導程序 |
是 |
否 |
否 |
GRUB |
否 |
GNU的LILO替代程序 |
是 |
否 |
否 |
Loadlin |
否 |
從DOS引導Linux |
是 |
否 |
否 |
ROLO |
否 |
從ROM引導Linux而不需要BIOS |
是 |
否 |
否 |
Etherboot |
否 |
通過以太網(wǎng)卡啟動Linux系統(tǒng)的固件 |
是 |
否 |
否 |
LinuxBIOS |
否 |
完全替代BUIS的Linux引導程序 |
是 |
否 |
否 |
BLOB |
否 |
LART等硬件平臺的引導程序 |
否 |
是 |
否 |
U-boot |
是 |
通用引導程序 |
是 |
是 |
是 |
RedBoot |
是 |
基于eCos的引導程序 |
是 |
是 |
是 |
對于每種體系結(jié)構(gòu),都有一系列開放源碼Bootloader可以選用。
(1)X86
X86的工作站和服務器上一般使用LILO和GRUB。LILO是Linux發(fā)行版主流的Bootloader。不過Redhat Linux發(fā)行版已經(jīng)使用了GRUB,GRUB比LILO有更有好的顯示界面,使用配置也更加靈活方便。
在某些X86嵌入式單板機或者特殊設備上,會采用其他Bootloader,例如:ROLO。這些Bootloader可以取代BIOS的功能,能夠從FLASH中直接引導Linux啟動?,F(xiàn)在ROLO支持的開發(fā)板已經(jīng)并入U-Boot,所以U-Boot也可以支持X86平臺。
(2)ARM
ARM處理器的芯片商很多,所以每種芯片的開發(fā)板都有自己的Bootloader。結(jié)果ARM bootloader也變得多種多樣。最早有為ARM720處理器的開發(fā)板的固件,又有了armboot,StrongARM平臺的blob,還有S3C2410處理器開發(fā)板上的vivi等?,F(xiàn)在armboot已經(jīng)并入了U-Boot,所以U-Boot也支持ARM/XSCALE平臺。U-Boot已經(jīng)成為ARM平臺事實上的標準Bootloader。
(3)PowerPC
PowerPC平臺的處理器有標準的Bootloader,就是ppcboot。PPCBOOT在合并armboot等之后,創(chuàng)建了U-Boot,成為各種體系結(jié)構(gòu)開發(fā)板的通用引導程序。U-Boot仍然是PowerPC平臺的主要Bootloader。
(4)MIPS
MIPS公司開發(fā)的YAMON是標準的Bootloader,也有許多MIPS芯片商為自己的開發(fā)板寫了Bootloader?,F(xiàn)在,U-Boot也已經(jīng)支持MIPS平臺。
(5)SH
SH平臺的標準Bootloader是sh-boot。Redboot在這種平臺上也很好用。
(6)M68K
M68K平臺沒有標準的Bootloader。Redboot能夠支持m68k系列的系統(tǒng)。
值得說明的是Redboot,它幾乎能夠支持所有的體系結(jié)構(gòu),包括MIPS、SH、M68K等體系結(jié)構(gòu)。Redboot是以eCos為基礎,采用GPL許可的開源軟件工程。現(xiàn)在由core eCos的開發(fā)人員維護,源碼下載網(wǎng)站是http://www./snapshots。Redboot的文檔也相當完善,有詳細的使用手冊《RedBoot User’s Guide》。
6.2.1 U-Boot工程簡介
最早,DENX軟件工程中心的Wolfgang Denk基于8xxrom的源碼創(chuàng)建了PPCBOOT工程,并且不斷添加處理器的支持。后來,Sysgo Gmbh把ppcboot移植到ARM平臺上,創(chuàng)建了ARMboot工程。然后以ppcboot工程和armboot工程為基礎,創(chuàng)建了U-Boot工程。
現(xiàn)在U-Boot已經(jīng)能夠支持PowerPC、ARM、X86、MIPS體系結(jié)構(gòu)的上百種開發(fā)板,已經(jīng)成為功能最多、靈活性最強并且開發(fā)最積極的開放源碼Bootloader。目前仍然由DENX的Wolfgang Denk維護。
U-Boot的源碼包可以從sourceforge網(wǎng)站下載,還可以訂閱該網(wǎng)站活躍的U-Boot Users郵件論壇,這個郵件論壇對于U-Boot的開發(fā)和使用都很有幫助。
U-Boot軟件包下載網(wǎng)站:http:///project/u-boot。
U-Boot郵件列表網(wǎng)站:http://lists./lists/listinfo/u-boot-users/。
DENX相關(guān)的網(wǎng)站:http://www./re/DPLG.html。
6.2.2 U-Boot源碼結(jié)構(gòu)
從網(wǎng)站上下載得到U-Boot源碼包,例如:U-Boot-1.1.2.tar.bz2
解壓就可以得到全部U-Boot源程序。在頂層目錄下有18個子目錄,分別存放和管理不同的源程序。這些目錄中所要存放的文件有其規(guī)則,可以分為3類。
· 第1類目錄與處理器體系結(jié)構(gòu)或者開發(fā)板硬件直接相關(guān);
· 第2類目錄是一些通用的函數(shù)或者驅(qū)動程序;
· 第3類目錄是U-Boot的應用程序、工具或者文檔。
表6.2列出了U-Boot頂層目錄下各級目錄存放原則。
表6.2 U-Boot的源碼頂層目錄說明
目 錄 |
特 性 |
解 釋 說 明 |
board |
平臺依賴 |
存放電路板相關(guān)的目錄文件,例如:RPXlite(mpc8xx)、smdk2410(arm920t)、sc520_cdp(x86) 等目錄 |
cpu |
平臺依賴 |
存放CPU相關(guān)的目錄文件,例如:mpc8xx、ppc4xx、arm720t、arm920t、 xscale、i386等目錄 |
lib_ppc |
平臺依賴 |
存放對PowerPC體系結(jié)構(gòu)通用的文件,主要用于實現(xiàn)PowerPC平臺通用的函數(shù) |
目 錄 |
特 性 |
解 釋 說 明 |
lib_arm |
平臺依賴 |
存放對ARM體系結(jié)構(gòu)通用的文件,主要用于實現(xiàn)ARM平臺通用的函數(shù) |
lib_i386 |
平臺依賴 |
存放對X86體系結(jié)構(gòu)通用的文件,主要用于實現(xiàn)X86平臺通用的函數(shù) |
include |
通用 |
頭文件和開發(fā)板配置文件,所有開發(fā)板的配置文件都在configs目錄下 |
common |
通用 |
通用的多功能函數(shù)實現(xiàn) |
lib_generic |
通用 |
通用庫函數(shù)的實現(xiàn) |
Net |
通用 |
存放網(wǎng)絡的程序 |
Fs |
通用 |
存放文件系統(tǒng)的程序 |
Post |
通用 |
存放上電自檢程序 |
drivers |
通用 |
通用的設備驅(qū)動程序,主要有以太網(wǎng)接口的驅(qū)動 |
Disk |
通用 |
硬盤接口程序 |
Rtc |
通用 |
RTC的驅(qū)動程序 |
Dtt |
通用 |
數(shù)字溫度測量器或者傳感器的驅(qū)動 |
examples |
應用例程 |
一些獨立運行的應用程序的例子,例如helloworld |
tools |
工具 |
存放制作S-Record 或者 U-Boot格式的映像等工具,例如mkimage |
Doc |
文檔 |
開發(fā)使用文檔 |
U-Boot的源代碼包含對幾十種處理器、數(shù)百種開發(fā)板的支持??墒菍τ谔囟ǖ拈_發(fā)板,配置編譯過程只需要其中部分程序。這里具體以S3C2410 arm920t處理器為例,具體分析S3C2410處理器和開發(fā)板所依賴的程序,以及U-Boot的通用函數(shù)和工具。
6.2.3 U-Boot的編譯
U-Boot的源碼是通過GCC和Makefile組織編譯的。頂層目錄下的Makefile首先可以設置開發(fā)板的定義,然后遞歸地調(diào)用各級子目錄下的Makefile,最后把編譯過的程序鏈接成U-Boot映像。
1.頂層目錄下的Makefile
它負責U-Boot整體配置編譯。按照配置的順序閱讀其中關(guān)鍵的幾行。
每一種開發(fā)板在Makefile都需要有板子配置的定義。例如smdk2410開發(fā)板的定義如下。
smdk2410_config : unconfig
@./mkconfig $(@:_config=) arm arm920t smdk2410 NULL s3c24x0
執(zhí)行配置U-Boot的命令make smdk2410_config,通過./mkconfig腳本生成include/config.
mk的配置文件。文件內(nèi)容正是根據(jù)Makefile對開發(fā)板的配置生成的。
ARCH = arm
CPU = arm920t
BOARD = smdk2410
SOC = s3c24x0
上面的include/config.mk文件定義了ARCH、CPU、BOARD、SOC這些變量。這樣硬件平臺依賴的目錄文件可以根據(jù)這些定義來確定。SMDK2410平臺相關(guān)目錄如下。
board/smdk2410/
cpu/arm920t/
cpu/arm920t/s3c24x0/
lib_arm/
include/asm-arm/
include/configs/smdk2410.h
再回到頂層目錄的Makefile文件開始的部分,其中下列幾行包含了這些變量的定義。
# load ARCH, BOARD, and CPU configuration
include include/config.mk
export ARCH CPU BOARD VENDOR SOC
Makefile的編譯選項和規(guī)則在頂層目錄的config.mk文件中定義。各種體系結(jié)構(gòu)通用的規(guī)則直接在這個文件中定義。通過ARCH、CPU、BOARD、SOC等變量為不同硬件平臺定義不同選項。不同體系結(jié)構(gòu)的規(guī)則分別包含在ppc_config.mk、arm_config.mk、mips_config.mk等文件中。
頂層目錄的Makefile中還要定義交叉編譯器,以及編譯U-Boot所依賴的目標文件。
ifeq ($(ARCH),arm)
CROSS_COMPILE = arm-linux- //交叉編譯器的前綴
#endif
export CROSS_COMPILE
…
# U-Boot objects....order is important (i.e. start must be first)
OBJS = cpu/$(CPU)/start.o //處理器相關(guān)的目標文件
…
LIBS = lib_generic/libgeneric.a //定義依賴的目錄,每個目錄下先把目標文件連接成*.a文件。
LIBS += board/$(BOARDDIR)/lib$(BOARD).a
LIBS += cpu/$(CPU)/lib$(CPU).a
ifdef SOC
LIBS += cpu/$(CPU)/$(SOC)/lib$(SOC).a
endif
LIBS += lib_$(ARCH)/lib$(ARCH).a
…
然后還有U-Boot映像編譯的依賴關(guān)系。
ALL = u-boot.srec u-boot.bin System.map
all: $(ALL)
u-boot.srec: u-boot
$(OBJCOPY) ${OBJCFLAGS} -O srec $< $@
u-boot.bin: u-boot
$(OBJCOPY) ${OBJCFLAGS} -O binary $< $@
……
u-boot: depend $(SUBDIRS) $(OBJS) $(LIBS) $(LDSCRIPT)
UNDEF_SYM='$(OBJDUMP) -x $(LIBS) \
|sed -n -e 's/.*\(__u_boot_cmd_.*\)/-u\1/p'|sort|uniq`;\
$(LD) $(LDFLAGS) $$UNDEF_SYM $(OBJS) \
--start-group $(LIBS) $(PLATFORM_LIBS) --end-group \
-Map u-boot.map -o u-boot
Makefile缺省的編譯目標為all,包括u-boot.srec、u-boot.bin、System.map。u-boot.srec和u-boot.bin又依賴于U-Boot。U-Boot就是通過ld命令按照u-boot.map地址表把目標文件組裝成u-boot。
其他Makefile內(nèi)容就不再詳細分析了,上述代碼分析應該可以為閱讀代碼提供了一個線索。
2.開發(fā)板配置頭文件
除了編譯過程Makefile以外,還要在程序中為開發(fā)板定義配置選項或者參數(shù)。這個頭文件是include/configs/<board_name>.h。<board_name>用相應的BOARD定義代替。
這個頭文件中主要定義了兩類變量。
一類是選項,前綴是CONFIG_,用來選擇處理器、設備接口、命令、屬性等。例如:
#define CONFIG_ARM920T 1
#define CONFIG_DRIVER_CS8900 1
另一類是參數(shù),前綴是CFG_,用來定義總線頻率、串口波特率、Flash地址等參數(shù)。例如:
#define CFG_FLASH_BASE 0x00000000
#define CFG_PROMPT "=>"
3.編譯結(jié)果
根據(jù)對Makefile的分析,編譯分為2步。第1步配置,例如:make smdk2410_config;第2步編譯,執(zhí)行make就可以了。
編譯完成后,可以得到U-Boot各種格式的映像文件和符號表,如表6.3所示。
表6.3 U-Boot編譯生成的映像文件
文 件 名 稱 |
說 明 |
文 件 名 稱 |
說 明 |
System.map |
U-Boot映像的符號表 |
u-boot.bin |
U-Boot映像原始的二進制格式 |
u-boot |
U-Boot映像的ELF格式 |
u-boot.srec |
U-Boot映像的S-Record格式 |
U-Boot的3種映像格式都可以燒寫到Flash中,但需要看加載器能否識別這些格式。一般u-boot.bin最為常用,直接按照二進制格式下載,并且按照絕對地址燒寫到Flash中就可以了。U-Boot和u-boot.srec格式映像都自帶定位信息。
4.U-Boot工具
在tools目錄下還有些U-Boot的工具。這些工具有的也經(jīng)常用到。表6.4說明了幾種工具的用途。
表6.4 U-Boot的工具
工 具 名 稱 |
說 明 |
工 具 名 稱 |
說 明 |
bmp_logo |
制作標記的位圖結(jié)構(gòu)體 |
img2srec |
轉(zhuǎn)換SREC格式映像 |
envcrc |
校驗u-boot內(nèi)部嵌入的環(huán)境變量 |
mkimage |
轉(zhuǎn)換U-Boot格式映像 |
gen_eth_addr |
生成以太網(wǎng)接口MAC地址 |
updater |
U-Boot自動更新升級工具 |
這些工具都有源代碼,可以參考改寫其他工具。其中mkimage是很常用的一個工具,Linux內(nèi)核映像和ramdisk文件系統(tǒng)映像都可以轉(zhuǎn)換成U-Boot的格式。
6.2.4 U-Boot的移植
U-Boot能夠支持多種體系結(jié)構(gòu)的處理器,支持的開發(fā)板也越來越多。因為Bootloader是完全依賴硬件平臺的,所以在新電路板上需要移植U-Boot程序。
開始移植U-Boot之前,先要熟悉硬件電路板和處理器。確認U-Boot是否已經(jīng)支持新開發(fā)板的處理器和I/O設備。假如U-Boot已經(jīng)支持一塊非常相似的電路板,那么移植的過程將非常簡單。
移植U-Boot工作就是添加開發(fā)板硬件相關(guān)的文件、配置選項,然后配置編譯。
開始移植之前,需要先分析一下U-Boot已經(jīng)支持的開發(fā)板,比較出硬件配置最接近的開發(fā)板。選擇的原則是,首先處理器相同,其次處理器體系結(jié)構(gòu)相同,然后是以太網(wǎng)接口等外圍接口。還要驗證一下這個參考開發(fā)板的U-Boot,至少能夠配置編譯通過。
以S3C2410處理器的開發(fā)板為例,U-Boot-1.1.2版本已經(jīng)支持SMDK2410開發(fā)板。我們可以基于SMDK2410移植,那么先把SMDK2410編譯通過。
我們以S3C2410開發(fā)板fs2410為例說明。移植的過程參考SMDK2410開發(fā)板,SMDK2410在U-Boot-1.1.2中已經(jīng)支持。
移植U-Boot的基本步驟如下。
(1)在頂層Makefile中為開發(fā)板添加新的配置選項,使用已有的配置項目為例。
smdk2410_config : unconfig
@./mkconfig $(@:_config=) arm arm920t smdk2410 NULL s3c24x0
參考上面2行,添加下面2行。
fs2410_config : unconfig
@./mkconfig $(@:_config=) arm arm920t fs2410 NULL s3c24x0
(2)創(chuàng)建一個新目錄存放開發(fā)板相關(guān)的代碼,并且添加文件。
board/fs2410/config.mk
board/fs2410/flash.c
board/fs2410/fs2410.c
board/fs2410/Makefile
board/fs2410/memsetup.S
board/fs2410/u-boot.lds
(3)為開發(fā)板添加新的配置文件
可以先復制參考開發(fā)板的配置文件,再修改。例如:
$cp include/configs/smdk2410.h include/configs/fs2410.h
如果是為一顆新的CPU移植,還要創(chuàng)建一個新的目錄存放CPU相關(guān)的代碼。
(4)配置開發(fā)板
$ make fs2410_config
(5)編譯U-Boot
執(zhí)行make命令,編譯成功可以得到U-Boot映像。有些錯誤是跟配置選項是有關(guān)系的,通常打開某些功能選項會帶來一些錯誤,一開始可以盡量跟參考板配置相同。
(6)添加驅(qū)動或者功能選項
在能夠編譯通過的基礎上,還要實現(xiàn)U-Boot的以太網(wǎng)接口、Flash擦寫等功能。
對于FS2410開發(fā)板的以太網(wǎng)驅(qū)動和smdk2410完全相同,所以可以直接使用。CS8900驅(qū)動程序文件如下。
drivers/cs8900.c
drivers/cs8900.h
對于Flash的選擇就麻煩多了,Flash芯片價格或者采購方面的因素都有影響。多數(shù)開發(fā)板大小、型號不都相同。所以還需要移植Flash的驅(qū)動。每種開發(fā)板目錄下一般都有flash.c這個文件,需要根據(jù)具體的Flash類型修改。例如:
board/fs2410/flash.c
(7)調(diào)試U-Boot源代碼,直到U-Boot在開發(fā)板上能夠正常啟動。
調(diào)試的過程可能是很艱難的,需要借助工具,并且有些問題可能困擾很長時間。
6.2.5 添加U-Boot命令
U-Boot的命令為用戶提供了交互功能,并且已經(jīng)實現(xiàn)了幾十個常用的命令。如果開發(fā)板需要很特殊的操作,可以添加新的U-Boot命令。
U-Boot的每一個命令都是通過U_Boot_CMD宏定義的。這個宏在include/command.h頭文件中定義,每一個命令定義一個cmd_tbl_t結(jié)構(gòu)體。
#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
這樣每一個U-Boot命令有一個結(jié)構(gòu)體來描述。結(jié)構(gòu)體包含的成員變量:命令名稱、最大參數(shù)個數(shù)、重復數(shù)、命令執(zhí)行函數(shù)、用法、幫助。
從控制臺輸入的命令是由common/command.c中的程序解釋執(zhí)行的。find_cmd()負責匹配輸入的命令,從列表中找出對應的命令結(jié)構(gòu)體。
基于U-Boot命令的基本框架,來分析一下簡單的icache操作命令,就可以知道添加新命令的方法。
(1)定義CACHE命令。在include/cmd_confdefs.h中定義了所有U-Boot命令的標志位。
#define CFG_CMD_CACHE 0x00000010ULL /* icache, dcache */
如果有更多的命令,也要在這里添加定義。
(2)實現(xiàn)CACHE命令的操作函數(shù)。下面是common/cmd_cache.c文件中icache命令部分的代碼。
#if (CONFIG_COMMANDS & CFG_CMD_CACHE)
static int on_off (const char *s)
{ //這個函數(shù)解析參數(shù),判斷是打開cache,還是關(guān)閉cache
if (strcmp(s, "on") == 0) { //參數(shù)為“on”
return (1);
} else if (strcmp(s, "off") == 0) { //參數(shù)為“off”
return (0);
}
return (-1);
}
int do_icache ( cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{ //對指令cache的操作函數(shù)
switch (argc) {
case 2: /* 參數(shù)個數(shù)為1,則執(zhí)行打開或者關(guān)閉指令cache操作 */
switch (on_off(argv[1])) {
case 0: icache_disable(); //打開指令cache
break;
case 1: icache_enable (); //關(guān)閉指令cache
break;
}
/* FALL TROUGH */
case 1: /* 參數(shù)個數(shù)為0,則獲取指令cache狀態(tài)*/
printf ("Instruction Cache is %s\n",
icache_status() ? "ON" : "OFF");
return 0;
default: //其他缺省情況下,打印命令使用說明
printf ("Usage:\n%s\n", cmdtp->usage);
return 1;
}
return 0;
}
……
U_Boot_CMD( //通過宏定義命令
icache, 2, 1, do_icache, //命令為icache,命令執(zhí)行函數(shù)為do_icache()
"icache - enable or disable instruction cache\n", //幫助信息
"[on, off]\n"
" - enable or disable instruction cache\n"
);
……
#endif
U-Boot的命令都是通過結(jié)構(gòu)體__U_Boot_cmd_##name來描述的。根據(jù)U_Boot_CMD在include/command.h中的兩行定義可以明白。
#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
還有,不要忘了在common/Makefile中添加編譯的目標文件。
(3)打開CONFIG_COMMANDS選項的命令標志位。這個程序文件開頭有#if語句需要預處理是否包含這個命令函數(shù)。CONFIG_COMMANDS選項在開發(fā)板的配置文件中定義。例如:SMDK2410平臺在include/configs/smdk2410.h中有如下定義。
/***********************************************************
* Command definition
***********************************************************/
#define CONFIG_COMMANDS \
(CONFIG_CMD_DFL | \
CFG_CMD_CACHE | \
CFG_CMD_REGINFO | \
CFG_CMD_DATE | \
CFG_CMD_ELF)
按照這3步,就可以添加新的U-Boot命令。
6.3 U-Boot的調(diào)試
新移植的U-Boot不能正常工作,這時就需要調(diào)試了。調(diào)試U-Boot離不開工具,只有理解U-Boot啟動過程,才能正確地調(diào)試U-Boot源碼。
6.3.1 硬件調(diào)試器
硬件電路板制作完成以后,這時上面還沒有任何程序,就叫作裸板。首要的工作是把程序或者固件加載到裸板上,這就要通過硬件工具來完成。習慣上,這種硬件工具叫作仿真器。
仿真器可以通過處理器的JTAG等接口控制板子,直接把程序下載到目標板內(nèi)存,或者進行Flash編程。如果板上的Flash是可以拔插的,就可以通過專用的Flash燒寫器來完成。在第4章介紹過目標板跟主機之間的連接,其中JTAG等接口就是專門用來連接仿真器的。
仿真器還有一個重要的功能就是在線調(diào)試程序,這對于調(diào)試Bootloader和硬件測試程序很有用。
從最簡單的JTAG電纜,到ICE仿真器,再到可以調(diào)試Linux內(nèi)核的仿真器。
復雜的仿真器可以支持與計算機間的以太網(wǎng)或者USB接口通信。
對于U-Boot的調(diào)試,可以采用BDI2000。BDI2000完全可以反匯編地跟蹤Flash中的程序,也可以進行源碼級的調(diào)試。
使用BDI2000調(diào)試U-boot的方法如下。
(1)配置BDI2000和目標板初始化程序,連接目標板。
(2)添加U-Boot的調(diào)試編譯選項,重新編譯。
U-Boot的程序代碼是位置相關(guān)的,調(diào)試的時候盡量在內(nèi)存中調(diào)試,可以修改連接定位地址TEXT_BASE。TEXT_BASE在board/<board_name>/config.mk中定義。
另外,如果有復位向量也需要先從鏈接腳本中去掉。鏈接腳本是board/<board_name>/
u-boot.lds。
添加調(diào)試選項,在config.mk文件中查找,DBGFLAGS,加上-g選項。然后重新編譯U-Boot。
(3)下載U-Boot到目標板內(nèi)存。
通過BDI2000的下載命令LOAD,把程序加載到目標板內(nèi)存中。然后跳轉(zhuǎn)到U-Boot入口。
(4)啟動GDB調(diào)試。
啟動GDB調(diào)試,這里是交叉調(diào)試的GDB。GDB與BDI2000建立鏈接,然后就可以設置斷點執(zhí)行了。
$ arm-linux-gdb u-boot
(gdb)target remote 192.168.1.100:2001
(gdb)stepi
(gdb)b start_armboot
(gdb)c
6.3.2 軟件跟蹤
假如U-Boot沒有任何串口打印信息,手頭又沒有硬件調(diào)試工具,那樣怎么知道U-Boot執(zhí)行到什么地方了呢?可以通過開發(fā)板上的LED指示燈判斷。
開發(fā)板上最好設計安裝八段數(shù)碼管等LED,可以用來顯示數(shù)字或者數(shù)字位。
U-Boot可以定義函數(shù)show_boot_progress (int status),用來指示當前啟動進度。在include/common.h頭文件中聲明這個函數(shù)。
#ifdef CONFIG_SHOW_BOOT_PROGRESS
void show_boot_progress (int status);
#endif
CONFIG_SHOW_BOOT_PROGRESS是需要定義的。這個在板子配置的頭文件中定義。CSB226開發(fā)板對這項功能有完整實現(xiàn),可以參考。在頭文件include/configs/csb226.h中,有下列一行。
#define CONFIG_SHOW_BOOT_PROGRESS 1
函數(shù)show_boot_progress (int status)的實現(xiàn)跟開發(fā)板關(guān)系密切,所以一般在board目錄下的文件中實現(xiàn)??匆幌?/SPAN>CSB226在board/csb226/csb226.c中的實現(xiàn)函數(shù)。
/** 設置CSB226板的0、1、2三個指示燈的開關(guān)狀態(tài)
* csb226_set_led: - switch LEDs on or off
* @param led: LED to switch (0,1,2)
* @param state: switch on (1) or off (0)
*/
void csb226_set_led(int led, int state)
{
switch(led) {
case 0: if (state==1) {
GPCR0 |= CSB226_USER_LED0;
} else if (state==0) {
GPSR0 |= CSB226_USER_LED0;
}
break;
case 1: if (state==1) {
GPCR0 |= CSB226_USER_LED1;
} else if (state==0) {
GPSR0 |= CSB226_USER_LED1;
}
break;
case 2: if (state==1) {
GPCR0 |= CSB226_USER_LED2;
} else if (state==0) {
GPSR0 |= CSB226_USER_LED2;
}
break;
}
return;
}
/** 顯示啟動進度函數(shù),在比較重要的階段,設置三個燈為亮的狀態(tài)(1, 5, 15)*/
void show_boot_progress (int status)
{
switch(status) {
case 1: csb226_set_led(0,1); break;
case 5: csb226_set_led(1,1); break;
case 15: csb226_set_led(2,1); break;
}
return;
}
這樣,在U-Boot啟動過程中就可以通過show_boot_progresss指示執(zhí)行進度。比如hang()函數(shù)是系統(tǒng)出錯時調(diào)用的函數(shù),這里需要根據(jù)特定的開發(fā)板給定顯示的參數(shù)值。
void hang (void)
{
puts ("### ERROR ### Please RESET the board ###\n");
#ifdef CONFIG_SHOW_BOOT_PROGRESS
show_boot_progress(-30);
#endif
for (;;);
6.3.3 U-Boot啟動過程
盡管有了調(diào)試跟蹤手段,甚至也可以通過串口打印信息了,但是不一定能夠判斷出錯原因。如果能夠充分理解代碼的啟動流程,那么對準確地解決和分析問題很有幫助。
開發(fā)板上電后,執(zhí)行U-Boot的第一條指令,然后順序執(zhí)行U-Boot啟動函數(shù)。函數(shù)調(diào)用順序如圖6.3所示。
看一下board/smsk2410/u-boot.lds這個鏈接腳本,可以知道目標程序的各部分鏈接順序。第一個要鏈接的是cpu/arm920t/start.o,那么U-Boot的入口指令一定位于這個程序中。下面詳細分析一下程序跳轉(zhuǎn)和函數(shù)的調(diào)用關(guān)系以及函數(shù)實現(xiàn)。
1.cpu/arm920t/start.S
這個匯編程序是U-Boot的入口程序,開頭就是復位向量的代碼。
圖6.3 U-Boot啟動代碼流程圖
_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 //中斷向量
…
/* the actual reset code */
reset: //復位啟動子程序
/* 設置CPU為SVC32模式 */
mrs r0,cpsr
bic r0,r0,#0x1f
orr r0,r0,#0xd3
msr cpsr,r0
/* 關(guān)閉看門狗 */
/* 這些初始化代碼在系統(tǒng)重起的時候執(zhí)行,運行時熱復位從RAM中啟動不執(zhí)行 */
#ifdef CONFIG_INIT_CRITICAL
bl cpu_init_crit
#endif
relocate: /* 把U-Boot重新定位到RAM */
adr r0, _start /* r0是代碼的當前位置 */
ldr r1, _TEXT_BASE /* 測試判斷是從Flash啟動,還是RAM */
cmp r0, r1 /* 比較r0和r1,調(diào)試的時候不要執(zhí)行重定位 */
beq stack_setup /* 如果r0等于r1,跳過重定位代碼 */
/* 準備重新定位代碼 */
ldr r2, _armboot_start
ldr r3, _bss_start
sub r2, r3, r2 /* r2 得到armboot的大小 */
add r2, r0, r2 /* r2 得到要復制代碼的末尾地址 */
copy_loop: /* 重新定位代碼 */
ldmia r0!, {r3-r10} /*從源地址[r0]復制 */
stmia r1!, {r3-r10} /* 復制到目的地址[r1] */
cmp r0, r2 /* 復制數(shù)據(jù)塊直到源數(shù)據(jù)末尾地址[r2] */
ble copy_loop
/* 初始化堆棧等 */
stack_setup:
ldr r0, _TEXT_BASE /* 上面是128 KiB重定位的u-boot */
sub r0, r0, #CFG_MALLOC_LEN /* 向下是內(nèi)存分配空間 */
sub r0, r0, #CFG_GBL_DATA_SIZE /* 然后是bdinfo結(jié)構(gòu)體地址空間 */
#ifdef CONFIG_USE_IRQ
sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
#endif
sub sp, r0, #12 /* 為abort-stack預留3個字 */
clear_bss:
ldr r0, _bss_start /* 找到bss段起始地址 */
ldr r1, _bss_end /* bss段末尾地址 */
mov r2, #0x00000000 /* 清零 */
clbss_l:str r2, [r0] /* bss段地址空間清零循環(huán)... */
add r0, r0, #4
cmp r0, r1
bne clbss_l
/* 跳轉(zhuǎn)到start_armboot函數(shù)入口,_start_armboot字保存函數(shù)入口指針 */
ldr pc, _start_armboot
_start_armboot: .word start_armboot //start_armboot函數(shù)在lib_arm/board.c中實現(xiàn)
/* 關(guān)鍵的初始化子程序 */
cpu_init_crit:
…… //初始化CACHE,關(guān)閉MMU等操作指令
/* 初始化RAM時鐘。
* 因為內(nèi)存時鐘是依賴開發(fā)板硬件的,所以在board的相應目錄下可以找到memsetup.S文件。
*/
mov ip, lr
bl memsetup //memsetup子程序在board/smdk2410/memsetup.S中實現(xiàn)
mov lr, ip
mov pc, lr
2.lib_arm/board.c
start_armboot是U-Boot執(zhí)行的第一個C語言函數(shù),完成系統(tǒng)初始化工作,進入主循環(huán),處理用戶輸入的命令。
void start_armboot (void)
{
DECLARE_GLOBAL_DATA_PTR;
ulong size;
init_fnc_t **init_fnc_ptr;
char *s;
/* Pointer is writable since we allocated a register for it */
gd = (gd_t*)(_armboot_start - CFG_MALLOC_LEN - sizeof(gd_t));
/* compiler optimization barrier needed for GCC >= 3.4 */
__asm__ __volatile__("": : :"memory");
memset ((void*)gd, 0, sizeof (gd_t));
gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));
memset (gd->bd, 0, sizeof (bd_t));
monitor_flash_len = _bss_start - _armboot_start;
/* 順序執(zhí)行init_sequence數(shù)組中的初始化函數(shù) */
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
/*配置可用的Flash */
size = flash_init ();
display_flash_config (size);
/* _armboot_start 在u-boot.lds鏈接腳本中定義 */
mem_malloc_init (_armboot_start - CFG_MALLOC_LEN);
/* 配置環(huán)境變量,重新定位 */
env_relocate ();
/* 從環(huán)境變量中獲取IP地址 */
gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr");
/* 以太網(wǎng)接口MAC 地址 */
……
devices_init (); /* 獲取列表中的設備 */
jumptable_init ();
console_init_r (); /* 完整地初始化控制臺設備 */
enable_interrupts (); /* 使能例外處理 */
/* 通過環(huán)境變量初始化 */
if ((s = getenv ("loadaddr")) != NULL) {
load_addr = simple_strtoul (s, NULL, 16);
}
/* main_loop()總是試圖自動啟動,循環(huán)不斷執(zhí)行 */
for (;;) {
main_loop (); /* 主循環(huán)函數(shù)處理執(zhí)行用戶命令 -- common/main.c */
}
/* NOTREACHED - no way out of command loop except booting */
}
3.init_sequence[]
init_sequence[]數(shù)組保存著基本的初始化函數(shù)指針。這些函數(shù)名稱和實現(xiàn)的程序文件在下列注釋中。
init_fnc_t *init_sequence[] = {
cpu_init, /* 基本的處理器相關(guān)配置 -- cpu/arm920t/cpu.c */
board_init, /* 基本的板級相關(guān)配置 -- board/smdk2410/smdk2410.c */
interrupt_init, /* 初始化例外處理 -- cpu/arm920t/s3c24x0/interrupt.c */
env_init, /* 初始化環(huán)境變量 -- common/cmd_flash.c */
init_baudrate, /* 初始化波特率設置 -- lib_arm/board.c */
serial_init, /* 串口通訊設置 -- cpu/arm920t/s3c24x0/serial.c */
console_init_f, /* 控制臺初始化階段1 -- common/console.c */
display_banner, /* 打印u-boot信息 -- lib_arm/board.c */
dram_init, /* 配置可用的RAM -- board/smdk2410/smdk2410.c */
display_dram_config, /* 顯示RAM的配置大小 -- lib_arm/board.c */
NULL,
};
6.3.4 U-Boot與內(nèi)核的關(guān)系
U-Boot作為Bootloader,具備多種引導內(nèi)核啟動的方式。常用的go和bootm命令可以直接引導內(nèi)核映像啟動。U-Boot與內(nèi)核的關(guān)系主要是內(nèi)核啟動過程中參數(shù)的傳遞。
1.go命令的實現(xiàn)
/* common/cmd_boot.c */
int do_go (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
ulong addr, rc;
int rcode = 0;
if (argc < 2) {
printf ("Usage:\n%s\n", cmdtp->usage);
return 1;
}
addr = simple_strtoul(argv[1], NULL, 16);
printf ("## Starting application at 0x%08lX ...\n", addr);
/*
* pass address parameter as argv[0] (aka command name),
* and all remaining args
*/
rc = ((ulong (*)(int, char *[]))addr) (--argc, &argv[1]);
if (rc != 0) rcode = 1;
printf ("## Application terminated, rc = 0x%lX\n", rc);
return rcode;
}
go命令調(diào)用do_go()函數(shù),跳轉(zhuǎn)到某個地址執(zhí)行的。如果在這個地址準備好了自引導的內(nèi)核映像,就可以啟動了。盡管go命令可以帶變參,實際使用時一般不用來傳遞參數(shù)。
2.bootm命令的實現(xiàn)
/* common/cmd_bootm.c */
int do_bootm (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
ulong iflag;
ulong addr;
ulong data, len, checksum;
ulong *len_ptr;
uint unc_len = 0x400000;
int i, verify;
char *name, *s;
int (*appl)(int, char *[]);
image_header_t *hdr = &header;
s = getenv ("verify");
verify = (s && (*s == 'n')) ? 0 : 1;
if (argc < 2) {
addr = load_addr;
} else {
addr = simple_strtoul(argv[1], NULL, 16);
}
SHOW_BOOT_PROGRESS (1);
printf ("## Booting image at %08lx ...\n", addr);
/* Copy header so we can blank CRC field for re-calculation */
memmove (&header, (char *)addr, sizeof(image_header_t));
if (ntohl(hdr->ih_magic) != IH_MAGIC)
{
puts ("Bad Magic Number\n");
SHOW_BOOT_PROGRESS (-1);
return 1;
}
SHOW_BOOT_PROGRESS (2);
data = (ulong)&header;
len = sizeof(image_header_t);
checksum = ntohl(hdr->ih_hcrc);
hdr->ih_hcrc = 0;
if(crc32 (0, (char *)data, len) != checksum) {
puts ("Bad Header Checksum\n");
SHOW_BOOT_PROGRESS (-2);
return 1;
}
SHOW_BOOT_PROGRESS (3);
/* for multi-file images we need the data part, too */
print_image_hdr ((image_header_t *)addr);
data = addr + sizeof(image_header_t);
len = ntohl(hdr->ih_size);
if(verify) {
puts (" Verifying Checksum ... ");
if(crc32 (0, (char *)data, len) != ntohl(hdr->ih_dcrc)) {
printf ("Bad Data CRC\n");
SHOW_BOOT_PROGRESS (-3);
return 1;
}
puts ("OK\n");
}
SHOW_BOOT_PROGRESS (4);
len_ptr = (ulong *)data;
……
switch (hdr->ih_os) {
default: /* handled by (original) Linux case */
case IH_OS_LINUX:
do_bootm_linux (cmdtp, flag, argc, argv,
addr, len_ptr, verify);
break;
……
}
bootm命令調(diào)用do_bootm函數(shù)。這個函數(shù)專門用來引導各種操作系統(tǒng)映像,可以支持引導Linux、vxWorks、QNX等操作系統(tǒng)。引導Linux的時候,調(diào)用do_bootm_linux()函數(shù)。
3.do_bootm_linux函數(shù)的實現(xiàn)
/* lib_arm/armlinux.c */
void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
ulong addr, ulong *len_ptr, int verify)
{
DECLARE_GLOBAL_DATA_PTR;
ulong len = 0, checksum;
ulong initrd_start, initrd_end;
ulong data;
void (*theKernel)(int zero, int arch, uint params);
image_header_t *hdr = &header;
bd_t *bd = gd->bd;
#ifdef CONFIG_CMDLINE_TAG
char *commandline = getenv ("bootargs");
#endif
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);
/* Check if there is an initrd image */
if(argc >= 3) {
SHOW_BOOT_PROGRESS (9);
addr = simple_strtoul (argv[2], NULL, 16);
printf ("## Loading Ramdisk Image at %08lx ...\n", addr);
/* Copy header so we can blank CRC field for re-calculation */
memcpy (&header, (char *) addr, sizeof (image_header_t));
if (ntohl (hdr->ih_magic) != IH_MAGIC) {
printf ("Bad Magic Number\n");
SHOW_BOOT_PROGRESS (-10);
do_reset (cmdtp, flag, argc, argv);
}
data = (ulong) & header;
len = sizeof (image_header_t);
checksum = ntohl (hdr->ih_hcrc);
hdr->ih_hcrc = 0;
if(crc32 (0, (char *) data, len) != checksum) {
printf ("Bad Header Checksum\n");
SHOW_BOOT_PROGRESS (-11);
do_reset (cmdtp, flag, argc, argv);
}
SHOW_BOOT_PROGRESS (10);
print_image_hdr (hdr);
data = addr + sizeof (image_header_t);
len = ntohl (hdr->ih_size);
if(verify) {
ulong csum = 0;
printf (" Verifying Checksum ... ");
csum = crc32 (0, (char *) data, len);
if (csum != ntohl (hdr->ih_dcrc)) {
printf ("Bad Data CRC\n");
SHOW_BOOT_PROGRESS (-12);
do_reset (cmdtp, flag, argc, argv);
}
printf ("OK\n");
}
SHOW_BOOT_PROGRESS (11);
if ((hdr->ih_os != IH_OS_LINUX) ||
(hdr->ih_arch != IH_CPU_ARM) ||
(hdr->ih_type != IH_TYPE_RAMDISK)) {
printf ("No Linux ARM Ramdisk Image\n");
SHOW_BOOT_PROGRESS (-13);
do_reset (cmdtp, flag, argc, argv);
}
/* Now check if we have a multifile image */
} else if ((hdr->ih_type == IH_TYPE_MULTI) && (len_ptr[1])) {
ulong tail = ntohl (len_ptr[0]) % 4;
int i;
SHOW_BOOT_PROGRESS (13);
/* skip kernel length and terminator */
data = (ulong) (&len_ptr[2]);
/* skip any additional image length fields */
for (i = 1; len_ptr[i]; ++i)
data += 4;
/* add kernel length, and align */
data += ntohl (len_ptr[0]);
if (tail) {
data += 4 - tail;
}
len = ntohl (len_ptr[1]);
} else {
/* no initrd image */
SHOW_BOOT_PROGRESS (14);
len = data = 0;
}
if (data) {
initrd_start = data;
initrd_end = initrd_start + len;
} else {
initrd_start = 0;
initrd_end = 0;
}
SHOW_BOOT_PROGRESS (15);
debug ("## Transferring control to Linux (at address %08lx) ...\n",
(ulong) theKernel);
#if defined (CONFIG_SETUP_MEMORY_TAGS) || \
defined (CONFIG_CMDLINE_TAG) || \
defined (CONFIG_INITRD_TAG) || \
defined (CONFIG_SERIAL_TAG) || \
defined (CONFIG_REVISION_TAG) || \
defined (CONFIG_LCD) || \
defined (CONFIG_VFD)
setup_start_tag (bd);
#ifdef CONFIG_SERIAL_TAG
setup_serial_tag (¶ms);
#endif
#ifdef CONFIG_REVISION_TAG
setup_revision_tag (¶ms);
#endif
#ifdef CONFIG_SETUP_MEMORY_TAGS
setup_memory_tags (bd);
#endif
#ifdef CONFIG_CMDLINE_TAG
setup_commandline_tag (bd, commandline);
#endif
#ifdef CONFIG_INITRD_TAG
if (initrd_start && initrd_end)
setup_initrd_tag (bd, initrd_start, initrd_end);
#endif
setup_end_tag (bd);
#endif
/* we assume that the kernel is in place */
printf ("\nStarting kernel ...\n\n");
cleanup_before_linux ();
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
}
do_bootm_linux()函數(shù)是專門引導Linux映像的函數(shù),它還可以處理ramdisk文件系統(tǒng)的映像。這里引導的內(nèi)核映像和ramdisk映像,必須是U-Boot格式的。U-Boot格式的映像可以通過mkimage工具來轉(zhuǎn)換,其中包含了U-Boot可以識別的符號。
6.4 使用U-Boot
U-Boot是“Monitor”。除了Bootloader的系統(tǒng)引導功能,它還有用戶命令接口,提供了一些復雜的調(diào)試、讀寫內(nèi)存、燒寫Flash、配置環(huán)境變量等功能。掌握U-Boot的使用,將極大地方便嵌入式系統(tǒng)的開發(fā)。
6.4.1 燒寫U-Boot到Flash
新開發(fā)的電路板沒有任何程序可以執(zhí)行,也就不能啟動,需要先將U-Boot燒寫到Flash中。
如果主板上的EPROM或者Flash能夠取下來,就可以通過編程器燒寫。例如:計算機BIOS就存儲在一塊256KB的Flash上,通過插座與主板連接。
但是多數(shù)嵌入式單板使用貼片的Flash,不能取下來燒寫。這種情況可以通過處理器的調(diào)試接口,直接對板上的Flash編程。
處理器調(diào)試接口是為處理器芯片設計的標準調(diào)試接口,包含BDM、JTAG和EJTAG 3種接口標準。JTAG接口在第4章已經(jīng)介紹過;BDM(Background Debug Mode)主要應用在PowerPC8xx系列處理器上;EJTAG主要應用在MIPS處理器上。這3種硬件接口標準定義有所不同,但是功能基本相同,下面都統(tǒng)稱為JTAG接口。
JTAG接口需要專用的硬件工具來連接。無論從功能、性能角度,還是從價格角度,這些工具都有很大差異。關(guān)于這些工具的選擇,將在第6.4.1節(jié)詳細介紹。
最簡單方式就是通過JTAG電纜,轉(zhuǎn)接到計算機并口連接。這需要在主機端開發(fā)燒寫程序,還需要有并口設備驅(qū)動程序。開發(fā)板上電或者復位的時候,燒寫程序探測到處理器并且開始通信,然后把Bootloader下載并燒寫到Flash中。這種方式速率很慢,可是價格非常便宜。一般來說,平均每秒鐘可以燒寫100~200個字節(jié)。
燒寫完成后,復位實驗板,串口終端應該顯示U-Boot的啟動信息。
6.4.2 U-Boot的常用命令
U-Boot上電啟動后,敲任意鍵可以退出自動啟動狀態(tài),進入命令行。
U-Boot 1.1.2 (Apr 26 2005 - 12:27:13)
U-Boot code: 11080000 -> 1109614C BSS: -> 1109A91C
RAM Configuration:
Bank #0: 10000000 32 MB
Micron StrataFlash MT28F128J3 device initialized
Flash: 32 MB
In: serial
Out: serial
Err: serial
Hit any key to stop autoboot: 0
U-Boot>
在命令行提示符下,可以輸入U-Boot的命令并執(zhí)行。U-Boot可以支持幾十個常用命令,通過這些命令,可以對開發(fā)板進行調(diào)試,可以引導Linux內(nèi)核,還可以擦寫Flash完成系統(tǒng)部署等功能。掌握這些命令的使用,才能夠順利地進行嵌入式系統(tǒng)的開發(fā)。
輸入help命令,可以得到當前U-Boot的所有命令列表。每一條命令后面是簡單的命令說明。
=> help
- alias for 'help'
autoscr - run script from memory
base - print or set address offset
bdinfo - print Board Info structure
boot - boot default, i.e., run 'bootcmd'
bootd - boot default, i.e., run 'bootcmd'
bootm - boot application image from memory
bootp - boot image via network using BootP/TFTP protocol
cmp - memory compare
coninfo - print console devices and information
cp - memory copy
crc32 - checksum calculation
dhcp - invoke DHCP client to obtain IP/boot params
echo - echo args to console
erase - erase FLASH memory
flinfo - print FLASH memory information
go - start application at address 'addr'
help - print online help
iminfo - print header information for application image
imls - list all images found in flash
itest - return true/false on integer compare
loadb - load binary file over serial line (kermit mode)
loads - load S-Record file over serial line
loop - infinite loop on address range
md - memory display
mm - memory modify (auto-incrementing)
mtest - simple RAM test
mw - memory write (fill)
nfs - boot image via network using NFS protocol
nm - memory modify (constant address)
printenv - print environment variables
protect - enable or disable FLASH write protection
rarpboot - boot image via network using RARP/TFTP protocol
reset - Perform RESET of the CPU
run - run commands in an environment variable
saveenv - save environment variables to persistent storage
setenv - set environment variables
sleep - delay execution for some time
tftpboot - boot image via network using TFTP protocol
version - print monitor version
=>
U-Boot還提供了更加詳細的命令幫助,通過help命令還可以查看每個命令的參數(shù)說明。由于開發(fā)過程的需要,有必要先把U-Boot命令的用法弄清楚。接下來,根據(jù)每一條命令的幫助信息,解釋一下這些命令的功能和參數(shù)。
=> help bootm
bootm [addr [arg ...]]
- boot application image stored in memory
passing arguments 'arg ...'; when booting a Linux kernel,
'arg' can be the address of an initrd image
bootm命令可以引導啟動存儲在內(nèi)存中的程序映像。這些內(nèi)存包括RAM和可以永久保存的Flash。
第1個參數(shù)addr是程序映像的地址,這個程序映像必須轉(zhuǎn)換成U-Boot的格式。
第2個參數(shù)對于引導Linux內(nèi)核有用,通常作為U-Boot格式的RAMDISK映像存儲地址;也可以是傳遞給Linux內(nèi)核的參數(shù)(缺省情況下傳遞bootargs環(huán)境變量給內(nèi)核)。
=> help bootp
bootp [loadAddress] [bootfilename]
bootp命令通過bootp請求,要求DHCP服務器分配IP地址,然后通過TFTP協(xié)議下載指定的文件到內(nèi)存。
第1個參數(shù)是下載文件存放的內(nèi)存地址。
第2個參數(shù)是要下載的文件名稱,這個文件應該在開發(fā)主機上準備好。
=> help cmp
cmp [.b, .w, .l] addr1 addr2 count
- compare memory
cmp命令可以比較2塊內(nèi)存中的內(nèi)容。.b以字節(jié)為單位;.w以字為單位;.l以長字為單位。注意:cmp.b中間不能保留空格,需要連續(xù)敲入命令。
第1個參數(shù)addr1是第一塊內(nèi)存的起始地址。
第2個參數(shù)addr2是第二塊內(nèi)存的起始地址。
第3個參數(shù)count是要比較的數(shù)目,單位按照字節(jié)、字或者長字。
=> help cp
cp [.b, .w, .l] source target count
- copy memory
cp命令可以在內(nèi)存中復制數(shù)據(jù)塊,包括對Flash的讀寫操作。
第1個參數(shù)source是要復制的數(shù)據(jù)塊起始地址。
第2個參數(shù)target是數(shù)據(jù)塊要復制到的地址。這個地址如果在Flash中,那么會直接調(diào)用寫Flash的函數(shù)操作。所以U-Boot寫Flash就使用這個命令,當然需要先把對應Flash區(qū)域擦干凈。
第3個參數(shù)count是要復制的數(shù)目,根據(jù)cp.b cp.w cp.l分別以字節(jié)、字、長字為單位。
=> help crc32
crc32 address count [addr]
- compute CRC32 checksum [save at addr]
crc32命令可以計算存儲數(shù)據(jù)的校驗和。
第1個參數(shù)address是需要校驗的數(shù)據(jù)起始地址。
第2個參數(shù)count是要校驗的數(shù)據(jù)字節(jié)數(shù)。
第3個參數(shù)addr用來指定保存結(jié)果的地址。
=> help echo
echo [args..]
- echo args to console; \c suppresses newline
echo命令回顯參數(shù)。
=> help erase
erase start end
- erase FLASH from addr 'start' to addr 'end'
erase N:SF[-SL]
- erase sectors SF-SL in FLASH bank # N
erase bank N
- erase FLASH bank # N
erase all
- erase all FLASH banks
erase命令可以擦Flash。
參數(shù)必須指定Flash擦除的范圍。
按照起始地址和結(jié)束地址,start必須是擦除塊的起始地址;end必須是擦除末尾塊的結(jié)束地址。這種方式最常用。舉例說明:擦除0x20000 – 0x3ffff區(qū)域命令為erase 20000 3ffff。
按照組和扇區(qū),N表示Flash的組號,SF表示擦除起始扇區(qū)號,SL表示擦除結(jié)束扇區(qū)號。另外,還可以擦除整個組,擦除組號為N的整個Flash組。擦除全部Flash只要給出一個all的參數(shù)即可。
=> help flinfo
flinfo
- print information for all FLASH memory banks
flinfo N
- print information for FLASH memory bank # N
flinfo命令打印全部Flash組的信息,也可以只打印其中某個組。一般嵌入式系統(tǒng)的Flash只有一個組。
=> help go
go addr [arg ...]
- start application at address 'addr'
passing 'arg' as arguments
go命令可以執(zhí)行應用程序。
第1個參數(shù)是要執(zhí)行程序的入口地址。
第2個可選參數(shù)是傳遞給程序的參數(shù),可以不用。
=> help iminfo
iminfo addr [addr ...]
- print header information for application image starting at
address 'addr' in memory; this includes verification of the
image contents (magic number, header and payload checksums)
iminfo可以打印程序映像的開頭信息,包含了映像內(nèi)容的校驗(序列號、頭和校驗和)。
第1個參數(shù)指定映像的起始地址。
可選的參數(shù)是指定更多的映像地址。
=> help loadb
loadb [ off ] [ baud ]
- load binary file over serial line with offset 'off' and baudrate 'baud'
loadb命令可以通過串口線下載二進制格式文件。
=> help loads
loads [ off ]
- load S-Record file over serial line with offset 'off'
loads命令可以通過串口線下載S-Record格式文件。
=> help mw
mw [.b, .w, .l] address value [count]
- write memory
mw命令可以按照字節(jié)、字、長字寫內(nèi)存,.b .w .l的用法與cp命令相同。
第1個參數(shù)address是要寫的內(nèi)存地址。
第2個參數(shù)value是要寫的值。
第3個可選參數(shù)count是要寫單位值的數(shù)目。
=> help nfs
nfs [loadAddress] [host ip addr:bootfilename]
nfs命令可以使用NFS網(wǎng)絡協(xié)議通過網(wǎng)絡啟動映像。
=> help nm
nm [.b, .w, .l] address
- memory modify, read and keep address
nm命令可以修改內(nèi)存,可以按照字節(jié)、字、長字操作。
參數(shù)address是要讀出并且修改的內(nèi)存地址。
=> help printenv
printenv
- print values of all environment variables
printenv name ...
- print value of environment variable 'name'
printenv命令打印環(huán)境變量。
可以打印全部環(huán)境變量,也可以只打印參數(shù)中列出的環(huán)境變量。
=> help protect
protect on start end
- protect Flash from addr 'start' to addr 'end'
protect on N:SF[-SL]
- protect sectors SF-SL in Flash bank # N
protect on bank N
- protect Flash bank # N
protect on all
- protect all Flash banks
protect off start end
- make Flash from addr 'start' to addr 'end' writable
protect off N:SF[-SL]
- make sectors SF-SL writable in Flash bank # N
protect off bank N
- make Flash bank # N writable
protect off all
- make all Flash banks writable
protect命令是對Flash寫保護的操作,可以使能和解除寫保護。
第1個參數(shù)on代表使能寫保護;off代表解除寫保護。
第2、3參數(shù)是指定Flash寫保護操作范圍,跟擦除的方式相同。
=> help rarpboot
rarpboot [loadAddress] [bootfilename]
rarboot命令可以使用TFTP協(xié)議通過網(wǎng)絡啟動映像。也就是把指定的文件下載到指定地址,然后執(zhí)行。
第1個參數(shù)是映像文件下載到的內(nèi)存地址。
第2個參數(shù)是要下載執(zhí)行的映像文件。
=> help run
run var [...]
- run the commands in the environment variable(s) 'var'
run命令可以執(zhí)行環(huán)境變量中的命令,后面參數(shù)可以跟幾個環(huán)境變量名。
=> help setenv
setenv name value ...
- set environment variable 'name' to 'value ...'
setenv name
- delete environment variable 'name'
setenv命令可以設置環(huán)境變量。
第1個參數(shù)是環(huán)境變量的名稱。
第2個參數(shù)是要設置的值,如果沒有第2個參數(shù),表示刪除這個環(huán)境變量。
=> help sleep
sleep N
- delay execution for N seconds (N is _decimal_ !!!)
sleep命令可以延遲N秒鐘執(zhí)行,N為十進制數(shù)。
=> help tftpboot
tftpboot [loadAddress] [bootfilename]
tftpboot命令可以使用TFTP協(xié)議通過網(wǎng)絡下載文件。按照二進制文件格式下載。另外使用這個命令,必須配置好相關(guān)的環(huán)境變量。例如serverip和ipaddr。
第1個參數(shù)loadAddress是下載到的內(nèi)存地址。
第2個參數(shù)是要下載的文件名稱,必須放在TFTP服務器相應的目錄下。
這些U-Boot命令為嵌入式系統(tǒng)提供了豐富的開發(fā)和調(diào)試功能。在Linux內(nèi)核啟動和調(diào)試過程中,都可以用到U-Boot的命令。但是一般情況下,不需要使用全部命令。比如已經(jīng)支持以太網(wǎng)接口,可以通過tftpboot命令來下載文件,那么還有必要使用串口下載的loadb嗎?反過來,如果開發(fā)板需要特殊的調(diào)試功能,也可以添加新的命令。
在建立交叉開發(fā)環(huán)境和調(diào)試Linux內(nèi)核等章節(jié)時,在ARM平臺上移植了U-Boot,并且提供了具體U-Boot的操作步驟。
6.4.3 U-Boot的環(huán)境變量
有點類似Shell,U-Boot也使用環(huán)境變量??梢酝ㄟ^printenv命令查看環(huán)境變量的設置。
U-Boot> printenv
bootdelay=3
baudrate=115200
netmask=255.255.0.0
ethaddr=12:34:56:78:90:ab
bootfile=uImage
bootargs=console=ttyS0,115200 root=/dev/ram rw initrd=0x30800000,8M
bootcmd=tftp 0x30008000 zImage;go 0x30008000
serverip=192.168.1.1
ipaddr=192.168.1.100
stdin=serial
stdout=serial
stderr=serial
Environment size: 337/131068 bytes
U-Boot>
表6.5是常用環(huán)境變量的含義解釋。通過printenv命令可以打印出這些變量的值。
表6.5 U-Boot環(huán)境變量的解釋說明
環(huán) 境 變 量 |
解 釋 說 明 |
bootdelay |
定義執(zhí)行自動啟動的等候秒數(shù) |
baudrate |
定義串口控制臺的波特率 |
netmask |
定義以太網(wǎng)接口的掩碼 |
ethaddr |
定義以太網(wǎng)接口的MAC地址 |
bootfile |
定義缺省的下載文件 |
bootargs |
定義傳遞給Linux內(nèi)核的命令行參數(shù) |
bootcmd |
定義自動啟動時執(zhí)行的幾條命令 |
serverip |
定義tftp服務器端的IP地址 |
ipaddr |
定義本地的IP地址 |
stdin |
定義標準輸入設備,一般是串口 |
stdout |
定義標準輸出設備,一般是串口 |
stderr |
定義標準出錯信息輸出設備,一般是串口 |
U-Boot的環(huán)境變量都可以有缺省值,也可以修改并且保存在參數(shù)區(qū)。U-Boot的參數(shù)區(qū)一般有EEPROM和Flash兩種設備。
環(huán)境變量的設置命令為setenv,在6.2.2節(jié)有命令的解釋。
舉例說明環(huán)境變量的使用。
=>setenv serverip 192.168.1.1
=>setenv ipaddr 192.168.1.100
=>setenv rootpath "/usr/local/arm/3.3.2/rootfs"
=>setenv bootargs "root=/dev/nfs rw nfsroot=\$(serverip):\$(rootpath) ip=
\$(ipaddr) "
=>setenv kernel_addr 30000000
=>setenv nfscmd "tftp \$(kernel_addr) uImage; bootm \$(kernel_addr) "
=>run nfscmd
上面定義的環(huán)境變量有serverip ipaddr rootpath bootargs kernel_addr。環(huán)境變量bootargs中還使用了環(huán)境變量,bootargs定義命令行參數(shù),通過bootm命令傳遞給內(nèi)核。環(huán)境變量nfscmd中也使用了環(huán)境變量,功能是把uImage下載到指定的地址并且引導起來??梢酝ㄟ^run命令執(zhí)行nfscmd腳本。