16

在过去的 3-4 天里,我一直在为此苦苦思索,但找不到合适的解释性文档(来自 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(可在此处找到:链接)在第 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 = "All CPUs except Me" 并通过 GICD_SGIR GIC 寄存器发送(写入)
  • 由于我没有传递其他内核的任何执行起始地址,所以在这一切之后什么都没有发生

....更新....

我已经开始查看 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. 我在哪里可以找到有关pick-one ARM 板的此协议的信息?- 我可以在 ARM 官方网站或开发板网页上找到它吗?
4

4 回答 4

9

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

  • 使 CPU 进入睡眠状态的软件组件是引导加载程序(在我的例子中是 U-Boot)
  • Linux 不知何故知道引导加载程序是如何做到这一点的(在每个板的 Linux 内核中硬编码)并且知道如何再次唤醒它们

对于我的 ODROID-XU 板,描述此过程的来源是UBOOT ODROID-v2012.07和此处的 linux 内核:LINUX 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 个辅助 CPU 中的 3 个

现在对于 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 内核:通用 - smp.c(查看函数__cpu_up),平台特定(odroid-xu):platsmp.c(函数boot_secondary,由通用 __cpu_up 调用;另请查看platform_smp_prepare_cpus [底部] =>实际设置引导基础和电源基础值)

于 2013-11-25T10:38:51.273 回答
2

为了清楚起见和将来参考,由于缺少 Exynos 启动协议的适当文档,这里缺少一条微妙的信息(注意这个问题应该真正标记为“Exynos 5”而不是“Cortex-A15”——它是一个 SoC-具体的事情和 ARM 所说的只是一般建议)。从冷启动开始,辅助核心不在 WFI 中,它们仍然处于关闭状态

更简单的最小解决方案(基于 Linux 的 hotplug 所做的),我在编写引导 shim 以使管理程序在 XU 上运行的过程中制定,需要两个步骤:

  1. 先将入口点地址写入Exynos持笔(0x02073000 + 0x1c)
  2. 然后戳电源控制器以打开相关内核:这样,它们会退出安全启动路径,进入握笔以找到等待它们的入口点,跳过 WFI 循环,甚至无需触摸GIC。

除非您正在计划一个完整的 CPU 热插拔实施,否则您可以跳过检查集群 ID - 如果我们正在启动,我们在集群 0 上并且没有其他地方(应该不需要检查具有向后集群寄存器的预生产芯片在 Odroid 上也是 - 当然是给我的)。

根据我的调查,启动 A7s 涉及更多一点。从Exynos big.LITTLE switcher driver判断,您似乎需要拨出一组单独的电源控制器寄存器才能首先启用集群 1(您可能也需要弄乱 CCI,尤其是要打开 MMU 和缓存) - 我没有走得更远,因为到那时它更“有趣”而不是“做真正的工作”......

顺便说一句,三星在 5410 上的 CPU 热插拔主线补丁使核心电源控制内容比其下游代码 IMO 中的混乱更加清晰。

于 2014-01-03T15:32:52.457 回答
1

QEMU 使用 PSCI

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

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 部分提供了一个带有自旋锁的完整可运行示例:多核汇编语言是什么样的?

然后该hvc指令由 EL2 处理程序处理,另请参见:ARM 部分:什么是操作系统上下文中的环 0 和环 3?

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>;
    };

The hvc instruction is called from: 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)

which ends up going to: https://github.com/torvalds/linux/blob/v4.19/arch/arm64/kernel/smccc-call.S#L51

于 2018-11-26T00:38:12.117 回答
0

访问www.arm.com并下载 DS-5 开发套件的评估副本。安装后,在示例下会有一个startup_Cortex-A15MPCore directory. 看startup.s

于 2013-11-21T21:48:59.863 回答