ARM:启动/唤醒/启动其他CPU内核/ AP并传递执行起始地址?

时间:2013-11-18 19:05:17

标签: arm multiprocessing wakeup cpu-cores cortex-a

在过去的3-4天里,我一直在用这个来敲打我的脑袋,我找不到DECENT解释性文件(来自ARM或非官方的)来帮助我。 我有一个 ODROID-XU板(big.LITTLE 2 x Cortex-A15 + 2 x Cortex-A7)板,我正在尝试更多地了解ARM架构。在我的“试验”代码中,我现在已经到了我想要从其WFI(等待中断)状态唤醒其他核心的阶段。

我仍在尝试找到的缺失信息是:

1。当获取内存映射GIC的基地址时,我明白我需要读取CBAR;但没有任何文档解释如何安排CBAR中的位(2个PERIPHBASE值)来获取最终的GIC基址

2. 当通过GICD_SGIR寄存器发送SGI时,我应该选择0到15之间的中断ID?这有关系吗?

3. 当通过GICD_SGIR寄存器发送SGI时,如何告知其他内核从哪里开始执行

4. U-BOOT引导程序加载我的代码的事实如何影响此上下文?

Cortex-A系列程序员指南v3.0 (在此处找到:link)在第22.5.2节中说明了以下内容(Linux中的SMP启动) ,页面 271 ):

  

当主核心正在启动时,辅助核心将保持待机状态,使用   WFI指令。它(主核心)将为辅助核心提供启动地址,并使用一个唤醒它们   处理器间中断(IPI),即通过GIC发出信号的SGI

Linux如何做到这一点?文档 S 不提供有关“它将为辅助核心提供启动地址”的任何其他详细信息。

我的挫折感正在增长,我会非常感谢答案。 非常感谢你提前!

额外详细信息

我使用的文档:

  • ARMv7-A& R架构参考手册
  • Cortex-A15 TRM(技术参考手册)
  • Cortex-A15 MPCore TRM
  • Cortex-A系列程序员指南v3.0
  • GICv2架构规范

我现在做了什么:

  • UBOOT将我加载到0x40008000;我已经设置了翻译表(TTB),相应地写了TTBR0和TTBCR并将0x40008000映射到0x8000_0000(2GB),所以我也启用了MMU
  • 设置我自己的
  • 的异常处理程序
  • 我在串口上有Printf功能(ODROID-XU上的UART2)

以上所有内容似乎都能正常运作。

我现在要做的是:

  • 获取GIC基地址=>目前我读CBAR并且我只是将其值(和&)与0xFFFF8000一起使用并将其用作GIC基地址,尽管我几乎可以肯定这是不对的
  • 通过写入值为0x1的GICD_CTLR
  • ,启用GIC分配器(偏离GIC基地址偏移0x1000?)
  • 使用以下参数构造一个SGI:Group = 0,ID = 0,TargetListFilter =“除了我以外的所有CPU”并通过GICD_SGIR GIC寄存器发送(写入)
  • 由于我没有为其他核心传递任何执行起始地址,因此在所有这些
  • 之后没有任何反应

.... .... UPDATE

我开始查看Linux内核和QEMU源代码以寻找答案。这是我发现的(如果我错了请纠正我):

  • 当电路板通电时,所有核心从复位向量开始执行
  • 软件(固件)组件在辅助核心和其他一些代码上执行WFI,这些代码将作为这些辅助核心与主核心之间的协议,当后者想要唤醒时他们又来了
  • 例如, EnergyCore ECX-1000(Highbank)板上使用的协议如下:

**(1)** the secondary cores enter WFI and when

**(2)** the primary core sends an SGI to wake them up

**(3)** they check if the value at address (0x40 + 0x10 * coreid) is non-null;

**(4)** if it is non-null, they use it as an address to jump to (execute a BX)

**(5)** otherwise, they re-enter standby state, by re-executing WFI

**(6)** So, if I had an EnergyCore ECX-1000 board, I should write (0x40 + 0x10 * coreid) with the address I want each of the cores to jump to and send an SGI

问题:

  • 1。这样做的软件组件是什么?是我在SD卡上写的BL1二进制文件,还是U-BOOT?
  • 2。据我所知,这个软件协议因板而异。是这样,还是只依赖于底层处理器?
  • 第3。哪里可以找到有关此协议的选择ARM板的信息? - 我可以在官方ARM网站或董事会网页上找到它吗?

4 个答案:

答案 0 :(得分:9)

好的,我回来了。以下是结论:

  • 让CPU进入睡眠状态的软件组件是引导程序(在我的情况下是U-Boot)
  • Linux以某种方式知道引导程序如何执行此操作(在每个主板的Linux内核中进行硬编码)并知道如何再次唤醒它们

对于我的ODROID-XU板,描述这个过程的源代码是UBOOT ODROID-v2012.07,并且在这里找到了linux内核:LINUX ODROIDXU-3.4.y(如果我从分支机构查看内核版本会更好{{3}因为前者没有启动所有8个处理器,只有4个处理器,但后者确实如此)。

无论如何,这是我提出的源代码,我将发布上述源代码树中的相关源文件,帮助我编写此代码:

typedef unsigned int DWORD;
typedef unsigned char BOOLEAN;
#define FAILURE (0)
#define SUCCESS (1)
#define NR_EXTRA_CPUS (3) // actually 7, but this kernel version can't wake them up all -> check kernel version 3.12 if you need this

// Hardcoded in the kernel and in U-Boot; here I've put the physical addresses for ease
// In my code (and in the linux kernel) these addresses are actually virtual
// (thus the 'VA' part in S5P_VA_...); note: mapped with memory type DEVICE
#define S5P_VA_CHIPID (0x10000000)
#define S5P_VA_SYSRAM_NS (0x02073000)
#define S5P_VA_PMU (0x10040000)
#define EXYNOS_SWRESET ((DWORD) S5P_VA_PMU + 0x0400)
// Other hardcoded values
#define EXYNOS5410_REV_1_0 (0x10)
#define EXYNOS_CORE_LOCAL_PWR_EN (0x3)

BOOLEAN BootAllSecondaryCPUs(void* CPUExecutionAddress){

// 1. Get bootBase (the address where we need to write the address where the woken CPUs will jump to)
//    and powerBase (we also need to power up the cpus before waking them up (?))
DWORD bootBase, powerBase, powerOffset, clusterID;

asm volatile ("mrc p15, 0, %0, c0, c0, 5" : "=r" (clusterID));
clusterID = (clusterID >> 8);
powerOffset = 0;
if( (*(DWORD*)S5P_VA_CHIPID & 0xFF) < EXYNOS5410_REV_1_0 )
{
    if( (clusterID & 0x1) == 0 ) powerOffset = 4;
}
else if( (clusterID & 0x1) != 0 ) powerOffset = 4;

bootBase = S5P_VA_SYSRAM_NS + 0x1C;
powerBase = (S5P_VA_PMU + 0x2000) + (powerOffset * 0x80);

// 2. Power up each CPU, write bootBase and send a SEV (they are in WFE [wait-for-event] standby state)
for (i = 1; i <= NR_EXTRA_CPUS; i++)
{
    // 2.1 Power up this CPU
    powerBase += 0x80;
    DWORD powerStatus = *(DWORD*)( (DWORD) powerBase + 0x4);

    if ((powerStatus & EXYNOS_CORE_LOCAL_PWR_EN) == 0)
    {
        *(DWORD*) powerBase = EXYNOS_CORE_LOCAL_PWR_EN;
        for (i = 0; i < 10; i++) // 10 millis timeout
        {
            powerStatus = *(DWORD*)((DWORD) powerBase + 0x4);
            if ((powerStatus & EXYNOS_CORE_LOCAL_PWR_EN) == EXYNOS_CORE_LOCAL_PWR_EN)
                break;
            DelayMilliseconds(1); // not implemented here, if you need this, post a comment request 
        }
        if ((powerStatus & EXYNOS_CORE_LOCAL_PWR_EN) != EXYNOS_CORE_LOCAL_PWR_EN)
            return FAILURE;
    }
    if ( (clusterID & 0x0F) != 0 )
    {
        if ( *(DWORD*)(S5P_VA_PMU + 0x0908) == 0 )
        do { DelayMicroseconds(10); } // not implemented here, if you need this, post a comment request
        while (*(DWORD*)(S5P_VA_PMU + 0x0908) == 0);
        *(DWORD*) EXYNOS_SWRESET = (DWORD)(((1 << 20) | (1 << 8)) << i);
    }

    // 2.2 Write bootBase and execute a SEV to finally wake up the CPUs
    asm volatile ("dmb" : : : "memory");
    *(DWORD*) bootBase = (DWORD) CPUExecutionAddress;
    asm volatile ("isb");
    asm volatile ("\n   dsb\n   sev\n   nop\n");
}
return SUCCESS;
}

这成功唤醒了7个辅助CPU中的3个

现在,对于u-boot和linux内核中相关源文件的简短列表:

  • UBOOT:odroid-3.12.y - 注意行 363-369 ,辅助CPU如何在WFE中等待 _hotplug_addr的值非归零并跳转到它; _hotplug_addr实际上是上面代码中的bootBase; 也是行 282-285 告诉我们_hotplug_addr要重新定位在 CONFIG_PHY_IRAM_NS_BASE + _hotplug_addr - nscode_base ( _hotplug_addr - nscode_base 0x1C CONFIG_PHY_IRAM_NS_BASE为0x02073000 ,因此上面的硬编码在linux内核中

  • LINUX KERNEL:通用 - lowlevel_init.S (查看函数 __ cpu_up ),特定于平台(odroid-xu):{{ 3}} (函数 boot_secondary ,由泛型__cpu_up调用;同时查看 platform_smp_prepare_cpus [在底部] =&gt;这是实际设置启动的函数基数和权力基础值)

答案 1 :(得分:2)

为了清晰和未来的参考,由于缺乏Exynos引导协议的适当文档,这里缺少一些微妙的信息(这个问题应该真的标记为“Exynos 5”而不是“Cortex-A15” - 它是特定于SoC的东西以及ARM所说的只是一般性建议。 从冷启动开始,辅助核心不在WFI中,它们仍然关闭

更简单的最小解决方案(基于Linux的hotplug所做的),我在编写启动填充程序以在XU上运行虚拟机管理程序的过程中制定出来,需要两个步骤:

  1. 首先将入口点地址写入Exynos握笔(0x02073000 + 0x1c)
  2. 然后戳电源控制器以打开相关的核心:这样,他们从安全启动路径退出到握笔中找到等待它们的入口点,跳过WFI循环,甚至无需触及GIC。
  3. 除非你计划一个完整的CPU热插拔实现,否则你可以跳过检查群集ID - 如果我们正在启动,我们就在群集0上而不是其他地方(检查具有向后群集寄存器的预生产芯片在Odroid上也应该没必要 - 当然对我来说。)

    从我的调查来看,启动A7更加复杂。从the Exynos big.LITTLE switcher driver来看,似乎你需要先选择一组独立的电源控制器寄存器来启用集群1(你可能也需要搞乱CCI,尤其是让MMU和缓存打开) - I没有进一步发展,因为到那时它更“玩得开心”而不是“做实事”......

    顺便说一句,Samsung's mainline patch for CPU hotplug on the 5410使核心功率控制的东西比下游代码IMO中的混乱更清晰。

答案 2 :(得分:0)

转到www.arm.com并下载DS-5开发套件的评估版。安装后,在示例下将有startup_Cortex-A15MPCore directory。看看startup.s

答案 3 :(得分:0)

QEMU使用PSCI

https://developer.arm.com/docs/den0022/latest/arm-power-state-coordination-interface-platform-design-document上记录了ARM电源状态协调接口(PSCI),它控制诸如打开和关闭内核电源之类的事情。

TL; DR,这是用于唤醒QEMU v3.0.0 ARMv8 aarch64上的CPU 1的aarch64代码段:

/* PSCI function identifier: CPU_ON. */
ldr w0, =0xc4000003
/* Argument 1: target_cpu */
mov x1, 1
/* Argument 2: entry_point_address */
ldr x2, =cpu1_entry_address
/* Argument 3: context_id */
mov x3, 0
/* Unused hvc args: the Linux kernel zeroes them,
 * but I don't think it is required.
 */
hvc 0

以及对于ARMv7:

ldr r0, =0x84000003
mov r1, #1
ldr r2, =cpu1_entry_address
mov r3, #0
hvc 0

此答案的ARM部分提供了带有自旋锁的完整可运行示例:What does multicore assembly language look like?

然后hvc指令由EL2处理程序处理,另请参见:What are Ring 0 and Ring 3 in the context of operating systems?的ARM部分

Linux内核

在Linux v4.19中,该地址通过设备树通知Linux内核,例如QEMU自动生成以下形式的条目:

    psci {
            method = "hvc";
            compatible = "arm,psci-0.2", "arm,psci";
            cpu_on = <0xc4000003>;
            migrate = <0xc4000005>;
            cpu_suspend = <0xc4000001>;
            cpu_off = <0x84000002>;
    };

hvc的调用来自:https://github.com/torvalds/linux/blob/v4.19/drivers/firmware/psci.c#L178

static int psci_cpu_on(unsigned long cpuid, unsigned long entry_point)

最终将转到:https://github.com/torvalds/linux/blob/v4.19/arch/arm64/kernel/smccc-call.S#L51