Многоядерные микроконтроллеры уже обыденность. Первый многоядерный микроконтроллер — TMS320C80 — был выпущен фирмой Texas Instruments более 30 лет назад. Два ядра есть и у микроконтроллера ESP32, столь любимого пользователями Хабра и не только.

В автомобилестроении многоядерные микроконтроллеры также получают широкое распространение. К примеру, в обозначении микроконтроллера китайской фирмы FlagChip FC7300F8MDT буква T означает, что внутри SoC‑а находятся аж три процессорных ядра ARM Cortex‑M7.

Попробуем осуществить запуск этих ядер на микроконтроллере FC7300F8MDT. То есть включить последовательно все ядра этого микроконтроллера одно за другим, убедиться, что ядра работают, обслуживать отдельный экземпляр UART‑Console на каждом ядре, разобраться, как обмениваться данными между ядрами.

Определения

Читатель, знакомый с программированием микроконтроллеров в общих чертах, может смело пропустить этот раздел.

Стек — часть RAM памяти. В стеке хранятся локальные переменные, переменные обработчика прерываний, адрес возврата в предыдущую функцию, значения регистров процессора.

Прерывание — это внешнее событие, которое может спровоцировать оборудование, когда
требуется получить внимание процессора. Прерывания инициируют вызов функции обработчика прерывания.

Вектор прерывания — это пара чисел. Первое число — закреплённый за устройством номер, который идентифицирует соответствующий обработчик прерываний. Второе — адрес функции обработчика этого прерывания в физической памяти процессора.

Таблица векторов прерываний — это таблица адресов функций обработчиков
прерываний. Её можно представить в виде таблицы из двух колонок, где первая — это номер прерывания, вторая — абсолютный адрес обработчика прерываний в on‑chip NOR‑Flash памяти. Обычно в этой таблице сотни записей. У каждой модели микроконтроллера она уникальная. В бинарном файле перечислена вторая колонка как массив uint32 индекс массива‑номер прерывания, значение — адрес ISR.

Чаще всего таблица векторов прерываний расположена в самом начале бинарного файла с прошивкой.

Реализация

Пример того, как осуществить физический пуск соседних ядер свободно представлен в SDK вендором в папке MultiCore_AMP_Demo.

FC7300_SDK_Vx_x_x\Example\FC7300\_Others\MultiCore_AMP_Demo

Основная идея в том, чтобы при сборке многоядерных программ надо код для каждого ядра размещать в отдельном диапазоне ROM памяти. У каждого ядра своя таблица векторов прерываний, свой указатель на вершину стеке, свой reset_handler, своя функция main(), своя куча, свой стек. Стек и кучу надо отражать в *.ld файле.

При этом у всех трёх ядер общие обработчики прерываний (большинство ISR), общие глобальные переменные, общие периферийные регистры микроконтроллера.

Если у core2 произойдет прерывание, то локальные переменные прерывания будут размещены как раз в стеке ядра core2. Аналогично у остальных ядер. Отсутствует отдельный стек прерываний для всех ядер. Поэтому условно, делая в отладчике Ozone пошаговую отладку ядра 1, вы даже не увидите прерываний от core0 или core2.

Core 0

Core 1

Core 2

isr_vector_base

0×01000000

0×01200000

0×01400000

name 

__vector_table

__core1_vector_table

__core2_vector_table

функция main

main

Core1_main

Core2_main

Name Flash

PFLASH_0

PFLASH_1

PFLASH_2

StartUp Name

Reset_Handler

Core1_Reset_Handler

Core2_Reset_Handler

StackTop

0×21042000

0×21042800

0×21043000

StackSize

4 k

4 k

4 k

Конфиг для Ozone

FC7300F8M

FC7300F8M_CORE1

FC7300F8M_CORE2

section

text

Code_Core1

Code_Core2

section

isr_vector

core1_isr_vector

core2_isr_vector

При подаче питания по умолчанию стартует core0. Перед пуском ядер core1 и core2 можно убедится, что по физическим адресам в flash в самом деле прописаны валидные значения указателя на верхушку стека и адрес функции reset_handler(). Спасибо архитектуре ARM, в Cortex‑M процессорах можно до запуска кода выполнить базовые проверки на валидность данных, то есть убедится, что все три таблицы векторов прерываний в самом деле прописаны в on‑chip NOR Flash памяти.

Core0

Core2

Данные валидные. Видно, что стек принадлежит RAM памяти, а Reset_handler — ROM памяти. На всякий случай я добавил отложенный пуск второго ядра на 10-й секунде с момента подачи электропитания. Core0 включает core1 и core2. Пуск 3-го ядра на 15 ой секунде.

Если внутри кода на core0 подать команду пуска ядра3, то ядро3 стартует через 5 m сек.

Чтобы понять, что ядро стартовало, я каждому ядру присвоил отдельный LED, UART и структуру супер‑цикла. Как видно в console, core1 в само деле запустилось и стало прокручивать второй экземпляр супер‑цикла.

Вот работают все три ядра

Надо отметить, что только нулевое ядро (core0) может включать ядра core1 и core2.

Если core0 включит core1, а core1 попытается включить core2, то core1 зависнет в HardFault_Handler.

В примерах startup кода от вендора названия функций для обработчиков прерываний для всех трех ядер одинаковые. Это надо учитывать при разработке ПО. Например SysTick достаточно запустить только на нулевом ядре. Остальные ядра могут просто читать переменную в глобальной памяти, которая содержит UpTime.

Если Вы хотите отлаживать прошивку на другом ядре, то надо запустить Ozone с другим конфигом.

Ядро

Конфиг

Core0

FC7300F8M

Core1

FC7300F8M_CORE1

Core2

FC7300F8M_CORE2

Важный момент. В стартап-файле таблица векторов прерываний должна во что бы то ни стало сохраняться в бинарнике. Для этого её надо пометить ключевым словом progbits:

/* Set vector table */
section.isr_vector,“a”,%progbits
align 2

/* vector table */
__vector_table:
long __Core0_StackTop /* Top of Stack */
long Reset_Handler /* Reset Handler */
long NMI_Handler /* NMI Handler */
long HardFault_Handler /* Hard Fault Handler */
long MemManage_Handler /* MPU Fault Handler */
long BusFault_Handler /* Bus Fault Handler */
long UsageFault_Handler /* Usage Fault Handler */

Обмен данными между ядрами

Когда ядра работают, они должны как‑то обмениваться информацией друг с другом. Для обмена последовательностями байт между ядрами существует специальная подсистема процессора, которая называется MailBox. Примеры её использования представлены в примере FC7300_SDK_Vx_x_x\Example\FC7300\Mailbox\LongMessage в SDK.

Работает это так. Ядро Core0 создает пару переменных: указатель на массив и его размер. Далее передает структуру в трансивер (Send Request). На другом ядре в Core1 программа получает (Receive) этот указатель и размер.

Трансивер MailBox в SoC‑е в единственном экземпляре. Поэтому и в коде должен быть только один глобальный экземпляр структуры типа MB_HandleType. Один на все 3 ядра.

Тут надо отметить, что передаваемые данные должны быть в глобальной памяти. Если вы выделите на стеке массив 8 байт и дадите указатель на него и размер в mailbox, то другое ядро примет мусор.

Передали

mbs 1 2 0×11223344556677
I,[MAILBOX] Send,Core:0→1,Ch2,Size:7,[11223344556677]

Приняли

I,[MAILBOX] IamCore:1,CH:2,Core:0,Sec:0,Sup:0,Addr:0×0x210439c0,Len:7,
11 223 301 009 006
11 223 344 556 677 <‑ Tx
11 223 301 009 006 <‑ Rx

Отладка

Для отладки процесса передачи данных между ядрами мне пришлось собрать прототип. Идея проста. На каждое ядро повесить UART‑console, через переходник CP2102 подключаться на ПК утилитой Putty/TeraTerm и исполнять команды отправки данных в каждой консоли.

Ядро

core0

core1

core2

UART

UART1

UART2

UART3

console

console1

console2

console3

UART_TX

PB20

PD17

PA24

UART_RX

PC14

PA30

PA23

Затем проверять, что в консоли соседнего ядра в самом деле появилось сообщение о приёме массива.

Ядра 1 и 2 успешно принимают данные.

Отправка в обратную сторону Core1 → Core0 и Core2 → Core0

А теперь проверим как работает этот mailbox. Замысел такой. Выполним запрос на отправку массива и тотчас же попортим данные.

#define MAILBOX_NUM 1
bool test_mailbox_to_core2_corrupt(void){
    LOG_INFO(TEST, "%s():", __FUNCTION__);
    set_log_level(MAILBOX, LOG_LEVEL_DEBUG);
    MailBoxHandle_t* Node = MailBoxGetNode(MAILBOX_NUM);
    uint8_t txBuff[10] = {3};
    ASSERT_TRUE(  array_incr(txBuff, sizeof(txBuff))    );
    ASSERT_TRUE(  mailbox_send(MAILBOX_NUM, 2, txBuff , sizeof(txBuff)   ) );
    // corrupt tx data
    memset(Node->TxData,0x44,sizeof(txBuff));
    set_log_level(MAILBOX, LOG_LEVEL_INFO);
    return true;
}

Как показал модульный тест, на приёмной стороне данные не портятся, а новая запись просто добавляется, как суффикс в конец принятого массива, как будто внутри SoC есть какой‑то аппаратный массив.

Если между приемом и отправкой 10-байтового массива изменить только один байт внутри (TxData[5]=0xAA), то MailBox заново доставит этот же 10-байтовый массив, только с изменённым байтом.

Если при помощи mailbox отправить данные на не запущенное ядро, то ничего плохого не произойдет. Отправившее ядро продолжит работать. При этом ядро может отправить через mailbox массив самому себе.

Осуществить перезапуск конкретного процессорного ядра можно, просто вызвав его Reset Handler. Но надо учитывать, что глобальные переменные в RAM памяти сохранят свои значения, поэтому надо заново инициализировать все программные компоненты, с которыми работает данное процессорное ядро.

№

Reset Handler

Core

0

Reset_Handler

Core0

1

Core1_Reset_Handler

Core1

2

Core2_Reset_Handler

Core2

Как можно заметить в примерах от SDK, перед функцией Core1_main для ядра1 есть строка __attribute__((section(".Code_Core1"))).

Ключевое слово __attribute__((section(".Code_Core1"))) указывает компилятору, что данную функцию надо размещать в интервале Flash памяти с именем Code_Core1 и при запуске ядра настраивать таблицу векторов прерывания на этот адрес.

Ссылки

  1. Как программировать многоядерные микроконтроллеры (@mingl)

  2. FC7300Fxx Product Brief

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