1 u-boot在汇编启动阶段对系统的一些初始化
当cpu交由u-boot接管进入u-boot后,首先会到_start符号处开始执行初始化,并在此期间完成一些必要的系统寄存器相关的初始化,包括保存boot参数,进行地址无关fixed,系统寄存器复位,底层平台相关初始化等,启动代码位于arch/arm/cpu/armv8/start.S,入口地址为_start。
1.1 启动前为后续流程做的一些平台相关操作
从_start开始,u-boot会根据board定义做一些平台化相关的初始化工作或者是保存一些重要寄存器信息,代码如下:
/*************************************************************************
*
* Startup Code (reset vector)
*
*************************************************************************/
.globl _start
_start: ------------------------------------------------------------------------ (1)
#if defined(CONFIG_LINUX_KERNEL_IMAGE_HEADER) ---------------------------------- (2)
#include
#elif defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK) -------------------------------- (3)
/*
* Various SoCs need something special and SoC-specific up front in
* order to boot, allow them to set that in their boot0.h file and then
* use it here.
*/
#include
#else
b reset ----------------------------------------------------------------- (4)
#endif
.align 3
.globl _TEXT_BASE ------------------------------------------------------------ (5)
_TEXT_BASE:
.quad CONFIG_SYS_TEXT_BASE
/*
* These are defined in the linker script.
*/
.globl _end_ofs -------------------------------------------------------------- (5)
_end_ofs:
.quad _end - _start
.globl _bss_start_ofs
_bss_start_ofs:
.quad __bss_start - _start
.globl _bss_end_ofs
_bss_end_ofs:
.quad __bss_end - _start
reset:
/* Allow the board to save important registers */
b save_boot_params ----------------------------------------------------- (6)
.globl save_boot_params_ret
save_boot_params_ret:
... /* 此处省略无关代码,待分析到时再展开代码 */
...
WEAK(save_boot_params)
b save_boot_params_ret /* back to my caller */
ENDPROC(save_boot_params)
#endif
(1)_start段标记为全局可见并在链接脚本中被声明为入口地址,表示u-boot的入口地址为_start;
(2)首先进入入口后有两种可配置情况,一种就是定义了LINUX_KERNEL_IMAGE_HEADER,
boot0-linux-kernel-header.h展开部分后如下:
.macro le64sym, sym
.long sym()_lo32
.long sym()_hi32
.endm
.globl _start
_start:
/*
* DO NOT MODIFY. Image header expected by Linux boot-loaders.
*/
b reset /* branch to kernel start, magic */
.long 0 /* reserved */
le64sym _kernel_offset_le /* Image load offset from start of RAM, little-endian */
le64sym _kernel_size_le /* Effective size of kernel image, little-endian */
le64sym _kernel_flags_le /* Informative flags, little-endian */
.quad 0 /* reserved */
.quad 0 /* reserved */
.quad 0 /* reserved */
.ascii "ARMx64" /* Magic number */
.long 0 /* reserved */
此处将与在链接脚本中定义的LINUX_KERNEL_IMAGE_HEADER对应起来,为u-boot头部添加一个类似与Linux arm64 的Image头部,首先是起始8字节,如果没有定义efi相关的功能则是一个跳转指令,跳转到reset段继续执行启动流程,其他如链接脚本中解释一致;
(3)第二种可能的配置,就是定义了ENABLE_ARM_SOC_BOOT0_HOOK配置,此处的头文件根据不同board会引用到不同头文件,如瑞芯微的最终会引用到如下部分代码头文件:
#ifdef CONFIG_SPL_BUILD
/*
* We need to add 4 bytes of space for the 'RK33' at the
* beginning of the executable. However, as we want to keep
* this generic and make it applicable to builds that are like
* the RK3368 (TPL needs this, SPL doesn't) or the RK3399 (no
* TPL, but extra space needed in the SPL), we simply insert
* a branch-to-next-instruction-word with the expectation that
* the first one may be overwritten, if this is the first stage
* contained in the final image created with mkimage)...
*/
b 1f /* if overwritten, entry-address is at the next word */
1:
#endif
#if CONFIG_IS_ENABLED(ROCKCHIP_EARLYRETURN_TO_BROM)
adr r3, entry_counter
ldr r0, [r3]
cmp r0, #1 /* check if entry_counter == 1 */
beq reset /* regular bootup */
add r0, #1
str r0, [r3] /* increment the entry_counter in memory */
mov r0, #0 /* return 0 to the BROM to signal 'OK' */
bx lr /* return control to the BROM */
entry_counter:
.word 0
#endif
#if (defined(CONFIG_SPL_BUILD) || defined(CONFIG_ARM64))
/* U-Boot proper of armv7 do not need this */
b reset
#endif
#if !defined(CONFIG_ARM64)
/*
* For armv7, the addr '_start' will used as vector start address
* and write to VBAR register, which needs to aligned to 0x20.
*/
.align(5), 0x0
_start:
ARM_VECTORS
#endif
#if !defined(CONFIG_TPL_BUILD) && defined(CONFIG_SPL_BUILD) &&
(CONFIG_ROCKCHIP_SPL_RESERVE_IRAM > 0)
.space CONFIG_ROCKCHIP_SPL_RESERVE_IRAM /* space for the ATF data */
#endif
因为有些设备boot到u-boot之前已经有安全固件了,所以此时控制权交给u-boot时,其实是可能有一些传递参数信息的要求,比如这里瑞芯微芯片通过bootrom boot到tpl后,后续在完成tpl初始化后会将控制权再交还给bootrom固件,由bootrom固件继续加载spl,所以这里在进行u-boot流程之前保存了bootrom的返回地址,以便后续瑞芯微板级软件使用。定义有可能也是arm32的模式,所以还可能在入口地址处保存异常向量表;
(4)如果对应board没有上述两种需求,那么_start段则是一条最简单的跳转指令b reset跳转到reset处继续启动流程初始化;
(5)在_start到reset之间,有一个.align 3用于8字节对齐,因为可能在读取常量地址之前各自平台做了自己代码逻辑导致当前地址并不是8字节对齐的,这里不管是否对齐都强制对齐了一下,之后还保存了一些常量信息,其中包括_TEXT_BASE保存了链接地址,用于在启动地址无关功能时进行对运行时地址的偏移计算,其他几个偏移值目前未使用;
(6)save_boot_params用于保存一些board相关的重要寄存器,此处定义为了一个弱函数,为直接跳转回save_boot_params_ret继续往下执行,如果某些board需要保存寄存器参数则可以在自己的lowlevel.S文件中实现此函数。一般由atf,bl2或者rom跳转到spl或u-boot时厂商可能需要在两个固件之间传递参数,比如由bl2在寄存器x0,x1,x2中分别存入了一些固件的地址信息,那么u-boot则可以在早期通过此函数保存这些信息,并在后续某个时机中使用。
1.2 开启地址无关后的重定位地址操作
在由save_boot_params跳转回save_boot_params_ret后启动流程继续往下执行来到下面代码段:
save_boot_params_ret:
#if CONFIG_POSITION_INDEPENDENT
/* Verify that we're 4K aligned. */
adr x0, _start
ands x0, x0, #0xfff --------------------------------------------------------- (1)
b.eq 1f
0:
/*
* FATAL, can't continue.
* U-Boot needs to be loaded at a 4K aligned address.
*
* We use ADRP and ADD to load some symbol addresses during startup.
* The ADD uses an absolute (non pc-relative) lo12 relocation
* thus requiring 4K alignment.
*/
wfi
b 0b
1:
/*
* Fix .rela.dyn relocations. This allows U-Boot to be loaded to and
* executed at a different address than it was linked at.
*/
pie_fixup:
adr x0, _start /* x0 <- Runtime value of _start */
ldr x1, _TEXT_BASE /* x1 <- Linked value of _start */
subs x9, x0, x1 /* x9 <- Run-vs-link offset */
beq pie_fixup_done ------------------------------------------------------------- (2)
adrp x2, __rel_dyn_start /* x2 <- Runtime &__rel_dyn_start */ ----------- (3)
add x2, x2, #:lo12:__rel_dyn_start
adrp x3, __rel_dyn_end /* x3 <- Runtime &__rel_dyn_end */
add x3, x3, #:lo12:__rel_dyn_end
pie_fix_loop: ---------------------------------------------------------------------- (4)
ldp x0, x1, [x2], #16 /* (x0, x1) <- (Link location, fixup) */
ldr x4, [x2], #8 /* x4 <- addend */
cmp w1, #1027 /* relative fixup? */
bne pie_skip_reloc
/* relative fix: store addend plus offset at dest location */
add x0, x0, x9
add x4, x4, x9
str x4, [x0]
pie_skip_reloc:
cmp x2, x3
b.lo pie_fix_loop
pie_fixup_done:
#endif
此部分的功能主要是在定义POSITION_INDEPENDENT后,进行地址无关的相对地址修复,以此保证后续在跳入c语言部分时可正常执行,一般不定义此配置则是继续往下执行boot流程。
(1)正如在链接脚本中说的,地址无关功能最基本需要保证加载地址是4K对齐的,经过一些测试发现某些情况需要64K对齐,这里不展开说明。此处使用adr指令获取_start的运行时地址并检验是否是4K对齐,如果是则继续往下执行,如果不是则调用wfi指令挂死在此处。wfi为等待中断指令,只有在接收到中断事件是才会唤醒cpu继续往下执行。(补充一个知识点:wfi指令只能被中断唤醒,wfe指令可以被sev指令唤醒也可以被中断唤醒)
(2)通过对运行时地址和加载地址相减得到一个地址偏移值,如果偏移值等于0,说明加载地址和运行时地址是一致的不需要进行地址修复,则直接跳pie_fixup_done继续下面流程初始化,否则就进入地址修复逻辑。
(3)首先需要说明为什么不使用adr指令而是adrp指令,adr指令是一个小范围读相对pc指针地址内存的指令,可以使用adr说明读取的地址一定是离pc指针很近的位置,而当读取__rel_dyn_start这种并不能确定实际地址在哪里的地址时则只能使用大范围读地址指令的adrp指令,不过adrp指令是以页为单位进行读取的,所以add x2, x2, #:lo12:__rel_dyn_start的作用就是将__rel_dyn_start地址的页内偏移读取出来并与页对齐的运行地址相加得到实际的运行地址。(此操作指令在Linux上被封装为adr_l指令)
(4)在对地址修复分析时,首先需要了解一个elf动态库重定位的知识点,先来看一个结构体定义:
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} Elf64_Rela;
当对动态库进行地址重定向时首先会去查找rela.dyn段中的信息,此段中每一组信息就是上面结构体定义的信息,对于64为系统的elf则是24字节为一个表,r_offset保存需要重定位作用的地址位置,r_info描述此表重定位类型此类型特定于处理器,如arm64位则是:
/* AArch64 relocs */
#define R_AARCH64_NONE 0 /* No relocation */
#define R_AARCH64_RELATIVE 1027 /* Adjust by program base */
r_addend是一个常量加数,用于计算存储在可重定位字段中的值。
对表进行重定位有以下公式:
重定位为值 = *(r_offset + 实际偏移值) = 实际偏移值 + r_addend。
根据此公式则可以重定向每一个需要重定向的地址值。
因此在此处同样是如此,x0保存r_offset值,x1保存r_info值,x4保存r_addend,并通过x1与R_AARCH64_RELATIVE比较看是否属于aarch64相对地址类型,如果不是则不是需要重定向的一组表则跳到pie_skip_reloc判断是否完成所有rela.dyn段重定向,如果是则继续往下执行启动流程初始化,如果不是则跳回pie_fix_loop继续下一组表的重定向。当判断属于aarch64相对地址类型时进入重定向逻辑,首先需要知道x9是运行地址减去链接地址得到的偏移地址,那么实际运行地址也等于链接地址加上x9,所以add x0, x0, x9,add x4, x4, x9,x0是需要修复的重定位运行地址,x4是实际运行时需要附加的常量值,根据上面的公式,将x4这个由附加常量值加偏移值得到的运行时附加常量值写入到x0这个运行时重定向地址即可完成整个重定向修复功能。此处没有实际代码或者流程图展示,逻辑比较绕,大致逻辑就是根据重定向段中每24个字节组成的一个表读取出实际需要进行重定向的地址,将这个地址的运行地址找出来并往这个地址写入附加常量值加偏移值即可完成一次重定向。在完成段中所有重定向后此时地址已经被修复好了,后续调用任何绝对地址引用的指令也不会出问题了。当然这个操作是耗时的,一般也不会有board会开启此功能。
1.3 进入_main之前系统寄存器初始化和从核的引导
在完成地址无关fixup后,u-boot开始对一些系统寄存器进行初始化,第一段代码如下:
pie_fixup_done:
#endif
#ifdef CONFIG_SYS_RESET_SCTRL
bl reset_sctrl --------------------------------------------------------------------- (1)
#endif
#if defined(CONFIG_ARMV8_SPL_EXCEPTION_VECTORS) || !defined(CONFIG_SPL_BUILD) ---------- (2)
.macro set_vbar, regname, reg
msr regname, reg
.endm
adr x0, vectors
#else
.macro set_vbar, regname, reg
.endm
#endif
/*
* Could be EL3/EL2/EL1, Initial State:
* Little Endian, MMU Disabled, i/dCache Disabled
*/
switch_el x1, 3f, 2f, 1f ---------------------------------------------------------- (3)
3: set_vbar vbar_el3, x0
mrs x0, scr_el3
orr x0, x0, #0xf /* SCR_EL3.NS|IRQ|FIQ|EA */
msr scr_el3, x0
msr cptr_el3, xzr /* Enable FP/SIMD */
b 0f
2: mrs x1, hcr_el2
tbnz x1, #34, 1f /* HCR_EL2.E2H */
set_vbar vbar_el2, x0
mov x0, #0x33ff
msr cptr_el2, x0 /* Enable FP/SIMD */
b 0f
1: set_vbar vbar_el1, x0
mov x0, #3 << 20
msr cpacr_el1, x0 /* Enable FP/SIMD */
0:
#ifdef COUNTER_FREQUENCY -------------------------------------------------------------- (4)
branch_if_not_highest_el x0, 4f
ldr x0, =COUNTER_FREQUENCY
msr cntfrq_el0, x0 /* Initialize CNTFRQ */
#endif
4: i*** ------------------------------------------------------------------------------- (5)
...
...
#ifdef CONFIG_SYS_RESET_SCTRL
reset_sctrl:
switch_el x1, 3f, 2f, 1f
3:
mrs x0, sctlr_el3
b 0f
2:
mrs x0, sctlr_el2
b 0f
1:
mrs x0, sctlr_el1
0:
ldr x1, =0xfdfffffa
and x0, x0, x1
switch_el x1, 6f, 5f, 4f
6:
msr sctlr_el3, x0
b 7f
5:
msr sctlr_el2, x0
b 7f
4:
msr sctlr_el1, x0
7:
d*** sy
i***
b __asm_invalidate_tlb_all ----------------------------------------------------- (6)
ret
#endif
(1)一般情况下此功能不需要使用,但是一些由其他固件引导启动的u-boot,board希望系统行为能按照自己预期行为执行而不受上一级加载器的影响,所以使用CONFIG_SYS_RESET_SCTRL来决定是否重置系统控制寄存器,包括保证处理器处于小端,关闭data cache,关闭mmu。其中switch_el是一个宏,用于读取当前所处的异常级别,根据所处异常级别调用对应的系统控制寄存器。某些时候u-boot的加载并不是一定在el3级别,当存在atf等时,el3由atf控制,atf会将u-boot的运行级别切换到el2,以便保证自己的控制级别,所以u-boot通过switch_el来选择自己能够控制的系统寄存器。
(2)定义设置异常向量表的宏,将异常向量表的地址写入/reg设置的系统寄存器即可完成异常向量表的设置,这里u-boot是需要设置异常向量表的,而spl默认是不需要设置异常向量表的,毕竟spl只是一个加载器只会运行一次,不过当定义了CONFIG_ARMV8_SPL_EXCEPTION_VECTORS时可以为spl也设置一个异常向量表。
(3)同样的使用switch_el来跳转到对应级别的路径上去执行,在进行系统寄存器设置时,因为在这之前已经由SYS_RESET_SCTRL或者board自己保证处理器处于小端,mmu关,i-cache和d-cache处于关闭状态了,所以这里直接进行对应级别系统寄存器设置,首先是跳转到对应表设置对应级别的异常向量表。接着会有如下三种情况:
当处于EL3时,会设置安全配置系统寄存器(scr_el3),会将低四位bit设置为0xf,表示设置处理器处于非安全模式,任何级别的物理irq中断,物理fiq,异常abort中断,异常SError中断都将被路由到el3级别。后续这些设置将在启动Linux时被修改,这些设置仅用于在u-boot阶段。接着将cptr_el3清零,使用xzr是可以快速操作寄存器为零。这里保证任何级别下访问SIMD和floating-point指令不会导致触发异常陷入el3。
当处于EL2时,首先根据HCR_EL2.E2H判断系统是一个虚拟机管理器还是主机系统,当E2H = 0时,表示系统处于主机系统只需要做el3一样的操作配置SIMD和FP指令不会陷入el2即可。
当系统处于EL1时,则什么也不需要操作只需要配置SIMD和FP指令不会陷入el1。
(4)u-boot在启动时系统的时钟频率不一定配置了,所以当在include/configs/xxxxxx.h中定义了COUNTER_FREQUENCY的频率值时,说明需要在此处配置系统时钟,所以根据宏 branch_if_not_highest_el判断当系统不处于EL3时则需要设置系统的时钟工作频率cntfrq_el0,后续Linux或者u-boot根据读出的这个值计算出系统每纳秒的滴答数从而供软件获取时间流逝值。
(5)i***指令用于确保上述操作指令被正确真正的执行了,属于同步指令的一种。
(6)在进行系统控制器复位时,d*** sy,i***,__asm_invalidate_tlb_all三个操作在这里的意义是,因为对处理器的小端,mmu,d-cache进行了复位,所以这里必须通过d***和i***确保数据和指令全部执行和写入,这里进行了mmu和cache关闭操作,那么如果有缓存的tlb在这个时候这些缓存的tlb数据就是无效的,这里对可能缓存的tlb进行全部无效化,确保后续任何可能的mmu开启操作不会使用到这些无用的tlb条目而导致系统异常。
第二段代码如下:
4: i***
/*
* Enable SMPEN bit for coherency.
* This register is not architectural but at the moment
* this bit should be set for A53/A57/A72.
*/
#ifdef CONFIG_ARMV8_SET_SMPEN -------------------------------------------------------- (1)
switch_el x1, 3f, 1f, 1f
3:
mrs x0, S3_1_c15_c2_1 /* cpuectlr_el1 */
orr x0, x0, #0x40
msr S3_1_c15_c2_1, x0
i***
1:
#endif
/* Apply ARM core specific erratas */
bl apply_core_errata ------------------------------------------------------------ (2)
/*
* Cache/BPB/TLB Invalidate
* i-cache is invalidated before enabled in icache_enable()
* tlb is invalidated before mmu is enabled in dcache_enable()
* d-cache is invalidated before enabled in dcache_enable()
*/
/* Processor specific initialization */
bl lowlevel_init ---------------------------------------------------------------- (3)
...
...
WEAK(apply_core_errata)
...
...
WEAK(lowlevel_init)
mov x29, lr /* Save LR */
#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3) ----------------------------------- (4)
branch_if_slave x0, 1f
ldr x0, =GICD_BASE
bl gic_init_secure
1:
#if defined(CONFIG_GICV3)
ldr x0, =GICR_BASE
bl gic_init_secure_percpu
#elif defined(CONFIG_GICV2)
ldr x0, =GICD_BASE
ldr x1, =GICC_BASE
bl gic_init_secure_percpu
#endif
#endif
#ifdef CONFIG_ARMV8_MULTIENTRY ------------------------------------------------------ (5)
branch_if_master x0, x1, 2f
/*
* Slave should wait for master clearing spin table.
* This sync prevent salves observing incorrect
* value of spin table and jumping to wrong place.
*/
#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3)
#ifdef CONFIG_GICV2
ldr x0, =GICC_BASE
#endif
bl gic_wait_for_interrupt
#endif
/*
* All slaves will enter EL2 and optionally EL1.
*/
adr x4, lowlevel_in_el2
ldr x5, =ES_TO_AARCH64
bl armv8_switch_to_el2
lowlevel_in_el2:
#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
adr x4, lowlevel_in_el1
ldr x5, =ES_TO_AARCH64
bl armv8_switch_to_el1
lowlevel_in_el1:
#endif
#endif /* CONFIG_ARMV8_MULTIENTRY */
2:
mov lr, x29 /* Restore LR */
ret
ENDPROC(lowlevel_init)
(1)首先解释S3_1_c15_c2_1,有一些架构定义系统寄存器是不能被编译器识别的,如果要访问则只能直接通过如下定义的形式被ARM汇编器识别并编译成二进制:S3__ 。这里使用S3_1_C15_C2_0,则不需要将编码交给汇编程序,如果直接使用对应的CPUACTLR_EL1则必须把它的编码交给汇编程序,而由于arm汇编程序并不识别此系统器从而指导编译报错退出。这里是当定义了CONFIG_ARMV8_SET_SMPEN并处于EL3级别时设置cpuectlr_el1的SMPEN位用于开启多核之间的数据一致性功能,当然这个功能是因为设计的问题导致只是一些a系列处理器需要设置,如a53,a57,a72。
(2)因为一些基于armv8架构设计的处理器本身会存在一些bug,所以这里对特定处理器进行勘误设置,相当于打补丁的意思,感兴趣的可以去看看每个a系列有哪些勘误需要被设置。
(3)低平台初始化,这里可以在汇编阶段对对应平台board进行初始化,lowlevel_init是一个弱符号,可以由厂商自己实现,这里分析armv8的标准lowlevel_init进行的初始化。(4)中详述。
(4)首先是对GIC的一些初始化,同样的需要根据使用的gic版本在Kconfig中选中CONFIG_GICV2或者CONFIG_GICV3,当定义了这两个宏其中一个时就会对gic进行相应初始化。
当定义的是GICV2时,则需要在include/configs/xxxxx.h中定义GICD_BASE和GICC_BASE,分别说明GIC的分发器基地址和cpu接口寄存器基地址。
当定义的是GICV3时,则需要在include/configs/xxxxx.h中定义GICD_BASE,说明GIC的主分发器基地址,因为cpu接口在使用gicv3时可通过armv8拓展的系统寄存器接口访问,所以这里不用定义cpu接口的基地址,gicv3使用系统寄存器访问cpu接口的原因,主要是因为cpu对gic cpu接口寄存器的访问是频繁的,为了少一些对系统总线的访问,直接在v3中使用系统寄存器访问,大大提高了cpu接口的读写速度,提高整体性能。
当是boot cpu执行时则会调用gic_init_secure对应安全组等相关的初始化,具体代码在arch/arm/lib/gic_64.S中,这里不展开只说明具体做了哪些事情:
如果是gicv3则主要是对分发器的初始化:激活group0,激活非安全的group1,激活安全的group1,激活安全和非安全的亲和性路由(绑定中断到某些cpu功能)。读取支持的最大spi中断数量,并配置。
如果是gicv2则首先对分发器进行初始化,激活group0和group1,和读取spi的支持数并配置。接着初始化cpu接口寄存器,激活group0和group1,并配置允许非安全模式下对GICC_PMR的访问,GICC_PMR是中断优先级的配置,在linux上没有对优先级有过多使用,只有一次性配置。
当是从核cpu执行到这里时则会根据branch_if_slave跳转到1标号处进行percpu的gic初始化,branch_if_slave的实现是读取cpu各自的标识符寄存器mpidr_el1获取自己的簇号进行区分的,细节可以查看此系统寄存器的描述。
同样的对percpu的gic初始化是gic_init_secure_percpu,意思就是每个cpu都要进行相应的设置,比如gicv3会对每个cpu的标识进行识别,以便精确ppi中断的分发,所以会将每个cpu的mpidr_el1值写入对应的每个cpu的重分发器里,所以称为percpu初始化,这也是gicv3能支持到128个cpu的原因,而gicv2只能支持8个cpu核心,具体实现可看arch/arm/lib/gic_64.S。
(这里说一个之前遇到的bug,在进行bring up时,linux无法产生任何中断,包括核间中断,后续发现就是u-boot中这段初始化没有调用,导致没有配置中断在非安全状态下路由到哪个group,所以即便在el1或者el0产生了中断,中断也被路由到了group0,而linux的gic设置为:只处理非安全状态下的group1中断,所以linux无法产生中断)
(5)u-boot虽然是运行单个cpu的程序,但也允许从核cpu进入u-boot,这里典型的例子就是使用SPIN_TABLE方式启动从核在u-boot中自选等待进入linux。当要使用spin_table时则必须开启CONFIG_ARMV8_MULTIENTRY 选项,用于让从核进入u-boot进行基本初始化。首先如果是主核自然不用走从核流程则直接在这里返回完成lowlevel_init的调用。如果是从核并且定义了gicv2或者gicv3则首先是当从核进入u-boot后,进入gic_wait_for_interrupt使用wfi休眠,等待主核的事件唤醒,也就是smp_kick_all_cpus操作,让从核cpu可以继续往下执行。bl armv8_switch_to_el2因为u-boot处于el3级别时,当启动linux时会将异常级别降低到el2或者el1来启动linux,根据具体设置来切换,而处于el3时则会将异常级别切换到el2,所以这里的操作是与主核一致,从核要进入linux,首先就是要将自己的异常级别从el3切换到el2。是否真实的能切到el2还要根据自己当前的级别,如果已经是el1了自然无法切换到el2。
当然如果定义了CONFIG_ARMV8_SWITCH_TO_EL1,说明还得切换从核到el1,这里将从核切换到el1级别去。
第三段代码如下:
#if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_SPL_BUILD) --------------------- (1)
branch_if_master x0, x1, master_cpu
b spin_table_secondary_jump
/* never return */
#elif defined(CONFIG_ARMV8_MULTIENTRY)
branch_if_master x0, x1, master_cpu
/*
* Slave CPUs
*/
slave_cpu: ----------------------------------------------------------------------------- (2)
wfe
ldr x1, =CPU_RELEASE_ADDR
ldr x0, [x1]
cbz x0, slave_cpu
br x0 /* branch to the given address */
#endif /* CONFIG_ARMV8_MULTIENTRY */
master_cpu:
bl _main
... /* arch/arm/cpu/armv8/spin_table_v8.S */
ENTRY(spin_table_secondary_jump) ------------------------------------------------------ (3)
.globl spin_table_reserve_begin
spin_table_reserve_begin:
0: wfe
ldr x0, spin_table_cpu_release_addr
cbz x0, 0b
br x0
.globl spin_table_cpu_release_addr
.align 3
spin_table_cpu_release_addr:
.quad 0
.globl spin_table_reserve_end
spin_table_reserve_end:
ENDPROC(spin_table_secondary_jump)
(1)首先是当定义了CONFIG_ARMV8_SPIN_TABLE启动从核时并且不处于SPL中,spl是不需要启动从核的。当是主核运行时直接跳转到_main离开系统寄存器初始化,开始下一段board_f等的u-boot初始化流程,如果是从核则会跳转到spin_table_secondary_jump(3),在这里,从核cpu将会开始自旋,使用wfe指令休眠,并在接收到中断或者sev事件时醒来并读取spin_table_cpu_release_addr地址里的值,如果是0说明并不是引导进入linux的事件发生则继续调用wfe休眠等待唤醒,如果spin_table_cpu_release_addr地址里不是0说明这时候已经由linux唤醒了,从核需要进入linux开始下一段旅程,则直接跳转到指定的地址里去继续执行。spin_table_cpu_release_addr地址将会在u-boot加载linux的设备树时直接写入到linux使用的设备树的对应节点中去,以便不需要由人为指定地址,linux和u-boot即可完成从核启动的交接,这就是spin_table的工作原理。
(2)当不是spin_table启动从核时,说明这是由用户定义需要在u-boot或者其他地方使用从核,所以直接通过CPU_RELEASE_ADDR宏定义了一个跳转地址的存储地址,在用户任何时候需要从核执行时往CPU_RELEASE_ADDR地址写入对应要跳转的地址后开始执行从核。
综上在完成系统寄存器配置和从核引导启动后,u-boot将会跳转至_main离开系统寄存的初始化,开始u-boot本身的初始化。
原作者:张衡天
|