[uboot] (第五章)uboot流程——uboot啟動流程
轉自https://blog.csdn.net/ooonebook/article/details/53070065
以下例子都以project X專案tiny210(s5pv210平臺,armv7架構)為例
[uboot] uboot流程系列:
[project X] tiny210(s5pv210)上電啟動流程(BL0-BL2)
[project X] tiny210(s5pv210)從儲存裝置載入程式碼到DDR
[uboot] (第一章)uboot流程——概述
[uboot] (第二章)uboot流程——uboot-spl編譯流程
[uboot] (第三章)uboot流程——uboot-spl程式碼流程
[uboot] (第四章)uboot流程——uboot編譯流程
[uboot] (番外篇)global_data介紹
[uboot] (番外篇)uboot relocation介紹
建議先看《[project X] tiny210(s5pv210)上電啟動流程(BL0-BL2)》,根據例子瞭解一下上電之後的BL0\BL1\BL2階段,以及各個階段的執行位置,功能。
建議可以和《[uboot] (番外篇)global_data介紹》和《[uboot] (番外篇)uboot relocation介紹》結合起來看。
=================================================================================
一、uboot說明
1、uboot要做的事情
CPU初始剛上電的狀態。需要小心的設定好很多狀態,包括cpu狀態、中斷狀態、MMU狀態等等。其次,就是要根據硬體資源進行板級的初始化,程式碼重定向等等。最後,就是進入命令列狀態,等待處理命令。
在armv7架構的uboot,主要需要做如下事情
-
arch級的初始化
- 關閉中斷,設定svc模式
- 禁用MMU、TLB
- 關鍵暫存器的設定,包括時鐘、看門狗的暫存器
-
板級的初始化
- 堆疊環境的設定
- 程式碼重定向之前的板級初始化,包括串列埠、定時器、環境變數、I2C\SPI等等的初始化
- 進行程式碼重定向
- 程式碼重定向之後的板級初始化,包括板級程式碼中定義的初始化操作、emmc、nand flash、網路、中斷等等的初始化。
- 進入命令列狀態,等待終端輸入命令以及對命令進行處理
上述工作,也就是uboot流程的核心。
2、疑問
-
在前面的文章中雖然已經說明了,在spl的階段中已經對arch級進行了初始化了,為什麼uboot裡面還要對arch再初始化一遍?
回答:spl對於啟動uboot來說並不是必須的,在某些情況下,上電之後uboot可能在ROM上或者flash上開始執行而並沒有使用spl。這些都是取決於平臺的啟動機制。因此uboot並不會考慮spl是否已經對arch進行了初始化操作,uboot會完整的做一遍初始化動作,以保證cpu處於所要求的狀態下。 -
和spl在啟動過程的差異在哪裡?
回答:以tiny210而言,前期arch的初始化流程基本上是一致的,出現本質區別的是在board_init_f開始的。- spl的board_init_f是由board自己實現相應的功能,例如tiny210則是在board/samsung/tiny210/board.c中。其主要實現了複製uboot到ddr中,並且跳轉到uboot的對應位置上。一般spl在這裡就可以完成自己的工作了。
- uboot的board_init_f是在common下實現的,其主要實現uboot relocate前的板級初始化以及relocate的區域規劃,其還需要往下走其他初始化流程。
3、程式碼入口
project-X/u-boot/arch/arm/cpu/u-boot.lds
ENTRY(_start)
所以uboot-spl的程式碼入口函式是_start
對應於路徑project-X/u-boot/arch/arm/lib/vector.S的_start,後續就是從這個函式開始分析。
二、程式碼整體流程
1、首先看一下主枝幹的流程(包含了arch級的初始化)
在arch級初始化是和spl完全一致的
_start———–>reset————–>關閉中斷
………………………………|
………………………………———->cpu_init_cp15———–>關閉MMU,TLB
………………………………|
………………………………———->cpu_init_crit————->lowlevel_init————->關鍵暫存器的配置和初始化
………………………………|
………………………………———->_main————–>進入板級初始化,具體看下面
2、板級初始化的流程
_main————–>board_init_f_alloc_reserve —————>堆疊、GD、early malloc空間的分配
…………|
…………————->board_init_f_init_reserve —————>堆疊、GD、early malloc空間的初始化
…………|
…………————->board_init_f —————>uboot relocate前的板級初始化以及relocate的區域規劃
…………|
…………————->relocate_code、relocate_vectors —————>進行uboot和異常中斷向量表的重定向
…………|
…………————->舊堆疊的清空
…………|
…………————->board_init_r —————>uboot relocate後的板級初始化
…………|
…………————->run_main_loop —————>進入命令列狀態,等待終端輸入命令以及對命令進行處理
三、arch級初始化程式碼分析
1、_start
上述已經說明了_start是整個uboot的入口,其程式碼如下:
arch/arm/lib/vector.S
_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
.word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
b reset
會跳轉到reset中。
2、reset
建議先參考[kernel 啟動流程] (第二章)第一階段之——設定SVC、關閉中斷,瞭解一下為什麼要設定SVC、關閉中斷以及如何操作。
程式碼如下:
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
@@ 跳轉到主函式,也就是板級初始化函式
@@ 下一節中進行說明。
3、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)
4、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.
@@ 其實,在tiny210的專案中,已經在spl裡面對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已經執行完成。
三、板級初始化程式碼分析
1、_main
板級初始化程式碼的入口就是_main。從這裡開始分析。
建議可以和《[uboot] (番外篇)global_data介紹》和《[uboot] (番外篇)uboot relocation介紹》結合起來看。
程式碼如下,去除無關程式碼部分
arch/arm/lib/crt0.S
ENTRY(_main)
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
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
@@ 以上是堆疊、GD、early malloc空間的分配,具體參考《[uboot] (番外篇)global_data介紹》
mov r0, #0
bl board_init_f
@@ uboot relocate前的板級初始化以及relocate的區域規劃,後續小節繼續說明
@@ 其中relocate區域規劃也可以參考一下《[uboot] (番外篇)uboot relocation介紹》
/*
* Set up intermediate environment (new sp and gd) and call
* relocate_code(addr_moni). Trick here is that we'll return
* 'here' but relocated.
*/
ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
ldr r9, [r9, #GD_BD] /* r9 = gd->bd */
sub r9, r9, #GD_SIZE /* new GD is below bd */
adr lr, here
ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */
add lr, lr, r0
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
b relocate_code
here:
/*
* now relocate vectors
*/
bl relocate_vectors
@@ GD、uboot、異常中斷向量表的relocate,可以參考《[uboot] (番外篇)uboot relocation介紹》,這裡不詳細說明
/* Set up final (full) environment */
bl c_runtime_cpu_setup /* we still call old routine here */
@@ 通過操作協處理器的c7暫存器來關閉Icache
ldr r0, =__bss_start /* this is auto-relocated! */
ldr r3, =__bss_end /* this is auto-relocated! */
mov r1, #0x00000000 /* prepare zero to clear BSS */
subs r2, r3, r0 /* r2 = memset len */
bl memset
@@ 因為堆疊段已經被relocate,所以這裡需要清空原來的堆疊段的內容
bl coloured_LED_init
bl red_led_on
@@ LED燈的初始化,可以不實現,想要實現的話,可以在board裡重新實現一個函式定義。
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov r0, r9 /* gd_t */
ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
/* call board_init_r */
ldr pc, =board_init_r /* this is auto-relocated! */
/* we should not return here. */
@@ uboot relocate後的板級初始化,注意,uboot必須在這裡就完成工作,或者在裡面實現死迴圈,不應該返回。
ENDPROC(_main)
通過上述,有兩個很重要的初始化函式,board_init_f和board_init_r,後續繼續說明。
2、board_init_f
程式碼如下:
common/board_f.c
void board_init_f(ulong boot_flags)
{
gd->flags = boot_flags;
gd->have_console = 0;
// 設定global_data裡面的一些標誌位
if (initcall_run_list(init_sequence_f))
hang();
// 呼叫initcall_run_list依次執行init_sequence_f函式數組裡面的函式,initcall_run_list這裡不深究
// 一旦init_sequence_f的函數出錯,會導致initcall_run_list返回不為0,而從卡掉
}
開啟DEBUG巨集之後,可以通過log觀察哪些init函式被呼叫,如下log:
uboot log中有如下log:
initcall: 23e005a4
根據u-boot.map可以發現對應
.text.print_cpuinfo
0x23e005a4 0x8 arch/arm/cpu/armv7/built-in.o
0x23e005a4 print_cpuinfo
也就是說print_cpuinfo被initcall呼叫了。
所以uboot relocate之前的板級初始化的核心就是init_sequence_f中定義的函數了。
如下,這裡只做簡單的說明,需要的時候再具體分析:
static init_fnc_t init_sequence_f[] = {
setup_mon_len,
// 計算整個映象的長度gd->mon_len
initf_malloc,
// early malloc的記憶體池的設定
initf_console_record,
// console的log的快取
arch_cpu_init, /* basic arch cpu dependent setup */
// cpu的一些特殊的初始化
initf_dm,
arch_cpu_init_dm,
mark_bootstage, /* need timer, go after init dm */
/* TODO: can any of this go into arch_cpu_init()? */
env_init, /* initialize environment */
// 環境變數的初始化,後續會專門研究一下關於環境變數的內容
init_baud_rate, /* initialze baudrate settings */
// 波特率的初始化
serial_init, /* serial communications setup */
// 串列埠的初始化
console_init_f, /* stage 1 init of console */
// console的初始化
print_cpuinfo, /* display cpu info (and speed) */
// 列印CPU的資訊
init_func_i2c,
init_func_spi,
// i2c和spi的初始化
dram_init, /* configure available RAM banks */
// ddr的初始化,最重要的是ddr ram size的設定!!!!gd->ram_size
// 如果說uboot是在ROM、flash中執行的話,那麼這裡就必須要對DDR進行初始化
//========================================
setup_dest_addr,
reserve_round_4k,
reserve_trace,
setup_machine,
reserve_global_data,
reserve_fdt,
reserve_arch,
reserve_stacks,
// ==以上部分是對relocate區域的規劃,具體參考《[uboot] (番外篇)uboot relocation介紹》
setup_dram_config,
show_dram_config,
display_new_sp,
reloc_fdt,
setup_reloc,
// relocation之後gd一些成員的設定
NULL,
};
注意,必須保證上述的函式都正確地返回0值,否則會導致hang。
3、board_init_r
程式碼如下:
common/board_r.c
void board_init_r(gd_t *new_gd, ulong dest_addr)
{
if (initcall_run_list(init_sequence_r))
hang();
// 呼叫initcall_run_list依次執行init_sequence_r函式數組裡面的函式,initcall_run_list這裡不深究
// 一旦init_sequence_r的函數出錯,會導致initcall_run_list返回不為0,而從卡掉
/* NOTREACHED - run_main_loop() does not return */
hang();
// uboot要求在這個函式裡面終止一切工作,或者進入死迴圈,一旦試圖返回,則直接hang。
}
所以uboot relocate之前的板級初始化的核心就是init_sequence_r中定義的函數了。
如下,這裡只做簡單的說明,需要的時候再具體分析:
common/board_r.c
init_fnc_t init_sequence_r[] = {
initr_trace,
// trace相關的初始化
initr_reloc,
// gd中一些關於relocate的標識的設定
initr_reloc_global_data,
// relocate之後,gd中一些的成員的重新設定
initr_malloc,
// malloc記憶體池的設定
initr_console_record,
bootstage_relocate,
initr_bootstage,
#if defined(CONFIG_ARM) || defined(CONFIG_NDS32)
board_init, /* Setup chipselects */
// 板級自己需要的特殊的初始化函式,如board/samsung/tiny210/board.c中定義了board_init這個函式
#endif
stdio_init_tables,
initr_serial,
// 串列埠初始化
initr_announce,
// 列印uboot執行位置的log
initr_logbuffer,
// logbuffer的初始化
power_init_board,
#ifdef CONFIG_CMD_NAND
initr_nand,
// 如果使用nand flash,那麼這裡需要對nand進行初始化
#endif
#ifdef CONFIG_GENERIC_MMC
initr_mmc,
// 如果使用emmc,那麼這裡需要對nand進行初始化
#endif
initr_env,
// 初始化環境變數
initr_secondary_cpu,
stdio_add_devices,
initr_jumptable,
console_init_r, /* fully init console as a device */
interrupt_init,
// 初始化中斷
#if defined(CONFIG_ARM) || defined(CONFIG_AVR32)
initr_enable_interrupts,
// 使能中斷
#endif
run_main_loop,
// 進入一個死迴圈,在死迴圈裡面處理終端命令。
};
最終,uboot執行到了run_main_loop,並且在run_main_loop進入命令列狀態,等待終端輸入命令以及對命令進行處理。
到此,uboot流程也就完成了,後續會專門說明uboot的run_main_loop是怎麼執行的。