[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裡面去了。