參考:http://blog.csdn.net/hare_lee/article/details/6916325
********************************************* 前序 *****************************************************
bootloader是系統上電后最初加載運行的代碼。它提供了處理器上電復位后最開始需要執行的初始化代碼。
PC機上引導程序一般由BIOS開始執行,然后讀取硬盤中位於MBR(Main Boot Record,主引導記錄)中的Bootloader(例如LILO或GRUB),並進一步引導操作系統的啟動。
嵌入式系統中通常沒有像BIOS那樣的固件程序,因此整個系統的加載啟動就完全由bootloader來完成,它主要的功能是加載與引導內核映像。
一個嵌入式的存儲設備通過通常包括四個分區:
第一分區:存放的當然是u-boot
第二個分區:存放着u-boot要傳給系統內核的參數
第三個分區:是系統內核(kernel)
第四個分區:則是根文件系統
進入開發板/sys/class/mtd/目錄下,執行ls命令查看 :
mtd0 mtd1 mtd2 mtd3 mtd4 mtd5
mtd0ro mtd1ro mtd2ro mtd3ro mtd4ro mtd5ro
開發板存儲設置被分成5個區:
cat mtd0/name U-Boot
cat mtd1/name U-Boot Env
cat mtd2/name U-Boot Logo
cat mtd3/name Kernel
cat mtd4/name File System
Bootloader介紹
Bootloader的定義:Bootloader是在操作系統運行之前執行的一小段程序,通過這一小段程序,我們可以初始化硬件設備、建立內存空間的映射表,從而建立適當的系統軟硬件環境,為最終調用操作系統內核做好准備。意思就是說如果我們要想讓一個操作系統在我們的板子上運轉起來,我們就必須首先對我們的板子進行一些基本配置和初始化,然后才可以將操作系統引導進來運行。
具體在Bootloader中完成了哪些操作我們會在后面分析到,這里我們先來回憶一下PC的體系結構:PC機中的引導加載程序是由BIOS和位於硬盤MBR中的OS Boot Loader(比如LILO和GRUB等)一起組成的,BIOS在完成硬件檢測和資源分配后,將硬盤MBR中的Boot Loader讀到系統的RAM中,然后將控制權交給OS Boot Loader。Boot Loader的主要運行任務就是將內核映象從硬盤上讀到RAM中,然后跳轉到內核的入口點去運行,即開始啟動操作系統。
在嵌入式系統中,通常並沒有像BIOS那樣的固件程序(注:有的嵌入式cpu也會內嵌一段短小的啟動程序),因此整個系統的加載啟動任務就完全由Boot Loader來完成。
比如在一個基於ARM7TDMI core的嵌入式系統中,系統在上電或復位時通常都從地址0x00000000處開始執行,而在這個地址處安排的通常就是系統的Boot Loader程序。(先想一下,通用PC和嵌入式系統為何會在此處存在如此的差異呢?)
Bootloader是基於特定硬件平台來實現的,因此幾乎不可能為所有的嵌入式系統建立一個通用的Bootloader,不同的處理器架構都有不同的Bootloader,Bootloader不但依賴於cpu的體系結構,還依賴於嵌入式系統板級設備的配置。對於2塊不同的板子而言,即使他們使用的是相同的處理器,要想讓運行在一塊板子上的Bootloader程序也能運行在另一塊板子上,一般也需要修改Bootloader的源程序。
Bootloader的啟動方式
Bootloader的啟動方式主要有網絡啟動方式、磁盤啟動方式和Flash啟動方式,當然還可以有其他啟動方式,例如:MMC等。
1、網絡啟動方式
圖1 Bootloader網絡啟動方式示意圖
如圖1所示,里面主機和目標板,他們中間通過網絡來連接,首先目標板的DHCP/BIOS通過BOOTP服務來為Bootloader分配IP地址,配置網絡參數,這樣才能支持網絡傳輸功能。
我們使用的u-boot可以直接設置網絡參數,因此這里就不用使用DHCP的方式動態分配IP了。
接下來目標板的Bootloader通過TFTP服務將內核映像下載到目標板上,然后通過網絡文件系統來建立主機與目標板之間的文件通信過程,之后的系統更新通常也是使用Boot Loader的這種工作模式。
工作於這種模式下的Boot Loader通常都會向它的終端用戶提供一個簡單的命令行接口。
2、磁盤啟動方式
這種方式主要是用在台式機和服務器上的,這些計算機都使用BIOS引導,並且使用磁盤作為存儲介質,這里面兩個重要的用來啟動linux的有LILO和GRUB,這里就不再具體說明了。
3、Flash啟動方式
這是我們最常用的方式。Flash有NOR Flash和NAND Flash兩種。NOR Flash可以支持隨機訪問,所以代碼可以直接在Flash上執行,Bootloader一般是存儲在Flash芯片上的。另外Flash上還存儲着參數、內核映像和文件系統。
這種啟動方式與網絡啟動方式之間的不同之處就在於,在網絡啟動方式中,內核映像和文件系統首先是放在主機上的,然后經過網絡傳輸下載進目標板的,而這種啟動方式中內核映像和文件系統則直接是放在Flash中的,這兩點在我們u-boot的使用過程中都用到了。
u-boot是一種普遍用於嵌入式系統中的Bootloader。
********************************************* 第一、U-Boot介紹 *****************************************************
U-boot的定義
U-boot,全稱Universal Boot Loader,是由DENX小組的開發的遵循GPL條款的開放源碼項目,它的主要功能是完成硬件設備初始化、操作系統代碼搬運,並提供一個控制台及一個指令集在操作系統運行前操控硬件設備。
U-boot之所以這么通用,原因是他具有很多特點:開放源代碼、支持多種嵌入式操作系統內核、支持多種處理器系列、較高的穩定性、高度靈活的功能設置、豐富的設備驅動源碼以及較為豐富的開發調試文檔與強大的網絡技術支持。另外u-boot對操作系統和產品研發提供了靈活豐富的支持,主要表現在:可以引導壓縮或非壓縮系統內核,可以靈活設置/傳遞多個關鍵參數給操作系統,適合系統在不同開發階段的調試要求與產品發布,支持多種文件系統,支持多種目標板環境參數存儲介質,采用CRC32校驗,可校驗內核及鏡像文件是否完好,提供多種控制台接口,使用戶可以在不需要ICE的情況下通過串口/以太網/USB等接口下載數據並燒錄到存儲設備中去(這個功能在實際的產品中是很實用的,尤其是在軟件現場升級的時候),以及提供豐富的設備驅動等。
U-boot源代碼的目錄結構
board 中存放於開發板相關的配置文件,每一個開發板都以子文件夾的形式出現。
Commom 文件夾實現u-boot行下支持的命令,每一個命令對應一個文件。
cpu 中存放特定cpu架構相關的目錄,每一款cpu架構都對應了一個子目錄。
Doc 是文檔目錄,有u-boot非常完善的文檔。
Drivers 中是u-boot支持的各種設備的驅動程序。
Fs 是支持的文件系統,其中最常用的是JFFS2文件系統。
Include 文件夾是u-boot使用的頭文件,還有各種硬件平台支持的匯編文件,系統配置文件和文件系統支持的文件。
Net 是與網絡協議相關的代碼,bootp協議、TFTP協議、NFS文件系統得實現。
Tooles 是生成U-boot的工具。
對u-boot的目錄有了一些了解后,分析啟動代碼的過程就方便多了,其中比較重要的目錄就是/board、/cpu、/drivers和/include目錄,如果想實現u-boot在一個平台上的移植,就要對這些目錄進行深入的分析。
-------------------------------------------------------------------------------------------------------------------------------------------
什么是《編譯地址》?什么是《運行地址》?
1. 編譯地址:32位的處理器,它的每一條指令是4個字節,以4個字節存儲順序,進行順序執行,CPU是順序執行的,只要沒發生什么跳轉,它會順序進行執行行, 編譯器會對每一條指令分配一個編譯地址,這是編譯器分配的,在編譯過程中分配的地址,我們稱之為編譯地址。
2. 運行地址:是指程序指令真正運行的地址,是由用戶指定的,用戶將運行地址燒錄到哪里,哪里就是運行的地址。
比如有一個指令的編譯地址是0x5,實際運行的地址是0x200,如果用戶將指令燒到0x200上,那么這條指令的運行地址就是0x200,
當編譯地址和運行地址不同的時候會出現什么結果?結果是不能跳轉,編譯后會產生跳轉地址,如果實際地址和編譯后產生的地址不相等,那么就不能跳轉。
C語言編譯地址:都希望把編譯地址和實際運行地址放在一起的,但是匯編代碼因為不需要做C語言到匯編的轉換,可以認為的去寫地址,所以直接寫的就是他的運行地址這就是為什么任何bootloader剛開始會有一段匯編代碼,因為起始代碼編譯地址和實際地址不相等,這段代碼和匯編無關,跳轉用的運行地址。
編譯地址和運行地址如何來算呢?
1.假如有兩個編譯地址a=0x10,b=0x7,b的運行地址是0x300,那么a的運行地址就是b的運行地址加上兩者編譯地址的差值,a-b=0x10-0x7=0x3,a的運行地址就是0x300+0x3=0x303。
2.假設uboot上兩條指令的編譯地址為a=0x33000007和b=0x33000001,這兩條指令都落在bank6上,現在要計算出他們對應的運行地址,要找出運行地址的始地址,這個是由用戶燒錄進去的,假設運行地址的首地址是0x0,則a的運行地址為0x7,b為0x1,就是這樣算出來的。
為什么要分配編譯地址?這樣做有什么好處,有什么作用?
比如在函數a中定義了函數b,當執行到函數b時要進行指令跳轉,要跳轉到b函數所對應的起始地址上去,編譯時,編譯器給每條指令都分配了編譯地址,如果編譯器已經給分配了地址就可以直接進行跳轉,查找b函數跳轉指令所對應的表,進行直接跳轉,因為有個編譯地址和指令對應的一個表,如果沒有分配,編譯器就查找不到這個跳轉地址,要進行計算,非常麻煩。
什么是《相對地址》?
以NOR Flash為例,NOR Falsh是映射到bank0上面,SDRAM是映射到bank6上面,uboot和內核最終是在SDRAM上面運行,最開始我們是從Nor Flash的零地址開始往后燒錄,uboot中至少有一段代碼編譯地址和運行地址是不一樣的,編譯uboot或內核時,都會將編譯地址放入到SDRAM中,他們最終都會在SDRAM中執行,剛開始uboot在Nor Flash中運行,運行地址是一個低端地址,是bank0中的一個地址,但編譯地址是bank6中的地址,這樣就會導致絕對跳轉指令執行的失敗,所以就引出了相對地址的概念。
那么什么是相對地址呢?
至少在bank0中uboot這段代碼要知道不能用b+編譯地址這樣的方法去跳轉指令,因為這段代碼的編譯地址和運行地址不一樣,那如何去做呢?
要去計算這個指令運行的真實地址,計算出來后再做跳轉,應該是b+運行地址,不能出現b+編譯地址,而是b+運行地址,而運行地址是算出來的。
-------------------------------------------------------------------------------------------------------------------------------------------
U-Boot工作過程
大多數 Boot Loader 都包含兩種不同的操作模式:"啟動加載"模式和"下載"模式,這種區別僅對於開發人員才有意義。
但從最終用戶的角度看,Boot Loader 的作用就是:用來加載操作系統,而並不存在所謂的啟動加載模式與下載工作模式的區別。
(一)啟動加載(Boot loading)模式:這種模式也稱為"自主"(Autonomous)模式。
也即 Boot Loader 從目標機上的某個固態存儲設備上將操作系統加載到 RAM 中運行,整個過程並沒有用戶的介入。
這種模式是 Boot Loader 的正常工作模式,因此在嵌入式產品發布的時侯,Boot Loader 顯然必須工作在這種模式下。
(二)下載(Downloading)模式:在這種模式下,目標機上的 Boot Loader 將通過串口連接或網絡連接等通信手段從主機(Host)下載文件,比如:下載內核映像和根文件系統映像等。
從主機下載的文件通常首先被 Boot Loader保存到目標機的RAM 中,然后再被 BootLoader寫到目標機上的FLASH類固態存儲設備中。
Boot Loader 的這種模式通常在第一次安裝內核與根文件系統時被使用;此外,以后的系統更新也會使用 Boot Loader 的這種工作模式。工作於這種模式下的 Boot Loader 通常都會向它的終端用戶提供一個簡單的命令行接口。
這種工作模式通常在第一次安裝內核與跟文件系統時使用。或者在系統更新時使用。進行嵌入式系統調試時一般也讓bootloader工作在這一模式下。
UBoot 這樣功能強大的 Boot Loader 同時支持這兩種工作模式,而且允許用戶在這兩種工作模式之間進行切換。
大多數 bootloader 都分為階段 1(stage1)和階段 2(stage2)兩大部分,uboot 也不例外。
依賴於 CPU 體系結構的代碼(如 CPU 初始化代碼等)通常都放在階段 1 中且通常用匯編語言實現;而階段 2 則通常用 C 語言來實現,這樣可以實現復雜的功能,而且有更好的可讀性和移植性。
******************************************** 第二、U-Boot總體分析 ****************************************************
系統啟動的入口點。既然我們現在要分析u-boot的啟動過程,就必須先找到u-boot最先實現的是哪些代碼,最先完成的是哪些任務。
另一方面一個可執行的image必須有一個入口點,並且只能有一個全局入口點,所以要通知編譯器這個入口在哪里。由此我們可以找到程序的入口點是在/board/ti/ti8168_dvr/u-boot.lds中指定的,其中ENTRY(_start)說明程序從_start開始運行,而他指向的是cpu/arm_cortexa8/start.o文件。
因為我們用的是cortex-a8的cpu架構,在復位后從地址0x00000000取它的第一條指令,所以我們將Flash映射到這個地址上,這樣在系統加電后,cpu將首先執行u-boot程序。
u-boot的啟動過程是多階段實現的,分了兩個階段。
依賴於cpu體系結構的代碼(如設備初始化代碼等)通常都放在stage1中,而且通常都是用匯編語言來實現,以達到短小精悍的目的。
而stage2則通常是用C語言來實現的,這樣可以實現復雜的功能,而且代碼具有更好的可讀性和可移植性。
U-Boot啟動內核的過程可以分為兩個階段,兩個階段的功能如下:
(1)第一階段的功能
Ø 硬件設備初始化
Ø 加載U-Boot第二階段代碼到RAM空間
Ø 設置好棧
Ø 跳轉到第二階段代碼入口
(2)第二階段的功能
Ø 初始化本階段使用的硬件設備
Ø 檢測系統內存映射
Ø 將內核從Flash讀取到RAM中
Ø 為內核設置啟動參數
Ø 調用內核
代碼真正開始是在_start,設置異常向量表,這樣在cpu發生異常時就跳轉到/arch/arm/lib/interrupts中去執行相應得中斷代碼。
在interrupts文件中大部分的異常代碼都沒有實現具體的功能,只是打印一些異常消息,其中關鍵的是reset中斷代碼,跳到reset入口地址。
reset復位入口之前有一些段的聲明。
因為我們用的是 cortex-a8 的 cpu 架構,在CPU復位后從iROM地址0x00000000取它的第一條指令,執行iROM代碼的功能是把flash中的前16K的代碼加載到iRAM中,系統上電后將首先執行 u-boot 程序。
1.stage1:cpu/arm_cortexa8/start.S
2.當系統啟動時, ARM CPU 會跳到 0x00000000去執行,一般 BootLoader 包括如下幾個部分:
1> 建立異常向量表
2> 顯示的切換到 SVC 且 32 指令模式
3> 設置異常向量表
4> 關閉 TLB,MMU,cache,刷新指令 cache 數據 cache
5> 關閉內部看門狗
6> 禁止所有的中斷
7> 串口初始化
8> tzpc(TrustZone Protection Controller)
9> 配置系統時鍾頻率和總線頻率
10> 設置內存區的控制寄存器
11> 設置堆棧
12> 代碼的搬移階段
代碼的搬移階段:為了獲得更快的執行速度,通常把stage2加載到RAM空間中來執行,因此必須為加載Boot Loader的stage2准備好一段可用的RAM空間范圍。空間大小最好是memory page大小(通常是4KB)的倍數,一般而言,1M的RAM空間已經足夠了。
flash中存儲的u-boot可執行文件中,代碼段、數據段以及BSS段都是首尾相連存儲的,所以在計算搬移大小的時候就是利用了用BSS段的首地址減去代碼的首地址,這樣算出來的就是實際使用的空間。
程序用一個循環將代碼搬移到0x81180000,即RAM底端1M空間用來存儲代碼。然后程序繼續將中斷向量表搬到RAM的頂端。
由於stage2通常是C語言執行代碼,所以還要建立堆棧去。在堆棧區之前還要將malloc分配的空間以及全局數據所需的空間空下來,他們的大小是由宏定義給出的,可以在相應位置修改。
13> 跳到 C 代碼部分執行
基本內存分布圖(只供參考):
3. 下來是u-boot啟動的第二個階段,是用c代碼寫的,
這部分是一些相對變化不大的部分,我們針對不同的板子改變它調用的一些初始化函數,並且通過設置一些宏定義來改變初始化的流程,
所以這些代碼在移植的過程中並不需要修改,也是錯誤相對較少出現的文件。
在文件的開始先是定義了一個函數指針數組,通過這個數組,程序通過一個循環來按順序進行常規的初始化,並在其后通過一些宏定義來初始化一些特定的設備。
在最后程序進入一個循環,main_loop。這個循環接收用戶輸入的命令,以設置參數或者進行啟動引導。
********************************************* 第三、代碼分析 **************************************************
1 定義入口
由於一個可執行的 Image 必須有一個入口點,並且只能有一個全局入口,通常這個入口放在 ROM(Flash)的 0x0地址,因此,必須通知編譯器以使其知道這個入口,該工作可通過修改連接器腳本來完成。
1. board/ti/ti8168_dvr/uboot.lds: ENTRY(_start) ==> arch/arm/cpu/cortex-a8/start.S: .globl _start
2. uboot 代碼區(TEXT_BASE = 0x08070000) 定義在 board/ti/ti8168_dvr/config.mk
第一階段對應的文件是 arch/arm/cpu/cortex-a8/start.S 和 arch/arm/cpu/cortex-a8/ti81xx/lowlevel_init.S。
U-Boot啟動第一階段流程如下:
根據cpu/cortex_a8/u-boot.lds中指定的連接方式:
看一下uboot.lds文件,在board/ti/ti8168_dvr/目錄下面,uboot.lds是告訴編譯器這些段改怎么划分。
GUN編譯過的段,最基本的三個段是RO,RW,ZI,RO表示只讀,對應於具體的指代碼段,RW是數據段,ZI是歸零段,就是全局變量的那段。
Uboot代碼這么多,如何保證start.s會第一個執行,編譯在最開始呢?就是通過uboot.lds鏈接文件進行
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x00000000;//起始地址 . = ALIGN(4);//4字節對齊 .text : //test指代碼段,上面3行標識是不占用任何空間的 { arch/arm/cpu/arm_cortexa8/start.o (.text) //這里把start.o放在第一位就表示把start.s編譯時放到最開始,這就是為什么把uboot燒到起始地址上它肯定運行的是start.s arch/arm/cpu/arm_cortexa8/ti81xx/lowlevel_init.o (.text) *(.text) } . = ALIGN(4);//前面的 “.” 代表當前值,是計算一個當前的值,是計算上面占用的整個空間,再加一個單元就表示它現在的位置 .rodata : { *(.rodata) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); .got : { *(.got) } . = .; __u_boot_cmd_start = .; .u_boot_cmd : { *(.u_boot_cmd) } __u_boot_cmd_end = .; . = ALIGN(4); __bss_start = .; .bss (NOLOAD) : { *(.bss) . = ALIGN(4); } _end = .; } OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") /*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*/ OUTPUT_ARCH(arm) ENTRY(_start)
第一個鏈接的是cpu/arm_cortexa8/start.o,因此u-boot.bin的入口代碼在cpu/arm_cortexa8/start.o中,其源代碼在cpu/arm_cortexa8/start.S中。下面我們來分析cpu/arm_cortexa8/start.S的執行。
2. 硬件設備初始化
1> 設置異常向量
下面代碼是系統啟動后U-boot上電后運行的第一段代碼,它是什么意思?
u-boot對應的第一階段代碼放在cpu/arm_cortexa8/start.S文件中,入口代碼如下:
.globl _start /*聲明一個符號可被其它文件引用,相當於聲明了一個全局變量,.globl與.global相同*/
_start: b reset /* 復位,b是不帶返回的跳轉(bl是帶返回的跳轉),意思是無條件直接跳轉到reset標號出執行程序*/
ldr pc, _undefined_instruction /* 未定義指令向量 l---dr相當於mov操作*/
ldr pc, _software_interrupt /* 軟件中斷向量 */
ldr pc, _prefetch_abort /* 預取指令異常向量 */
ldr pc, _data_abort /* 數據操作異常向量 */
ldr pc, _not_used /* 未使用 */
ldr pc, _irq /* irq中斷向量 */
ldr pc, _fiq /* fiq中斷向量 */
/* 中斷向量表入口地址 */
_undefined_instruction: .word undefined_instruction /*就是在當前地址,即_undefined_instruction 處存放 undefined_instruction*/
_software_interrupt: .word software_interrupt
_prefetch_abort: .word prefetch_abort
_data_abort: .word data_abort
_not_used: .word not_used
_irq: .word irq
_fiq: .word fiq //word偽操作用於分配一段字內存單元(分配的單元都是字對齊的),並用偽操作中的expr初始化
_pad: .word 0x12345678 /* now 16*4=64 */
.global _end_vect
_end_vect:
.balignl 16,0xdeadbeef
他們是系統定義的異常,一上電程序跳轉到reset異常處執行相應的匯編指令,下面定義出的都是不同的異常,比如軟件發生軟中斷時,CPU就會去執行軟中斷的指令,這些異常中斷在CUP中地址是從0開始,每個異常占4個字節。
ldr pc, _undefined_instruction:表示把_undefined_instruction存放的數值存放到pc指針上。
_undefined_instruction: .word undefined_instruction:表示未定義的這個異常是由.word來定義的,它表示定義一個字,一個32位的數。
.word后面的數:表示把該標識的編譯地址寫入當前地址,標識是不占用任何指令的。把標識存放的數值copy到指針pc上面,那么標識上存放的值是什么?是由.word undefined_instruction來指定的,pc就代表你運行代碼的地址,她就實現了CPU要做一次跳轉時的工作。
以上代碼設置了ARM異常向量表,各個異常向量介紹如下:
地址 |
異常 |
進入模式 |
描述 |
0x00000000 |
復位 |
管理模式 |
復位電平有效時,產生復位異常,程序跳轉到復位處理程序處執行 |
0x00000004 |
未定義指令 |
未定義模式 |
遇到不能處理的指令時,產生未定義指令異常 |
0x00000008 |
軟件中斷 |
管理模式 |
執行SWI指令產生,用於用戶模式下的程序調用特權操作指令 |
0x0000000c |
預存指令 |
中止模式 |
處理器預取指令的地址不存在,或該地址不允許當前指令訪問,產生指令預取中止異常 |
0x00000010 |
數據操作 |
中止模式 |
處理器數據訪問指令的地址不存在,或該地址不允許當前指令訪問時,產生數據中止異常 |
0x00000014 |
未使用 |
未使用 |
未使用 |
0x00000018 |
IRQ |
IRQ |
外部中斷請求有效,且CPSR中的I位為0時,產生IRQ異常 |
0x0000001c |
FIQ |
FIQ |
快速中斷請求引腳有效,且CPSR中的F位為0時,產生FIQ異常 |
在cpu/arm_cortexa8/start.S中還有這些異常對應的異常處理程序。當一個異常產生時,CPU根據異常號在異常向量表中找到對應的異常向量,然后執行異常向量處的跳轉指令,CPU就跳轉到對應的異常處理程序執行。
其中復位異常向量的指令“b reset”決定了U-Boot啟動后將自動跳轉到標號“reset”處執行。
_TEXT_BASE: .word TEXT_BASE //0x07080000,在board/ti/ti8168_dvr/config.mk中,這段話表示,用戶告訴編譯器編譯地址的起始地址 .globl _armboot_start _armboot_start: .word _start /*_start 是uboot的第一行代碼的標號,代表的是第一行代碼的地址*/ .globl _bss_start _bss_start: .word __bss_start //在cpu/arm_cortexa8/u-boot.lds中定義 .globl _bss_end _bss_end: .word _end //在cpu/arm_cortexa8/u-boot.lds中定義 #ifdef CONFIG_USE_IRQ // 這個宏沒有定義,預編譯不執行 /* IRQ stack memory (calculated at run-time) */ .globl IRQ_STACK_START IRQ_STACK_START: .word 0x0badc0de /* IRQ stack memory (calculated at run-time) */ .globl FIQ_STACK_START FIQ_STACK_START: .word 0x0badc0de #endif
2> CPU進入SVC模式
reset: /* * set the cpu to SVC32 mode */ mrs r0, cpsr bic r0, r0, #0x1f /*工作模式位清零 */ orr r0, r0, #0xd3 /*工作模式位設置為“10011”(管理模式),並將中斷禁止位和快中斷禁止位置1 */ msr cpsr, r0
以上代碼將CPU的工作模式位設置為管理模式,即設置相應的CPSR程序狀態字,並將中斷禁止位和快中斷禁止位置一,從而屏蔽了IRQ和FIQ中斷。
操作系統先注冊一個總的中斷,然后去查是由哪個中斷源產生的中斷,再去查用戶注冊的中斷表,查出來后就去執行用戶定義的用戶中斷處理函數。
#if (CONFIG_OMAP34XX) // 這個宏沒有定義,下面的代碼不會預編譯 /* Copy vectors to mask ROM indirect addr */ adr r0, _start @ r0 <- current position of code add r0, r0, #4 @ skip reset vector mov r2, #64 @ r2 <- size to copy add r2, r0, r2 @ r2 <- source end address mov r1, #SRAM_OFFSET0 @ build vect addr mov r3, #SRAM_OFFSET1 add r1, r1, r3 mov r3, #SRAM_OFFSET2 add r1, r1, r3 next: ldmia r0!, {r3 - r10} @ copy from source address [r0] stmia r1!, {r3 - r10} @ copy to target address [r1] cmp r0, r2 @ until source end address [r2] bne next @ loop until equal */ #if !defined(CONFIG_SYS_NAND_BOOT) && !defined(CONFIG_SYS_ONENAND_BOOT) /* No need to copy/exec the clock code - DPLL adjust already done * in NAND/oneNAND Boot. */ bl cpy_clk_code @ put dpll adjust code behind vectors #endif /* NAND Boot */ #endif
3> CPU初始化
/* the mask ROM code should have PLL and others stable */ #ifndef CONFIG_SKIP_LOWLEVEL_INIT bl cpu_init_crit #endif
cpu_init_crit這段代碼在U-Boot正常啟動時才需要執行,若將U-Boot從RAM中啟動則應該注釋掉這段代碼。下面分析一下cpu_init_crit到底做了什么:
/************************************************************************* * * CPU_init_critical registers * * setup important registers * setup memory timing * *************************************************************************/ cpu_init_crit: /* * Invalidate L1 I/D */ mov r0, #0 @ set up for MCR mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs //將0寫入c8,使TLB內容無效 mcr p15, 0, r0, c7, c5, 0 @ invalidate icache //將0寫入c7,使Cache內容無效 /* * disable MMU stuff and caches */ mrc p15, 0, r0, c1, c0, 0 bic r0, r0, #0x00002000 @ clear bits 13 (--V-) bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM) orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align orr r0, r0, #0x00000800 @ set bit 12 (Z---) BTB mcr p15, 0, r0, c1, c0, 0 //修改CP15的c1寄存器來實現關閉MMU /* * Jump to board specific initialization... * The Mask ROM will have already initialized * basic memory. Go here to bump up clock rate and handle * wake up conditions. */ mov ip, lr @ persevere link reg across call bl lowlevel_init @ go setup pll,mux,memory mov lr, ip @ restore link mov pc, lr @ back to my caller
代碼中的c0,c1,c7,c8都是cortex-a8的協處理器CP15的寄存器。其中c7是cache控制寄存器,c8是TLB控制寄存器。325~327行代碼將0寫入c7、c8,使Cache,TLB內容無效。
第332~337行代碼關閉了MMU。這是通過修改CP15的c1寄存器來實現的,先看CP15的c1寄存器的格式(僅列出代碼中用到的位):
CP15的c1寄存器格式(部分)
15 |
14 |
13 |
12 |
11 |
10 |
9 |
8 |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
. |
. |
V |
I |
. |
. |
R |
S |
B |
. |
. |
. |
. |
C |
A |
M |
各個位的意義如下:
V :表示異常向量表所在的位置,0:異常向量在0x00000000;1:異常向量在 0xFFFF0000
I : 0 :關閉ICaches;1 :開啟ICaches
R、S : 用來與頁表中的描述符一起確定內存的訪問權限
B : 0 :CPU為小字節序;1 : CPU為大字節序
C : 0:關閉DCaches;1:開啟DCaches
A : 0:數據訪問時不進行地址對齊檢查;1:數據訪問時進行地址對齊檢查。
M : 0:關閉MMU;1:開啟MMU
為什么要關閉catch和MMU呢?catch和MMU是做什么用的?
MMU是Memory Management Unit的縮寫,中文名是內存管理單元,它是中央處理器(CPU)中用來管理虛擬存儲器、物理存儲器的控制線路, 同時也負責虛擬地址映射為物理地址,以及提供硬件機制的內存訪問授權
概述:
一,關catch
catch和MMU是通過CP15管理的,剛上電的時候,CPU還不能管理他們
上電的時候MMU必須關閉,指令catch可關閉,可不關閉,但數據catch一定要關閉
否則可能導致剛開始的代碼里面,去取數據的時候,從catch里面取,而這時候RAM中數據還沒有catch過來,導致數據預取異常
二:關MMU
因為MMU是;把虛擬地址轉化為物理地址得作用
而目的是設置控制寄存器,而控制寄存器本來就是實地址(物理地址),再使能MMU,不就是多此一舉了嗎?
詳細分析---
Catch是cpu內部的一個2級緩存,它的作用是將常用的數據和指令放在cpu內部,MMU是用來把虛實地址轉換為物理地址用的
我們的目的:是設置控制的寄存器,寄存器都是實地址(物理地址),如果既要開啟MMU又要做虛實地址轉換的話,中間還多一步,多此一舉了嘛?
先要把實地址轉換成虛地址,然后再做設置,但對uboot而言就是起到一個簡單的初始化的作用和引導操作系統,如果開啟MMU的話,很麻煩,也沒必要,所以關閉MMU.
說到catch就必須提到一個關鍵字Volatile,以后在設置寄存器時會經常遇到,他的本質:是告訴編譯器不要對我的代碼進行優化,作用是讓編寫者感覺不倒變量的變化情況(也就是說,讓它執行速度加快吧)
優化的過程:是將常用的代碼取出來放到catch中,它沒有從實際的物理地址去取,它直接從cpu的緩存中去取,但常用的代碼就是為了感覺一些常用變量的變化。
優化原因:如果正在取數據的時候發生跳變,那么就感覺不到變量的變化了,所以在這種情況下要用Volatile關鍵字告訴編譯器不要做優化,每次從實際的物理地址中去取指令,這就是為什么關閉catch關閉MMU。但在C語言中是不會關閉catch和MMU的,會打開,如果編寫者要感覺外界變化,或變化太快,從catch中取數據會有誤差,就加一個關鍵字Volatile。
4> 初始化RAM控制寄存器
bl lowlevel_init下來初始化各個bank,把各個bank設置必須搞清楚,對以后移植復雜的uboot有很大幫助。
設置完畢后拷貝uboot代碼到4k空間,拷貝完畢后執行內存中的uboot代碼,其中的lowlevel_init就完成了內存初始化的工作,由於內存初始化是依賴於開發板的,因此lowlevel_init的代碼一般放在board下面相應的目錄中。
對於ti8168_dvr,lowlevel_init在arch/arm/arm_cortexa8/ti81xx/level_init.S中定義如下:
/***************************************************************************** * lowlevel_init: - Platform low level init. * Corrupted Register : r0, r1, r2, r3, r4, r5, r6 ****************************************************************************/ .globl lowlevel_init lowlevel_init: /* The link register is saved in ip by start.S */ mov r6, ip /* check if we are already running from RAM */ //判斷程序是否已經在 RAM 中運行 ldr r2, _lowlevel_init ldr r3, _TEXT_BASE sub r4, r2, r3 sub r0, pc, r4 /* require dummy instr or subtract pc by 4 instead i'm doing stack init */ ldr sp, SRAM_STACK mark1: ldr r5, _mark1 sub r5, r5, r2 /* bytes between mark1 and lowlevel_init */ sub r0, r0, r5 /* r0 <- _start w.r.t current place of execution */ mov r10, #0x0 /* r10 has in_ddr used by s_init() */ #ifdef CONFIG_NOR_BOOT cmp r0, #0x08000000 /* check for running from NOR */ beq ocmc_init_start /* if == then running from NOR */ ands r0, r0, #0xC0000000 /* MSB 2 bits <> 0 then we are in ocmc or DDR */ cmp r0, #0x40000000 /* if running from ocmc */ beq nor_init_start /* if == skip ocmc init and jump to nor init */ mov r10, #0x01 /* if <> we are running from DDR hence skip ddr init */ /* by setting in_ddr to 1 */ b s_init_start /* and jump to s_init */ #else ands r0, r0, #0xC0000000 /* MSB 2 bits <> 0 then we are in ocmc or DDR */ cmp r0, #0x80000000 bne s_init_start mov r10, #0x01 b s_init_start #endif #ifdef CONFIG_NOR_BOOT ocmc_init_start: /**** enable ocmc 0 ****/ /* CLKSTCTRL */ ldr r5, cm_alwon_ocmc_0_clkstctrl_addr mov r2, #0x2 str r2, [r5] /* wait for gpmc enable to settle */ ocmc0_wait0: ldr r2, [r5] ands r2, r2, #0x00000100 cmp r2, #0x00000100 bne ocmc0_wait0 /* CLKCTRL */ ldr r5, cm_alwon_ocmc_0_clkctrl_addr mov r2, #0x2 str r2, [r5] /* wait for gpmc enable to settle */ ocmc0_wait1: ldr r2, [r5] ands r2, r2, #0x00030000 cmp r2, #0 bne ocmc0_wait1 #ifdef CONFIG_TI816X /**** enable ocmc 1 ****/ /* CLKSTCTRL */ ldr r5, cm_alwon_ocmc_1_clkstctrl_addr mov r2, #0x2 str r2, [r5] /* wait for gpmc enable to settle */ ocmc1_wait0: ldr r2, [r5] ands r2, r2, #0x00000100 cmp r2, #0x00000100 bne ocmc1_wait0 /* CLKCTRL */ ldr r5, cm_alwon_ocmc_1_clkctrl_addr mov r2, #0x2 str r2, [r5] /* wait for gpmc enable to settle */ ocmc1_wait1: ldr r2, [r5] ands r2, r2, #0x00030000 cmp r2, #0 bne ocmc1_wait1 #endif nor_init_start: /* gpmc init */ bl cpy_nor_gpmc_code /* copy nor gpmc init code to sram */ mov r0, pc add r0, r0, #12 /* 12 is for next three instructions */ mov lr, r0 /* gpmc init code in sram should return to s_init_start */ ldr r0, sram_pc_start mov pc, r0 /* transfer ctrl to nor_gpmc_init() in sram */ #endif s_init_start: mov r0, r10 /* passing in_ddr in r0 */ bl s_init // 調用C語言初始化系統時鍾和MUX /* back to arch calling code */ mov pc, r6
5> 重定位
ti8168_dvr 開發板沒有在這里進行重定位,但是我們還是要分析一下這里的重定位:
#ifndef CONFIG_SKIP_RELOCATE_UBOOT relocate: @ relocate U-Boot to RAM adr r0, _start @ r0 <- current position of code ldr r1, _TEXT_BASE @ test if we run from flash or RAM cmp r0, r1 @ don't reloc during debug beq stack_setup
注釋:
(1 ) 使用相對尋址,以 PC 值為基地址計算出當前代碼的開始地址,通過反匯編u-boot.bin,_start在FLASH上代表地址:0x0000_06E0 ,指令 addr r0 ,_start地址為0x0000_0744 ,執行到該指令時,PC=0x0000_0744+8=0x0000_074c,這里,編譯器將用指令SUB r0,PC,#6C ;因此 r0=0x0000_06E0,相當於把代碼的開始地址送到了r0 中。事實上,在 0x0 地址,存在一條跳轉指令,跳到了_start 標簽處,而_start標簽處才是 b reset 真正的跳轉。所以,程序在 FLASH 上運行的真正 reset 跳轉,是在_start=0x0000_06E0 地址對應的 b reset 指令。
(2 ) 把_TEXT_BASE 地址對應的內存里的值(TEXT_BASE) 送到r1 寄存器,這里是0x07080000
(3 ) 比較 r0 寄存器和 r1 寄存器的值。
(4 ) 如果 r0=r1,就跳去執行 stack_setup 程序段,設置內存中的棧空間。比較的目的就是看當前程序的運行是在內存里還是在FLASH里,如果是 debug 模式,那么 u-boot是在內存里運行,其開始地址就是TEXT_BASE ,即0x70800000 。上面的4 句代碼,就是比較一下,看看當前程序代碼在什么位置,如果已經在內存指定的0x07080000 這個位置了,就不必再進行重定位了,而直接進行堆棧設置。
6> 設置堆棧
/* Set up the stack */ stack_setup: ldr r0, _TEXT_BASE @ upper 128 KiB: relocated uboot sub r0, r0, #CONFIG_SYS_MALLOC_LEN @ malloc area sub r0, r0, #CONFIG_SYS_GBL_DATA_SIZE @ bdinfo //跳過全局數據區
注釋:
這段代碼在TEXT_BASE (0x0708_0000)的下面,也就是挨着這個地址往下,建立:動態內存區域和全局數據結構區域。只要將sp指針指向一段沒有被使用的內存就完成棧的設置了。根據上面的代碼可以知道U-Boot內存使用情況了。
使用SourceInsight 跟蹤到 ti8168_dvr.h 文件中,有:
/* * Size of malloc() pool */ #define CONFIG_SYS_MALLOC_LEN (CONFIG_ENV_SIZE + 32 * 1024) /* size in bytes reserved for initial data */ #define CONFIG_SYS _GBL_DATA_SIZE 128 #define CONFIG_ENV_SIZE 0x2000
由此可見,從TEXT_BASE 往下的32kB+8KB 空間用作動態內存分配;再繼續往下128 個字節做為全局數據結構指針。
#ifdef CONFIG_USE_IRQ sub r0, r0, #(CONFIG_STACKSIZE_IRQ + CONFIG_STACKSIZE_FIQ) #endif sub sp, r0, #12 @ leav e 3 words for abort-stack and sp, sp, #~7 @ 8 byte alinged for (ldr/str)d
注釋:
如果使用外部中斷IRQ ,在全局數據結構指針繼續往低地址方向分配,分配的空間大小由CONFIG_STACKSIZE_IRQ 和CONFIG_STACKSIZE_FIQ 在I.MX51_bbg_android.h 文件中定義。默認時,該頭文件中,沒有定義 CONFIG_USE_IRQ,也沒有定義空間大小,因此意味着此時不對IRQ 和FIQ 進行空間預留。
只要將sp指針指向一段沒有被使用的內存就完成棧的設置了。根據上面的代碼可以知道U-Boot內存使用情況了,如下圖所示:
7> 代碼的搬移階段
ldr r2, _armboot_start ldr r3, _bss_start sub r2, r3, r2 @ r2 <- size of armboot add r2, r0, r2 @ r2 <- source end address
注釋:
(1 ) 把_armboot_start程序段的首地址讀進 r2 寄存器。事實上,根據_armboot_start標號的定義:_armboot_star :.word _start,_armboot_start是內存地址,這個地址中存放着TEXT_BASE+_start,即 0x90708_06E0 。所以_armboot_start所對應的內存地址是真正的內存中u-boot 第一條指令的地址。
(2 ) 把_bss_start代碼段首地址讀進r3 寄存器
(3 ) 寄存器 r3 的值-寄存器r2 的值,差值送回寄存器 r2 ,這個差值就是_armboot_start代碼(u-boot 代碼段)所占用的內存空間大小。
(4 ) 計算出_armboot_start代碼的結束地址。寄存器 r0 中保存着 FLASH上代碼的開始地址,r2 中保存着u-boot 代碼的容量,那么r0+r2?r2 后,r2 中保存的就是u-boot 在FLASH上的最后一句代碼的地址。
copy_loop: @ copy 32 bytes at a time ldmia r0!, {r3 - r10} @ copy from source address [r0] stmia r1!, {r3 - r10} @ copy to target address [r1] cmp r0, r2 @ until source end addreee [r2] ble copy_loop #endif /* CONFIG_SKIP_RELOCATE_UBOOT */
注釋:上面4 句話用來實現 U-BOOT 代碼從FLASH—>DDR2 MEM 的搬移。
(1 ) r0=_start=0x0000_06E0 ,即指向 FLASH中代碼開始地址;讀 8 個字(32 字節),分別存到r3-r10 這個8 個32 為通用寄存器中。然后,r0+8 ?r0
(2 ) r1=TEXT_BASE=0x9780_0000,指向內存基地址;把 r3-r10 這8 個寄存器中的數據寫入內存中,因此一次stmia 指令傳輸8 個字,共 32 字節。然后,r1+8 ?r1
(3 ) r2 為FLASH上代碼結束地址。比較r0 和r2 ,即是否到達代碼結尾。
(4 ) 如果沒有到達代碼結尾,繼續循環復制,直到完成。
8> 清除BSS段
/* Clear BSS (if any). Is below tx (watch load addr - need space) */ clear_bss: ldr r0, _bss_start @ find start of bss segment /* BSS段開始地址,在u-boot.lds中指定*/ ldr r1, _bss_end @ stop here /* BSS段結束地址,在u-boot.lds中指定*/ mov r2, #0x00000000 @ clear value /* 將bss段清零*/ clbss_l: str r2, [r0] @ clear BSS location cmp r0, r1 @ are we at the end yet add r0, r0, #4 @ increment clear index pointer bne clbss_l @ keep clearing till at end
注釋:
這段代碼,對bss 段進行初始化,從.lds 文件可以知道它的開始和結束位置。從對u-boot.bin的反匯編結果看,_bss_start=0x0708_55CC,這個地址恰好位於內存中 u-boot 映像的上方相鄰地址;而bss 段的結束地址_bss_end=0x0708_AA14。前后地址相減,可以算出bss 段占用的空間是 213KB 。
這段初始化的方式是把 bss 段全部寫 0 ,寄存器 r0 所指示的目標地址指針按照+4遞增方式循環,直到全部初始化完成。
初始值為0,無初始值的全局變量,靜態變量將自動被放在BSS段。應該將這些變量的初始值賦為0,否則這些變量的初始值將是一個隨機的值,若有些程序直接使用這些沒有初始化的變量將引起未知的后果。
9> 跳轉到第二階段代碼入口start_armboot處。
ldr pc, _start_armboot @ jump to C code
_start_armboot: .word start_armboot
---------------------------------------------------------------------------------------------------------------------------------------------
問題一:如果換一塊開發板有可能改哪些東西?
首先,cpu的運行模式,如果需要對cpu進行設置那就設置,管看門狗,關中斷不用改,時鍾有可能要改,如果能正常使用則不用改,關閉catch和MMU不用改,設置bank有可能要改。最后一步拷貝時看地址會不會變,如果變化也要改,執行內存中代碼,地址有可能要改。
---------------------------------------------------------------------------------------------------------------------------------------------
問題二:Nor Flash和Nand Flash本質區別:
就在於是否進行代碼拷貝,也就是下面代碼所表述:無論是Nor Flash還是Nand Flash,核心思想就是將uboot代碼搬運到內存中去運行,但是沒有拷貝bss后面這段代碼,只拷貝bss前面的代碼,bss代碼是放置全局變量的。Bss段代碼是為了清零,拷貝過去再清零重復操作
----------------------------------------------------------------------------------------------------------------------------------------------