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

16
我已经苦苦思索了3-4天,但我无法找到一份像样的说明文档(来自ARM或非官方),以帮助我理解更多关于ARM架构的知识。我有一个 ODROID-XU板(big.LITTLE 2 x Cortex-A15 + 2 x Cortex-A7),我试图更好地了解ARM架构。在我的“实验”代码中,我现在已经到达了一步,即想要从WFI(wait-for-interrupt)状态唤醒其他核心。
我仍在寻找的缺失信息是: 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指令。它(主核)将向次要核提供启动地址,并使用Inter-Processor Interrupt(IPI)唤醒它们,这意味着通过GIC发送的SGI信号

Linux是如何做到的?文档S没有提供关于“它将向次要核提供启动地址”的其他详细信息。

我的挫败感越来越强,我将非常感谢您的答案。 非常感谢您的帮助!

额外的细节

我使用的文档:

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

到目前为止,我已经完成了以下工作:

  • UBOOT将我加载到0x40008000;我设置了转换表(TTBs),相应地编写了TTBR0和TTBCR,并将0x40008000映射到0x8000_0000(2GB),因此我还启用了MMU
  • 设置了自己的异常处理程序
  • 我在串行(ODROID-XU上的UART2)上获得了Printf功能

所有上述内容似乎都正常工作。

现在我正在尝试做的是:

  • 获取GIC基地址 => 目前我读取CBAR,然后使用其值与0xFFFF8000进行AND(&),并将其用作GIC基地址,尽管我几乎确定这是不正确的
  • 通过写入值为0x1的GICD_CTLR来启用距离GIC基地址偏移量为0x1000的GIC分配器
  • 使用以下参数构建SGI:组= 0,ID = 0,TargetListFilter =“除我之外的所有CPU”,并通过GICD_SGIR GIC寄存器发送它(写入)
  • 由于我没有为其他核心传递任何执行起始地址,因此在所有这些操作之后什么都不会发生

....更新....

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

  • 当开机时,所有核心都从复位向量开始执行
  • 一个软件(固件)组件在次要核心上执行WFI和一些其他代码,这些代码将作为协议在这些次要核心和主核心之间起作用,当后者再次想要唤醒它们时
  • 例如,在板上使用的协议如下:

**(1)** 次要核心进入WFI,然后

**(2)** 主核心发送SGI以唤醒它们

**(3)** 它们检查地址(0x40 + 0x10 * coreid)处的值是否非空;

**(4)** 如果它不为空,它们将其用作要跳转到的地址(执行BX)

**(5)** 否则,它们重新进入待机状态,重新执行WFI

**(6)** 所以,如果我有一张EnergyCore ECX-1000板,我应该使用我想要每个核心跳转到的地址写入(0x40 + 0x10 * coreid),并发送一个SGI

问题:

  • 1. 这是哪个软件组件执行的?是我在SD卡上编写的BL1二进制文件还是U-BOOT?
  • 2. 据我所知,这种软件协议因板而异。是这样吗,还是只取决于底层处理器?
  • 3. 在哪里可以找到关于选择的ARM板的此协议信息? - 我可以在官方ARM网站或板网页上找到吗?

我相信这种初始化序列(即从待机状态唤醒辅助核心)是由芯片制造商预加载的ROM代码完成的(例如,TI、三星等)。例如,在TI OMAP中就是这样完成的。http://omappedia.org/wiki/Bootloader_Project - Adi
我想你的意思是“即将辅助核心置于待机状态”。 - Zuzu Corneliu
这种详细信息可以在某些制造商的SoC技术数据表中提供。这就是为什么我首先会尝试查看三星资源,特别是Exynos 5。我没有能够快速找到合适的数据表,因此我提供了一个类似的OMAP芯片链接,只是为了让您了解TI如何处理这些事情。 - Adi
我仍在搜索并尝试从内核源代码中获取一些信息,希望我能在那里找到更多相关的信息。 - Zuzu Corneliu
不幸的是,一些制造商并不会免费提供所有相关的SoC信息(读作:他们确实会,但在NDA下)。正如你所说,这个SoC似乎属于这个群体。但再次强调,这种功能不应该暴露给用户。这就是为什么TI将其处理为ROM代码的原因。我希望三星也是这样做的。因此,我不认为您会在Linux内核中找到任何此类实现细节。 - Adi
显示剩余5条评论
4个回答

10

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

  • 将CPU置于睡眠状态的软件组件是引导加载程序(在我的情况下是U-Boot)
  • Linux以某种方式知道引导加载程序如何实现这一点(对于每个板卡,在Linux内核中硬编码),并知道如何再次唤醒它们。

对于我的ODROID-XU板卡,描述此过程的源代码位于UBOOT ODROID-v2012.07LINUX ODROIDXU-3.4.y(如果我查看分支odroid-3.12.y的内核版本会更好,因为前者不会启动所有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个中的3个次要CPU

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

  • UBOOT:lowlevel_init.S - 注意第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为0x1CCONFIG_PHY_IRAM_NS_BASE为0x02073000,因此上述硬编码在linux内核中)

  • LINUX KERNEL:通用 - smp.c(查看函数__cpu_up),特定平台(odroid-xu):platsmp.c(函数boot_secondary,由通用__cpu_up调用;还要查看platform_smp_prepare_cpus [在底部] => 这实际上是设置启动基础和电源基础值的函数)


2
为了清晰并作为未来参考,由于Exynos启动协议缺乏适当的文档说明(注意:这个问题应该标记为“Exynos 5”,而不是“Cortex-A15”——这是一个SoC特定的问题,ARM只是提供了一般建议),这里缺少一个微妙的信息。从冷启动开始,辅助核心不处于WFI状态,它们仍然处于关闭状态。最简单的最小解决方案(基于Linux热插拔的做法)是,在编写引导程序以在XU上运行虚拟机的过程中,我想出了两个步骤:首先将入口点地址写入Exynos保持区(0x02073000 + 0x1c),然后触发电源控制器以打开相关的核心:这样,它们就会跳出安全启动路径进入保持区,等待入口点的到来,跳过WFI循环,甚至不需要接触GIC。
除非您计划进行全面的CPU热插拔实现,否则可以跳过检查群集ID - 如果我们正在引导,则在群集0上,没有其他地方(在Odroid上检查具有反向群集寄存器的预生产芯片应该也是不必要的 - 对我来说肯定是这样)。
从我的调查中,启动A7需要更多的操作。根据Exynos big.LITTLE switcher driver,似乎您需要先触发一组单独的电源控制器寄存器以启用群集1(并且您可能需要对CCI进行调整,特别是为了使MMU和高速缓存工作)-到那时候,我更多的是“玩乐”而不是“做真正的工作”...
顺便说一句,三星5410 CPU热插拔的主线补丁使核心功率控制相关的内容比他们下游代码中的混乱清晰得多,我个人认为。

感谢您的澄清,我想说我已经从标题中删除了“Cortex-A15”,但还没有添加“Exynos”部分,因为这个讨论可以帮助所有ARM程序员,无论SoC如何,即唤醒其他内核的过程也可以类似地找到其他特定SoC;因此,谷歌搜索也可以将它们引导到这里。 - Zuzu Corneliu

1

QEMU使用PSCI

ARM电源状态协调接口(PSCI)的文档位于:https://developer.arm.com/docs/den0022/latest/arm-power-state-coordination-interface-platform-design-document,它控制诸如核心开启和关闭等事项。

简而言之,这是在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部分找到:多核汇编语言是什么样子的?

hvc指令由EL2处理程序处理,详见:操作系统上下文中的Ring 0和Ring 3是什么?的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


0

前往www.arm.com下载DS-5开发套件的评估副本。安装后,在示例中将会有一个startup_Cortex-A15MPCore目录,查看其中的startup.s文件。


2
如果再多阐述一些,这将会更有帮助。startup.s 文件到底是什么意思呢? - user1618143
嗨,抱歉没有早点发布这个消息,我已经成功解决了这个问题;明天我会发布我的结论和解决方案,现在我真的没有时间。 - Zuzu Corneliu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接