需求
为了提升uboot阶段并行化效率,希望在uboot中使用另外一个CPU CORE执行函数,加快系统启动时间。
只要core0和core1两个CPU并发起来,暂时不考虑CPU之间的同步与竞争。
背景知识(可直接跳过)
ARMv8寄存器
通用寄存器
1. X0~X7参数和结果寄存器,用于参数传递、返回结果,也可用作临时寄存器或调用者保护的寄存器。
2. X9~X15调用者保护寄存器:如果函数调用者用到了这些寄存器,在调用函数之前,需要将这些寄存器压栈,函数返回后再从栈中恢复。
3. X19~X28:被调用者保护寄存。
4. 具有特殊用途的寄存器
- X8间接结果寄存器:用于传递间接结果的地址,例如,函数返回大型结构,大小超过X0-X7,当然对于这种情况,一般推荐返回是指针,而不是结构体
- 内部调用临时寄存器X16-X17
- X18临时寄存器,uboot的全局变量gd地址就存在X18(后面为从核配置gd要用到)
- X29:栈帧寄存器
- X30:链接寄存器
特殊寄存器
- zero寄存器:读出来的都是0。XZR和WZR分别对应aarch32和aarch64。
- PC:程序计数器。(armv8中PC不能被大多数指令访问,尤其是不能直接修改(不能mov PC xxx)使用跳转(bl b等)或堆栈相关指令间接修改 PC)
- SP:堆栈指针,指向栈顶。有SP_EL0、SP_EL1、SP_EL2、SP_EL3。
- ELR:(exception link register)异常链接寄存器,保存异常返回地址。EL0没有这个寄存器、EL1/2/3有自己的单独的ELR。比如:当处理器将异常处理交给EL1处理时,会将返回地址保存在ELR_EL1中,在异常返回时,PC从ELR_EL1恢复。
- SPSR:程序状态保存寄存器,EL0没有这个寄存器、EL1/2/3有自己的单独的SPSR,用于异常。
- LR:链接寄存器,执行子程序调用时,存放pc寄存器地址,以便在执行完程序后返回程序。
- PSTATE:处理器状态寄存器。包括几个寄存器:NZCV(条件寄存器),DAIF(异常屏蔽寄存器),SPSEL(sp选择寄存器),CurrentEL(异常等级寄存器)。
- CPSR:当前程序状态寄存器,保存当前程序状态信息。
PE异常等级EL
armv8将cpu分为4个Exception Level(异常等级),数字越大,权限越大。
- EL0:一般的应用程序
- EL1:操作系统(Linux、windows)
- EL2:虚拟化,虚拟机管理,可以不实现
- EL3:安全固件,如ARM Trusted Firmware
通过svc、hvc、smc三个指令陷入不同的异常等级。为了支持EL3,ARM设计了ATF(ARM Trusted Firmware),用来响应EL3访问(可以理解为专门提供高特权级EL3的服务)。
Linux kernel中SMP多核启动过程简述
主核通过smc
进去el3请求开核服务,ATF会响应这种请求,然后启动从核并设置一些寄存器(scr_el3、spsr_el3、elr_el3)
,然后主核恢复现场,eret1
回到EL1,从核开核后从bl3_warm_entrypoint
开始执行,通过el3_exit
返回到el1的elr_el3
设置的地址。
psci_cpu_on
主要完成开核工作,然后设置一些异常返回后(EL1->EL3->EL1)
的寄存器值,返回后会ep->pc写到cpu_context结构的CTX_ELR_EL3
(从核会启动后会从这个地址取指执行)
实际上所有的从核启动后,都会从bl3_warm_entrypoint
,在psci_plat_pm_ops
中设置。每个平台都有自己的启动地址寄存器,通过写这个寄存器来获得上电后执行的指令地址。
->psci_dt_init() //drivers/firmware/psci.c
->init_fn()
->get_set_conduit_method() //根据设备树method属性设置 invoke_psci_fn = __invoke_psci_fn_smc; (method="smc")
->psci_cpu_on()
->invoke_psci_fn()
->__invoke_psci_fn_smc()
-> arm_smccc_smc(function_id, arg0, arg1, arg2, 0, 0, 0, 0, &res) //这个时候x0=function_id x1=arg0, x2=arg1, x3arg2,...
->__arm_smccc_smc()
... //具体的汇编代码
启动从核的时,会调用psci_cpu_on()
,最终调用smc
。传递的第一个参数为function_id
,就是期望smc
做哪些事情;第二个是cpu id
,标识启动哪个CPU;第三个参数为从核启动后进入内核执行的地址secondary_entry
(这是个物理地址)。
最后smc调用时的参数为__invoke_psci_fn_smc
(期望功能,CPU的id,唤醒后的从核执行位置,0)
__invoke_psci_fn_smc
在内核中的实现:
static unsigned long __invoke_psci_fn_smc(unsigned long function_id,
unsigned long arg0, unsigned long arg1,
unsigned long arg2)
{
struct arm_smccc_res res;
arm_smccc_smc(function_id, arg0, arg1, arg2, 0, 0, 0, 0, &res);
return res.a0;
}
具体做法
运行环境
- CPU:CortexA53 *4,uboot主核:core0,需要唤醒从核:core1
- uboot版本:2018.09-00006
先由core0陷入ATF唤醒core1
上面也说了在armv8中一般通过ATF(ARM Trusted Firmware)执行PSCI功能。在uboot中主核(core0)是运行的,所以可以通过core0来唤醒core1。
__invoke_psci_fn_smc(ARM_PSCI_0_2_FN_CPU_ON, SLAVE_CORE_ID, (uint64_t)slave_e, 0);
何时唤醒:
在common/board_r.c
的init_sequence_r
里唤醒core1。这里已经进入后板级初始化了,core0本身已经稳定下来了,开始probe mmc了。
initr_barrier,
initr_malloc,
+#ifdef CONFIG_PARALLEL_CPU_CORE_ONE
+ wake_slave_core,
+#endif /*CONFIG_PARALLEL_CPU_CORE_ONE*/
log_init
wake_slave_core的实现:
int wake_slave_core(void)
{
uint64_t __maybe_unused ret;
uintptr_t wakeup_entry = (uintptr_t)psci_cpu_entry;
wait_salve_core = __wait_salve_core;
writel(0, CPU_RELEASE_ADDR);
DEBUG_LOG("wake up cpu: core%d\n", SLAVE_CORE_ID);
debug("### gd = 0x%p, gd->malloc_base = 0x%lx, gd size = 0x%lx\n",
gd, gd->malloc_base, sizeof(struct global_data));
ret = __invoke_psci_fn_smc(ARM_PSCI_0_2_FN_CPU_ON,
SLAVE_CORE_ID, (uint64_t)slave_e, 0);
gd_master_core = (uint64_t)gd;
gd_slave_core1 = (uint64_t)gd + sizeof(struct global_data);
sp_slave_core1 = (uint64_t)gd + sizeof(struct global_data)
* 2 + SLAVE_CORE_STACK_SIZE;
sp_slave_core1 = sp_slave_core1 - sizeof(struct reserve_mem);
r_mem = (struct reserve_mem*)sp_slave_core1;
*((volatile unsigned int*)(&r_mem->done)) = 0;
debug("### gd_slave_core1 = 0x%llx, sp_slave_core1 = 0x%llx, &done = 0x%p\n",
gd_slave_core1, sp_slave_core1, &r_mem->done);
flush_dcache_all();
writel((uint32_t)wakeup_entry, CPU_RELEASE_ADDR);
isb();
asm volatile("sev");
__invoke_psci_fn_smc(ARM_PSCI_0_2_FN_AFFINITY_INFO, 1, 0, 0);
debug("### %s return, ret = %llu\n", __func__, ret);
return 0;
}
其中core1起来后运行的回调slave_e由汇编完成(因为C环境未就绪):
根据异常级别,设置异常向量的起始地址,通过CPTR
寄存器启用FP/SIMD
。在EL3
级别时,设置SCR_EL3
的NS,IRQ,FIQ和EA四个bit位,将中断,快速中断,同步外部终止和系统错误转发到异常级别3。
ubootarch/arm/cpu/armv8/start.S
ENTRY(slave_e)
adr x0, vectors /*重定向vBAR?*/
/*
* Could be EL3/EL2/EL1, Initial State:
* Little Endian, MMU Disabled, i/dCache Disabled
*/
switch_el x1, 3f, 2f, 1f
3: set_vbar vbar_el3, x0 /*设置EL3中断向量表基地址*/
mrs x0, scr_el3
orr x0, x0, #0xf /* SCR_EL3.NS|IRQ|FIQ|EA */
msr scr_el3, x0 /*SCR_EL3寄存器和中断路由相关*/
msr cptr_el3, xzr /* 启用FP/SIMD */
#ifdef COUNTER_FREQUENCY /*未用到*/
ldr x0, =COUNTER_FREQUENCY
msr cntfrq_el0, x0 /* Initialize CNTFRQ */
#endif /*COUNTER_FREQUENCY*/
b 0f
2: 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:
/*
* 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 /*多核之间的数据一致性,XJ3开启了此选项*/
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
1:
#endif /* CONFIG_ARMV8_SET_SMPEN */
/* Apply ARM core specific erratas */
bl apply_core_errata /*ARM的软件补丁,bl跳转能够返回*/
1: wfe /*core1进入wfe状态,等待sev唤醒*/
ldr x1, =CPU_RELEASE_ADDR
ldr x0, [x1]
cbz x0, 1b
str xzr, [x1]
br x0 /* 跳转到CPU_RELEASE_ADDR指向的地址 ,无返回*/
ENDPROC(slave_e)
switch_el
是一个宏,临时用x1
寄存器作判断当前的EL
,然后跳转到不同的标号位置。
.macro switch_el, xreg, el3_label, el2_label, el1_label
mrs \xreg, CurrentEL //将CurrentEL搬运至xreg
cmp \xreg, 0xc
b.eq \el3_label //如果xreg==0xc,则跳转到el3_label
cmp \xreg, 0x8
b.eq \el2_label
cmp \xreg, 0x4
b.eq \el1_label
.endm
set_vbar
是设置异常向量表基地址
总结:
- 完成slave_e后,让core1处于WFE状态。
- WFE唤醒后,core1会跳转到CPU_RELEASE_ADDR指向的内容,不再回来。
- 不只是core1,core2和core3都可以用这个作为smc唤醒后的第一次初始化寄存器。
为core1,设置gd、sp和保留内存
gd(global data)是uboot中的全局变量,大部分都是指针,用来记录环境变量、malloc范围、ram范围、DM框架下的外设等信息。
为了防止core0和core1之间的竞争,两个core个用个的gd变量。
/*
** +---------------------+ High addr
** | reserve_mem | //占64字节
** +---------------------+
** | core1 sp | //设置为64KB
** +---------------------+
** | core1 gd |
** +---------------------+
** | core0 gd |
** +---------------------+ Low addr
*/
gd_master_core = (uint64_t)gd; //这个gd是core0,防止混淆,重新记录为gd_master_core
gd_slave_core1 = (uint64_t)gd + sizeof(struct global_data);
sp_slave_core1 = (uint64_t)gd + sizeof(struct global_data)
* 2 + SLAVE_CORE_STACK_SIZE;
sp_slave_core1 = sp_slave_core1 - sizeof(struct reserve_mem);
r_mem = (struct reserve_mem*)sp_slave_core1; //指向预留内存的底部
flush_dcache_all();
在core0的gd上面紧挨着设为core1的gd。core1的gd上设为core1的sp(设为64K),栈顶预留64字节用来存放core1的临时变量。
core1的gd和sp空间,需要memset才能使用。
这些为core1预留的内存,不应该被core0误写,所以需要和core0 gd一样提前预留出来。
@@ -49,7 +53,13 @@ ulong board_init_f_alloc_reserve(ulong top)
top -= CONFIG_VAL(SYS_MALLOC_F_LEN);
#endif
/* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
+#ifdef CONFIG_PARALLEL_CPU_CORE_ONE
+ /* add core1 global data and stack region*/
+ top = rounddown(top - 2 * sizeof(struct global_data) - SLAVE_CORE_STACK_SIZE,
+ 16);
+#else
top = rounddown(top-sizeof(struct global_data), 16);
+#endif /* CONFIG_PARALLEL_CPU_CORE_ONE */
return top;
}
@@ -107,13 +117,23 @@ void board_init_f_init_reserve(ulong base)
gd_ptr = (struct global_data *)base;
/* zero the area */
+#ifdef CONFIG_PARALLEL_CPU_CORE_ONE
+ memset(gd_ptr, '\0', 2 * sizeof(*gd));
+#else
memset(gd_ptr, '\0', sizeof(*gd));
+#endif /* CONFIG_PARALLEL_CPU_CORE_ONE */
+
/* set GD unless architecture did it already */
#if !defined(CONFIG_ARM)
arch_setup_gd(gd_ptr);
#endif
/* next alloc will be higher by one GD plus 16-byte alignment */
+#ifdef CONFIG_PARALLEL_CPU_CORE_ONE
+ /* add core1 global data and stack region*/
+ base += roundup(2 * sizeof(struct global_data) + SLAVE_CORE_STACK_SIZE, 16);
+#else
base += roundup(sizeof(<