2016年7月20日 星期三

[Embedded] U-Boot的開機流程


以 U-Boot source code 為例,整理 embedded system 的開機流程

1.  開機流程
CPU上電後,會到固定位置讀取指令並且執行,不同架構其位置不同,比方說 ARM 結構的 CPU 會從位址 0x00000000 開始執行,MIPS結構的 CPU 會從位址 0xBFC00000 開始,。 
若要手動搞定一個嵌入式系統,首先要作的便是安排系統記憶體的編排方式,透過編寫 linker script,可以控制 CPU 上電後會先開始執行的指令,因而讓開機時便開始執行 U-Boot。 
以下是一個 linker script 例子,此例中,會將 start.o 擺放在 0x00000000 的位置。系統開機時便會在從此處開始取出指令開始執行。一般作法在 start.o 會做些硬體的初始化,然後再執行 u-boot.  (此例子取自 u-boot-2013.04-rc1.tar.bz2 ,參考\board\samsung\smdk6400\u-boot-nand.ld)
/*
* (C) Copyright 2002
* Gary Jennejohn, DENX Software Engineering, <garyj@denx.de>
*
* (C) Copyright 2008
* Guennadi Liakhovetki, DENX Software Engineering, <lg@denx.de>
*
* See file CREDITS for list of people who contributed to this
* project.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307 USA
*/
/* linker script 可用來將 object file (.o) 連結成一個輸出文件(例如:elf file),建議先搞懂ELF,再看這支程式 */
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start) /* 設定入口函數名稱為 _start, 這是CPU執行的第一個程式, 若不指定,預設採用 .text section 的第一個Byte的位置 */
SECTIONS /* 描述如何將輸入檔案的各個section,組合成輸出檔案中所對應的section。*/
{
. = 0x00000000; /* Section 的起始位置為 0x0 */
/* ”.” 是個位置計數器,紀錄當前位置在目標文件中的虛擬地址 */
/* ”.” 是個自動增加的計數器,當一個 section生成後,”.”的值會自動加上該 section 的長度 */
. = ALIGN(4); /* 程式碼以4位元組對齊 */
.text : /* 放置程式碼的位置,首先放置 start.o,接著才放置其他 .o 的程式(即 .text section) */
{
arch/arm/cpu/arm1176/start.o (.text)
*(.text)
}
. = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) } /* 唯讀 */
. = ALIGN(4);
.data : { *(.data) } /* 已初始化的全局變數 */
. = ALIGN(4);
.got : { *(.got) } /* Global Offset Tables,一個全域的參考表格,可用來查找 symble 與 address 的對應關係 */
. = ALIGN(4);
.u_boot_list : {
#include <u-boot.lst>
}
. = ALIGN(4);
.mmudata : { *(.mmudata) }
. = ALIGN(4);
.rel.dyn : {
__rel_dyn_start = .; /* 建立一個符號叫 __rel_dyn_start,當成此區段起始點 */
*(.rel*) /* 合併全部輸入檔的 .rel 到這各個區段 */
__rel_dyn_end = .; /* 建立一個符號叫 __rel_dyn_end,當成此區段終點 */
}
.dynsym : {
__dynsym_start = .;
*(.dynsym)
}
_end = .;
.bss __rel_dyn_start (OVERLAY) : { /* 未初始化的全局變數 */
__bss_start = .;
*(.bss)
. = ALIGN(4);
__bss_end__ = .;
}
/DISCARD/ : { *(.dynstr*) }
/DISCARD/ : { *(.dynamic*) }
/DISCARD/ : { *(.plt*) }
/DISCARD/ : { *(.interp*) }
/DISCARD/ : { *(.gnu*) }
}
view raw u-boot-nand.lds hosted with ❤ by GitHub

2.  U-Boot 有兩種工作模式
(1)啟動載入(Boot loading)模式。
上電後,Bootloader 從板子上的某個固態存放裝置上將作業系統載入到RAM中運行
(2)下載(Downloading)模式。
在這種模式下,開發人員可以使用各種命令,(xmodem, tftp, usb and etc...)通過串口連接或網路連接等通信手段從主機(Host)下載檔案(比如內核映射、檔案系統映射),將它們直接放在記憶體運行或是燒入Flash類固態存放裝置中。

3. U-Boot啟動過程分為兩個階段
a. Phase 1,
硬體設備初始化, 準備 RAM 空間(對DDR晶片初始化), 設置 CPU 工作模式為管理模式(SVC)複製 U-Boot程式至 RAM, 啟動 MMU, 設置堆疊,  跳轉到 phase2的程式碼進入點. 
b. Phase 2,
初始化要使用的硬體設備, 檢測系統記憶體映象 (memory map), 指定初始化函數清單(如:cpu_init, interrupt_init),為核心設定啟動參數, 進入 U-Boot 命令列 (無窮迴圈)

4.從 U-Boot 程式碼分析其流程

start.s (程式碼及註解請看這裡)
禁止 FIQ, IRQ中斷,並將 ARM CPU 設定為 SVC(Supervisor Calls) 模式 
設定 Cache和 MMU
若是從 nand boot,則應該已經做了硬體的Reset,因此不需清除Cache與暫停MMU。否則要 
  • 設定 Cache,此時應該先讓 ICache, DCache, TLB 都失效。
  • 設定 MMU,包含 Endian, Address Align Check, 並通知 MMU 不要使用 ICache, DCache。
  • 設定CPU內的記憶體 TCM(Tightly-coupled memory),此部分也是可分為 Instruction TCM, Data TCM
設定週邊設備(peripheral)對應的記憶體位址 
執行 lowlevel_init()
此函數用來初始化板子相關設定,程式碼會放在 board 目錄下,例如:board/samsung/smdk6400/lowlevel_init.s,此函數會設定LED,WatchDog,Interrupt,System Clock,Serial Port,NAND Flash,DRAM。建置一個小的 Stack,清除BSS段。 
執行 _main() 
此函數用來建立C語言的執行環境,執行結束之後會返回 start.s。實作可參考 crt0.s。
relocate code
將 ELF 對應的各個區塊,複製到記憶體內。然後啟動MMU,致能TLB。此時還不需要 ICache, DCache,因此不用變更設定。 
最後設定 exception handler,處理異常事件

crt0.s (C-runtime startup Code,編譯成 Thumb code),程式碼位置在 arch/arm/lib/crt0.s,主要動作如下
配置一小塊堆疊,用來放置 160bytes 的 Global Data,將此記憶體位置紀錄在 r8,供後續使用。 
呼叫 board_init_f(ulong bootflag),此函數是U-Boot執行的第一個 C 函數,程式碼在arch\arm\lib\board.c,此函數會清空 Global Data,將 init_sequence[] 內定義的所有函數都執行一遍,其初始化工作可能包含 serial, baudrate, consol, system information, i2c 跟 dram,並規劃SDRAM空間配置,由位址最大處往前,分別分配 TLB, .Bss, .Data, .Text等區段,接著再分配 malloc空間,global data, abort_stack。

如果是 non-SPL build,會繼續下列流程
呼叫 relocate_code(addr_sp, gd, addr_moni),跳回到 start.S中,此時會將 u-boot 程式碼複製到 SDRAM中。複製成功後,執行 bx lr,跳回 crt0.S。 
呼叫 c_runtime_cpu_setup,此時一樣會跳至 start.S,然後立刻透過 bx lr 返回 crt0.S。 
清空BSS,控制LED閃爍。
呼叫 board_init_r(gd_t *id, ulong dest_addr),此函數是 U-Boot 執行的第二個 C函數,也是做些初始化動作,注意的是許多外接設備都在此時才初始化,如 :Ethernet, I2Cfast, LCD, KEYBOARD, USB, SCSI, KGDB等, 最後會進入一個無窮迴圈裡,持續的執行 main_loop(), 
main_loop()的主要作用就是 U-Boot 啟動管理,此時可能會 delay 數秒 (CONFIG_BOOTDELAY),等待使用者輸入指令。main_loop()的實作可參考 common/main.c。此函數會取得環境參數中的 bootcmd,執行 bootcmd 載入linux kernel,後續任務便由 kernel 接手。 
可透過 bootm 命令載入 kernel,例如:"bootm 0x80700000"。
cmd_bootm.c
設定 kernel 的 entry point 
取出 zImage 所儲存的 64 bytes header,判斷 magic number是否正確? 
設定 ramdisk 
取得 fdt (flattened device tree),設定各項周邊設備。 
執行 kernel,函數宣告為 void (*theKernel) (int, char **, char **, int *);,此函數內容還需要再研究。

以ARM開機為例,做個簡單總結,CPU開機時會取出 0x00000000 處的程式碼 (start.s),初始化相關設備,並準備好C語言執行環境,將 u-boot 程式 relocate 至 SDRAM,執行 u-boot 程式,初始化所有設備,最後由 u-boot 傳入各項參數,啟動 kernel。