ARM: Запустите / включите / включите остальные ядра /AP ЦП и передайте начальный адрес выполнения?
Я бился головой с этим последние 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. Какой идентификатор прерывания я должен выбрать при отправке SGI через регистр GICD_SGIR между 0 и 15? Это имеет значение?
3. Когда я посылаю SGI через регистр GICD_SGIR, как я могу указать другим ядрам, С чего НАЧАТЬ ИСПОЛНЕНИЕ?
4. Как тот факт, что мой код загружается загрузчиком U-BOOT, влияет на этот контекст?
В Руководстве для программистов Cortex-A Series v3.0 (находится здесь: ссылка) говорится следующее в разделе 22.5.2 (загрузка SMP в Linux, стр. 271):
Пока основное ядро загружается, вторичные ядра будут находиться в режиме ожидания, используя инструкцию WFI. Он (первичное ядро) предоставит начальный адрес вторичным ядрам и разбудит их, используя межпроцессорное прерывание (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 и просто И (&) его значение с 0xFFFF8000 и использую его в качестве базового адреса GIC, хотя я почти уверен, что это неправильно
- Включить GIC-распределитель (по смещению 0x1000 от базового адреса GIC?), Записав GICD_CTLR со значением 0x1
- Создайте SGI со следующими параметрами: Group = 0, ID = 0, TargetListFilter = "Все процессоры, кроме меня" и отправьте его (запишите) через GIC-регистр GICD_SGIR.
- Поскольку я не передал начальный адрес выполнения для других ядер, после всего этого ничего не происходит
....ОБНОВИТЬ....
Я начал искать ядро 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. Какой программный компонент это делает? Это двоичный файл BL1, который я написал на SD-карте, или U-BOOT?
- 2. Насколько я понимаю, этот программный протокол отличается от платы к плате. Это так или все зависит только от базового процессора?
- 3. Где я могу найти информацию об этом протоколе для платы ARM с одним выбором? - Могу ли я найти его на официальном сайте ARM или на веб-странице форума?
4 ответа
Хорошо, я вернулся, детка. Вот выводы:
- Программный компонент, который переводит ЦП в спящий режим - это загрузчик (в моем случае U-Boot)
- Linux почему-то знает, как это делает загрузчик (жестко запрограммирован в ядре Linux для каждой платы) и знает, как их разбудить
Для моей платы ODROID-XU источниками, описывающими этот процесс, являются UBOOT ODROID-v2012.07 и ядро linux, найденное здесь: LINUX ODROIDXU-3.4.y (было бы лучше, если бы я посмотрел версию ядра из ветки odroid-3.12. у, так как первый не запускает все 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_NX0 0, 080) в ядре 0,71
LINUX KERNEL: generic - smp.c (посмотрите на функцию __cpu_up), зависит от платформы (odroid-xu): platsmp.c (функция boot_secondary, вызываемая generic __cpu_up; также посмотрите на platform_smp_prepare_cpus [внизу] => это функция, которая на самом деле устанавливает базовую загрузку и значения мощности базы)
Для ясности и использования в будущем, здесь не хватает тонкой части информации из-за отсутствия надлежащей документации по протоколу загрузки Exynos (примечание: этот вопрос должен быть помечен как "Exynos 5", а не как "Cortex-A15" - это SoC- конкретная вещь и то, что говорит ARM, является лишь общей рекомендацией). При холодной загрузке вторичные ядра отсутствуют в WFI, они все еще отключены.
Более простое минимальное решение (основанное на том, что делает горячее подключение Linux), которое я разработал в процессе написания загрузочной прокладки для запуска гипервизора на XU, состоит из двух шагов:
- Сначала запишите адрес точки входа в ручку Exynos
(0x02073000 + 0x1c)
- Затем нажмите контроллер питания, чтобы включить соответствующие ядра: таким образом, они выпадают из безопасного пути загрузки в удерживающее перо, чтобы найти точку входа, ожидающую их, пропуская цикл WFI и устраняя необходимость даже касаться GIC на всех.
Если вы не планируете полноценную реализацию горячего подключения процессора, вы можете пропустить проверку идентификатора кластера - если мы загружаемся, мы находимся в кластере 0 и больше нигде (проверка на готовые микросхемы с обратными регистрами кластера должна быть ненужной на одройде тоже - конечно был для меня).
Из моего расследования, запуск A7s немного сложнее. Судя по драйверу переключателя exynos big.LITTLE, вам кажется, что вам нужно сначала выбрать отдельный набор регистров контроллера питания, чтобы сначала включить кластер 1 (и, возможно, вам также придется возиться с CCI, особенно для включения MMU и кешей) - Я не продвинулся дальше, так как к этому моменту это было больше "развлекаться", чем "делать настоящую работу"...
Кроме того, основной патч Samsung для горячей замены процессора на 5410 делает управление питанием ядра более понятным, чем путаница в их нисходящем коде, IMO.
QEMU использует PSCI
Интерфейс координации состояния питания ARM (PSCI) задокументирован по адресу: https://developer.arm.com/docs/den0022/latest/arm-power-state-coordination-interface-platform-design-document и управляет такими вещами, как питание включение и выключение ядер.
TL; DR это фрагмент кода aarch64 для пробуждения ЦП 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 этого ответа: Как выглядит многоядерный язык ассемблера?
hvc
Затем инструкция обрабатывается обработчиком EL2, см. также раздел ARM: Что такое кольцо 0 и кольцо 3 в контексте операционных систем?
Этот адрес сообщается ядру Linux через дерево устройств, например, QEMU автоматически генерирует запись в форме:
psci {
method = "hvc";
compatible = "arm,psci-0.2", "arm,psci";
cpu_on = <0xc4000003>;
migrate = <0xc4000005>;
cpu_suspend = <0xc4000001>;
cpu_off = <0x84000002>;
};
Зайдите на сайт http://www.arm.com/ и загрузите ознакомительную копию комплекта разработки DS-5. После установки под примерами будет startup_Cortex-A15MPCore directory
, смотреть на startup.s
,