6.1 Bootloader
對于計算機系統來(lái)說(shuō),從開(kāi)機上電到操作系統啟動(dòng)需要一個(gè)引導過(guò)程。嵌入式Linux系統同樣離不開(kāi)引導程序,這個(gè)引導程序就叫作Bootloader。
6.1.1 Bootloader介紹
Bootloader是在操作系統運行之前執行的一段小程序。通過(guò)這段小程序,我們可以初始化硬件設備、建立內存空間的映射表,從而建立適當的系統軟硬件環(huán)境,為最終調用操作系統內核做好準備。
對于嵌入式系統,Bootloader是基于特定硬件平臺來(lái)實(shí)現的。因此,幾乎不可能為所有的嵌入式系統建立一個(gè)通用的Bootloader,不同的處理器架構都有不同的Bootloader。Bootloader不但依賴(lài)于CPU的體系結構,而且依賴(lài)于嵌入式系統板級設備的配置。對于2塊不同的嵌入式板而言,即使它們使用同一種處理器,要想讓運行在一塊板子上的Bootloader程序也能運行在另一塊板子上,一般也都需要修改Bootloader的源程序。
反過(guò)來(lái),大部分Bootloader仍然具有很多共性,某些Bootloader也能夠支持多種體系結構的嵌入式系統。例如,U-Boot就同時(shí)支持PowerPC、ARM、MIPS和X86等體系結構,支持的板子有上百種。通常,它們都能夠自動(dòng)從存儲介質(zhì)上啟動(dòng),都能夠引導操作系統啟動(dòng),并且大部分都可以支持串口和以太網(wǎng)接口。
本章將對各種Bootloader總結分類(lèi),分析它們的共同特點(diǎn)。以U-Boot為例,詳細討論Bootloader的設計與實(shí)現。
6.1.2 Bootloader的啟動(dòng)
Linux系統是通過(guò)Bootloader引導啟動(dòng)的。一上電,就要執行Bootloader來(lái)初始化系統??梢酝ㄟ^(guò)第4章的Linux啟動(dòng)過(guò)程框圖回顧一下。
系統加電或復位后,所有CPU都會(huì )從某個(gè)地址開(kāi)始執行,這是由處理器設計決定的。比如,X86的復位向量在高地址端,ARM處理器在復位時(shí)從地址0x00000000取第一條指令。嵌入式系統的開(kāi)發(fā)板都要把板上ROM或Flash映射到這個(gè)地址。因此,必須把Bootloader程序存儲在相應的Flash位置。系統加電后,CPU將首先執行它。
主機和目標機之間一般有串口可以連接,Bootloader軟件通常會(huì )通過(guò)串口來(lái)輸入輸出。例如:輸出出錯或者執行結果信息到串口終端,從串口終端讀取用戶(hù)控制命令等。
Bootloader啟動(dòng)過(guò)程通常是多階段的,這樣既能提供復雜的功能,又有很好的可移植性。例如:從Flash啟動(dòng)的Bootloader多數是兩階段的啟動(dòng)過(guò)程。從后面U-Boot的內容可以詳細分析這個(gè)特性。
大多數Bootloader都包含2種不同的操作模式:本地加載模式和遠程下載模式。這2種操作模式的區別僅對于開(kāi)發(fā)人員才有意義,也就是不同啟動(dòng)方式的使用。從最終用戶(hù)的角度看,Bootloader的作用就是用來(lái)加載操作系統,而并不存在所謂的本地加載模式與遠程下載模式的區別。
因為Bootloader的主要功能是引導操作系統啟動(dòng),所以我們詳細討論一下各種啟動(dòng)方式的特點(diǎn)。
1.網(wǎng)絡(luò )啟動(dòng)方式
這種方式開(kāi)發(fā)板不需要配置較大的存儲介質(zhì),跟無(wú)盤(pán)工作站有點(diǎn)類(lèi)似。但是使用這種啟動(dòng)方式之前,需要把Bootloader安裝到板上的EPROM或者Flash中。Bootloader通過(guò)以太網(wǎng)接口遠程下載Linux內核映像或者文件系統。第4章介紹的交叉開(kāi)發(fā)環(huán)境就是以網(wǎng)絡(luò )啟動(dòng)方式建立的。這種方式對于嵌入式系統開(kāi)發(fā)來(lái)說(shuō)非常重要。
使用這種方式也有前提條件,就是目標板有串口、以太網(wǎng)接口或者其他連接方式。串口一般可以作為控制臺,同時(shí)可以用來(lái)下載內核影像和RAMDISK文件系統。串口通信傳輸速率過(guò)低,不適合用來(lái)掛接NFS文件系統。所以以太網(wǎng)接口成為通用的互連設備,一般的開(kāi)發(fā)板都可以配置10M以太網(wǎng)接口。
對于PDA等手持設備來(lái)說(shuō),以太網(wǎng)的RJ-45接口顯得大了些,而USB接口,特別是USB的迷你接口,尺寸非常小。對于開(kāi)發(fā)的嵌入式系統,可以把USB接口虛擬成以太網(wǎng)接口來(lái)通訊。這種方式在開(kāi)發(fā)主機和開(kāi)發(fā)板兩端都需要驅動(dòng)程序。
另外,還要在服務(wù)器上配置啟動(dòng)相關(guān)網(wǎng)絡(luò )服務(wù)。Bootloader下載文件一般都使用TFTP網(wǎng)絡(luò )協(xié)議,還可以通過(guò)DHCP的方式動(dòng)態(tài)配置IP地址。
DHCP/BOOTP服務(wù)為Bootloader分配IP地址,配置網(wǎng)絡(luò )參數,然后才能夠支持網(wǎng)絡(luò )傳輸功能。如果Bootloader可以直接設置網(wǎng)絡(luò )參數,就可以不使用DHCP。
TFTP服務(wù)為Bootloader客戶(hù)端提供文件下載功能,把內核映像和其他文件放在/tftpboot目錄下。這樣Bootloader可以通過(guò)簡(jiǎn)單的TFTP協(xié)議遠程下載內核映像到內存。如圖6.1所示。
圖6.1 網(wǎng)絡(luò )啟動(dòng)示意圖
大部分引導程序都能夠支持網(wǎng)絡(luò )啟動(dòng)方式。例如:BIOS的PXE(Preboot Execution Environment)功能就是網(wǎng)絡(luò )啟動(dòng)方式;U-Boot也支持網(wǎng)絡(luò )啟動(dòng)功能。
2.磁盤(pán)啟動(dòng)方式
傳統的Linux系統運行在臺式機或者服務(wù)器上,這些計算機一般都使用BIOS引導,并且使用磁盤(pán)作為存儲介質(zhì)。如果進(jìn)入BIOS設置菜單,可以探測處理器、內存、硬盤(pán)等設備,可以設置BIOS從軟盤(pán)、光盤(pán)或者某塊硬盤(pán)啟動(dòng)。也就是說(shuō),BIOS并不直接引導操作系統。那么在硬盤(pán)的主引導區,還需要一個(gè)Bootloader。這個(gè)Bootloader可以從磁盤(pán)文件系統中把操作系統引導起來(lái)。
Linux傳統上是通過(guò)LILO(LInux LOader)引導的,后來(lái)又出現了GNU的軟件GRUB(GRand Unified Bootloader)。這2種Bootloader廣泛應用在X86的Linux系統上。你的開(kāi)發(fā)主機可能就使用了其中一種,熟悉它們有助于配置多種系統引導功能。
LILO軟件工程是由Werner Almesberger創(chuàng )建,專(zhuān)門(mén)為引導Linux開(kāi)發(fā)的?,F在LILO的維護者是John Coffman,最新版本下載站點(diǎn):
http://lilo.go.dyndns.org。LILO有詳細的文檔,例如LILO套件中附帶使用手冊和參考手冊。此外,還可以在LDP的“LILO mini-HOWTO”中找到LILO的使用指南。
GRUB是GNU計劃的主要bootloader。GRUB最初是由Erich Boleyn為GNU Mach操作系統撰寫(xiě)的引導程序。后來(lái)有Gordon Matzigkeit和Okuji Yoshinori接替Erich的工作,繼續維護和開(kāi)發(fā)GRUB。GRUB的網(wǎng)站http://www.gnu.org/software/grub/上有對套件使用的說(shuō)明文件,叫作《GRUB manual》。GRUB能夠使用TFTP和BOOTP或者DHCP通過(guò)網(wǎng)絡(luò )啟動(dòng),這種功能對于系統開(kāi)發(fā)過(guò)程很有用。
除了傳統的Linux系統上的引導程序以外,還有其他一些引導程序,也可以支持磁盤(pán)引導啟動(dòng)。例如:LoadLin可以從DOS下啟動(dòng)Linux;還有ROLO、LinuxBIOS,U-Boot也支持這種功能。
3.Flash啟動(dòng)方式
大多數嵌入式系統上都使用Flash存儲介質(zhì)。Flash有很多類(lèi)型,包括NOR Flash、NAND Flash和其他半導體盤(pán)。其中,NOR Flash(也就是線(xiàn)性Flash)使用最為普遍。
NOR Flash可以支持隨機訪(fǎng)問(wèn),所以代碼是可以直接在Flash上執行的。Bootloader一般是存儲在Flash芯片上的。另外,Linux內核映像和RAMDISK也可以存儲在Flash上。通常需要把Flash分區使用,每個(gè)區的大小應該是Flash擦除塊大小的整數倍。圖6.2是Bootloader和內核映像以及文件系統的分區表。
圖6.2 Flash存儲示意圖
Bootloader一般放在Flash的底端或者頂端,這要根據處理器的復位向量設置。要使Bootloader的入口位于處理器上電執行第一條指令的位置。
接下來(lái)分配參數區,這里可以作為Bootloader的參數保存區域。
再下來(lái)內核映像區。Bootloader引導Linux內核,就是要從這個(gè)地方把內核映像解壓到RAM中去,然后跳轉到內核映像入口執行。
然后是文件系統區。如果使用Ramdisk文件系統,則需要Bootloader把它解壓到RAM中。如果使用JFFS2文件系統,將直接掛接為根文件系統。這兩種文件系統將在第12章詳細講解。
最后還可以分出一些數據區,這要根據實(shí)際需要和Flash大小來(lái)考慮了。
這些分區是開(kāi)發(fā)者定義的,Bootloader一般直接讀寫(xiě)對應的偏移地址。到了Linux內核空間,可以配置成MTD設備來(lái)訪(fǎng)問(wèn)Flash分區。但是,有的Bootloader也支持分區的功能,例如:Redboot可以創(chuàng )建Flash分區表,并且內核MTD驅動(dòng)可以解析出redboot的分區表。
除了NOR Flash,還有NAND Flash、Compact Flash、DiskOnChip等。這些Flash具有芯片價(jià)格低,存儲容量大的特點(diǎn)。但是這些芯片一般通過(guò)專(zhuān)用控制器的I/O方式來(lái)訪(fǎng)問(wèn),不能隨機訪(fǎng)問(wèn),因此引導方式跟NOR Flash也不同。在這些芯片上,需要配置專(zhuān)用的引導程序。通常,這種引導程序起始的一段代碼就把整個(gè)引導程序復制到RAM中運行,從而實(shí)現自舉啟動(dòng),這跟從磁盤(pán)上啟動(dòng)有些相似。
6.1.3 Bootloader的種類(lèi)
嵌入式系統世界已經(jīng)有各種各樣的Bootloader,種類(lèi)劃分也有多種方式。除了按照處理器體系結構不同劃分以外,還有功能復雜程度的不同。
首先區分一下“Bootloader”和“Monitor”的概念。嚴格來(lái)說(shuō),“Bootloader”只是引導設備并且執行主程序的固件;而“Monitor”還提供了更多的命令行接口,可以進(jìn)行調試、讀寫(xiě)內存、燒寫(xiě)Flash、配置環(huán)境變量等?!癕onitor”在嵌入式系統開(kāi)發(fā)過(guò)程中可以提供很好的調試功能,開(kāi)發(fā)完成以后,就完全設置成了一個(gè)“Bootloader”。所以,習慣上大家把它們統稱(chēng)為Bootloader。
表6.1列出了Linux的開(kāi)放源碼引導程序及其支持的體系結構。表中給出了X86 ARM PowerPC體系結構的常用引導程序,并且注明了每一種引導程序是不是“Monitor”。
表6.1 開(kāi)放源碼的Linux 引導程序
Bootloader
Monitor
描 述
x86
ARM
PowerPC
LILO
否
Linux磁盤(pán)引導程序
是
否
否
GRUB
否
GNU的LILO替代程序
是
否
否
Loadlin
否
從DOS引導Linux
是
否
否
ROLO
否
從ROM引導Linux而不需要BIOS
是
否
否
Etherboot
否
通過(guò)以太網(wǎng)卡啟動(dòng)Linux系統的固件
是
否
否
LinuxBIOS
否
完全替代BUIS的Linux引導程序
是
否
否
BLOB
否
LART等硬件平臺的引導程序
否
是
否
U-boot
是
通用引導程序
是
是
是
RedBoot
是
基于eCos的引導程序
是
是
是
對于每種體系結構,都有一系列開(kāi)放源碼Bootloader可以選用。
(1)X86
X86的工作站和服務(wù)器上一般使用LILO和GRUB。LILO是Linux發(fā)行版主流的Bootloader。不過(guò)Redhat Linux發(fā)行版已經(jīng)使用了GRUB,GRUB比LILO有更有好的顯示界面,使用配置也更加靈活方便。
在某些X86嵌入式單板機或者特殊設備上,會(huì )采用其他Bootloader,例如:ROLO。這些Bootloader可以取代BIOS的功能,能夠從FLASH中直接引導Linux啟動(dòng)?,F在ROLO支持的開(kāi)發(fā)板已經(jīng)并入U-Boot,所以U-Boot也可以支持X86平臺。
(2)ARM
ARM處理器的芯片商很多,所以每種芯片的開(kāi)發(fā)板都有自己的Bootloader。結果ARM bootloader也變得多種多樣。最早有為ARM720處理器的開(kāi)發(fā)板的固件,又有了armboot,StrongARM平臺的blob,還有S3C2410處理器開(kāi)發(fā)板上的vivi等?,F在armboot已經(jīng)并入了U-Boot,所以U-Boot也支持ARM/XSCALE平臺。U-Boot已經(jīng)成為ARM平臺事實(shí)上的標準Bootloader。
(3)PowerPC
PowerPC平臺的處理器有標準的Bootloader,就是ppcboot。PPCBOOT在合并armboot等之后,創(chuàng )建了U-Boot,成為各種體系結構開(kāi)發(fā)板的通用引導程序。U-Boot仍然是PowerPC平臺的主要Bootloader。
(4)MIPS
MIPS公司開(kāi)發(fā)的YAMON是標準的Bootloader,也有許多MIPS芯片商為自己的開(kāi)發(fā)板寫(xiě)了Bootloader?,F在,U-Boot也已經(jīng)支持MIPS平臺。
(5)SH
SH平臺的標準Bootloader是sh-boot。Redboot在這種平臺上也很好用。
(6)M68K
M68K平臺沒(méi)有標準的Bootloader。Redboot能夠支持m68k系列的系統。
值得說(shuō)明的是Redboot,它幾乎能夠支持所有的體系結構,包括MIPS、SH、M68K等體系結構。Redboot是以eCos為基礎,采用GPL許可的開(kāi)源軟件工程?,F在由core eCos的開(kāi)發(fā)人員維護,源碼下載網(wǎng)站是
http://www.ecoscentric.com/snapshots。Redboot的文檔也相當完善,有詳細的使用手冊《RedBoot User’s Guide》。
6.2.1 U-Boot工程簡(jiǎn)介
最早,DENX軟件工程中心的Wolfgang Denk基于8xxrom的源碼創(chuàng )建了PPCBOOT工程,并且不斷添加處理器的支持。后來(lái),Sysgo Gmbh把ppcboot移植到ARM平臺上,創(chuàng )建了ARMboot工程。然后以ppcboot工程和armboot工程為基礎,創(chuàng )建了U-Boot工程。
現在U-Boot已經(jīng)能夠支持PowerPC、ARM、X86、MIPS體系結構的上百種開(kāi)發(fā)板,已經(jīng)成為功能最多、靈活性最強并且開(kāi)發(fā)最積極的開(kāi)放源碼Bootloader。目前仍然由DENX的Wolfgang Denk維護。
U-Boot的源碼包可以從sourceforge網(wǎng)站下載,還可以訂閱該網(wǎng)站活躍的U-Boot Users郵件論壇,這個(gè)郵件論壇對于U-Boot的開(kāi)發(fā)和使用都很有幫助。
U-Boot軟件包下載網(wǎng)站:http://sourceforge.net/project/u-boot。
U-Boot郵件列表網(wǎng)站:http://lists.sourceforge.net/lists/listinfo/u-boot-users/。
DENX相關(guān)的網(wǎng)站:
http://www.denx.de/re/DPLG.html。
6.2.2 U-Boot源碼結構
從網(wǎng)站上下載得到U-Boot源碼包,例如:U-Boot-1.1.2.tar.bz2
解壓就可以得到全部U-Boot源程序。在頂層目錄下有18個(gè)子目錄,分別存放和管理不同的源程序。這些目錄中所要存放的文件有其規則,可以分為3類(lèi)。
· 第1類(lèi)目錄與處理器體系結構或者開(kāi)發(fā)板硬件直接相關(guān);
· 第2類(lèi)目錄是一些通用的函數或者驅動(dòng)程序;
· 第3類(lèi)目錄是U-Boot的應用程序、工具或者文檔。
表6.2列出了U-Boot頂層目錄下各級目錄存放原則。
表6.2 U-Boot的源碼頂層目錄說(shuō)明
目 錄
特 性
解 釋 說(shuō) 明
board
平臺依賴(lài)
存放電路板相關(guān)的目錄文件,例如:RPXlite(mpc8xx)、smdk2410(arm920t)、sc520_cdp(x86) 等目錄
cpu
平臺依賴(lài)
存放CPU相關(guān)的目錄文件,例如:mpc8xx、ppc4xx、arm720t、arm920t、 xscale、i386等目錄
lib_ppc
平臺依賴(lài)
存放對PowerPC體系結構通用的文件,主要用于實(shí)現PowerPC平臺通用的函數
目 錄
特 性
解 釋 說(shuō) 明
lib_arm
平臺依賴(lài)
存放對ARM體系結構通用的文件,主要用于實(shí)現ARM平臺通用的函數
lib_i386
平臺依賴(lài)
存放對X86體系結構通用的文件,主要用于實(shí)現X86平臺通用的函數
include
通用
頭文件和開(kāi)發(fā)板配置文件,所有開(kāi)發(fā)板的配置文件都在configs目錄下
common
通用
通用的多功能函數實(shí)現
lib_generic
通用
通用庫函數的實(shí)現
Net
通用
存放網(wǎng)絡(luò )的程序
Fs
通用
存放文件系統的程序
Post
通用
存放上電自檢程序
drivers
通用
通用的設備驅動(dòng)程序,主要有以太網(wǎng)接口的驅動(dòng)
Disk
通用
硬盤(pán)接口程序
Rtc
通用
RTC的驅動(dòng)程序
Dtt
通用
數字溫度測量器或者傳感器的驅動(dòng)
examples
應用例程
一些獨立運行的應用程序的例子,例如helloworld
tools
工具
存放制作S-Record 或者 U-Boot格式的映像等工具,例如mkimage
Doc
文檔
開(kāi)發(fā)使用文檔
U-Boot的源代碼包含對幾十種處理器、數百種開(kāi)發(fā)板的支持??墒菍τ谔囟ǖ拈_(kāi)發(fā)板,配置編譯過(guò)程只需要其中部分程序。這里具體以S3C2410 arm920t處理器為例,具體分析S3C2410處理器和開(kāi)發(fā)板所依賴(lài)的程序,以及U-Boot的通用函數和工具。
6.2.3 U-Boot的編譯
U-Boot的源碼是通過(guò)GCC和Makefile組織編譯的。頂層目錄下的Makefile首先可以設置開(kāi)發(fā)板的定義,然后遞歸地調用各級子目錄下的Makefile,最后把編譯過(guò)的程序鏈接成U-Boot映像。
1.頂層目錄下的Makefile
它負責U-Boot整體配置編譯。按照配置的順序閱讀其中關(guān)鍵的幾行。
每一種開(kāi)發(fā)板在Makefile都需要有板子配置的定義。例如smdk2410開(kāi)發(fā)板的定義如下。
smdk2410_config : unconfig
@./mkconfig $(@:_config=) arm arm920t smdk2410 NULL s3c24x0
執行配置U-Boot的命令make smdk2410_config,通過(guò)./mkconfig腳本生成include/config.
mk的配置文件。文件內容正是根據Makefile對開(kāi)發(fā)板的配置生成的。
ARCH = arm
CPU = arm920t
BOARD = smdk2410
SOC = s3c24x0
上面的include/config.mk文件定義了ARCH、CPU、BOARD、SOC這些變量。這樣硬件平臺依賴(lài)的目錄文件可以根據這些定義來(lái)確定。SMDK2410平臺相關(guān)目錄如下。
board/smdk2410/
cpu/arm920t/
cpu/arm920t/s3c24x0/
lib_arm/
include/asm-arm/
include/configs/smdk2410.h
再回到頂層目錄的Makefile文件開(kāi)始的部分,其中下列幾行包含了這些變量的定義。
# load ARCH, BOARD, and CPU configuration
include include/config.mk
export ARCH CPU BOARD VENDOR SOC
Makefile的編譯選項和規則在頂層目錄的config.mk文件中定義。各種體系結構通用的規則直接在這個(gè)文件中定義。通過(guò)ARCH、CPU、BOARD、SOC等變量為不同硬件平臺定義不同選項。不同體系結構的規則分別包含在ppc_config.mk、arm_config.mk、mips_config.mk等文件中。
頂層目錄的Makefile中還要定義交叉編譯器,以及編譯U-Boot所依賴(lài)的目標文件。
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 //定義依賴(lài)的目錄,每個(gè)目錄下先把目標文件連接成*.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映像編譯的依賴(lài)關(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又依賴(lài)于U-Boot。U-Boot就是通過(guò)ld命令按照u-boot.map地址表把目標文件組裝成u-boot。
其他Makefile內容就不再詳細分析了,上述代碼分析應該可以為閱讀代碼提供了一個(gè)線(xiàn)索。
2.開(kāi)發(fā)板配置頭文件
除了編譯過(guò)程Makefile以外,還要在程序中為開(kāi)發(fā)板定義配置選項或者參數。這個(gè)頭文件是include/configs/<board_name>.h。<board_name>用相應的BOARD定義代替。
這個(gè)頭文件中主要定義了兩類(lèi)變量。
一類(lèi)是選項,前綴是CONFIG_,用來(lái)選擇處理器、設備接口、命令、屬性等。例如:
#define CONFIG_ARM920T 1
#define CONFIG_DRIVER_CS8900 1
另一類(lèi)是參數,前綴是CFG_,用來(lái)定義總線(xiàn)頻率、串口波特率、Flash地址等參數。例如:
#define CFG_FLASH_BASE 0x00000000
#define CFG_PROMPT "=>"
3.編譯結果
根據對Makefile的分析,編譯分為2步。第1步配置,例如:make smdk2410_config;第2步編譯,執行make就可以了。
編譯完成后,可以得到U-Boot各種格式的映像文件和符號表,如表6.3所示。
表6.3 U-Boot編譯生成的映像文件
文 件 名 稱(chēng)
說(shuō) 明
文 件 名 稱(chēng)
說(shuō) 明
System.map
U-Boot映像的符號表
u-boot.bin
U-Boot映像原始的二進(jìn)制格式
u-boot
U-Boot映像的ELF格式
u-boot.srec
U-Boot映像的S-Record格式
U-Boot的3種映像格式都可以燒寫(xiě)到Flash中,但需要看加載器能否識別這些格式。一般u-boot.bin最為常用,直接按照二進(jìn)制格式下載,并且按照絕對地址燒寫(xiě)到Flash中就可以了。U-Boot和u-boot.srec格式映像都自帶定位信息。
4.U-Boot工具
在tools目錄下還有些U-Boot的工具。這些工具有的也經(jīng)常用到。表6.4說(shuō)明了幾種工具的用途。
表6.4 U-Boot的工具
工 具 名 稱(chēng)
說(shuō) 明
工 具 名 稱(chēng)
說(shuō) 明
bmp_logo
制作標記的位圖結構體
img2srec
轉換SREC格式映像
envcrc
校驗u-boot內部嵌入的環(huán)境變量
mkimage
轉換U-Boot格式映像
gen_eth_addr
生成以太網(wǎng)接口MAC地址
updater
U-Boot自動(dòng)更新升級工具
這些工具都有源代碼,可以參考改寫(xiě)其他工具。其中mkimage是很常用的一個(gè)工具,Linux內核映像和ramdisk文件系統映像都可以轉換成U-Boot的格式。
6.2.4 U-Boot的移植
U-Boot能夠支持多種體系結構的處理器,支持的開(kāi)發(fā)板也越來(lái)越多。因為Bootloader是完全依賴(lài)硬件平臺的,所以在新電路板上需要移植U-Boot程序。
開(kāi)始移植U-Boot之前,先要熟悉硬件電路板和處理器。確認U-Boot是否已經(jīng)支持新開(kāi)發(fā)板的處理器和I/O設備。假如U-Boot已經(jīng)支持一塊非常相似的電路板,那么移植的過(guò)程將非常簡(jiǎn)單。
移植U-Boot工作就是添加開(kāi)發(fā)板硬件相關(guān)的文件、配置選項,然后配置編譯。
開(kāi)始移植之前,需要先分析一下U-Boot已經(jīng)支持的開(kāi)發(fā)板,比較出硬件配置最接近的開(kāi)發(fā)板。選擇的原則是,首先處理器相同,其次處理器體系結構相同,然后是以太網(wǎng)接口等外圍接口。還要驗證一下這個(gè)參考開(kāi)發(fā)板的U-Boot,至少能夠配置編譯通過(guò)。
以S3C2410處理器的開(kāi)發(fā)板為例,U-Boot-1.1.2版本已經(jīng)支持SMDK2410開(kāi)發(fā)板。我們可以基于SMDK2410移植,那么先把SMDK2410編譯通過(guò)。
我們以S3C2410開(kāi)發(fā)板fs2410為例說(shuō)明。移植的過(guò)程參考SMDK2410開(kāi)發(fā)板,SMDK2410在U-Boot-1.1.2中已經(jīng)支持。
移植U-Boot的基本步驟如下。
(1)在頂層Makefile中為開(kāi)發(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 )建一個(gè)新目錄存放開(kāi)發(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)為開(kāi)發(fā)板添加新的配置文件
可以先復制參考開(kāi)發(fā)板的配置文件,再修改。例如:
$cp include/configs/smdk2410.h include/configs/fs2410.h
如果是為一顆新的CPU移植,還要創(chuàng )建一個(gè)新的目錄存放CPU相關(guān)的代碼。
(4)配置開(kāi)發(fā)板
$ make fs2410_config
(5)編譯U-Boot
執行make命令,編譯成功可以得到U-Boot映像。有些錯誤是跟配置選項是有關(guān)系的,通常打開(kāi)某些功能選項會(huì )帶來(lái)一些錯誤,一開(kāi)始可以盡量跟參考板配置相同。
(6)添加驅動(dòng)或者功能選項
在能夠編譯通過(guò)的基礎上,還要實(shí)現U-Boot的以太網(wǎng)接口、Flash擦寫(xiě)等功能。
對于FS2410開(kāi)發(fā)板的以太網(wǎng)驅動(dòng)和smdk2410完全相同,所以可以直接使用。CS8900驅動(dòng)程序文件如下。
drivers/cs8900.c
drivers/cs8900.h
對于Flash的選擇就麻煩多了,Flash芯片價(jià)格或者采購方面的因素都有影響。多數開(kāi)發(fā)板大小、型號不都相同。所以還需要移植Flash的驅動(dòng)。每種開(kāi)發(fā)板目錄下一般都有flash.c這個(gè)文件,需要根據具體的Flash類(lèi)型修改。例如:
board/fs2410/flash.c
(7)調試U-Boot源代碼,直到U-Boot在開(kāi)發(fā)板上能夠正常啟動(dòng)。
調試的過(guò)程可能是很艱難的,需要借助工具,并且有些問(wèn)題可能困擾很長(cháng)時(shí)間。
6.2.5 添加U-Boot命令
U-Boot的命令為用戶(hù)提供了交互功能,并且已經(jīng)實(shí)現了幾十個(gè)常用的命令。如果開(kāi)發(fā)板需要很特殊的操作,可以添加新的U-Boot命令。
U-Boot的每一個(gè)命令都是通過(guò)U_Boot_CMD宏定義的。這個(gè)宏在include/command.h頭文件中定義,每一個(gè)命令定義一個(gè)cmd_tbl_t結構體。
#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}
這樣每一個(gè)U-Boot命令有一個(gè)結構體來(lái)描述。結構體包含的成員變量:命令名稱(chēng)、最大參數個(gè)數、重復數、命令執行函數、用法、幫助。
從控制臺輸入的命令是由common/command.c中的程序解釋執行的。find_cmd()負責匹配輸入的命令,從列表中找出對應的命令結構體。
基于U-Boot命令的基本框架,來(lái)分析一下簡(jiǎn)單的icache操作命令,就可以知道添加新命令的方法。
(1)定義CACHE命令。在include/cmd_confdefs.h中定義了所有U-Boot命令的標志位。
#define CFG_CMD_CACHE 0x00000010ULL /* icache, dcache */
如果有更多的命令,也要在這里添加定義。
(2)實(shí)現CACHE命令的操作函數。下面是common/cmd_cache.c文件中icache命令部分的代碼。
#if (CONFIG_COMMANDS & CFG_CMD_CACHE)
static int on_off (const char *s)
{ //這個(gè)函數解析參數,判斷是打開(kāi)cache,還是關(guān)閉cache
if (strcmp(s, "on") == 0) { //參數為“on”
return (1);
} else if (strcmp(s, "off") == 0) { //參數為“off”
return (0);
}
return (-1);
}
int do_icache ( cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{ //對指令cache的操作函數
switch (argc) {
case 2: /* 參數個(gè)數為1,則執行打開(kāi)或者關(guān)閉指令cache操作 */
switch (on_off(argv[1])) {
case 0: icache_disable(); //打開(kāi)指令cache
break;
case 1: icache_enable (); //關(guān)閉指令cache
break;
}
/* FALL TROUGH */
case 1: /* 參數個(gè)數為0,則獲取指令cache狀態(tài)*/
printf ("Instruction Cache is %s\n",
icache_status() ? "ON" : "OFF");
return 0;
default: //其他缺省情況下,打印命令使用說(shuō)明
printf ("Usage:\n%s\n", cmdtp->usage);
return 1;
}
return 0;
}
……
U_Boot_CMD( //通過(guò)宏定義命令
icache, 2, 1, do_icache, //命令為icache,命令執行函數為do_icache()
"icache - enable or disable instruction cache\n", //幫助信息
"[on, off]\n"
" - enable or disable instruction cache\n"
);
……
#endif
U-Boot的命令都是通過(guò)結構體__U_Boot_cmd_##name來(lái)描述的。根據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)打開(kāi)CONFIG_COMMANDS選項的命令標志位。這個(gè)程序文件開(kāi)頭有#if語(yǔ)句需要預處理是否包含這個(gè)命令函數。CONFIG_COMMANDS選項在開(kāi)發(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的調試
新移植的U-Boot不能正常工作,這時(shí)就需要調試了。調試U-Boot離不開(kāi)工具,只有理解U-Boot啟動(dòng)過(guò)程,才能正確地調試U-Boot源碼。
6.3.1 硬件調試器
硬件電路板制作完成以后,這時(shí)上面還沒(méi)有任何程序,就叫作裸板。首要的工作是把程序或者固件加載到裸板上,這就要通過(guò)硬件工具來(lái)完成。習慣上,這種硬件工具叫作仿真器。
仿真器可以通過(guò)處理器的JTAG等接口控制板子,直接把程序下載到目標板內存,或者進(jìn)行Flash編程。如果板上的Flash是可以拔插的,就可以通過(guò)專(zhuān)用的Flash燒寫(xiě)器來(lái)完成。在第4章介紹過(guò)目標板跟主機之間的連接,其中JTAG等接口就是專(zhuān)門(mén)用來(lái)連接仿真器的。
仿真器還有一個(gè)重要的功能就是在線(xiàn)調試程序,這對于調試Bootloader和硬件測試程序很有用。
從最簡(jiǎn)單的JTAG電纜,到ICE仿真器,再到可以調試Linux內核的仿真器。
復雜的仿真器可以支持與計算機間的以太網(wǎng)或者USB接口通信。
對于U-Boot的調試,可以采用BDI2000。BDI2000完全可以反匯編地跟蹤Flash中的程序,也可以進(jìn)行源碼級的調試。
使用BDI2000調試U-boot的方法如下。
(1)配置BDI2000和目標板初始化程序,連接目標板。
(2)添加U-Boot的調試編譯選項,重新編譯。
U-Boot的程序代碼是位置相關(guān)的,調試的時(shí)候盡量在內存中調試,可以修改連接定位地址TEXT_BASE。TEXT_BASE在board/<board_name>/config.mk中定義。
另外,如果有復位向量也需要先從鏈接腳本中去掉。鏈接腳本是board/<board_name>/
u-boot.lds。
添加調試選項,在config.mk文件中查找,DBGFLAGS,加上-g選項。然后重新編譯U-Boot。
(3)下載U-Boot到目標板內存。
通過(guò)BDI2000的下載命令LOAD,把程序加載到目標板內存中。然后跳轉到U-Boot入口。
(4)啟動(dòng)GDB調試。
啟動(dòng)GDB調試,這里是交叉調試的GDB。GDB與BDI2000建立鏈接,然后就可以設置斷點(diǎn)執行了。
$ 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沒(méi)有任何串口打印信息,手頭又沒(méi)有硬件調試工具,那樣怎么知道U-Boot執行到什么地方了呢?可以通過(guò)開(kāi)發(fā)板上的LED指示燈判斷。
開(kāi)發(fā)板上最好設計安裝八段數碼管等LED,可以用來(lái)顯示數字或者數字位。
U-Boot可以定義函數show_boot_progress (int status),用來(lái)指示當前啟動(dòng)進(jìn)度。在include/common.h頭文件中聲明這個(gè)函數。
#ifdef CONFIG_SHOW_BOOT_PROGRESS
void show_boot_progress (int status);
#endif
CONFIG_SHOW_BOOT_PROGRESS是需要定義的。這個(gè)在板子配置的頭文件中定義。CSB226開(kāi)發(fā)板對這項功能有完整實(shí)現,可以參考。在頭文件include/configs/csb226.h中,有下列一行。
#define CONFIG_SHOW_BOOT_PROGRESS 1
函數show_boot_progress (int status)的實(shí)現跟開(kāi)發(fā)板關(guān)系密切,所以一般在board目錄下的文件中實(shí)現??匆幌翪SB226在board/csb226/csb226.c中的實(shí)現函數。
/** 設置CSB226板的0、1、2三個(gè)指示燈的開(kāi)關(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;
}
/** 顯示啟動(dòng)進(jìn)度函數,在比較重要的階段,設置三個(gè)燈為亮的狀態(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啟動(dòng)過(guò)程中就可以通過(guò)show_boot_progresss指示執行進(jìn)度。比如hang()函數是系統出錯時(shí)調用的函數,這里需要根據特定的開(kāi)發(fā)板給定顯示的參數值。
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啟動(dòng)過(guò)程
盡管有了調試跟蹤手段,甚至也可以通過(guò)串口打印信息了,但是不一定能夠判斷出錯原因。如果能夠充分理解代碼的啟動(dòng)流程,那么對準確地解決和分析問(wèn)題很有幫助。
開(kāi)發(fā)板上電后,執行U-Boot的第一條指令,然后順序執行U-Boot啟動(dòng)函數。函數調用順序如圖6.3所示。
看一下board/smsk2410/u-boot.lds這個(gè)鏈接腳本,可以知道目標程序的各部分鏈接順序。第一個(gè)要鏈接的是cpu/arm920t/start.o,那么U-Boot的入口指令一定位于這個(gè)程序中。下面詳細分析一下程序跳轉和函數的調用關(guān)系以及函數實(shí)現。
1.cpu/arm920t/start.S
這個(gè)匯編程序是U-Boot的入口程序,開(kāi)頭就是復位向量的代碼。
圖6.3 U-Boot啟動(dòng)代碼流程圖
_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: //復位啟動(dòng)子程序
/* 設置CPU為SVC32模式 */
mrs r0,cpsr
bic r0,r0,#0x1f
orr r0,r0,#0xd3
msr cpsr,r0
/* 關(guān)閉看門(mén)狗 */
/* 這些初始化代碼在系統重起的時(shí)候執行,運行時(shí)熱復位從RAM中啟動(dòng)不執行 */
#ifdef CONFIG_INIT_CRITICAL
bl cpu_init_crit
#endif
relocate: /* 把U-Boot重新定位到RAM */
adr r0, _start /* r0是代碼的當前位置 */
ldr r1, _TEXT_BASE /* 測試判斷是從Flash啟動(dòng),還是RAM */
cmp r0, r1 /* 比較r0和r1,調試的時(shí)候不要執行重定位 */
beq stack_setup /* 如果r0等于r1,跳過(guò)重定位代碼 */
/* 準備重新定位代碼 */
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 /* 復制數據塊直到源數據末尾地址[r2] */
ble copy_loop
/* 初始化堆棧等 */
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
sub sp, r0, #12 /* 為abort-stack預留3個(gè)字 */
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
/* 跳轉到start_armboot函數入口,_start_armboot字保存函數入口指針 */
ldr pc, _start_armboot
_start_armboot: .word start_armboot //start_armboot函數在lib_arm/board.c中實(shí)現
/* 關(guān)鍵的初始化子程序 */
cpu_init_crit:
…… //初始化CACHE,關(guān)閉MMU等操作指令
/* 初始化RAM時(shí)鐘。
* 因為內存時(shí)鐘是依賴(lài)開(kāi)發(fā)板硬件的,所以在board的相應目錄下可以找到memsetup.S文件。
*/
mov ip, lr
bl memsetup //memsetup子程序在board/smdk2410/memsetup.S中實(shí)現
mov lr, ip
mov pc, lr
2.lib_arm/board.c
start_armboot是U-Boot執行的第一個(gè)C語(yǔ)言函數,完成系統初始化工作,進(jìn)入主循環(huán),處理用戶(hù)輸入的命令。
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;
/* 順序執行init_sequence數組中的初始化函數 */
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 (); /* 使能例外處理 */
/* 通過(guò)環(huán)境變量初始化 */
if ((s = getenv ("loadaddr")) != NULL) {
load_addr = simple_strtoul (s, NULL, 16);
}
/* main_loop()總是試圖自動(dòng)啟動(dòng),循環(huán)不斷執行 */
for (;;) {
main_loop (); /* 主循環(huán)函數處理執行用戶(hù)命令 -- common/main.c */
}
/* NOTREACHED - no way out of command loop except booting */
}
3.init_sequence[]
init_sequence[]數組保存著(zhù)基本的初始化函數指針。這些函數名稱(chēng)和實(shí)現的程序文件在下列注釋中。
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與內核的關(guān)系
U-Boot作為Bootloader,具備多種引導內核啟動(dòng)的方式。常用的go和bootm命令可以直接引導內核映像啟動(dòng)。U-Boot與內核的關(guān)系主要是內核啟動(dòng)過(guò)程中參數的傳遞。
1.go命令的實(shí)現
/* 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命令調用do_go()函數,跳轉到某個(gè)地址執行的。如果在這個(gè)地址準備好了自引導的內核映像,就可以啟動(dòng)了。盡管go命令可以帶變參,實(shí)際使用時(shí)一般不用來(lái)傳遞參數。
2.bootm命令的實(shí)現
/* 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命令調用do_bootm函數。這個(gè)函數專(zhuān)門(mén)用來(lái)引導各種操作系統映像,可以支持引導Linux、vxWorks、QNX等操作系統。引導Linux的時(shí)候,調用do_bootm_linux()函數。
3.do_bootm_linux函數的實(shí)現
/* 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()函數是專(zhuān)門(mén)引導Linux映像的函數,它還可以處理ramdisk文件系統的映像。這里引導的內核映像和ramdisk映像,必須是U-Boot格式的。U-Boot格式的映像可以通過(guò)mkimage工具來(lái)轉換,其中包含了U-Boot可以識別的符號。
6.4 使用U-Boot
U-Boot是“Monitor”。除了Bootloader的系統引導功能,它還有用戶(hù)命令接口,提供了一些復雜的調試、讀寫(xiě)內存、燒寫(xiě)Flash、配置環(huán)境變量等功能。掌握U-Boot的使用,將極大地方便嵌入式系統的開(kāi)發(fā)。
6.4.1 燒寫(xiě)U-Boot到Flash
新開(kāi)發(fā)的電路板沒(méi)有任何程序可以執行,也就不能啟動(dòng),需要先將U-Boot燒寫(xiě)到Flash中。
如果主板上的EPROM或者Flash能夠取下來(lái),就可以通過(guò)編程器燒寫(xiě)。例如:計算機BIOS就存儲在一塊256KB的Flash上,通過(guò)插座與主板連接。
但是多數嵌入式單板使用貼片的Flash,不能取下來(lái)燒寫(xiě)。這種情況可以通過(guò)處理器的調試接口,直接對板上的Flash編程。
處理器調試接口是為處理器芯片設計的標準調試接口,包含BDM、JTAG和EJTAG 3種接口標準。JTAG接口在第4章已經(jīng)介紹過(guò);BDM(Background Debug Mode)主要應用在PowerPC8xx系列處理器上;EJTAG主要應用在MIPS處理器上。這3種硬件接口標準定義有所不同,但是功能基本相同,下面都統稱(chēng)為JTAG接口。
JTAG接口需要專(zhuān)用的硬件工具來(lái)連接。無(wú)論從功能、性能角度,還是從價(jià)格角度,這些工具都有很大差異。關(guān)于這些工具的選擇,將在第6.4.1節詳細介紹。
最簡(jiǎn)單方式就是通過(guò)JTAG電纜,轉接到計算機并口連接。這需要在主機端開(kāi)發(fā)燒寫(xiě)程序,還需要有并口設備驅動(dòng)程序。開(kāi)發(fā)板上電或者復位的時(shí)候,燒寫(xiě)程序探測到處理器并且開(kāi)始通信,然后把Bootloader下載并燒寫(xiě)到Flash中。這種方式速率很慢,可是價(jià)格非常便宜。一般來(lái)說(shuō),平均每秒鐘可以燒寫(xiě)100~200個(gè)字節。
燒寫(xiě)完成后,復位實(shí)驗板,串口終端應該顯示U-Boot的啟動(dòng)信息。
6.4.2 U-Boot的常用命令
U-Boot上電啟動(dòng)后,敲任意鍵可以退出自動(dòng)啟動(dòng)狀態(tài),進(jìn)入命令行。
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的命令并執行。U-Boot可以支持幾十個(gè)常用命令,通過(guò)這些命令,可以對開(kāi)發(fā)板進(jìn)行調試,可以引導Linux內核,還可以擦寫(xiě)Flash完成系統部署等功能。掌握這些命令的使用,才能夠順利地進(jìn)行嵌入式系統的開(kāi)發(fā)。
輸入help命令,可以得到當前U-Boot的所有命令列表。每一條命令后面是簡(jiǎn)單的命令說(shuō)明。
=> 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還提供了更加詳細的命令幫助,通過(guò)help命令還可以查看每個(gè)命令的參數說(shuō)明。由于開(kāi)發(fā)過(guò)程的需要,有必要先把U-Boot命令的用法弄清楚。接下來(lái),根據每一條命令的幫助信息,解釋一下這些命令的功能和參數。
=> 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命令可以引導啟動(dòng)存儲在內存中的程序映像。這些內存包括RAM和可以永久保存的Flash。
第1個(gè)參數addr是程序映像的地址,這個(gè)程序映像必須轉換成U-Boot的格式。
第2個(gè)參數對于引導Linux內核有用,通常作為U-Boot格式的RAMDISK映像存儲地址;也可以是傳遞給Linux內核的參數(缺省情況下傳遞bootargs環(huán)境變量給內核)。
=> help bootp
bootp [loadAddress] [bootfilename]
bootp命令通過(guò)bootp請求,要求DHCP服務(wù)器分配IP地址,然后通過(guò)TFTP協(xié)議下載指定的文件到內存。
第1個(gè)參數是下載文件存放的內存地址。
第2個(gè)參數是要下載的文件名稱(chēng),這個(gè)文件應該在開(kāi)發(fā)主機上準備好。
=> help cmp
cmp [.b, .w, .l] addr1 addr2 count
- compare memory
cmp命令可以比較2塊內存中的內容。.b以字節為單位;.w以字為單位;.l以長(cháng)字為單位。注意:cmp.b中間不能保留空格,需要連續敲入命令。
第1個(gè)參數addr1是第一塊內存的起始地址。
第2個(gè)參數addr2是第二塊內存的起始地址。
第3個(gè)參數count是要比較的數目,單位按照字節、字或者長(cháng)字。
=> help cp
cp [.b, .w, .l] source target count
- copy memory
cp命令可以在內存中復制數據塊,包括對Flash的讀寫(xiě)操作。
第1個(gè)參數source是要復制的數據塊起始地址。
第2個(gè)參數target是數據塊要復制到的地址。這個(gè)地址如果在Flash中,那么會(huì )直接調用寫(xiě)Flash的函數操作。所以U-Boot寫(xiě)Flash就使用這個(gè)命令,當然需要先把對應Flash區域擦干凈。
第3個(gè)參數count是要復制的數目,根據cp.b cp.w cp.l分別以字節、字、長(cháng)字為單位。
=> help crc32
crc32 address count [addr]
- compute CRC32 checksum [save at addr]
crc32命令可以計算存儲數據的校驗和。
第1個(gè)參數address是需要校驗的數據起始地址。
第2個(gè)參數count是要校驗的數據字節數。
第3個(gè)參數addr用來(lái)指定保存結果的地址。
=> help echo
echo [args..]
- echo args to console; \c suppresses newline
echo命令回顯參數。
=> 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。
參數必須指定Flash擦除的范圍。
按照起始地址和結束地址,start必須是擦除塊的起始地址;end必須是擦除末尾塊的結束地址。這種方式最常用。舉例說(shuō)明:擦除0x20000 – 0x3ffff區域命令為erase 20000 3ffff。
按照組和扇區,N表示Flash的組號,SF表示擦除起始扇區號,SL表示擦除結束扇區號。另外,還可以擦除整個(gè)組,擦除組號為N的整個(gè)Flash組。擦除全部Flash只要給出一個(gè)all的參數即可。
=> help flinfo
flinfo
- print information for all FLASH memory banks
flinfo N
- print information for FLASH memory bank # N
flinfo命令打印全部Flash組的信息,也可以只打印其中某個(gè)組。一般嵌入式系統的Flash只有一個(gè)組。
=> help go
go addr [arg ...]
- start application at address 'addr'
passing 'arg' as arguments
go命令可以執行應用程序。
第1個(gè)參數是要執行程序的入口地址。
第2個(gè)可選參數是傳遞給程序的參數,可以不用。
=> 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可以打印程序映像的開(kāi)頭信息,包含了映像內容的校驗(序列號、頭和校驗和)。
第1個(gè)參數指定映像的起始地址。
可選的參數是指定更多的映像地址。
=> help loadb
loadb [ off ] [ baud ]
- load binary file over serial line with offset 'off' and baudrate 'baud'
loadb命令可以通過(guò)串口線(xiàn)下載二進(jìn)制格式文件。
=> help loads
loads [ off ]
- load S-Record file over serial line with offset 'off'
loads命令可以通過(guò)串口線(xiàn)下載S-Record格式文件。
=> help mw
mw [.b, .w, .l] address value [count]
- write memory
mw命令可以按照字節、字、長(cháng)字寫(xiě)內存,.b .w .l的用法與cp命令相同。
第1個(gè)參數address是要寫(xiě)的內存地址。
第2個(gè)參數value是要寫(xiě)的值。
第3個(gè)可選參數count是要寫(xiě)單位值的數目。
=> help nfs
nfs [loadAddress] [host ip addr:bootfilename]
nfs命令可以使用NFS網(wǎng)絡(luò )協(xié)議通過(guò)網(wǎng)絡(luò )啟動(dòng)映像。
=> help nm
nm [.b, .w, .l] address
- memory modify, read and keep address
nm命令可以修改內存,可以按照字節、字、長(cháng)字操作。
參數address是要讀出并且修改的內存地址。
=> help printenv
printenv
- print values of all environment variables
printenv name ...
- print value of environment variable 'name'
printenv命令打印環(huán)境變量。
可以打印全部環(huán)境變量,也可以只打印參數中列出的環(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寫(xiě)保護的操作,可以使能和解除寫(xiě)保護。
第1個(gè)參數on代表使能寫(xiě)保護;off代表解除寫(xiě)保護。
第2、3參數是指定Flash寫(xiě)保護操作范圍,跟擦除的方式相同。
=> help rarpboot
rarpboot [loadAddress] [bootfilename]
rarboot命令可以使用TFTP協(xié)議通過(guò)網(wǎng)絡(luò )啟動(dòng)映像。也就是把指定的文件下載到指定地址,然后執行。
第1個(gè)參數是映像文件下載到的內存地址。
第2個(gè)參數是要下載執行的映像文件。
=> help run
run var [...]
- run the commands in the environment variable(s) 'var'
run命令可以執行環(huán)境變量中的命令,后面參數可以跟幾個(gè)環(huán)境變量名。
=> help setenv
setenv name value ...
- set environment variable 'name' to 'value ...'
setenv name
- delete environment variable 'name'
setenv命令可以設置環(huán)境變量。
第1個(gè)參數是環(huán)境變量的名稱(chēng)。
第2個(gè)參數是要設置的值,如果沒(méi)有第2個(gè)參數,表示刪除這個(gè)環(huán)境變量。
=> help sleep
sleep N
- delay execution for N seconds (N is _decimal_ !!!)
sleep命令可以延遲N秒鐘執行,N為十進(jìn)制數。
=> help tftpboot
tftpboot [loadAddress] [bootfilename]
tftpboot命令可以使用TFTP協(xié)議通過(guò)網(wǎng)絡(luò )下載文件。按照二進(jìn)制文件格式下載。另外使用這個(gè)命令,必須配置好相關(guān)的環(huán)境變量。例如serverip和ipaddr。
第1個(gè)參數loadAddress是下載到的內存地址。
第2個(gè)參數是要下載的文件名稱(chēng),必須放在TFTP服務(wù)器相應的目錄下。
這些U-Boot命令為嵌入式系統提供了豐富的開(kāi)發(fā)和調試功能。在Linux內核啟動(dòng)和調試過(guò)程中,都可以用到U-Boot的命令。但是一般情況下,不需要使用全部命令。比如已經(jīng)支持以太網(wǎng)接口,可以通過(guò)tftpboot命令來(lái)下載文件,那么還有必要使用串口下載的loadb嗎?反過(guò)來(lái),如果開(kāi)發(fā)板需要特殊的調試功能,也可以添加新的命令。
在建立交叉開(kāi)發(fā)環(huán)境和調試Linux內核等章節時(shí),在A(yíng)RM平臺上移植了U-Boot,并且提供了具體U-Boot的操作步驟。
6.4.3 U-Boot的環(huán)境變量
有點(diǎn)類(lèi)似Shell,U-Boot也使用環(huán)境變量??梢酝ㄟ^(guò)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)境變量的含義解釋。通過(guò)printenv命令可以打印出這些變量的值。
表6.5 U-Boot環(huán)境變量的解釋說(shuō)明
環(huán) 境 變 量
解 釋 說(shuō) 明
bootdelay
定義執行自動(dòng)啟動(dòng)的等候秒數
baudrate
定義串口控制臺的波特率
netmask
定義以太網(wǎng)接口的掩碼
ethaddr
定義以太網(wǎng)接口的MAC地址
bootfile
定義缺省的下載文件
bootargs
定義傳遞給Linux內核的命令行參數
bootcmd
定義自動(dòng)啟動(dòng)時(shí)執行的幾條命令
serverip
定義tftp服務(wù)器端的IP地址
ipaddr
定義本地的IP地址
stdin
定義標準輸入設備,一般是串口
stdout
定義標準輸出設備,一般是串口
stderr
定義標準出錯信息輸出設備,一般是串口
U-Boot的環(huán)境變量都可以有缺省值,也可以修改并且保存在參數區。U-Boot的參數區一般有EEPROM和Flash兩種設備。
環(huán)境變量的設置命令為setenv,在6.2.2節有命令的解釋。
舉例說(shuō)明環(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定義命令行參數,通過(guò)bootm命令傳遞給內核。環(huán)境變量nfscmd中也使用了環(huán)境變量,功能是把uImage下載到指定的地址并且引導起來(lái)??梢酝ㄟ^(guò)run命令執行nfscmd腳本。