1. 程式人生 > >[uboot] (第三章)uboot流程——uboot-spl程式碼流程

[uboot] (第三章)uboot流程——uboot-spl程式碼流程

以下例子都以project X專案tiny210(s5pv210平臺,armv7架構)為例。

建議先看《[project X] tiny210(s5pv210)上電啟動流程(BL0-BL2)》,根據例子瞭解一下上電之後的BL0\BL1\BL2階段,以及各個階段的執行位置,功能。

========================================================================================================

一、說明

1、uboot-spl入口說明

通過uboot-spl編譯指令碼project-X/u-boot/arch/arm/cpu/u-boot-spl.lds

ENTRY(_start)

所以uboot-spl的程式碼入口函式是_start  對應於路徑project-X/u-boot/arch/arm/lib/vector.S的_start,後續就是從這個函式開始分析。

2、CONFIG_SPL_BUILD說明

前面說過,在編譯SPL的時候,編譯引數會有如下語句:  project-X/u-boot/scripts/Makefile.spl

KBUILD_CPPFLAGS += -DCONFIG_SPL_BUILD

所以說在編譯SPL的程式碼的過程中,CONFIG_SPL_BUILD這個巨集是開啟的。 uboot-spl和uboot的程式碼是通用的,其區別就是通過CONFIG_SPL_BUILD巨集來進行區分的。

二、uboot-spl需要做的事情

CPU初始剛上電的狀態。需要小心的設定好很多狀態,包括cpu狀態、中斷狀態、MMU狀態等等。  在armv7架構的uboot-spl,主要需要做如下事情

  • 關閉中斷,svc模式
  • 禁用MMU、TLB
  • 晶片級、板級的一些初始化操作 
    • IO初始化
    • 時鐘
    • 記憶體
    • 選項,串列埠初始化
    • 選項,nand flash初始化
    • 其他額外的操作
  • 載入BL2,跳轉到BL2

上述工作,也就是uboot-spl程式碼流程的核心。

三、程式碼流程

1、程式碼整體流程

程式碼整體流程如下,以下列出來的就是spl核心函式。  _start———–>reset————–>關閉中斷  ………………………………|  ………………………………———->cpu_init_cp15———–>關閉MMU,TLB  ………………………………|  ………………………………———->cpu_init_crit————->lowlevel_init————->平臺級和板級的初始化  ………………………………|  ………………………………———->_main————–>board_init_f_alloc_reserve & board_init_f_init_reserve & board_init_f———->載入BL2,跳轉到BL2  board_init_f執行時已經是C語言環境了。在這裡需要結束掉SPL的工作,跳轉到BL2中。

2、_start

上述已經說明了_start是整個spl的入口,其程式碼如下:  arch/arm/lib/vector.S

_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
    .word   CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
    b   reset

會跳轉到reset中。 注意,spl的流程在reset中就應該被結束,也就是說在reset中,就應該轉到到BL2,也就是uboot中了。  後面看reset的實現。

3、reset

程式碼如下:  arch/arm/cpu/armv7/start.S

    .globl  reset
    .globl  save_boot_params_ret

reset:
    /* Allow the board to save important registers */
    b   save_boot_params
save_boot_params_ret:
    /*
     * disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
     * except if in HYP mode already
     */
    mrs r0, cpsr
    and r1, r0, #0x1f       @ mask mode bits
    teq r1, #0x1a       @ test for HYP mode
    bicne   r0, r0, #0x1f       @ clear all mode bits
    orrne   r0, r0, #0x13       @ set SVC mode
    orr r0, r0, #0xc0       @ disable FIQ and IRQ
    msr cpsr,r0
@@ 以上通過設定CPSR暫存器裡設定CPU為SVC模式,禁止中斷
@@ 具體操作可以參考《[kernel 啟動流程] (第二章)第一階段之——設定SVC、關閉中斷》的分析

    /* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
    bl  cpu_init_cp15
@@ 呼叫cpu_init_cp15,初始化協處理器CP15,從而禁用MMU和TLB。
@@ 後面會有一小節進行分析

    bl  cpu_init_crit
@@ 呼叫cpu_init_crit,進行一些關鍵的初始化動作,也就是平臺級和板級的初始化
@@ 後面會有一小節進行分析
#endif

    bl  _main
@@ 跳轉到主函式,也就是要載入BL2以及跳轉到BL2的主體部分

4、cpu_init_cp15

建議先參考[kernel 啟動流程] (第六章)第一階段之——開啟MMU兩篇文章的分析。  cpu_init_cp15主要用於對cp15協處理器進行初始化,其主要目的就是關閉其MMU和TLB。  程式碼如下(去掉無關部分的程式碼):  arch/arm/cpu/armv7/start.S

ENTRY(cpu_init_cp15)
    /*
     * Invalidate L1 I/D
     */
    mov r0, #0          @ set up for MCR
    mcr p15, 0, r0, c8, c7, 0   @ invalidate TLBs
    mcr p15, 0, r0, c7, c5, 0   @ invalidate icache
    mcr p15, 0, r0, c7, c5, 6   @ invalidate BP array
    mcr     p15, 0, r0, c7, c10, 4  @ DSB
    mcr     p15, 0, r0, c7, c5, 4   @ ISB
@@ 這裡只需要知道是對CP15處理器的部分暫存器清零即可。
@@ 將協處理器的c7\c8清零等等,各個暫存器的含義請參考《ARM的CP15協處理器的暫存器》

    /*
     * 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 11 (Z---) BTB
#ifdef CONFIG_SYS_ICACHE_OFF
    bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache
#else
    orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache
#endif
    mcr p15, 0, r0, c1, c0, 0
@@ 通過上述的文章的介紹,我們可以知道cp15的c1暫存器就是MMU控制器
@@ 上述對MMU的一些位進行清零和置位,達到關閉MMU和cache的目的,具體的話去看一下上述文章吧。

ENDPROC(cpu_init_cp15)

5、cpu_init_crit

cpu_init_crit,進行一些關鍵的初始化動作,也就是平臺級和板級的初始化。其程式碼核心就是lowlevel_init,如下  arch/arm/cpu/armv7/start.S

ENTRY(cpu_init_crit)
    /*
     * 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.
     */
    b   lowlevel_init       @ go setup pll,mux,memory
ENDPROC(cpu_init_crit)

所以說lowlevel_init就是這個函式的核心。 lowlevel_init一般是由板級程式碼自己實現的。但是對於某些平臺來說,也可以使用通用的lowlevel_init,其定義在arch/arm/cpu/lowlevel_init.S中  以tiny210為例,在移植tiny210的過程中,就需要在board/samsung/tiny210下,也就是板級目錄下面建立lowlevel_init.S,在內部實現lowlevel_init。(其實只要實現了lowlevel_init了就好,沒必要說在哪裡是實現,但是通常規範都是建立了lowlevel_init.S來專門實現lowlevel_init函式)。

在lowlevel_init中,我們要實現如下:  * 檢查一些復位狀態  * 關閉看門狗  * 系統時鐘的初始化  * 記憶體、DDR的初始化  * 串列埠初始化(可選)  * Nand flash的初始化

下面以tiny210的lowlevel_init為例(這裡說明一下,當時移植tiny210的時候,是直接把kangear的這個lowlevel_init.S檔案拿過來用的)  這部分程式碼和平臺相關性很強,簡單介紹一下即可  board/samsung/tiny210/lowlevel_init.S

lowlevel_init:
    push    {lr}

    /* check reset status  */

    ldr r0, =(ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET)
    ldr r1, [r0]
    bic r1, r1, #0xfff6ffff
    cmp r1, #0x10000
    beq wakeup_reset_pre
    cmp r1, #0x80000
    beq wakeup_reset_from_didle
@@ 讀取復位狀態暫存器0xE010_a000的值,判斷復位狀態。

    /* IO Retention release */
    ldr r0, =(ELFIN_CLOCK_POWER_BASE + OTHERS_OFFSET)
    ldr r1, [r0]
    ldr r2, =IO_RET_REL
    orr r1, r1, r2
    str r1, [r0]
@@ 讀取混合狀態暫存器E010_e000的值,對其中的某些位進行置位,復位後需要對某些wakeup位置1,具體我也沒搞懂。

    /* Disable Watchdog */
    ldr r0, =ELFIN_WATCHDOG_BASE    /* 0xE2700000 */
    mov r1, #0
    str r1, [r0]
@@ 關閉看門狗

@@ 這裡忽略掉一部分對外部SROM操作的程式碼

    /* when we already run in ram, we don't need to relocate U-Boot.
     * and actually, memory controller must be configured before U-Boot
     * is running in ram.
     */
    ldr r0, =0x00ffffff
    bic r1, pc, r0      /* r0 <- current base addr of code */
    ldr r2, _TEXT_BASE      /* r1 <- original base addr in ram */
    bic r2, r2, r0      /* r0 <- current base addr of code */
    cmp     r1, r2                  /* compare r0, r1                  */
    beq     1f          /* r0 == r1 then skip sdram init   */
@@ 判斷是否已經在SDRAM上運行了,如果是的話,就跳過以下兩個對ddr初始化的步驟
@@ 判斷方法如下:
@@ 1、獲取當前pc指標的地址,遮蔽其低24bit,存放與r1中
@@ 2、獲取_TEXT_BASE(CONFIG_SYS_TEXT_BASE)地址,也就是uboot程式碼段的連結地址,後續在uboot篇的時候會說明,並遮蔽其低24bit
@@ 3、如果相等的話,就跳過DDR初始化的部分

    /* init system clock */
    bl system_clock_init
@@ 初始化系統時鐘,後續有時間再研究一下具體怎麼配置的

    /* Memory initialize */
    bl mem_ctrl_asm_init
@@ 重點注意:在這裡初始化DDR的!!!後續會寫一篇文章說明一下s5pv210平臺如何初始化DDR.

1:
    /* for UART */
    bl uart_asm_init
@@ 串列埠初始化,到這裡串列埠會打印出一個'O'字元,後續通過寫字元到UTXH_OFFSET暫存器中,就可以在串列埠上輸出相應的字元。

    bl tzpc_init

#if defined(CONFIG_NAND)
    /* simple init for NAND */
    bl nand_asm_init
@@ 簡單地初始化一下NAND flash,有可能BL2的映象是在nand  flash上面的。
#endif

    /* Print 'K' */
    ldr r0, =ELFIN_UART_CONSOLE_BASE
    ldr r1, =0x4b4b4b4b
    str r1, [r0, #UTXH_OFFSET]
@@ 再串列埠上列印‘K’字元,表示lowlevel_init已經完成

    pop {pc}
@@ 彈出PC指標,即返回。

當串列埠中打印出‘OK’的字元的時候,說明lowlevel_init已經執行完成。 system_clock_init是初始化時鐘的地方。 mem_ctrl_asm_init這個函式是初始化DDR的地方。後續應該有研究一下這兩個函式。這裡先有個印象。

6、_main

spl的main的主要目標是呼叫board_init_f進行先前的板級初始化動作,在tiny210中,主要設計為,載入BL2到DDR上並且跳轉到BL2中。DDR在上述lowlevel_init中已經初始化好了。  由於board_init_f是以C語言的方式實現,所以需要先構造C語言環境。  注意:uboot-spl和uboot的程式碼是通用的,其區別就是通過CONFIG_SPL_BUILD巨集來進行區分的。  所以以下程式碼中,我們只列出spl相關的部分,也就是被CONFIG_SPL_BUILD包含的部分。  arch/arm/lib/crt0.S

ENTRY(_main)

/*
 * Set up initial C runtime environment and call board_init_f(0).
 */
@ 注意看這裡的註釋,也說明了以下程式碼的主要目的是,初始化C執行環境,呼叫board_init_f。
    ldr sp, =(CONFIG_SPL_STACK)
    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
    mov r0, sp
    bl  board_init_f_alloc_reserve
    mov sp, r0
    /* set up gd here, outside any C code */
    mov r9, r0
    bl  board_init_f_init_reserve

    mov r0, #0
    bl  board_init_f

ENDPROC(_main)

程式碼拆分如下:  (1)因為後面是C語言環境,首先是設定堆疊

    ldr sp, =(CONFIG_SPL_STACK)
@@ 設定堆疊為CONFIG_SPL_STACK

    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
@@ 堆疊是8位元組對齊,2^7bit=2^3byte=8byte

    mov r0, sp
@@ 把堆疊地址存放到r0暫存器中

關於CONFIG_SPL_STACK,我們通過前面的文章《[project X] tiny210(s5pv210)上電啟動流程(BL0-BL2)》  我們已經知道s5pv210的BL1(spl)是執行在IRAM的,並且IRAM的地址空間是0xD002_0000-0xD003_7FFF,IRAM前面的部分放的是BL1的程式碼部分,所以把IRAM最後的空間用來當作堆疊。  所以CONFIG_SPL_STACK定義如下:  include/configs/tiny210.h

#define CONFIG_SPL_STACK    0xD0037FFF

注意:上述還不是最終的堆疊地址,只是暫時的堆疊地址!!!

(2)為GD分配空間

    bl  board_init_f_alloc_reserve
@@ 把堆疊的前面一部分空間分配給GD使用

    mov sp, r0
@@ 重新設定堆疊指標SP

    /* set up gd here, outside any C code */
    mov r9, r0
@@ 儲存GD的地址到r9暫存器中

注意:雖然sp的地址和GD的地址是一樣的,但是堆疊是向下增長的,而GD則是佔用該地址後面的部分,所以不會有衝突的問題。 關於GD,也就是struct global_data,可以簡單的理解為uboot的全域性變數都放在了這裡,比較重要,所以後續有會寫篇文章說明一下global_data。這裡只需要知道在開始C語言環境的時候需要先為這個結構體分配空間。  board_init_f_alloc_reserve實現如下  common/init/board_init.c

ulong board_init_f_alloc_reserve(ulong top)
{
    /* Reserve early malloc arena */
    /* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
    top = rounddown(top-sizeof(struct global_data), 16);
// 現將top(也就是r0暫存器,前面說過存放了暫時的指標地址),減去sizeof(struct global_data),也就是預留出一部分空間給sizeof(struct global_data)使用。
// rounddown表示向下16個位元組對其

    return top;
// 到這裡,top就存放了GD的地址,也是SP的地址
//把top返回,注意,返回後,其實還是存放在了r0暫存器中。
}

還有一點,其實GD在spl中沒什麼使用,主要是用在uboot中,但在uboot中的時候還需要另外分配空間,在講述uboot流程的時候會說明。

(3)初始化GD空間  前面說了,此時r0暫存器存放了GD的地址。

    bl  board_init_f_init_reserve

board_init_f_init_reserve實現如下  common/init/board_init.c  編譯SPL的時候_USE_MEMCPY巨集沒有開啟,所以我們去掉了_USE_MEMCPY的無關部分。

void board_init_f_init_reserve(ulong base)
{
    struct global_data *gd_ptr;
    int *ptr;
    /*
     * clear GD entirely and set it up.
     * Use gd_ptr, as gd may not be properly set yet.
     */

    gd_ptr = (struct global_data *)base;
// 從r0獲取GD的地址
    /* zero the area */
    for (ptr = (int *)gd_ptr; ptr < (int *)(gd_ptr + 1); )
        *ptr++ = 0;
// 對GD的空間進行清零
}

(4)跳轉到板級前期的初始化函式中  如下程式碼

    bl  board_init_f

board_init_f需要由板級程式碼自己實現。  在這個函式中,tiny210主要是實現了從SD卡上載入了BL2到ddr上,然後跳轉到BL2的相應位置上  tiny210的實現如下:  board/samsung/tiny210/board.c

#ifdef CONFIG_SPL_BUILD
void board_init_f(ulong bootflag)
{
    __attribute__((noreturn)) void (*uboot)(void);
    int val;
#define DDR_TEST_ADDR 0x30000000
#define DDR_TEST_CODE 0xaa
    tiny210_early_debug(0x1);
    writel(DDR_TEST_CODE, DDR_TEST_ADDR);
    val = readl(DDR_TEST_ADDR);
    if(val == DDR_TEST_CODE)
        tiny210_early_debug(0x3);
    else
    {
        tiny210_early_debug(0x2);
        while(1);
    }
// 先測試DDR是否完成

    copy_bl2_to_ddr();
// 載入BL2的程式碼到ddr上

    uboot = (void *)CONFIG_SYS_TEXT_BASE;
// uboot函式設定為BL2的載入地址上
    (*uboot)();
// 呼叫uboot函式,也就跳轉到BL2的程式碼中
}
#endif

關於copy_bl2_to_ddr的實現,也就是如何從SD卡或者nand flash上載入BL2到DDR上的問題,請參考後續文章《[project X] tiny210(s5pv210)程式碼載入說明》。

到此,SPL的任務就完成了,也已經跳到了BL2也就是uboot裡面去了。