uboot阶段armv8处理器多核启动

本文介绍了在uboot阶段,如何利用ARMv8处理器特性,通过core0唤醒core1,提升启动效率。详细讨论了核心寄存器、异常等级、Linux内核SMP启动过程,并提供了具体的操作步骤,包括设置通用寄存器、唤醒从核、初始化全局数据等,最后探讨了多核环境下避免竞争的方法。
摘要由CSDN通过智能技术生成

需求

为了提升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:链接寄存器

特殊寄存器

在这里插入图片描述

  1. zero寄存器:读出来的都是0。XZR和WZR分别对应aarch32和aarch64。
  2. PC:程序计数器。(armv8中PC不能被大多数指令访问,尤其是不能直接修改(不能mov PC xxx)使用跳转(bl b等)或堆栈相关指令间接修改 PC)
  3. SP:堆栈指针,指向栈顶。有SP_EL0、SP_EL1、SP_EL2、SP_EL3。
  4. ELR:(exception link register)异常链接寄存器,保存异常返回地址。EL0没有这个寄存器、EL1/2/3有自己的单独的ELR。比如:当处理器将异常处理交给EL1处理时,会将返回地址保存在ELR_EL1中,在异常返回时,PC从ELR_EL1恢复。
  5. SPSR:程序状态保存寄存器,EL0没有这个寄存器、EL1/2/3有自己的单独的SPSR,用于异常。
  6. LR:链接寄存器,执行子程序调用时,存放pc寄存器地址,以便在执行完程序后返回程序。
  7. PSTATE:处理器状态寄存器。包括几个寄存器:NZCV(条件寄存器),DAIF(异常屏蔽寄存器),SPSEL(sp选择寄存器),CurrentEL(异常等级寄存器)。
  8. 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.cinit_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(<
  • 0
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值