1.開場白
環(huán)境:
uboot版本:uboot-2020.01
內(nèi)核源碼:linux-5.0
ubuntu版本:20.04.1
ATF版本:2.1
代碼閱讀工具:vim+ctags+cscope
一般嵌入式系統(tǒng)使用的都是對稱多處理器(Symmetric Multi-Processor, SMP)系統(tǒng),包含了多個cpu, 這幾個cpu都是相同的處理器,如4核Contex-A53。但是在系統(tǒng) 啟動階段他們的地位并不是相同的,其中core0是主cpu(也叫引導(dǎo)處理器),其他core是從cpu(也叫輔處理器),引導(dǎo)cpu負(fù)責(zé)執(zhí)行我們的啟動加載程序如uboot,以及初始化內(nèi)核,系統(tǒng)初始化完成之后主core會啟動從處理器。
一般主處理器啟動從處理器有以下三種:
**(2).spin-table **
(3).PSCI
第一種ACPI是高級配置與電源接口(Advanced Configuration and Power Interface)一般在x86平臺用的比較多,而后兩種spin-table(自旋表)和PSCI(電源狀態(tài)協(xié)調(diào)協(xié)議 Power State Coordination)會在arm平臺上使用,本系列 主要講解后兩種 。主要內(nèi)容分為上下兩篇如下:
上篇:
1.開場白
2.cpu啟動的一些基本概念
3.支持spin-table情況
下篇:
4.支持psci情況
5.從處理器啟動進(jìn)入內(nèi)核世界之后做了些什么
6.最后說兩句
2.cpu啟動的一些概念
1)cpu啟動的含義:cpu可以從內(nèi)存中取指、譯碼、執(zhí)行,當(dāng)然內(nèi)存可以是soc片內(nèi)的sram,也可以是ddr。
2)我們要知道,程序為何可以在多個cpu上并發(fā)執(zhí)行:他們有各自獨立的一套寄存器,如:程序計數(shù)器pc,棧指針寄存器sp,通用寄存器等,可以獨自 取指、譯碼、執(zhí)行,當(dāng)然內(nèi)存和外設(shè)資源是共享的,多核環(huán)境下當(dāng)訪問臨界區(qū) 資源一般 自旋鎖來防止競態(tài)發(fā)生。
3)soc啟動流程:soc啟動的一般會從片內(nèi)的rom, 也叫bootrom開始執(zhí)行第一條指令,這個地址是系統(tǒng)默認(rèn)的啟動地址,會在bootrom中由芯片廠家固化一段啟動代碼來加載啟動bootloader到片內(nèi)的sram,啟動完成后的bootloader除了做一些硬件初始化之外做的最重要的事情是初始化ddr,因為sram的空間比較小所以需要初始化擁有大內(nèi)存 ddr,最后會從網(wǎng)絡(luò)/usb下載 或從存儲設(shè)備分區(qū)上加載內(nèi)核到ddr某個地址,為內(nèi)核傳遞參數(shù)之后,然后bootloader就完成了它的使命,跳轉(zhuǎn)到內(nèi)核,就進(jìn)入了操作系統(tǒng)內(nèi)核的世界。
4)linux內(nèi)核啟動流程:bootloader將系統(tǒng)的控制權(quán)交給內(nèi)核之后,他首先會進(jìn)行處理器架構(gòu)相關(guān)初始化部分,如設(shè)置異常向量表,初始化mmu(之后內(nèi)核就從物理地址空間進(jìn)入了虛擬地址空間的世界,一切是那么的虛無縹緲,又是那么的恰到好處)等等,然后會清bss段,設(shè)置sp之后跳轉(zhuǎn)到C語言部分進(jìn)行更加復(fù)雜通用的初始化,其中會進(jìn)行內(nèi)存方面的初始化,調(diào)度器初始化,文件系統(tǒng)等內(nèi)核基礎(chǔ)組件 初始化工作,隨后會進(jìn)行關(guān)鍵的從處理器的引導(dǎo)過程,然后是各種實質(zhì)性的設(shè)備驅(qū)動的初始化,最后 創(chuàng)建系統(tǒng)的第一個用戶進(jìn)程init后進(jìn)入用戶空間執(zhí)行用戶進(jìn)程宣誓內(nèi)核初始化完成,可以進(jìn)程正常的調(diào)度執(zhí)行。
5)系統(tǒng)初始化階段大多數(shù)都是主處理器做初始化工作,所有不用考慮處理器并發(fā)情況,一旦從處理器被bingup起來,調(diào)度器和各自的運行隊列準(zhǔn)備就緒,多個任務(wù)就會均衡到各個處理器,開始了并發(fā)的世界,一切是那么的神奇。
3.支持spin-table情況
了解了關(guān)于cpu啟動的一些基本概念,下面開始我們的正題,講解arm64常用的兩種cpu啟動方式。首先,我們來看一下比較簡單的自旋表的方式啟動從處理器。
從bootloader說起(以uboot為例):首先,上電后主處理器和從處理器都會啟動,執(zhí)行uboot,從uboot的_start的匯編代碼開始執(zhí)行,主處理器在uboot中歡快的執(zhí)行后啟動內(nèi)核,進(jìn)入內(nèi)核執(zhí)行,而從處理器會執(zhí)行到spin_table_secondary_jump中(注意:之前執(zhí)行的代碼,設(shè)置的寄存器都是各cpu獨立的寄存器)
arch/arm/cpu/armv8/start.S:
19 .globl _start
20 _start:
...
151 #if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_SPL_BUILD)
152 branch_if_master x0, x1, master_cpu //判斷是否為主cpu(core0),是跳轉(zhuǎn)到master_cpu,否則往下走
153 b spin_table_secondary_jump //跳轉(zhuǎn)執(zhí)行
...
arch/arm/cpu/armv8/spin_table_v8.S:
9 ENTRY(spin_table_secondary_jump)
10 .globl spin_table_reserve_begin
11 spin_table_reserve_begin:
12 0: wfe
13 ldr x0, spin_table_cpu_release_addr
14 cbz x0, 0b
15 br x0
16 .globl spin_table_cpu_release_addr
17 .align 3
18 spin_table_cpu_release_addr:
19 .quad 0
20 .globl spin_table_reserve_end
21 spin_table_reserve_end:
22 ENDPROC(spin_table_secondary_jump)
在spin_table_secondary_jump中:首先會 執(zhí)行wfe指令,使得從處理器睡眠等待 。如果被喚醒,則從處理器會判斷spin_table_cpu_release_addr這個地址是否為0,為0則繼續(xù)跳轉(zhuǎn)到wfe處繼續(xù)睡眠,否則跳轉(zhuǎn)到spin_table_cpu_release_addr指定的地址處執(zhí)行。
那么這個地址什么時候會被設(shè)置呢?答案是:主處理器在uboot中讀取設(shè)備樹的相關(guān)節(jié)點屬性獲得,我們來看下如何獲得。 執(zhí)行路徑為:
do_bootm_linux
- >boot_prep_linux
- >image_setup_linux
- >image_setup_libfdt
- >arch_fixup_fdt
- >spin_table_update_dt
在spin_table_update_dt函數(shù)中做了幾件非常重要的事情:
arch/arm/cpu/armv8/spin_table.c:
11 int spin_table_update_dt(void *fdt)
12 {
13 int cpus_offset, offset;
14 const char *prop;
15 int ret;
16 unsigned long rsv_addr = (unsigned long)&spin_table_reserve_begin;
17 unsigned long rsv_size = &spin_table_reserve_end -
18 &spin_table_reserve_begin;
19 //獲取設(shè)備樹的cpus節(jié)點的偏移
20 cpus_offset = fdt_path_offset(fdt, "/cpus");
21 if (cpus_offset < 0)
22 return -ENODEV;
23 //尋找每一個device_type屬性為cpu的節(jié)點
24 for (offset = fdt_first_subnode(fdt, cpus_offset);
25 | offset >= 0;
26 | offset = fdt_next_subnode(fdt, offset)) {
27 prop = fdt_getprop(fdt, offset, "device_type", NULL);
28 if (!prop || strcmp(prop, "cpu"))
29 continue;
30
31 /*
32 |* In the first loop, we check if every CPU node specifies
33 |* spin-table. Otherwise, just return successfully to not
34 |* disturb other methods, like psci.
35 |*///獲得enable-method屬性,比較屬性值是否為 "spin-table"(即是使用自旋表啟動方式)
36 prop = fdt_getprop(fdt, offset, "enable-method", NULL);
37 if (!prop || strcmp(prop, "spin-table"))
38 return 0;
39 }
40
41 for (offset = fdt_first_subnode(fdt, cpus_offset);
42 | offset >= 0;
43 | offset = fdt_next_subnode(fdt, offset)) {
//找到cpu節(jié)點
44 prop = fdt_getprop(fdt, offset, "device_type", NULL);
45 if (!prop || strcmp(prop, "cpu"))
46 continue;
47 //重點:設(shè)置cpu-release-addr屬性值為spin_table_cpu_release_addr的地址!
48 ret = fdt_setprop_u64(fdt, offset, "cpu-release-addr",
49 (unsigned long)&spin_table_cpu_release_addr);
50 if (ret)
51 return -ENOSPC;
52 }
53 //設(shè)置設(shè)備樹的保留內(nèi)存 :添加一個內(nèi)存區(qū)域為16和17行描述的地址范圍(這是物理地址)
54 ret = fdt_add_mem_rsv(fdt, rsv_addr, rsv_size);
55 if (ret)
56 return -ENOSPC;
57
58 printf(" Reserved memory region for spin-table: addr=%lx size=%lx\\n",
59 | rsv_addr, rsv_size);
60
61 return 0;
62 }