ARM: запуск/пробуждение/вывод других ядер процессора/точек доступа и запуск начального адреса запуска?
Я бил головой об этом в течение последних 3-4 дней, и я не могу найти объяснительную документацию DECENT (от ARM или неофициальную), чтобы помочь мне.
У меня есть плата ODROID-XU (большая. LITTLE 2 x Cortex-A15 + 2 x Cortex-A7), и я пытаюсь понять немного больше об архитектуре ARM. В моем "экспериментирующем" коде теперь я пришел на сцену, где я хочу ОБРАТИТЬ ДРУГИЕ КОРЫ ИЗ ИХ WIF (ожидания для прерывания).
Отсутствующая информация, которую я все еще пытаюсь найти, это:
1. При получении базового адреса GIC-карты GIC я понимаю, что мне нужно прочитать CBAR; Но никакая часть документации не объясняет, как биты в CBAR (2 значения PERIPHBASE) должны быть организованы, чтобы добраться до конечного базового адреса GIC
2. При отправке SGI через регистр GICD_SGIR, какой идентификатор прерывания между 0 и 15 должен выбрать? Это имеет значение?
3. При отправке SGI через регистр GICD_SGIR, как я могу рассказать о других ядрах ГДЕ НАЧАТЬ ИСПОЛНЕНИЕ ОТ?
4. Как влияет на этот контекст тот факт, что мой код загружен загрузчиком U-BOOT?
Руководство по программированию Cortex-A v3.0 (найдено здесь: ссылка) гласит следующее в разделе 22.5. 2 (загрузка SMP в Linux, страница 271):
В то время как основное ядро загружается, вторичные ядра будут находиться в режиме ожидания, используя Инструкция WFI. Он (первичное ядро) предоставит начальный адрес вторичным ядрам и разбудит их, используя Inter-Processor Interrupt (IPI), что означает SGI, сигнализированный через GIC
Как 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 (2 ГБ), поэтому я также включил MMU
- Настройка обработчиков исключений моего собственного
- У меня есть функция Printf над последовательностью (UART2 на ODROID-XU)
Все вышеизложенное работает нормально.
То, что я пытаюсь сделать сейчас:
- Получить базовый адрес GIC = > в тот момент, когда я читаю CBAR, и я просто AND (&) его значение с 0xFFFF8000 и использовать это как базовый адрес GIC, хотя я почти уверен, что это не вправо
- Включить дистрибьютор GIC (со смещением 0x1000 от базового адреса GIC?), нажимая GICD_CTLR со значением 0x1
- Построить SGI со следующими параметрами: Group = 0, ID = 0, TargetListFilter = "Все CPU за исключением меня" и отправить его (записать) через регистр GICD_SGIR GIC
- Поскольку я не передал начальный адрес запуска для других ядер, ничего не происходит после всего этого
.... UPDATE....
Я начал смотреть на ядро Linux и исходные коды QEMU в поисках ответа. Вот что я узнал (пожалуйста, поправьте меня, если я ошибаюсь):
- При включении платы ВСЕ КОРЫ начинают выполнение из reset vector
- Компонент программного обеспечения (прошивки) выполняет 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. Что такое программный компонент, который делает это? Является ли это бинарным BL1, который я написал на SD-карте, или это U-BOOT?
- 2. Насколько я понимаю, этот программный протокол отличается от платы. Это так или зависит только от основного процессора?
- 3. Где я могу найти информацию об этом протоколе для одной платы ARM? - могу ли я найти его на официальном веб-сайте ARM или на веб-странице борта?
Ответы
Ответ 1
Хорошо, я вернулся. Вот выводы:
- Программный компонент, который заставляет процессоры спать, - это загрузчик (в моем случае 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;
}
Это успешно пробуждает 3 из 7 вторичных процессоров.
И теперь для этого короткого списка соответствующих исходных файлов в u-boot и ядре linux:
-
UBOOT: lowlevel_init.S - обратите внимание на строки 363-369, как вторичные процессоры подождите в 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: generic - smp.c (посмотрите на функцию __ cpu_up), < сильная > специфичная для платформы (odroid-xu): platsmp.c (функция boot_secondary, вызывается общим __cpu_up; platform_smp_prepare_cpus [внизу] = > , что функция, которая фактически устанавливает базовые значения загрузки и мощности)
Ответ 2
Для ясности и будущей ссылки здесь отсутствует небольшая часть информации из-за отсутствия надлежащей документации по протоколу загрузки Exynos (nb этот вопрос действительно должен быть помечен как "Exynos 5", а не "Cortex-A15" - it специфичная для SoC вещь и то, что говорит ARM, является лишь общей рекомендацией). При холодном загрузке вторичные ядра не находятся в WFI, они все еще отключены.
Более простое минимальное решение (на основе того, что делает Linux hotplug), которое я разработал в процессе написания загрузочной прокладки для получения гипервизора, работающего на XU, выполняет два шага:
- Сначала напишите адрес точки входа в холдинг Exynos pen
(0x02073000 + 0x1c)
- Затем вытащите контроллер питания для включения соответствующего ядра (я). Таким образом, они выходят из безопасного пути загрузки в удерживающее перо, чтобы найти точку входа, ожидающую их, пропуская цикл WFI и устраняя необходимость даже коснуться GIC.
Если вы не планируете реализацию полнофункционального CPU hotplug, вы можете пропустить проверку идентификатора кластера - если мы загружаемся, мы находимся в кластере 0 и нигде больше (проверка на наличие готовых чипов с обратными кластерными регистрами также должно быть ненужным для Оддерода - конечно, для меня).
Из моего исследования, стрельба по A7s немного больше задействована. Судя по драйверу Exynos big.LITTLE, кажется, вам нужно выставить отдельный набор регистров контроллера мощности, чтобы сначала включить кластер 1 (и вы может понадобиться также объединиться с CCI, особенно для того, чтобы иметь MMU и кеши) - я не стал дальше, поскольку к этому моменту это было более "весело", чем "делать настоящую работу"...
В стороне, основной патч Samsung для CPU hotplug на 5410 делает основной элемент управления питанием более ясным, чем беспорядок в их нижнем коде, ИМО.
Ответ 3
Перейдите в www.arm.com и загрузите там оценочную копию пакета разработки DS-5. После установки под примерами будет startup_Cortex-A15MPCore directory
. Посмотрите startup.s
.
Ответ 4
QEMU использует PSCI
Интерфейс взаимодействия с политикой питания ARM (PSCI) документируется по адресу: https://developer.arm.com/docs/den0022/latest/arm-power-state-coordination-interface-platform-design-document и контролирует такие функции, как питание включения и выключения ядер.
TL; DR - это фрагмент aarch64 для запуска CPU 1 на QEMU v3.0.0 ARMv8 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 этого ответа доступен полный runnable пример с spinlock. Как выглядит язык многоуровневой сборки?
Затем инструкция hvc
обрабатывается обработчиком EL2, см. Также раздел ARM: Что такое Ring 0 и Ring 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>;
};
Команда 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