Если вы читали мою предыдущую статью, вероятно вам интересна эта тема и вы хотите узнать больше. В этой статье рассмотрим очень частную, не простую, но от этого не менее необходимую задачу запуска двух разных Baremetal приложений на разных ядрах SoC Cyclone V. По сути такие системы называются AMP — asyncronus multi-processing. Чуть не забыл сказать, что на русском языке вы не найдете другого более правильного и подробного руководства к созданию таких систем, так что читаем!
Введение
Подразумевается, что читатель уже знаком со стандартными библиотеками Altera HW Manager и SoCAL. Но все же скажем пару слов о них. SoC Abstraction Layer (SoCAL) содержит в себе низкоуровневые функции для удобного установления/чтения битов, байтов, слов для прямого управления регистрами HPS. Hardware Manager (HW Manager) представляет из себя набор более сложных функций для написания baremetal приложений, драйверов, BPS и прочего. Обязательно читайте документацию по этому адресу /ip/altera/hps/altera_hps/doc/ или в .h файлах.
Загрузка программы
Для начала необходимо вспомнить как происходит загрузка программы, в моей первой статье об этом было сказано не много.
Процесс загрузки HPS имеет несколько стадий, попробуем разобраться в них…
Сразу после включения выполняется код расположенный прямо на Flash памяти Cortex-A9 называемый BootRom. Вы не можете изменить его или даже посмотреть его содержание. Он служит для первичной инициализации и в следующем этапе передает процесс загрузки в SSBL (Second Stage Boot Loader называемый коротко Preloader). Что необходимо знать для понимания процесса — это то, что код BootRom, в первую очередь, выбирает источник загрузки Preloader, ориентируюсь на внешние физические пины BSEL…
И так после выполнения кода BootRom начинает загружаться Preloader, необходимый для настройки Clock, SDRAM и прочего. После начинает выполняться программа...
Рассмотрим подробнее, что происходит после загрузки Preloader. Собственно после этого начинает выполняться программа, но не сразу с главной функции main(). Перед ней выполняется функция _main(), основная задача которой мэппинг приложения по заданным в scatter файле, адресам в памяти. Это значит что точка входа у приложения находится не в начале кода функции main() который мы пишем, а в специальной, невидимой при написании кода, служебной функции _main(), появляющаяся перед main() в процессе компиляции. Возможно это все уже знают, но на тот момент для меня это было откровением, я считал что точка входа находится в начале main().
Работа ядер
Все описанные процессы всегда выполняются на первом ядре cpu0, второе ядро всегда находится в состоянии сброса. Чтобы запустить второе ядро, необходимо сбросить соответствующий бит регистра MPUMODRST в группе RSTMGR. Ну и задать стартовый адрес PC в регистре CPU1STARTADDR в группе SYSMGR. Однако после включения PC cpu1 равен 0x0. После выполнения Preloader по адресу 0x0 ничего полезного нет, поэтому до запуска cpu1 необходимо разместить код BootROM в 0x0. Много времени потратил я, чтоб узнать, что только из кода BootROM происходит чтение регистра CPU1STARTADDR, после чего PC устанавливается в нужное значение. Как оказалось, разместить этот код не столь тривиально, как кажется на первый взгляд. Для этого нам понадобится функция alt_addr_space_remap из HW manager, из файла alt_address_space.h.
alt_addr_space_remap(ALT_ADDR_SPACE_MPU_ZERO_AT_BOOTROM, ALT_ADDR_SPACE_NONMPU_ZERO_AT_SDRAM, ALT_ADDR_SPACE_H2F_ACCESSIBLE, ALT_ADDR_SPACE_LWH2F_ACCESSIBLE);
Не спешите радоваться, этого не достаточно для того, чтобы BootROM оказался по адресу 0x0. Необходимо настроить фильтр адресов кэша L2. В описании функции alt_addr_space_remap сказано, если вам необходимо расположить BootROM по адресу 0x0, то настройте этот фильтр следующим образом, расположив код после функции.
uint32_t addr_filt_start;
uint32_t addr_filt_end;
alt_l2_addr_filter_cfg_get(&addr_filt_start, &addr_filt_end);
if (addr_filt_start != L2_CACHE_ADDR_FILTERING_START_RESET)
{
alt_l2_addr_filter_cfg_set(L2_CACHE_ADDR_FILTERING_START_RESET, addr_filt_end);
}
Только после этого зададим стартовый адрес и можем запустить ядро.
alt_write_word(ALT_SYSMGR_ROMCODE_CPU1STARTADDR_ADDR, ALT_SYSMGR_ROMCODE_CPU1STARTADDR_VALUE_SET(0x100000)); //set PC of cpu1 to 0x00100000
alt_write_word(ALT_RSTMGR_MPUMODRST_ADDR, alt_read_byte(ALT_RSTMGR_MPUMODRST_ADDR) & ALT_RSTMGR_MPUMODRST_CPU1_CLR_MSK);
Ну и что дальше? А дальше надо чуть-чуть разобраться со структурой проекта.
Именно такая структура для AMP проектов является самой оптимальной. Блок Vectors задает вектора прерывания и делает ветвление для разных процессоров. Вектора прерывания являются общими для каждого процессора. К сожалению этот блок может быть написан только на ассемблере, но к счастью мы не будем писать его с нуля а лишь только отредактируем файл библиотеки HW lib alt_interrupt_armcc.s. В нем объявлены необходимые вектора прерываний, стек прерываний, поддержка FPU VFPNEON. Допишем необходимый разветвитель.
PRESERVE8
AREA VECTORS, CODE, READONLY
ENTRY
EXPORT alt_interrupt_vector
IMPORT __main
EXPORT alt_int_handler_irq [WEAK]
alt_interrupt_vector
Vectors
LDR PC, alt_reset_addr
LDR PC, alt_undef_addr
LDR PC, alt_svc_addr
LDR PC, alt_prefetch_addr
LDR PC, alt_abort_addr
LDR PC, alt_reserved_addr
LDR PC, alt_irq_addr
LDR PC, alt_fiq_addr
alt_reset_addr DCD alt_int_handler_reset
alt_undef_addr DCD alt_int_handler_undef
alt_svc_addr DCD alt_int_handler_svc
alt_prefetch_addr DCD alt_int_handler_prefetch
alt_abort_addr DCD alt_int_handler_abort
alt_reserved_addr DCD alt_int_handler_reserve
alt_irq_addr DCD alt_int_handler_irq
alt_fiq_addr DCD alt_int_handler_fiq
alt_int_handler_reset
B alt_premain
alt_int_handler_undef
B alt_int_handler_undef
alt_int_handler_svc
B alt_int_handler_svc
alt_int_handler_prefetch
B alt_int_handler_prefetch
alt_int_handler_abort
B alt_int_handler_abort
alt_int_handler_reserve
B alt_int_handler_reserve
alt_int_handler_irq
B alt_int_handler_irq
alt_int_handler_fiq
B alt_int_handler_fiq
;=====
AREA ALT_INTERRUPT_ARMCC, CODE, READONLY
alt_premain FUNCTION
; Enable VFP / NEON.
MRC p15, 0, r0, c1, c0, 2 ; Read CP Access register
ORR r0, r0, #0x00f00000 ; Enable full access to NEON/VFP (Coprocessors 10 and 11)
MCR p15, 0, r0, c1, c0, 2 ; Write CP Access register
ISB
MOV r0, #0x40000000 ; Switch on the VFP and NEON hardware
VMSR fpexc, r0 ; Set EN bit in FPEXC
B __main
ENDFUNC
;=====
AREA ALT_INTERRUPT_ARMCC, CODE, READONLY
EXPORT alt_int_fixup_irq_stack
; void alt_int_fixup_irq_stack(uint32_t stack_irq);
; This is the same implementation of GNU but for ARMCC.
alt_int_fixup_irq_stack FUNCTION
; r4: stack_sys
MRS r3, CPSR
MSR CPSR_c, #(0x12 :OR: 0x80 :OR: 0x40)
MOV sp, r0
MSR CPSR_c, r3
BX lr
ENDFUNC
END
PRESERVE8
PRESERVE8
AREA VECTORS, CODE, READONLY
ENTRY
EXPORT alt_interrupt_vector
IMPORT __main
EXPORT alt_int_handler_irq [WEAK]
IMPORT secondaryCPUsInit
alt_interrupt_vector
Vectors
LDR PC, alt_reset_addr
LDR PC, alt_undef_addr
LDR PC, alt_svc_addr
LDR PC, alt_prefetch_addr
LDR PC, alt_abort_addr
LDR PC, alt_reserved_addr
LDR PC, alt_irq_addr
LDR PC, alt_fiq_addr
alt_reset_addr DCD alt_int_handler_reset
alt_undef_addr DCD alt_int_handler_undef
alt_svc_addr DCD alt_int_handler_svc
alt_prefetch_addr DCD alt_int_handler_prefetch
alt_abort_addr DCD alt_int_handler_abort
alt_reserved_addr DCD alt_int_handler_reserve
alt_irq_addr DCD alt_int_handler_irq
alt_fiq_addr DCD alt_int_handler_fiq
alt_int_handler_reset
B alt_premain
alt_int_handler_undef
B alt_int_handler_undef
alt_int_handler_svc
B alt_int_handler_svc
alt_int_handler_prefetch
B alt_int_handler_prefetch
alt_int_handler_abort
B alt_int_handler_abort
alt_int_handler_reserve
B alt_int_handler_reserve
alt_int_handler_irq
B alt_int_handler_irq
alt_int_handler_fiq
B alt_int_handler_fiq
;=====
AREA ALT_INTERRUPT_ARMCC, CODE, READONLY
alt_premain FUNCTION
IF {TARGET_FEATURE_NEON} || {TARGET_FPU_VFP}
; Enable VFP / NEON.
MRC p15, 0, r0, c1, c0, 2 ; Read CP Access register
ORR r0, r0, #0x00f00000 ; Enable full access to NEON/VFP (Coprocessors 10 and 11)
MCR p15, 0, r0, c1, c0, 2 ; Write CP Access register
ISB
MOV r0, #0x40000000 ; Switch on the VFP and NEON hardware
VMSR fpexc, r0 ; Set EN bit in FPEXC
ENDIF
MRC p15, 0, r0, c0, c0, 5 ; Read CPU ID register
ANDS r0, r0, #0x03 ; Mask off, leaving the CPU ID field
BEQ primaryCPUInit ; jump to cpu0 code init
BNE secondaryCPUsInit ; jump to cpu1 code init
primaryCPUInit ;jump to main()
B __main
ENDFUNC
;=====
AREA ALT_INTERRUPT_ARMCC, CODE, READONLY
EXPORT alt_int_fixup_irq_stack
; void alt_int_fixup_irq_stack(uint32_t stack_irq);
; This is the same implementation of GNU but for ARMCC.
alt_int_fixup_irq_stack FUNCTION
; r4: stack_sys
MRS r3, CPSR
MSR CPSR_c, #(0x12 :OR: 0x80 :OR: 0x40)
MOV sp, r0
MSR CPSR_c, r3
BX lr
ENDFUNC
END
Конечно теперь необходимо дописать функцию secondaryCPUsInit в другом файле
PRESERVE8
AREA CPU1, CODE, READONLY
ENTRY
IMPORT eth
IMPORT ||Image$$ARM_LIB_STACKHEAP$$ZI$$Base||
IMPORT ||Image$$ARM_LIB_STACKHEAP$$ZI$$Length||
IMPORT ||Image$$ARM_LIB_STACKHEAP$$ZI$$Limit||
cpu1_stackheap_base DCD ||Image$$ARM_LIB_STACKHEAP$$ZI$$Base||
cpu1_stackheap_lenth DCD ||Image$$ARM_LIB_STACKHEAP$$ZI$$Length||
cpu1_stackheap_limit DCD ||Image$$ARM_LIB_STACKHEAP$$ZI$$Limit||
Mode_USR EQU 0x10
Mode_FIQ EQU 0x11
Mode_IRQ EQU 0x12
Mode_SVC EQU 0x13
Mode_ABT EQU 0x17
Mode_UNDEF EQU 0x1B
Mode_SYS EQU 0x1F
Len_FIQ_Stack EQU 0x1000
Len_IRQ_Stack EQU 0x1000
I_Bit EQU 0x80 ; when I bit is set, IRQ is disabled
F_Bit EQU 0x40 ; when F bit is set, FIQ is disabled
EXPORT secondaryCPUsInit
secondaryCPUsInit FUNCTION
; stack_base could be defined above, or located in a scatter file
LDR R0, cpu1_stackheap_limit
MRC p15, 0, r1, c0, c0, 5 ; Read CPU ID register
ANDS r1, r1, #0x03 ; Mask off, leaving the CPU ID field
SUB r0, r0, r1, LSL #14 ; Stack -0x4000 for cpu1
; Enter each mode in turn and set up the stack pointer
MSR CPSR_c, #Mode_FIQ:OR:I_Bit:OR:F_Bit ; Interrupts disabled
MOV sp, R0
SUB R0, R0, #Len_FIQ_Stack
MSR CPSR_c, #Mode_IRQ:OR:I_Bit:OR:F_Bit ; Interrupts disabled
MOV sp, R0
SUB R0, R0, #Len_IRQ_Stack
MSR CPSR_c, #Mode_SVC:OR:I_Bit:OR:F_Bit ; Interrupts disabled
MOV sp, R0
; Leave processor in SVC mode
; Enables the SCU
MRC p15, 4, r0, c15, c0, 0 ; Read periph base address
LDR r1, [r0, #0x0] ; Read the SCU Control Register
ORR r1, r1, #0x1 ; Set bit 0 (The Enable bit)
STR r1, [r0, #0x0] ; Write back modifed value
;
; Join SMP
; ---------
MRC p15, 0, r0, c0, c0, 5 ; Read CPU ID register
ANDS r0, r0, #0x03 ; Mask off, leaving the CPU ID field
MOV r1, #0xF ; Move 0xF (represents all four ways) into r1
;secureSCUInvalidate
AND r0, r0, #0x03 ; Mask off unused bits of CPU ID
MOV r0, r0, LSL #2 ; Convert into bit offset (four bits per core)
AND r1, r1, #0x0F ; Mask off unused bits of ways
MOV r1, r1, LSL r0 ; Shift ways into the correct CPU field
MRC p15, 4, r2, c15, c0, 0 ; Read periph base address
STR r1, [r2, #0x0C] ; Write to SCU Invalidate All in Secure State
;joinSMP
; SMP status is controlled by bit 6 of the CP15 Aux Ctrl Reg
MRC p15, 0, r0, c1, c0, 1 ; Read ACTLR
MOV r1, r0
ORR r0, r0, #0x040 ; Set bit 6
CMP r0, r1
MCRNE p15, 0, r0, c1, c0, 1 ; Write ACTLR
;enableMaintenanceBroadcast
MRC p15, 0, r0, c1, c0, 1 ; Read Aux Ctrl register
MOV r1, r0
ORR r0, r0, #0x01 ; Set the FW bit (bit 0)
CMP r0, r1
MCRNE p15, 0, r0, c1, c0, 1 ; Write Aux Ctrl register
B main_cpu1
ENDFUNC
END
Признаюсь, этот код я только дописывал, а оригинал брал из примеров в папке DS-5. Я написал только настройку стека, и в конце B main_cpu1
для перехода в функцию. Ну вроде как SCU нужен, я оставил его, да и остальное не стал трогать Необходимо разобрать scatter файл, чтоб лучше понять, что происходит.
LD_SDRAM 0x00100000 0x80000000 ;SDRAM_load region for MPU from 1 Mb to 3 Gb. DE1-SoC has 2 Gb of DDR memory
{
VECTORS +0
{
* (VECTORS, +FIRST)
}
APP_CODE +0
{
* (+RO, +RW, +ZI)
}
;Application heap and stack cpu0
ARM_LIB_STACKHEAP +0 EMPTY 8000
{ }
CPU1_CODE 0x00200000 FIXED 0x00100000
{
start_cpu1.o(CPU1, +FIRST)
main_sc.o(+RO, +RW, +ZI)
}
}
VECTORS располагается в начале SDRAM по адресу 0x00100000 (написано в alt_interrupt_armcc.s), в 0x0 поставить нельзя, почему так посмотрите в Cyclone V Hard Processor System Technical Reference Manual. В области APP_CODE располагается весь код (main() первого ядра и остальные внешние функции), кроме функции main() для второго ядра.
ARM_LIB_STACKHEAP является зарезервированным словом для обозначения стека и хипа, и имеет размер 8000 байт, число большое, взял с запасом. Эта строчка позволяет провести настройку стека автоматически в функции _main(). Для второго ядра мы делаем это самостоятельно в файле start_cpu1.s. От нижней границы STACKHEAP отступаем вверх 4000 байт, перекрытия стеков возникнуть не должно. Пока не придумал способ выбора оптимального размера стека.
Область CPU1_CODE начинается с адреса 0х00200000 и имеет размер 1 Мб. Перед функцией main_cpu1(), написанной в отдельном файле main_sc.с располагается ассемблерный код нашего файла для старта второго ядра start_cpu1.s. В scatter файле необходимо указывать расширение .o, если вы хотите отдельно размещать код файлов по нужным адресам.
Таким образом имеем в одном проекте фактически две разные программы. В настройках дебаггера следует поменять Target на Debug Cortex-A9x2 SMP, тогда можно переключаться в процессе между двумя ядрами.
Бонус
Если вам пришлось решать задачу запуска двух разных программ на двух ядрах, то вам будет полезно знать как включать MMU и Кэш для обоих ядер. Без этого, любая программа сложнее мигания светодиодом, будет выполняться крайне медленно.
#include "alt_cache.h"
#include "alt_mmu.h"
/* MMU Page table - 16KB aligned at 16KB boundary */
#define ARRAY_SIZE(array) (sizeof(array) / sizeof(array[0]))
static uint32_t __attribute__ ((aligned (0x4000))) alt_pt_storage[4096];
static void *alt_pt_alloc(const size_t size, void *context)
static void mmu_init(void)
{
uint32_t *ttb1 = NULL;
// Populate the page table with sections (1 MiB regions).
ALT_MMU_MEM_REGION_t regions[] = {
// Memory area: 4 mb
{
.va = (void *)0x00000000,
.pa = (void *)0x00000000,
.size = 0x00400000,
.access = ALT_MMU_AP_PRIV_ACCESS,
.attributes = ALT_MMU_ATTR_WBA,
.shareable = ALT_MMU_TTB_S_SHAREABLE,
.execute = ALT_MMU_TTB_XN_DISABLE,
.security = ALT_MMU_TTB_NS_SECURE
},
// Device area: Everything else
{
.va = (void *)0x00400000,
.pa = (void *)0x00400000,
.size = 0xffc00000,
.access = ALT_MMU_AP_PRIV_ACCESS,
.attributes = ALT_MMU_ATTR_DEVICE_NS,
.shareable = ALT_MMU_TTB_S_NON_SHAREABLE,
.execute = ALT_MMU_TTB_XN_ENABLE,
.security = ALT_MMU_TTB_NS_SECURE
}
};
alt_mmu_init();
alt_mmu_va_space_storage_required(regions, ARRAY_SIZE(regions));
alt_mmu_va_space_create(&ttb1, regions, ARRAY_SIZE(regions), alt_pt_alloc, alt_pt_storage);
alt_mmu_va_space_enable(ttb1);
}
int main()
{
mmu_init();
alt_cache_system_enable();
}
Так выглядит часть кода для первого ядра. Поскольку MMU и Кэш данных и инструкций для каждого ядра свои, то в коде для второго ядра необходимо написать аналогичную функцию инициализации MMU и включения только соответствующих кэшей, поскольку L2 уже инициализирован первым ядром.
int main_cpu1()
{
mmu_init2();
alt_cache_l1_enable_all();
}
Такая конфигурация точно работает.
Стоит сказать пару слов о прерываниях. Тут все тривиально, сначала включаем GIC (это достаточно сделать только на первом ядре один раз), затем в каждом ядре нужно отдельно инициализировать и включить прерывание чисто для CPU. Для этого используются функции
alt_int_global_init();
alt_int_global_enable();
alt_int_cpu_init();
alt_int_cpu_enable();
По возникновению прерываний счетчик должен уходить по нужному вектору, который может быть объявлен только один раз. Именно по этой причине инициализация второго ядра начинается также из области VECTORS, а затем через условие переходит в файл start_cpu1. Потому, что иначе пришлось бы объявлять заново те же вектора, с теми же именами, но так сделать в одном проекте нельзя.
Вообще я даже пробовал сделать крайнее «извращение». Создал и скомпилировал два совершенно разных проекта, но разместил код в разные места, чтоб не было перекрытия. Преобразовал .axf в .bin. В коде первого ядра настроил адрес счетчика ровно в место main() кода второго ядра. Затем, через Hex редактор сшил два файла в один, с правильным размещением кода по адресу. Все работало, но как то хреново. Да и отлаживать такое чудо совершенно не удобно. Я подозревал, что это плохая затея, но было просто интересно проверить. На этом у меня все, спасибо всем, кто прочитал!
Литература
- Вообще всю подробную информацию о scatter синтаксисе ищите в документах нужной вам версии ARM Compiler armlink User Guide.
- Об ассемблере в ARM Compiler armasm User Guide.
Автор: pinchazer