image
Если вы читали мою предыдущую статью, вероятно вам интересна эта тема и вы хотите узнать больше. В этой статье рассмотрим очень частную, не простую, но от этого не менее необходимую задачу запуска двух разных 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 VFP\NEON. Допишем необходимый разветвитель.

alt_interrupt_armcc.s до редактирования
    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

alt_interrupt_armcc.s после редактирования
    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 в другом файле

start_cpu1.s
     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 файл, чтоб лучше понять, что происходит.

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 и Кэш для обоих ядер. Без этого, любая программа сложнее мигания светодиодом, будет выполняться крайне медленно.



MMU и Cache для первого ядра
#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 уже инициализирован первым ядром.

MMU и Cache для второго ядра
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 редактор сшил два файла в один, с правильным размещением кода по адресу. Все работало, но как то хреново. Да и отлаживать такое чудо совершенно не удобно. Я подозревал, что это плохая затея, но было просто интересно проверить. На этом у меня все, спасибо всем, кто прочитал!

Литература


  1. Вообще всю подробную информацию о scatter синтаксисе ищите в документах нужной вам версии ARM Compiler armlink User Guide.
  2. Об ассемблере в ARM Compiler armasm User Guide.
Поделиться с друзьями
-->

Комментарии (3)


  1. VBKesha
    10.07.2017 17:44

    Интересно а разобрался кто нибудь с USB контроллером в Baremetal?


    1. pinchazer
      10.07.2017 18:50
      +3

      У меня получилось разобраться с EMAC контроллером в Baremetal, получилось отсылать и принимать пакеты на MAC уровне, кстати возможно напишу статью об этом. Не думаю, что разобраться с USB намного сложнее.


  1. VBKesha
    10.07.2017 22:29
    +1

    кстати возможно напишу статью об этом

    Было бы интересно почитать!