前面分析了spl-uboot lds的链接脚本,提到了_start符号是整个程序的入口,链接器在链接时会查找目标文件中的_start符号代表的地址,把它设置为整个程序的入口地址。并且我们也知道start.S的代码段也是位于整个spl-uboot代码段最开始的位置,而_start符号对于Armv8架构来说位于则位于 arch\arm\cpu\armv8\start.S文件内 ,接下来我们将重点分析start.S都做了些什么。
.globl _start
_start:
b reset
_start 符号后紧跟的第一条指令指令为跳转指令,跳转到reset标号处,暂时接下往下看。
#ifdef CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK
/*
* 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
ARM_SOC_BOOT0_HOOK
#endif
上面这部分内容注释的很清楚,对于某些芯片需要特殊的一些初始配置的话,可以通过配置CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK并执行相应boot0.h中的代码来进行一些boot之前的操作。对于绝大多数CPU来说,这部分都是不需要的。
.align 3
.globl _TEXT_BASE
_TEXT_BASE:
.quad CONFIG_SYS_TEXT_BASE
/*
* These are defined in the linker script.
*/
.globl _end_ofs
_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
随后进行2^3 也就是8字节对齐。并声明若干个全局符号,这些符号的地址也是位于代码段中,后续反汇编便可以看到。我们先看下
_TEXT_BASE,该符号地址处存放的是一个8字节(.quad定义的是一个8字节的数据)的数 CONFIG_SYS_TEXT_BASE, 而CONFIG_SYS_TEXT_BASE对于ls1043 cpu来说定义如下:
#if defined(CONFIG_NAND_BOOT) || defined(CONFIG_SD_BOOT)
#define CONFIG_SYS_TEXT_BASE 0x82000000
#else
#define CONFIG_SYS_TEXT_BASE 0x60100000
#endif
由于ls1043 CPU支持不同启动方式,以NAND flash启动为例,CONFIG_SYS_TEXT_BASE 定义为0x82000000,为外部sdram地址空间,_TEXT_BASE的作用其实也是为了重定位使用,也是最终uboot在外部内存中实际存放的位置,也就是说需要将uboot代码从NANDFLASH搬运到该处。
/*
* These are defined in the linker script.
*/
.globl _end_ofs
_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
紧接着定义了几个符号_end_ofs _bss_start_ofs 和_bss_end_ofs,他们的地址也都是位于.text段中,值则是根据之前lds脚本中提到的长度为0的字符数组的值减去_start符号所在的地址,也就是离_start符号的地址值的偏移。还记得上篇分析链接脚本里面提到的对于ls1043 cpu,将bss段放在了外部存储sdram的0x80100000开始的512K空间内吗?所以,__bss_start的值即为0x80100000,而_start则是放在了.text段的最前面也就是内部sram空间0x10000000处,所以对于_bss_start_ofs来说,其值便为0x70100000。 对于_end_ofs,则根据_end-_start得到,而_end定义如下:
char _end[0] __attribute__((section(".__end")));
也就是说,_end和__bss_start一样是个不占空间的字符数组变量,其存放在.__end段中,这里为了更好说明,我再贴一下lds中的各段分布
.text : {
. = ALIGN(8);
*(.__image_copy_start)
CPUDIR/start.o (.text*)
*(.text*)
} >.sram
.rodata : {
. = ALIGN(8);
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*)))
} >.sram
.data : {
. = ALIGN(8);
*(.data*)
} >.sram
.u_boot_list : {
. = ALIGN(8);
KEEP(*(SORT(.u_boot_list*)));
} >.sram
.image_copy_end : {
. = ALIGN(8);
*(.__image_copy_end)
} >.sram
.end : {
. = ALIGN(8);
*(.__end)
} >.sram
可以看到_end位于.__end段,而.__end段在.end中,由于和.image_copy_end一样,这两个段中的两个变量所占空间都为0,所以实际上这里_end的值和__image_copy_end的值是相等的。
接下来重点分析上面提到的b reset后执行的代码:
reset:
#ifdef CONFIG_SYS_RESET_SCTRL
bl reset_sctrl
#endif
首先根据是否配置CONFIG_SYS_RESET_SCTRL来决定是否需要跳转到reset_sctrl执行。对于大多数单板是无需执行这一步的,这里为了详细描述,不妨也看看它到底做了哪些事情。
#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:
dsb sy
isb
b __asm_invalidate_tlb_all
ret
#endif
首先调用的是switch_el宏,获取当前的异常等级,并跳转到相应标号处执行,switch_el宏定义如下:
/*
* Branch according to exception level
*/
.macro switch_el, xreg, el3_label, el2_label, el1_label
mrs \xreg, CurrentEL
cmp \xreg, 0xc
b.eq \el3_label
cmp \xreg, 0x8
b.eq \el2_label
cmp \xreg, 0x4
b.eq \el1_label
.endm
首先将CurrentEL寄存器的值读取到x1寄存器中,随后分别和0xc 、0x8、以及0x4比较,若等于0xc则跳转到label 3处执行,若等于0x8 则跳到label 2处执行,若等于0x4则跳转到label 1处执行。我们查看Armv8寄存器手册,看看CurrentEL具体是怎么定义的。
可以看到,Bit3:2定义了当前异常等级,故0xc 对应EL3 ,0x8对应EL2,0x4对应EL1。以ls1043为例,虽然其并不会执行者段代码。ls1043上电起来后默认的异常等级为EL3,此时读当前异常等级为EL3 ,便跳转执行下面代码,
3:
mrs x0, sctlr_el3
b 0f
首先读取sctlr_el3的数据到x0寄存器中,随后跳转到标号0: 处执行。先来看一下sctlr_el3具体是什么寄存器。
EE bit位决定了EL3级别数据访问的大小端模式,也决定了EL3 在TLB进行虚实转换的stage1时的大小端格式,关于ARMv8虚拟内存架构相关的详细知识可以参考下面这个网页,非常详细。
https://armv8-ref.codingbelief.com/zh/chapter_d4/d4_the_aarch64_virtual_memory_system_archi.html
WXN bit位主要是用于控制可写内存区域是否是XN,什么意思呢?当该bit置1时,在EL3 TLB转换表里所有的 writable 的 memory region 都会被视为 XNExecute-never ,也就意味着相应的 memory region 将无法执行 instructions,按照我的理解应该就是禁用了TLB。
I bit位则是用于控制i-cache的开关。
SA bit位则用与控制SP对齐检查,当置位1时,在EL3下使用load 或者store指令时,当用SP作为基址并且不是16字节对齐的话,则会产生一个SP非对齐异常。
C BIT:d-cache,数据cache使能位。
A BIT:对齐检查使能,
M BIT :则是是否谁能MMU
了解该寄存器后我们往下看,做了什么操作。
0:
ldr x1, =0xfdfffffa
and x0, x0, x1
跳转到标号0,然后将x0寄存器中保存的sctlr_el3里的值和x1寄存器中的值0xfdfffffa进行位与再写回到x0中,其实就是将bit2 和bit0还有bit 25 清0。再将x0寄存器的数据写回到sctlr_el3。此时的状态为:小端,MMU 关闭 i/d cache 都关闭(i cache bit控制位默认复位起来就是0,关闭icache的状态)。sctrl_el2和sctlr_el1的流程类似,此处无需赘述。
接着往下分析:
/*
* Could be EL3/EL2/EL1, Initial State:
* Little Endian, MMU Disabled, i/dCache Disabled
*/
adr x0, vectors
switch_el x1, 3f, 2f, 1f
3: msr 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 */
#ifdef COUNTER_FREQUENCY
ldr x0, =COUNTER_FREQUENCY
msr cntfrq_el0, x0 /* Initialize CNTFRQ */
#endif
b 0f
2: msr vbar_el2, x0
mov x0, #0x33ff
msr cptr_el2, x0 /* Enable FP/SIMD */
b 0f
1: msr vbar_el1, x0
mov x0, #3 << 20
msr cpacr_el1, x0 /* Enable FP/SIMD */
上面这段代码中,首先将中断向量的地址保存到x0寄存器中,随后再次判断当前的异常等级,并将中断向量的地址写到各个EL对应的
VBAR寄存器中去。我们先看一下中断向量vectors的定义:路径为:
exceptions.S arch\arm\cpu\armv8
/*
* Exception vectors.
*/
.align 11
.globl vectors
vectors:
.align 7
b _do_bad_sync /* Current EL Synchronous Thread */
.align 7
b _do_bad_irq /* Current EL IRQ Thread */
.align 7
b _do_bad_fiq /* Current EL FIQ Thread */
.align 7
b _do_bad_error /* Current EL Error Thread */
.align 7
b _do_sync /* Current EL Synchronous Handler */
.align 7
b _do_irq /* Current EL IRQ Handler */
.align 7
b _do_fiq /* Current EL FIQ Handler */
.align 7
b _do_error /* Current EL Error Handler */
可以看到首先将首地址进行2^11次方也就是2K对齐,为什么需要2K对齐,是因为我们后面保存中断向量地址的VBAR寄存器低11位是不保存的,后面会看到这点。此外各个中断向量地址2^7次方也就是128字节对齐,为什么以128字节对齐?是因为arm定义的异常entry偏移是以128字节为单位。
可以看到每个中断向量根据中断类型的不同都是一条跳转指令,跳转到对应的中断处理函数,我们以_do_bad_sync为例,如下:
/*
* do_bad_sync handles the impossible case in the Synchronous Abort vector.
*/
void do_bad_sync(struct pt_regs *pt_regs, unsigned int esr)
{
efi_restore_gd();
printf("Bad mode in \"Synchronous Abort\" handler, esr 0x%08x\n", esr);
show_regs(pt_regs);
panic("Resetting CPU ...\n");
}
有些人可能会奇怪,我们栈都没有初始化,怎么使用C函数?很简单,没人说这个函数会在栈初始化之前就会被调用啊,另外相应的中断使能位也没打开,所以是此时是执行不到这段代码的。 我们接着分析这段代码做了什么,假设出现了Synchronous Abort(如未定义的指令、data abort、prefetch instruction abort、前面提到的SP未对齐异常,debug exception等等),则会进入到该函数中,首先调用efi_restore_gd(),这个应该是和EFI相关的东东,在ARM上应该用的不多,这里忽略不管。紧接着打印依据异常报错,随后将ARM CPU的各个寄存器值都打印出来方便定位问题,最后调用panic复位CPU,对于freescale的CPU来说最终会调用到下面。
void reset_cpu(ulong addr)
{
u32 __iomem *rstcr = (u32 *)CONFIG_SYS_FSL_RST_ADDR;
u32 val;
/* Raise RESET_REQ_B */
val = scfg_in32(rstcr);
val |= 0x02;
scfg_out32(rstcr, val);
}
分析完中断向量后,我们接着之前分析,将中断向量表的首地址也就是vectors标号对应的地址根据当前异常等级写到对应的VBAR_ELn中去。下面看一下VBAR_ELn中的定义:
可以看到,低11bit都为0,所以这符合之前中断向量表首地址分配前,先进行了2K对齐。
另外,我们可以看到当异常等级为EL3时,将scr_el3寄存器的低4bit置为了1。我们分析一下这个寄存器:
其中BIT3 EA bit位置1表示,在任何异常等级下发生的External Abort 和 SError 中断都路由到EL3进行处理。FIQ, bit [2]置1则表示在任何异常等级下发生的物理FIQ(快速中断请求)路由到EL3进行处理。IRQ, bit [1]置1表示在任何异常等级下发生的物理IRQ(中断请求)路由到EL3进行处理,NS, bit [0]置1表示EL0和EL1 这两个异常等级时非安全态,在这两个异常等级下的内存访问是不能访问安全域的内存空间的。
此外,EL3下还采用了一条指令msr cptr_el3, xzr /* Enable FP/SIMD */ 对cptr_el3 的三个bit位清零,分别是:
TCPAC, bit [31] ,当清0时表示打开
TFP, bit [10]:清0时 打开SIMD和FP浮点运算功能。
TTA, bit [20],打开系统寄存器访问trace寄存器权限。
最后在EL3这部分代码还根据是否定义COUNTER_FREQUENCY,来配置cntfrq_el0 系统时钟计数器的频率,这个频率我们经常可以用来进行比较精确的延时。对于ls1043 cpu来说,CPU的外部输入时钟是100MHZ,而内部时钟计数器的频率默认为25MHZ,所以这里其定义了该宏并且配置的值为25Mhz,如下:
#ifdef COUNTER_FREQUENCY
ldr x0, =COUNTER_FREQUENCY
msr cntfrq_el0, x0 /* Initialize CNTFRQ */
#endif
/* Generic Timer Definitions */
#define COUNTER_FREQUENCY 25000000 /* 25MHz */
对于EL2和EL1等级下,之后的代码和EL3一样使能SIMD和FP功能。之所以EL2和EL1可以访问相应的寄存器,就是因为之前配置了 cptr_el3寄存器开放了访问权限。
紧接着代码如下:
/* Enalbe SMPEN bit for coherency.
* This register is not architectural but at the moment
* this bit should be set for A53/A57/A72.
*/
mrs x0, S3_1_c15_c2_1 /* cpuactlr_el1 */
orr x0, x0, #0x40
msr S3_1_c15_c2_1, x0
这里很多人会有一个疑问,为啥我查armv8寄存器手册搜不到cpuactlr_el1寄存器?S3_1_c15_c2_1 又是个啥东东。其实ARMv8 architecture 定义了一些处理器必须实现的寄存器如 SCTLR_EL1外,也预留了一部分编码空间给处理器特定的或者成为IMP DEF(未实现定义)的寄存器。这样处理器的设计者便可以选择处理器到底要实现哪些这种寄存器。为此,通过注释我们其实可以发现,对于特定的Armv8架构CPU如A53的CPU便实现了该寄存器,我们查A53的手册如下:
https://developer.arm.com/docs/ddi0500/g/preface
其访问方式如下:
所以说这里给其写上0x40,则将SMPEN的bit位置1,也就是使能了CPU各个核之间的数据一致性功能。
接下来的部分便是arm一些勘误的修复函数,如下:
/* Apply ARM core specific erratas */
bl apply_core_errata
这部分略过,因为里面很多勘误对于很多CPU来说都不存在,所以基本上没做啥事情。
我们接着分析:
/*
* 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
走到这一步,可以看到目前的状态是Cache/BPB/TLB 都是无效的,因为前面已经将这些都关闭了。随后一个跳转指令跳转到
lowlevel_init函数,即:
WEAK(lowlevel_init)
//此处省略代码,方便阅读
ENDPROC(lowlevel_init)
首先可以看到lowlevel_init函数是一个弱函数,所以说对于不同的CPU可以自己定义一个同名的函数进行覆盖,对于ls1043 cpu来说其自己也实现了相应的lowlevel_init函数(路径:lowlevel.S arch\arm\cpu\armv8\fsl-layerscape )。这里为了具有普适性,我们只分析下默认的lowlevel_init都做了哪些事情。
mov x29, lr /* Save LR */
#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3)
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
首先将lr寄存器的值保存到x29寄存器中,为什么要这么做呢?首先在调用lowlevel_init函数前,会先将调用完后返回的地址保存到lr寄存器中,以便函数执行完之后知道要返回到哪里继续往下执行。又由于lowlevel_init里面会进一步调用其他函数,导致lr的值会被修改,所以需要保存最早的返回地址,以便可以正确返回。
随后根据当前CPU GIC的版本选择不同的分支,所谓GIC就是ARM的通用中断控制器,对于ls1043 CPU来说,其实现的GIC版本为GICv2。我们以GICV2为例,首先通过branch_if_slave这个宏判断当前执行该代码的CPU是master 核还是slave核,对于ls1043 cpu来说,其主要为1个cluster 4个核,其中核0就是master ,其他都为slave。我们看一下它是怎么判断是否是master核的。
/*
* Branch if current processor is a slave,
* choose processor with all zero affinity value as the master.
*/
.macro branch_if_slave, xreg, slave_label
#ifdef CONFIG_ARMV8_MULTIENTRY
/* NOTE: MPIDR handling will be erroneous on multi-cluster machines */
mrs \xreg, mpidr_el1
tst \xreg, #0xff /* Test Affinity 0 */
b.ne \slave_label
lsr \xreg, \xreg, #8
tst \xreg, #0xff /* Test Affinity 1 */
b.ne \slave_label
lsr \xreg, \xreg, #8
tst \xreg, #0xff /* Test Affinity 2 */
b.ne \slave_label
lsr \xreg, \xreg, #16
tst \xreg, #0xff /* Test Affinity 3 */
b.ne \slave_label
#endif
.endm
首先传入的参数是x0寄存器和要跳转的label为1,此外对于多核CPU来说,是配置了CONFIG_ARMV8_MULTIENTRY,从ls1043 ardb的配置文件也可以看到CONFIG_ARMV8_MULTIENTRY=y
随后通过读取mpidr_el1寄存器来获取当前的核ID,
依次读取每个Affinity域里面的ID号,如果值不为0,则表示是一个slave core,就会跳到lable1 后的代码执行。这里假设我们读到的是master 主核,那么会多执行下面两步
ldr x0, =GICD_BASE
bl gic_init_secure
也就是说只有master核才能配置。首先将GICD_BASE这个地址保存到x0(x0 是可以作为传参寄存器直接用的), 当做参数传递给gic_init_secure函数。GICD_BASE是什么呢?其实是ARM GIC里面distributor相关寄存器组的基地址,关于什么是distributor,这里给个说明:【中断分发器,用来收集所有的中断来源,并且为每个中断源设置中断优先级,中断分组,中断目的core。当有中断产生时,将当前最高优先级中断,发送给对应的cpu interface】。这里不详细展开,感兴趣的可以去看些GIC的详细描述,后续有时间也会专门总结下。
接着我们看看gic_init_secure里面做了哪些事情。
函数路径:
gic_64.S arch\arm\lib
ENTRY(gic_init_secure)
/*
* Initialize Distributor
* x0: Distributor Base
*/
#if defined(CONFIG_GICV3)
mov w9, #0x37 /* EnableGrp0 | EnableGrp1NS */
/* EnableGrp1S | ARE_S | ARE_NS */
str w9, [x0, GICD_CTLR] /* Secure GICD_CTLR */
ldr w9, [x0, GICD_TYPER]
and w10, w9, #0x1f /* ITLinesNumber */
cbz w10, 1f /* No SPIs */
add x11, x0, (GICD_IGROUPRn + 4)
add x12, x0, (GICD_IGROUPMODRn + 4)
mov w9, #~0
0: str w9, [x11], #0x4
str wzr, [x12], #0x4 /* Config SPIs as Group1NS */
sub w10, w10, #0x1
cbnz w10, 0b
#elif defined(CONFIG_GICV2)
mov w9, #0x3 /* EnableGrp0 | EnableGrp1 */
str w9, [x0, GICD_CTLR] /* Secure GICD_CTLR */
ldr w9, [x0, GICD_TYPER]
and w10, w9, #0x1f /* ITLinesNumber */
cbz w10, 1f /* No SPIs */
add x11, x0, GICD_IGROUPRn
mov w9, #~0 /* Config SPIs as Grp1 */
str w9, [x11], #0x4
0: str w9, [x11], #0x4
sub w10, w10, #0x1
cbnz w10, 0b
ldr x1, =GICC_BASE /* GICC_CTLR */
mov w0, #3 /* EnableGrp0 | EnableGrp1 */
str w0, [x1]
mov w0, #1 << 7 /* allow NS access to GICC_PMR */
str w0, [x1, #4] /* GICC_PMR */
#endif
1:
ret
ENDPROC(gic_init_secure)
这里以ls1043为例,其GIC的版本是V2,走下面的分支,首先将0x3写到w9寄存器(w9就是相当于32bit的x9寄存器),让后将w9中的值写到x0 加上偏移GICD_CTRL地址里面去。我们上面知道x0传进来的是GIC distributor的寄存器基地址,所以这里其实就是去配置distributor的ctrl寄存器也就是GICD_CTRL寄存器,我们看看这个寄存器的定义。
这里将bit 0 和bit 1都置1,使能了Grp0和Grp1的中断上送到CPU interface开关。其实对于中断来说,包括下面三种:
SPI中断、SGI中断和PPI中断,其中SPI中断是共享中断,而SGI中断是软件产生中断,是某个核通过写GICD_SGIR寄存器产生的一种中断,其作用主要是用于核间通信。而SPI中断和PPI中断分别为共享外设中断和私有外设中断,主要的区别在于,对于SPI中断,Distributor可以路由到多个核去处理,也就是说多个核都可以来处理这个这个中断请求。而PPI就是指定只有某个核才能处理的中断类型。对于这些不同类型的中断,其也有对应的中断ID,GIC 的Distributor前面说过可以对中断进行分组,将不同的中断分配到Grp0和Grp1中去。所以这里首先是使能了Grp0和Grp1的中断上送。也就是说属于这两个组的中断现在有资格进行后续流程如最低上报优先级门限来比较,从而来决定是否可以上送给对应cpu interface。
紧接着读取GICR_TYPER寄存器的低5bit获取多有少个中断线,如下:
前面我们知道对于不同类型的中断,其分配了不同的中断ID,如下:
ID0-ID15 are used for SGIs
ID16-ID31 are used for PPIs
Interrupt numbers ID32-ID1019 are used for SPIs
还有几个特殊的终端号这里略过不讲。
而这里ITLineNumber的值决定了GIC支持的最大中断号(即0-num of ID - 1),其实对于不同的CPU来说其实现的GIC不一定支持1020这个最大规定的中断个数。所以可以通过读取ITLineNumber来获得当前CPU支持的最大中断号,也就是最大支持多少个中断。这里计算方法是32(N+1),若这里N=0,那么就意为着最大支持32个中断,也就是中断ID(0 - 31)对应的就是所有的SGI和PPI中断。也就意味着不支持SPI中断。其实也就相当于SPI的中断数 = 32* ITLineNumber的值,所以上面代码中下面的部分:
and w10, w9, #0x1f /* ITLinesNumber */
cbz w10, 1f /* No SPIs */
其实就是判断该CPU有多少个SPI中断,如果ITLinesNumber = 0,表示没有SPI中断,也就直接跳到标号1处退出该函数了。
如果ITLinesNumber 的值不为0,则执行下面的部分:
add x11, x0, GICD_IGROUPRn
mov w9, #~0 /* Config SPIs as Grp1 */
str w9, [x11], #0x4
0: str w9, [x11], #0x4
sub w10, w10, #0x1
cbnz w10, 0b
这里的逻辑很简单,首先X0里面保存的还是GICD相关寄存器的基址,GICD_IGROUPRn 的值其实就是GICD_IGROUPRn(看到没这里有个小写的n表示这是一组寄存器,也就是多个寄存器) 这一组寄存器中首个寄存器离GICD相关寄存器的基址的偏移,我们再看一下GICD_IGROUPRn 的定义:
什么意思呢,其实GICD_IGROUPRn 是一组连续的寄存器,每个寄存器的宽度是32bit,每个bit代表对应的中断ID归属哪个中断Group。举例来说假设GICD_IGROUPR0为首个寄存器,那么其bit0代表中断ID为0的中断,如果bit0的值为1,则表示将中断ID为0的中断分到中断Group1组中去,同理bit31 的值为1 则表示将中断ID为31的中断分到中断Group1组中去。以此类推,第二个寄存器GICD_IGROUPR1的bit0 则不是代表中断ID为1,而是中断ID为32的中断了,以此类推往后叠加。
所以我们看到:
add x11, x0, GICD_IGROUPRn
mov w9, #~0 /* Config SPIs as Grp1 */
str w9, [x11], #0x4
这里其实跳过了首个GICD_IGROUPRn 寄存器,为什么呢,因为前面0-31的中断对应的是SGI和PPI中断,而我们这个函数其实只是将SPI中断划到中断Group1里面去,所以从中断ID32开始,也就是对应第二个GICD_IGROUPRn 寄存器开始,所以加了个4字节偏移。然后将其中的值全部置1,也就是将中断ID32-中断ID63都划到了Group1。
0: str w9, [x11], #0x4
sub w10, w10, #0x1
cbnz w10, 0b
这部分其实就是一个循环,首先将x11接着往后移4字节,也就相当于下一个GICD_IGROUPRn 寄存器的地址,从前面代码可以知道w10里面保存的是ITLinesNumber 的值,一个ITLinesNumber 对应32个中断,假设这里ITLinesNumber = 4. 那么SPI总的中断数为(4+1)*32 - 32(前面PPI和SGI) = 128,对应中断ID为(32-159) 。当进循环之前已经将32-63都置1了,也就是将第二个GICD_IGROUPRn 的32bit都置1了,然后进来后,地址在往后偏移先4字节,相当于第三个GICD_IGROUPRn 的地址,将该寄存器的值都置为1,对应中断号64-95,然后将w10 也就是ITLinesNumber -1 此时w10 = 3,然后,再判断此时w10是否等于0,若不等于0,继续跳到上面地址再向后移4个字节....直到将所有SPI中断都划到中断Group1中去。
随后主从核都会执行下面代码:
#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
即都会跳转到gic_ini_secure_percpu去,同时传进去的参数分别是GIC distributor寄存器组的基地址和cpu interface组的基地址
为了不过于细节,下面我们只分析gic_init_sercure_percpu中GICv2部分代码,GICv3部分有兴趣的朋友自行分析。
ENTRY(gic_init_secure_percpu)
#if defined(CONFIG_GICV3)
......
#elif defined(CONFIG_GICV2)
/*
* Initialize SGIs and PPIs
* x0: Distributor Base
* x1: Cpu Interface Base
*/
mov w9, #~0 /* Config SGIs and PPIs as Grp1 */
str w9, [x0, GICD_IGROUPRn] /* GICD_IGROUPR0 */
mov w9, #0x1 /* Enable SGI 0 */
str w9, [x0, GICD_ISENABLERn]
/* Initialize Cpu Interface */
mov w9, #0x1e7 /* Disable IRQ/FIQ Bypass & */
/* Enable Ack Group1 Interrupt & */
/* EnableGrp0 & EnableGrp1 */
str w9, [x1, GICC_CTLR] /* Secure GICC_CTLR */
mov w9, #0x1 << 7 /* Non-Secure access to GICC_PMR */
str w9, [x1, GICC_PMR]
#endif
ret
ENDPROC(gic_init_secure_percpu)
首先通过将GICD_IGROUPRN寄存器组的第一个寄存器也就是SGI和PPI中断都分配到中断Grp1组中去。
然后将GICD_ISENABLERn寄存器组的第一个寄存器的最低bit位置1,我们看一下GICD_ISENABLERn的寄存器是怎么定义的:
由此,可知将bit0置1即使能SGI0中断。之所以使能SGI0中断,主要是因为,从核执行到一定阶段会一直在某段代码自旋,而主核接着往后执行,当执行到一定阶段后,主核则会唤醒从核跳转到某个地址继续执行,而主核唤醒其他从核时,就是通过SGI中断进行唤醒(还记得之前说过SGI中断是用于核间通信的吗?)。接下去的代码则是去初始化cpu interface,所谓cpu interface和distributor一样是GIC的组成部分。每个处理器通过cpu interface与GIC相连,其主要提供以下功能:
接着分析代码,首先将cpu interface的GICC_CTLR寄存器写0x1e7
可以看到首先将bit0 bit1 置1,其实就是使能Group0和 Group1的中断有资格通过CPU interface 上送到对应处理器进行处理,和前面distributor使能Group1 中断类似,其实就是相当于分开关一样。bit2 AckCtl 置1决定了,当挂起的中断是中断Group1的中断时, 此时可以通过读取GICC_IAR寄存器或者GICC_HPPIR 器返回的获取到该中断的中断ID,则会此外,此时对 GICC_IAR 寄存器进行一次读操作便意味着确认并激活该中断,而若AckCtl 位为0,则不会确认Group1 的中断,所以这里本质上是使能Group1中断的ack确认功能。一个中断只有能被确认后,才会送到处理器进行处理。
然后将bit5-bit8都置1即disable Group0和 Group1 中IRQ/FIQ 的Bypass功能,所谓Bypass功能是指,一个CPU interface可以选择是否开启中断信号bypass功能, 开启该功能后,当cpu interface对某个中断关闭了向处理器发送中断请求信号功能时,会产生一个系统legacy中断信号发送给处理器,直接bypass GIC处理。
当处理器处理完一个中断的时候,需要通过写GICC_EOIR寄存器告诉cpu interface 我处理完了,而bit 9的EOImodeS主要控制访问GICC_EOIR的行为。当EOImodeS = 0 时,当处理器处理完中断并对GICC_EOIR寄存器进行操作,会产生两种效果:
1.将该中断状态从激活态变为去激活。
2. 进行优先级 drop以便接着处理下一个挂起的中断中优先级最高的中断。
而如果EOImodeS = 1时,则对GICC_EOIR寄存器进行操作只会进行优先级drop动作,要想将中断状态从激活态变为去激活态则需要通过写另一个GICC_DIR寄存器才能实现。
至于bit 10是bit9的非安全访问时的copy。Armv8里有很多安全和非安全访问两种情况下专门的寄存器和bit位,这个概念知道就好。
最后执行的代码是将GICC_PMR寄存器的bit7置1,
这个寄存器控制的就是cpu interface的优先级mask,其主要作用是,当请求的中断的中断优先级比这个寄存器里面配置的优先级要低,中断直接会被屏蔽掉不上送给处理器,就相当于一个门限。需要注意的是值越小,优先级越高,所以0是最高优先级。这里将bit7置1,则门限是128,也就是说优先级level值小于128的中断才能上送。
接下来要执行的代码如下:
#ifdef CONFIG_ARMV8_MULTIENTRY
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.
*/
bl armv8_switch_to_el2
#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
bl armv8_switch_to_el1
#endif
#endif /* CONFIG_ARMV8_MULTIENTRY */
2:
mov lr, x29 /* Restore LR */
ret
首先判断是否是主核,是的话直跳到lable2 返回,而从核则首先调用gic_wait_for_interrupt函数,然后再从EL3降级到EL2。
我们先看下gic_wait_for_interrupt做了什么。
/*************************************************************************
* For Gicv2:
* void gic_wait_for_interrupt(CpuInterfaceBase);
* For Gicv3:
* void gic_wait_for_interrupt(void);
*
* Wait for SGI 0 from master.
*
*************************************************************************/
ENTRY(gic_wait_for_interrupt)
#if defined(CONFIG_GICV3)
gic_wait_for_interrupt_m x9
#elif defined(CONFIG_GICV2)
gic_wait_for_interrupt_m x0, w9
#endif
ret
ENDPROC(gic_wait_for_interrupt)
使用的是宏展开如下:
#if defined(CONFIG_GICV3)
.macro gic_wait_for_interrupt_m xreg1
0 : wfi
mrs \xreg1, ICC_IAR1_EL1
msr ICC_EOIR1_EL1, \xreg1
cbnz \xreg1, 0b
.endm
#elif defined(CONFIG_GICV2)
.macro gic_wait_for_interrupt_m xreg1, wreg2
0 : wfi
ldr \wreg2, [\xreg1, GICC_AIAR]
str \wreg2, [\xreg1, GICC_AEOIR]
and \wreg2, \wreg2, #0x3ff
cbnz \wreg2, 0b
.endm
#endif
我们只分析GICv2的分支,首先调用WFI(Wait for interrupt)指令等待中断到来,如果中断没来就在这等待,如果中断来了,读GICC_AIAR寄存器的值读取到w9寄存器中然后,即确认该中断并获取中断号。
然后将获取到的值写进GICC_AEOIR
也就是将该中断去激活。随后判断该中断的中断号是不是0也就是是不是SGI0中断,如果不是SGI0中断则继续循环跳到WFI(Wait for interrupt)指令等待中断到来,还记得SGI中断是核间通信使用的吗,所以只有当主核给从核发出一个SGI0中断时才会将从核从这里释放出来,而其他类型的中断到来都会直接在这里给去激活结束掉。这种spin机制正如其注释里所说:
/*
* Slave should wait for master clearing spin table.
* This sync prevent salves observing incorrect
* value of spin table and jumping to wrong place.
*/
当主核发出SGI中断唤醒从核时,接着就会往下执行
/*
* All slaves will enter EL2 and optionally EL1.
*/
bl armv8_switch_to_el2
#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
bl armv8_switch_to_el1
#endif
我们可以看一下armv8_switch_to_el2是怎么实现切换EL等级的。
ENTRY(armv8_switch_to_el2)
switch_el x0, 1f, 0f, 0f
0: ret
1: armv8_switch_to_el2_m x0
ENDPROC(armv8_switch_to_el2)
.macro armv8_switch_to_el2_m, xreg1
/* 64bit EL2 | HCE | SMD | RES1 (Bits[5:4]) | Non-secure EL0/EL1 */
mov \xreg1, #0x5b1
msr scr_el3, \xreg1
msr cptr_el3, xzr /* Disable coprocessor traps to EL3 */
mov \xreg1, #0x33ff
msr cptr_el2, \xreg1 /* Disable coprocessor traps to EL2 */
/* Initialize Generic Timers */
msr cntvoff_el2, xzr
/* Initialize SCTLR_EL2
*
* setting RES1 bits (29,28,23,22,18,16,11,5,4) to 1
* and RES0 bits (31,30,27,26,24,21,20,17,15-13,10-6) +
* EE,WXN,I,SA,C,A,M to 0
*/
mov \xreg1, #0x0830
movk \xreg1, #0x30C5, lsl #16
msr sctlr_el2, \xreg1
/* Return to the EL2_SP2 mode from EL3 */
mov \xreg1, sp
msr sp_el2, \xreg1 /* Migrate SP */
mrs \xreg1, vbar_el3
msr vbar_el2, \xreg1 /* Migrate VBAR */
mov \xreg1, #0x3c9
msr spsr_el3, \xreg1 /* EL2_SP2 | D | A | I | F */
msr elr_el3, lr
eret
.endm
上面代码最有意思的地方是使用eret。这部分内容后面补上,还是很有意思的。
到此为止,整个lowlevel_init函数分析完了。其主要做的事情就是配置GIC中断控制器的distributor和cpu interface相关寄存器,将中断进行分组,使能部分中断如SGI0等,设置cpu interface的优先级mask level。然后从核等待主核SGI0中断唤醒,随后从核切换EL2或者可选择性的是否切换到EL1。
最后的部分如下:
#ifdef CONFIG_ARMV8_MULTIENTRY
branch_if_master x0, x1, master_cpu
/*
* Slave CPUs
*/
slave_cpu:
wfe
ldr x1, =CPU_RELEASE_ADDR
ldr x0, [x1]
cbz x0, slave_cpu
br x0 /* branch to the given address */
master_cpu:
/* On the master CPU */
#endif /* CONFIG_ARMV8_MULTIENTRY */
bl _main
之后从核的一直读取CPU_RELEASE_ADDR这个地址处保存的值,当其为0时,则一直在这里循环等待。这个地址的值是要等主核执行到一定阶段时,才会修改,也就会再次唤醒从核接着跳转到该地址保存的跳转地址处继续执行,, 也就是从核在主核唤醒前,一直在这里自旋。
为此,整个Start.S中的流程就分析完了,到目前为止,主核将要跳转到_main执行,在主核没SGI0唤醒从核前,从核一直在等待中断,唤醒后,从核切换到EL2或EL1,然后等待跳转地址跳转。
后面则开始分析_main所在的crt0_64.S 都做了哪些事情。
以上为自己学习总结,如有错误,欢迎指正,谢谢!