從0移植uboot (二) _uboot啟動流程分析


經過了上一篇的配置,我們已經執行make就可以編譯出一個uboot.bin,但這還不夠,首先,此時的uboot並不符合三星芯片對bootloader的格式要求,同時,此時的uboot.bin也沒有結合我們的開發板進行配置,還無法使用。而要進行這樣的個性化配置,前提條件就是對uboot開機流程和編譯系統有所了解,本文主要討論前者。在三星的SoC中, 啟動流程可以分為三個階段BL0, BL1, BL2, BL3, 三星自己的手冊對BL1的解釋也不盡相同, 一種是將在iRAM中運行的程序都歸結為BL1; 一種是將iRAM中三星加密的代碼bl1.bin作為BL1, iRAM中剩余的部分作為BL2, 本文采用后者, 他們的主要分工如下:

  • BL0: ARM的起始地址都是0地址, 三星的芯片一般將0地址映射到iROM中, BL0就是指iROM中固化的啟動代碼, 主要負責加載BL1
  • BL1: 三星對於bootloader的加密代碼bl1.bin, 要放在外設中uboot.bin的頭上, 和一部分uboot.bin一起加載到iRAM中運行.
  • BL2: 從(nand/sd/usb)中拷貝的uboot.bin頭最大14K到iRAM中代碼中除去bl1.bin后剩余的部分, 負責設置CPU為SVC模式, 關閉MMU, 關閉中斷, 關閉iCache, 關閉看門狗, 初始化DRAM,初始化時鍾, 初始化串口, 設置棧, 校驗BL2並將其搬移到DRAM高位地址, 重定位到DRAM中執行BL3
  • BL3:是指在代碼重定向后在內存中執行的uboot的完整代碼, 負責初始化外設,更新向量表, 清BSS, 准備內核啟動參數, 加載並運行OS內核

可以借助下圖理解這個流程

我們常說的uboot是一個兩階段bootloader,就是指上述的BL2和BL3. BL2主要做硬件直接相關的初始化,使用匯編編寫;BL3主要為操作系統的運行准備環境,主要用C編寫,這里以ARM平台為例分析其啟動流程。下面是啟動過程中主要涉及的文件

arch/arm/cpu/armv7/start.S
board/samsung/myboard/lowlevel_init.S
arch/arm/lib/crt0.S
arch/arm/lib/board.c
arch/samsung/myboard/myboard.c

BL2

BL2的主要文件和任務流程如下

arch/arm/cpu/armv7/start.S
           1. 設置CPU為SVC模式
           2. 關閉MMU
           3. 關閉Cache
           4. 跳轉到lowlevel_init.S low_level_init
board/samsung/origen/lowlevel_init.S
           5. 初始化時鍾
           6. 初始化內存
           7. 初始化串口
           8. 關閉看門狗
           9. 跳轉到crt0.S _main
arch/arm/lib/crt0.S
           10. 設置棧
           11. 初始化C運行環境
           12. 調用board_init_f()
arch/arm/lib/board.c
           13. board_init_f對全局信息GD結構體進行填充
arch/arm/lib/crt0.S
           14. 代碼重定位------------BL2的最后的工作, 執行完就進入DRAM執行BL2

start.S

 39 .globl _start
 40 _start: b       reset
 41         ldr     pc, _undefined_instruction
 42         ldr     pc, _software_interrupt
 43         ldr     pc, _prefetch_abort
 44         ldr     pc, _data_abort
 45         ldr     pc, _not_used
 46         ldr     pc, _irq
 47         ldr     pc, _fiq

--40--> 異常向量表設置

126 reset:
127         bl      save_boot_params
131         mrs     r0, cpsr
132         bic     r0, r0, #0x1f
133         orr     r0, r0, #0xd3
134         msr     cpsr,r0

--126-->設置CPU為SVC模式

下面這三行代碼非常重要,是BL2啟動過程的交叉點

154         bl      cpu_init_cp15
155         bl      cpu_init_crit
158         bl      _main

--154-->跳轉執行cpu_init_cp15,即初始化CP15協處理器
--155-->跳轉執行cpu_init_crit,
--158-->跳轉執行_main

287 ENTRY(cpu_init_cp15)
291         mov     r0, #0                  @ set up for MCR
292         mcr     p15, 0, r0, c8, c7, 0   @ invalidate TLBs
293         mcr     p15, 0, r0, c7, c5, 0   @ invalidate icache
294         mcr     p15, 0, r0, c7, c5, 6   @ invalidate BP array
295         mcr     p15, 0, r0, c7, c10, 4  @ DSB
296         mcr     p15, 0, r0, c7, c5, 4   @ ISB
297 
301         mrc     p15, 0, r0, c1, c0, 0
302         bic     r0, r0, #0x00002000     @ clear bits 13 (--V-)
303         bic     r0, r0, #0x00000007     @ clear bits 2:0 (-CAM)
304         orr     r0, r0, #0x00000002     @ set bit 1 (--A-) Align
305         orr     r0, r0, #0x00000800     @ set bit 11 (Z---) BTB
307         bic     r0, r0, #0x00001000     @ clear bit 12 (I) I-cache
311         mcr     p15, 0, r0, c1, c0, 0
312         mov     pc, lr                  @ back to my caller
313 ENDPROC(cpu_init_cp15)

--291-->關閉Cache
--301-->關閉MMU

324 ENTRY(cpu_init_crit)
331         b       lowlevel_init           @ go setup pll,mux,memory
332 ENDPROC(cpu_init_crit)

--331-->跳轉到lowlevel_init,位於"board/samsung/origen/lowlevel_init.S",進行板級相關的設置。

lowlevel_init.S

這是位於目錄的初始化文件,主要完成特定開發板的初始化工作,包括時鍾、內存和串口等。

 82         bl system_clock_init
 85         bl mem_ctrl_asm_init
 87 1:
 88         /* for UART */
 89         bl uart_asm_init
 90         bl tzpc_init
 91         pop     {pc}
114 system_clock_init:
329 uart_asm_init:
357 tzpc_init:

--82-->初始化系統時鍾,即跳轉到114行
--85-->初始化系統內存
--89-->初始化UART串口,即跳轉到329行
--90-->初始化TrustZoneProtectorController,即跳轉到357行

執行完lowlevel_init.S,依據上面那三行代碼,執行流程就該回到start.S執行156行跳轉到_main

crt0.S

首要任務就是設置棧, 准備C語言運行的環境:

 96 _main:
102 #if defined(CONFIG_NAND_SPL)
103         /* deprecated, use instead CONFIG_SPL_BUILD */
104         ldr     sp, =(CONFIG_SYS_INIT_SP_ADDR)
105 #elif defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
106         ldr     sp, =(CONFIG_SPL_STACK)
107 #else
108         ldr     sp, =(CONFIG_SYS_INIT_SP_ADDR)
109 #endif
110         bic     sp, sp, #7      /* 8-byte alignment for ABI compliance */
111         sub     sp, #GD_SIZE    /* allocate one GD above SP */
112         bic     sp, sp, #7      /* 8-byte alignment for ABI compliance */
113         mov     r8, sp          /* GD is above SP */
114         mov     r0, #0
115         bl      board_init_f

_main
--104-->初始化SP,為C語言做准備
--110-->保存128B放GD結構體來存放全局信息,
--111-->GD的地址放在r8中,
--115-->跳轉到board_init_f(),這個整個初始化過程中第一次執行的C代碼

board.c

下面這個函數就是uboot初始化過程中執行的第一個C函數,可以看作這個文件的入口函數。其中最重要的就是准備全局信息GD結構體, 內核啟動參數就是來自於這個結構體

209 typedef int (init_fnc_t) (void);
243 init_fnc_t *init_sequence[] = {
244         arch_cpu_init,          /* basic arch cpu dependent setup */
245         mark_bootstage,
246 #ifdef CONFIG_OF_CONTROL
247         fdtdec_check_fdt,
            ...
277 void board_init_f(ulong bootflag)
278 {
        ...
291         gd->mon_len = _bss_end_ofs;
292 #ifdef CONFIG_OF_EMBED
293         /* Get a pointer to the FDT */
294         gd->fdt_blob = _binary_dt_dtb_start;
295 #elif defined CONFIG_OF_SEPARATE
296         /* FDT is at end of image */
297         gd->fdt_blob = (void *)(_end_ofs + _TEXT_BASE);
298 #endif
299         /* Allow the early environment to override the fdt address */
300         gd->fdt_blob = (void *)getenv_ulong("fdtcontroladdr", 16,
301                                                 (uintptr_t)gd->fdt_blob);
302 
303         for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
304                 if ((*init_fnc_ptr)() != 0) {
305                         hang ();
306                 }
307         }
        ...

board_init_f()
--243--> 全局的函數指針數組,每個指針都是int (*ptr)(void)型的。
--291-->mon_len 通過鏈接腳本可以知道存放的是 uboot 代碼大小;
--294-->fdt_blob 存放設備數地址;
--303--遍歷函數指針數組init_sequence中的每一個成員,就是將數組中的每一個初始化函數都執行一次,這種寫法可以借鑒

crt0.S

函數board_init_f()返回后,繼續執行crt0.S中115行之后的部分,主要的工作是執行代碼重定位,執行完這些之后,我們找到了最感興趣的下面這幾句

163         /* call board_init_r(gd_t *id, ulong dest_addr) */
164         mov     r0, r8                  /* gd_t */
165         ldr     r1, [r8, #GD_RELOCADDR] /* dest_addr */
166         /* call board_init_r */
167         ldr     pc, =board_init_r       /* this is auto-relocated! */

--167-->跳轉到board_init_r函數執行,這次跳出去這個文件的語句就執行完畢了,不會再回來了, 開始執行BL3.

BL3

這一階段涉及的文件及任務如下

arch/arm/lib/crt0.S
arch/arm/lib/board.c
           1. board_init_r()是進入定制板目錄的入口
common/main.c
           2. main_loop()中關閉中斷,執行命令以及加載引導內核

board.c

這也是最后一次跳轉到這個文件了,執行額函數如下

519 void board_init_r(gd_t *id, ulong dest_addr)
520 {
521         ulong malloc_start;
522 #if !defined(CONFIG_SYS_NO_FLASH)
523         ulong flash_size;
524 #endif
525 
526         gd->flags |= GD_FLG_RELOC;      /* tell others: relocation done */
527         bootstage_mark_name(BOOTSTAGE_ID_START_UBOOT_R, "board_init_r");
528 
529         monitor_flash_len = _end_ofs;
530 
531         /* Enable caches */
532         enable_caches();
533 
534         debug("monitor flash len: %08lX\n", monitor_flash_len);
535         board_init();   /* Setup chipselects */
        ...
650          /* set up exceptions */
651         interrupt_init();
652         /* enable exceptions */
653         enable_interrupts(); 
667         eth_initialize(gd->bd);
        ...
701         /* main_loop() can return to retry autoboot, if so just run it again. */
702         for (;;) {
703                 main_loop();
704         }
705 

board_init_r()
--532-->很多緊急工作都做完了,可以打開cache了
--535-->關鍵!!!這個就是我們苦苦尋找的板級定制文件的xxx.c的入口函數!!!
--651-->中斷初始化
--653-->使能中斷
--667-->網卡初始化,函數的實現在net/eth.c,會回調板級xxx.c中的board_eth_init()
--703-->**執行main_loop(),實現在common/main.c,它的主要功能就是循環檢測輸入的命令並執行,其中一個環境變量bootdelay(自啟動)的設置決定了是否啟動內核,如果延時大於等於零,並且沒有在延時過程中接收到按鍵,則引導內核

main.c

這個文件就是main_loop()所在的文件, 不論是啟動內核, 打印信息還是輸入命令, 都在這個函數中執行, 我們這里主要關心這個函數是如何啟動內核的, 函數的調用關系如下:

main_loop()
           ├──getenv
           │           └──getenv_f
           │                      ├──env_get_char
           │                      └──envmatch
           └──run_command_list
                      └──builtin_run_command_list
                                 └──builtin_run_command
                                            ├──process_macros
                                            ├──parse_line
                                            └──cmd_process
                                                       ├──find_cmd
                                                       ├──cmd_call
                                                       │           └──cmdtp->cmd//找到命令do_bootm
                                                       │                      └──bootm_start
                                                       │                                 └──boot_start_lmb
                                                       │                                            └──arch_lmb_reserve(struct lmb *lmb)
                                                       │                                                       └──cleanup_before_linux
                                                       │                                                                  └──disable_interrupts //關閉中斷
                                                       └──cmd_usage
                                                                             └──bootm_load_os

上面就是uboot啟動Linux前大體上做的最后的工作流程, uboot做了這么多, 其實都是為了能引導OS內核,針對ARM Linux, 它對啟動前的環境要求在內核文檔"/Documentation/kernel-parameters.txt""Documentation/arm/Booting"" 有敘述,其中就可以解釋uboot為什么做了上面這些工作. 文檔中對於在ARM平台中啟動Linux內核, 作了如下6條要求, 我們逐條解釋:

Setup and initialise the RAM.

這里的RAM指的是DRAM, 因為Linux內核需要在DRAM中運行, 而DRAM必須初始化, 這部分工作在BL2中完成了.

Initialise one serial port.

初始化一個串口, bootloader應該初始化一個串口, 這樣內核串口驅動才可以自動探測到給控制台用的串口是哪個
bootloader可以在taglist中使用console=來指定一個串口, 這部分工作在BL2中也做了

Detect the machine type.

探測板子類型, Linux內核需要知道自己運行的板子類型, 這部分工作也交給bootloader來完成, bootloader通過一些方式獲得了板子的類型后, 按照內核源碼的"arch/arm/tools/mach-types"中的描述, 將板子的type編號存儲在r1寄存器

Setup the kernel tagged list.

建立tag列表, 也就是內核參數, 在內核源碼和uboot源碼中都使用一個struct tag來描述. 數據結構 tag 和 tag_header 定義在 Linux 內核源碼的"include/asm/setup.h" 頭文件中:

  1. 有效的tagged list 必須以ATAG_CORE開始並且以ATAG_NONE結束, 空的ATAG_CORE的size域是0x00000002, ATAG_NONE的size域是0。
  2. list中可以放置任意數目的tag, 名稱相同的tag的后果未定義, 最終的取值可能是之前的, 也可能是之后的
  3. bootloader至少要將系統內存的位置, 內存的大小以及根文件系統的位置傳遞給內核
  4. taggedlist放置的位置不能和內核自解壓區域或initrd的bootp程序相沖突, 防止被重寫, 建議的地址是RAM的頭16KB
  5. bootloader必須把dtb放置在64bit對齊的已經初始化過的內存中, dtb的格式在"Documentation/devicetree/booting-without-of.txt"中, 其中定義了設備樹的大小
struct boot_param_header {
    __be32 magic;                   //設備樹魔數,固定為0xd00dfeed
    __be32 totalsize;               //整個設備樹的大小
    __be32 off_dt_struct;           //保存結構塊在整個設備樹中的偏移
    __be32 off_dt_strings;          //保存的字符串塊在設備樹中的偏移
    __be32 off_mem_rsvmap;          //保留內存區,該區保留了不能被內核動態分配的內存空間
    __be32 version;                 //設備樹版本
    __be32 last_comp_version;       //向下兼容版本號
    __be32 boot_cpuid_phys;         //為在多核處理器中用於啟動的主cpu的物理id
    __be32 dt_strings_size;         //字符串塊大小
    __be32 dt_struct_size;          //結構塊大小
};

在內核關於啟動參數的約定中, 它認為r2中的地址可能是設備樹的地址, 也可能是tagged list的地址, 所以, 拿到這個地址后,內核首要的工作就是判斷到底是什么,判斷的依據就是判斷其第一個32bit上存儲的到底是設備樹魔數0xd00dfeed還是ATAGS_CORE。參見內核"arch/arm/kernel/head-common.h"這里我有一個疑問, 既然r2可能會存放dtb的地址, 那此時內核是如何找到tagged list的呢???。實際開發中一般都是將tagged list的地址放到r2中。內核推薦將dtb放置在RAM開始的128MB處

Load initramfs.

加載ramfs,ramfs推薦正好放在設備樹上面

Calling the kernel image

啟動內核鏡像,如果使用的flash中的zImage,bootloader可以直接將zImage加載到內存並執行,Linux內核對非zImage內核鏡像的地址有更嚴格的要求————鏡像必須加載到PAGE_OFFSET + TEXT_OFFSET處。PAGE_OFFSET定義了虛擬地址空間中內核空間的起始地址,32bit系統中就是3GB處。TEXT_OFFSET表示內核空間中開始的用來保存內核的頁表(也就是進程0的PGD)、bootload和kernel傳遞參數的一塊空間的大小,對於arm,TEXT_OFFSET是32kB,由於內核空間的前896MB(3GB~3GB+896MB)是一一映射的,所以將內核加載到物理地址0x4000 0000其實就是加載到虛擬地址空間的3GB處,考慮到上述的32KB用來保存頁表、內核參數等,我們需要將內核解壓到0x40008000處運行,即虛擬地址的(3GB+32KB),如果使用uImage,這個參數在制作uImage的時候被寫入到了文件頭中。我們在uboot的啟動參數通常不指定加載到這個地址,因為內核自解壓之后會自搬移到0x40008000處開始執行,如果我們指定的就是這個地址,那么內核首先會自搬移到別處,解壓之后再搬回來執行,防止解壓過程中將未解壓的部分覆蓋造成錯誤。

無論是哪種啟動方式,內核啟動的時候都必須滿足下面的條件:

  1. r0=0;r1=板子類型號;r2=內核中tagged list或設備樹地址
  2. 所有的IRQ FIQ必須關閉
  3. 必須是ARM狀態, SVC模式
  4. MMU必須關閉
  5. iCache可以關閉也可以不管
  6. dCache必須關閉
  7. DMA設備必須關閉

我們已經分析了整個Uboot的啟動框架,在細節上,Uboot必須完成上面這些工作以滿足Linux的啟動要求。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM