Как программировать многоядерные микроконтроллеры

euyhlyenrswru2huilipfbov8gy.jpeg

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

При написании программ для ПК значительная часть работы по организации вычислений с использованием нескольких ядер берёт на себя операционная система и различные библиотеки. Поэтому программист может использовать высокоуровневые механизмы, сильно не вникая в аппаратную часть. Однако в случае с микроконтроллерами перед написанием кода полезно разобраться с тем, как этот код будет исполняться на низком уровне. В данной статье будут рассмотрены общие принципы построения и программирования многоядерных микроконтроллеров. В качестве примера будет разобран процесс создания небольшого демонстрационного проекта для микроконтроллера с двумя ядрами.

Микроконтроллеры разных производителей могут быть устроены по-разному. В данной статье рассмотрение ограничивается устройствами с архитектурой ARM Coretx M от NXP (LPC), STMicroelectronics и Infineon (Cypress).

Ниже приведены несколько ключевых характеристик электронного устройства, которые можно улучшить за счёт использования многоядерных микроконтроллеров при решении определённых типов задач.


  • Повышение производительности. Это, наверное, один из наиболее очевидных вариантов. Если микроконтроллер должен решать какую-то сложную задачу, которую можно разбить на несколько относительно независимых подзадач, эти подзадачи можно распределить между разными ядрами и за счёт этого ускорить вычисления.
  • Повышение энергоэффективности. В некоторых устройствах большую часть времени микроконтроллер осуществляет нетребовательные к вычислительным ресурсам операции (опрос датчиков, оповещение, индикация), но при возникновении определённого события требуется решать некоторую ресурсоёмкую задачу. Для таких устройств подойдут микроконтроллеры, в которых имеется не очень производительное, но энергоэффективное ядро (обычно Cortex M0) и как минимум одно высокопроизводительное ядро (например, Cortex M4). Первое ядро будет работать постоянно, а второе большую часть времени будет находиться в режиме сна. Высокопроизводительное ядро будет пробуждаться только в тех случаях, когда требуется большая вычислительная мощность.
  • Повышение компактности и упрощение печатной платы. Бывают устройства, в которых используется сразу несколько микроконтроллеров. Иногда можно сделать устройство компактнее и проще, заменив несколько обычных микроконтроллеров одним многоядерным. Те задачи, которые раньше решались разными микроконтроллерами, теперь будут решаться разными ядрами в пределах одного микроконтроллера. Общее число компонентов и проводников на плате при этом станет меньше.

Схемы построения многоядерных микроконтроллеров можно поделить на симметричные и асимметричные.

Симметричная схема предполагает одновременный запуск ядер. Каждое ядро загружается и начинает выполнять свою программу независимо от остальных ядер. Синхронизация ядер с помощью аппаратных и программных механизмов возможна. Но она не является обязательной.

Если схема асимметричная, то одно из ядер является ведущим (Master Core). Ведущее ядро запускается первым. Тактирование остальных ядер, являющихся ведомыми (Slave Core), в этот момент выключено, а сами ядра удерживаются в состоянии reset. Чтобы привести их в действие ведущее ядро должно включить подачу тактового сигнала и запустить сами ядра. Делается это по аналогии с настройкой тактирования периферийных блоков изменением соответствующих регистров блока System Control (в зависимости от производителя название может отличаться).

jlao4kow9nz5_4ouliojpn64bke.png

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

Стоит отметить, что сами ядра могут быть разными, в частности могут относиться к разным семействам. Часто в одном устройстве располагают простые ядра, обеспечивающие высокую энергоэффективность (Cortex M0), и более продвинутые ядра, предназначенные для сложных вычислений (Cortex M4 / M7).

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

rxtz_esmydm89vwtjeinjknjweo.png

Если спуститься на уровень ниже, то можно разделить многоядерные микроконтроллеры на простые и сложные. Устройство простых микроконтроллеров (например, LPC55S6x или Cypress CY8C6xx5) практически полностью отвечает описанным выше представлениям. Память и периферийные блоки расположены на определённых шинах. Все шины сведены в единую матрицу (bus matrix). Матрица шин позволяет соединить шины ядра с теми шинами памяти и шинами периферии, по которым в данный момент требуется передать данные. В целом всё аналогично одноядерным микроконтроллерам, с той лишь разницей, что вместо одного ядра к матрице шин подключено несколько ядер.

gemq1yjliydswx2ngwqjj56eybw.png


Пример реальной схемы многоядерного микроконтроллера с одной матрицей шин

1pdhzxr8wi3b0m3y_sm-mb0pau8.png

Но есть более сложные микроконтроллеры, такие как STM32H747. В таких контроллерах может быть несколько матриц шин, причём с разной разрядностью. Те или иные области памяти, а также периферийные блоки могут быть оптимизированы для работы с конкретными ядрами. Поэтому при работе с такими микроконтроллерами нужно более детально изучать их внутреннее устройство и учитывать дополнительные ограничения.


Пример реальной схемы микроконтроллера с несколькими матрицами шин

k1gw-y7okjnhk87adnty4qi6bc8.png

Матрица шин и другие подсистемы в большинстве случаев устроены таким образом, чтобы автоматически разрешать конфликты доступа. Если два ядра будут одновременно обращаться к одной области памяти или к одному и тому же периферийному блоку, их обращения будут автоматически выстраиваться друг за другом и обрабатываться по очереди. Информации о том, как именно это организовано, я не нашёл. Тем не мене, все происходит автоматически, и ошибок возникать не должно.

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

В первую очередь рекомендуется разнести по разным шинам считывание исполняемого кода. Это можно сделать следующим образом. Одно из ядер (ведущее) при старте перемещает прошивки остальных ядер из FLASH-памяти в SRAM. При этом прошивки для разных ядер раскладываются по разным банкам. После этого ведомые ядра запускаются с указанием соответствующих стартовых адресов.

Что касается периферийных блоков, то здесь нужно обращать внимание на имена шин (AHB1, AHB2, APB1, APB2…). По возможности нужно разделить шины между ядрами и сделать так, чтобы приходилось как можно реже обращаться к блокам, расположенным на «чужой» шине.

o1o1zgbsfnpkrvcbrmhihvsbkxu.png

Каждое ядро имеет собственный контроллер прерываний (NVIC). Поэтому все внутренние прерывания ядер настраиваются и работают полностью независимо. Что же касается запросов на прерывания, которые поступают из периферийных блоков, то они идут по общим линиям, которые параллельно подключены к блокам NVIC. Каждый периферийный блок может генерировать только один сигнал, который получат блоки NVIC сразу всех ядер. Для каждого ядра можно независимо настроить вектора, которые должны обрабатываться: назначить обработчики, задать приоритеты, включить/выключить обработку. Но настроить периферийные блоки так, чтобы они отправляли разные запросы разным ядрам, не получится. Например, не получится настроить блок GPIO так, чтобы по нарастающему фронту срабатывал обработчик в одном ядре, а по спадающему фронту на той же ножке запускался обработчик в другом ядре, поскольку эти события практически во всех микроконтроллерах используют одну общую линию.

fwlz813ihfskjqkjfmjnjp8bywu.png

Механизмы межъядерного взаимодействия обычно осуществляются посредством выделенного периферийного блока. У разных производителей этот блок называется по-разному. В микроконтроллерах STM32 этот блок называется Hardware Semaphore (HSEM), в NXP — Inter-CPU mailbox, в Cypress — Inter-Processor Communication Block (IPC). Устройство этих блоков немного отличается, но можно выделить два основных механизма.

Первый механизм позволяет отправлять сигналы другим ядрам посредством запросов на прерывания и иметь некоторую гарантию доставки этих сигналов. У каждого ядра есть специальный «почтовый ящик» (mailbox). Это регистр, к которому имеют доступ все остальные ядра. При записи числа в этот регистр NVIC соответствующего ядра получит запрос на прерывание. Передаваемое вместе с прерыванием число может трактоваться как код команды, как набор бит, кодирующих независимые действия, или как что-то ещё (определяется программистом).

jempw9vabiwhbymj4wi6aos-tk8.png

Второй механизм — это специальные регистры, которые поддерживают атомарную операцию чтения с последующим изменением значения. В простейшем случае такие регистры обнуляют своё значение при чтении. Такие регистры позволяют организовать контроль доступа к общим ресурсам. Например, установленная единица означает, что ресурс свободен. Если ядру требуется захватить ресурс, производится считывание регистра. Если ресурс был захвачен ранее, возвращается ноль. Если ресурс свободен, возвращается единица, а значение регистра сразу же обращается в ноль. Гарантируется, что при одновременном чтении только одно из ядер получит единицу. При освобождении ресурса, ядро должно записать единицу обратно в регистр. Пример опроса такого регистра двумя ядрами изображён на рисунке ниже.

9wb_iuzuwz6_zm7adplvioc0fzw.png

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

Многоядерные микроконтроллеры обычно имеют подсистему отладки с несколькими внутренними портами. Каждое ядро имеет свой порт отладки (Debug Access Port). Благодаря этому с помощью одного аппаратного программатора-отладчика можно одновременно и независимо осуществлять отладку нескольких ядер. Порты отладки для каждого из ядер (и других подсистем при их наличии) могут быть включены или выключены независимо. Программы с ПК могут получать доступ к разным портам отладки микроконтроллера через общий физический порт подсистемы отладки. Благодаря этому можно осуществлять одновременную отладку нескольких ядер.

6kq2o_j0wgrep3wnlcmgwincn7o.png

Теперь применим описанное выше на практике. Для этого напишем простое демонстрационное приложение, в рамках которого два ядра микроконтроллера будут мигать двумя светодиодами, при этом их действия будут синхронизированы.

Будет использоваться отладочная плата LPC55S69-EVK с микроконтроллером NXP LPC55S69, который имеет два ядра Cortex M33. Приложение будет создаваться с использованием среды IAR Embedded Workbench 8. При использовании других сред основные этапы, скорее всего, будут аналогичными. В конце статьи приведены ссылки на документы, описывающие процесс программирования многоядерных микроконтроллеров с использованием других сред.

-a9bz4u_ny7i6nd35jyzpfsbbfs.jpeg

Код будет написан на регистрах с использованием CMSIS. В больших проектах я предпочитаю использовать более высокоуровневые библиотеки. Но при освоении новых не очень сложных блоков я обычно сначала пишу простые демонстрационные программы на регистрах, поскольку в этом случае код оказывается близок к документации на микроконтроллер и явным образом демонстрирует все основные этапы настройки.


Создание прошивки для ядра 0

Прошивки для двух ядер можно писать и отлаживать независимо. Сначала напишем прошивку для ядра CPU0.

На данном этапе порядок создания и настройки проекта стандартный.


  1. Создать новый проект для языка C: Project > Create New Project. Выбрать C / main. В названии проекта имеет смысл подчеркнуть, что он относится к ядру 0, поскольку для второго ядра будет создан другой проект. Я назвал проект CPU0.
  2. Сохранить рабочее пространство: File > Save Workspace.
  3. В свойствах проекта во вкладке General Options > Target указать микроконтроллер.
    Здесь появляется первая особенность: из списка устройств нужно выбрать не только микроконтроллер, но и ядро, которое будет использоваться в данном проекте.
    grfov8wgp2o6ianpasv23xt6-d0.png
  4. Во вкладке General Options > Library Configuration поставить галочку Use CMSIS.
  5. В настройках компилятора (C/C++ Compiler > Preprocessor) указать путь до заголовочного файла с макроопределениями для используемого семейства микроконтроллеров: <Путь до SDK>\devices\LPC55S69. Здесь же в поле Defined symbols прописать CPU_LPC55S69JBD100_cm33_core0 (нужно для того, чтобы подключились правильные заголовочные файлы).
  6. Во вкладке Linker > Config поставить галочку Override default и указать путь до конфигурационного файла компоновщика LPC55S69_cm33_core0_flash.icf из SDK (находится в папке <Путь до SDK>\devices\LPC55S69\iar).
  7. Во вкладке Debugger > Setup в качестве отладчика (Driver) установить CMSIS DAP.
  8. Проверить, что во вкладке Debugger > Download стоят галочки Verify Download и Use flash loader (s).
  9. Добавить в проект файлы system_LPC55S69_cm33_core0.c и startup_LPC55S69_cm33_core0.s (можно найти в папках <Путь до SDK>\devices\LPC55S69 и <Путь до SDK>\devices\LPC55S69\iar соответственно). Важно обратить внимание, что для разных ядер эти файлы будут разными.
  10. Добавить код для мигания светодиодом, подключенным к ножке GPIO1_7.

    #include "LPC55S69_cm33_core0.h"
    
    #define LED_GPIO_BLOCK 1
    #define LED_GPIO_PIN 7
    
    static inline void Delay(void)
    {
     for(int i = 0; i < 8000000; i++);
    }
    
    int main()
    {  
     /**** LED setup ****/
     // Enable clocking for GPIO1 (located on AHB bus)
     SYSCON->AHBCLKCTRLX[0] |= SYSCON_AHBCLKCTRL0_GPIO1_MASK;
    
     // Set direction for GPIO1 7 as output
     GPIO->DIR[LED_GPIO_BLOCK] |= GPIO_DIR_DIRP(1 << LED_GPIO_PIN);
    
     while(1)
     {
       /**** LED blinking ****/
       GPIO->B[LED_GPIO_BLOCK][LED_GPIO_PIN] = 0;  // Turn led On
       Delay();
       GPIO->B[LED_GPIO_BLOCK][LED_GPIO_PIN] = 1;  // Turn led off
       Delay();
     }
    }

  11. Скомпилировать, запустить, проверить, что всё работает.


Создание прошивки для ядра 1

Теперь можно написать прошивку для второго ядра, которая будет мигать другим светодиодом. Для второго ядра нужно создать новый проект. Здесь в названии проекта также имеет смысл отразить номер ядра, для которого предназначен этот проект.

Повторить все действия из предыдущего раздела, но на шагах 3, 5, 6, 9, 10 в именах файлов и настроечных параметрах вместо core0 теперь должно быть core1.

Код можно оставить таким же, но макроопределение нужно заменить, чтобы второе ядро мигало светодиодом, подключенным к ножке GPIO1_4:

#define LED_GPIO_PIN 7  -->  #define LED_GPIO_PIN 4

Компилируем. Пробуем запустить отладку и… Получаем ошибку Reading CPU status failed.
1ke6mo4yotgjuegpbl4l1mdrddc.png

Если внимательно изучить документацию, можно найти информацию о том, что микроконтроллер LPC55S69 имеет ассиметричную схему запуска ядер. Ядро 0 является ведущим, ядро 1 — ведомым. Поэтому чтобы запустить выполнение прошивки ядра 1 из прошивки ядра 0 нужно:


  • Включить ядро 1.
  • Указать адрес, по которому ядро 1 должно загружать прошивку.
  • Включить тактирование ядра 1.
  • Отпустить reset ядра 1.

В код для ядра 0 нужно добавить пару магических макроопределений:

// Sequence required to be written
// to SYSCON->CPUCTRL register ORed with other bits
// to applay settings
// See UM11126 4.5.70 for more details
#define SYSCON_CPUCTRL_ENABLING_SEQ 0xC0C40000

// Warning!!! IAR specific code. 
// core1_image_start is defined in 
// LPC55S69_cm33_core0_flash.icf linker config file
extern unsigned char core1_image_start[];
#define CORE1_IMAGE_START core1_image_start

Затем в функцию main() добавить код инициализации ядра 1:

/**** CPU1 (slave) setup ****/
// Enable CPU1
SYSCON->CPUCFG |= SYSCON_CPUCFG_CPU1ENABLE_MASK;

// Setup firmware boot address
SYSCON->CPBOOT = SYSCON_CPBOOT_CPBOOT(CORE1_IMAGE_START);

// Enable clock and reset CPU1
SYSCON->CPUCTRL = SYSCON->CPUCTRL | SYSCON_CPUCTRL_ENABLING_SEQ | 
                  SYSCON_CPUCTRL_CPU1RSTEN_MASK | SYSCON_CPUCTRL_CPU1CLKEN_MASK;

// Release reset for CPU1
SYSCON->CPUCTRL = (SYSCON->CPUCTRL | SYSCON_CPUCTRL_ENABLING_SEQ) & 
                  (~SYSCON_CPUCTRL_CPU1RSTEN_MASK);

Скомпилировать, загрузить и один раз запустить обновлённую прошивку ядра 0.

После этого вернуться к проекту прошивки ядра 1 и запустить отладку. Теперь отладка должна запуститься.


Совместная отладка

У нас получилось запустить прошивки для разных ядер. Но современные среды разработки позволяют осуществлять отладку при одновременном запуске прошивок на двух ядрах. В некоторых случаях одновременная отладка двух ядер может оказаться полезной.

Чтобы воспользоваться таким режимом отладки, нужно сделать несколько дополнительных настроек:


  1. Перейти в проект ведомого ядра (ядро 1).
  2. Включить сохранение бинарного файла прошивки. Для этого нужно перейти во вкладку Converter > Output, поставить галочку Generate Additional Output и выбрать формат Raw binary.
    8g51enjc8m0_rdbt_todi4rapay.png
  3. Во вкладке Debugger > Download снять галочку Verify Download. При совместной отладке двух ядер эта проверка работает некорректно и мешает запуску.
    f5ya-qpkcjtucpl-ll5gwqwkmom.png
  4. Сохранить настройки и собрать проект.
  5. Перейти в проект ведущего ядра (ядро 0).
  6. В свойствах проекта перейти во вкладку Debugger > Multicore, выбирать вариант Simple и указать необходимые параметры проекта ведомого ядра.
    1_3hl9dj3oet6yo5fze0ygqqtk8.png
  7. Перейти во вкладку Linker > Input и подключить к проекту файл прошивки ведомого ядра. Для этого нужно задать имя символа (задаётся произвольным образом). В данном примере задано имя _CPU1_image, привязать к этому символу бинарный файл, который был получен в результате сборки проекта для ядра 1, указать секцию __sec_core и выравнивание 4.
    mm7ghz1laebdyunfthiohjq0e80.png
  8. Теперь можно сохранить настройки и запустить отладку. Если всё настроено правильно, при запуске отладки из проекта ведущего ядра должен открыться второй экземпляр среды разработки с проектом для ведомого ядра. Всё это занимает достаточно много места на экране, поэтому второй монитор может оказаться очень кстати.
    pkaqfsp8nppwsbna7mcjkavrti8.png
    В режиме совместной отладки в IAR появляется специальная панель, которая позволяет управлять отладкой сразу нескольких ядер.
    itll4pisbhlktjiu3ajcjktzsyq.png


Популярные ошибки

Если возникает ошибка Failed to synch with multicore partner, скорее всего проект ведомого ядра не скомпилирован. Нужно сначала собрать проект для CPU1 и только после этого запустить отладку проекта для CPU0.
b-4cc6gullkdwasvyxyiqmhaphw.png

Если при запуске отладки Вы получили ошибку

The debugging session could not be started.
Either the debugger initialization filed, or else the file "iar_all_modules_loaded" was missing, corrupt or of an unsupported format.

ir40uzpccpdbc03kvxbhejn6uzw.png
скорее всего, Вы забыли снять галочку Verify download на шаге 3. Нужно проверить, что она снята, пересобрать проект ведомого ядра и попробовать запустить совместную отладку снова.

Если отладка работает странно: некорректно работают условные переходы, не происходит вызовов функций, не работают точки останова, возможно, используемая отладочная информация не соответствует исполняемому коду. Первым делом нужно пересобрать проект ведомого ядра. Также нужно проверить настройки, сделанные на шагах 6 и 7. Если они не согласованы друг с другом (в настройках компоновщика указан бинарный файл одного проекта, а в настройках отладчика указан другой проект), явных ошибок при запуске может не возникнуть, отладка будет работать, но поведение будет некорректным. Нужно проверить соответствие бинарного файла в настройках компоновщика проекту, указанному в настройках отладчика.


Перемещение кода ведомого ядра в SRAM

В текущей версии приложения прошивки двух ядер хранятся во FLASH-памяти. Оба ядра используют одну и ту же шину для чтения команд. Это не очень эффективно. Поэтому рекомендуется переместить прошивку ведомого ядра в SRAM. Тогда ведущее ядро продолжит загружать команды из FLASH-памяти, а ведомое ядро будет загружать команды из SRAM по другой шине. Сделать это можно следующим образом:


  1. Перейти в проект ведомого ядра (ядро 1).
  2. Открыть свойства проекта и в настройках компоновщика (Linker > Config) заменить конфигурационный файл LPC55S69_cm33_core1_flash.icf на LPC55S69_cm33_core1_ram.icf.
  3. Пересобрать проект.
  4. Вернуться к проекту для ведущего ядра (ядро 0).
  5. Добавить в main.c:
    • #include "string.h" // for memcpy.
    • Выбрать и зафиксировать адрес в SRAM, по которому будет расположена прошивка ядра 1.
      // Address of RAM, where the image for CPU1 should be copied
      // According to UM11126 2.1.5 it is SRAM 3 on CM33 data bus
      #define CORE1_BOOT_ADDRESS (void *)0x20033000
    • Добавить функцию для определения размера прошивки.
      // Firmware image size calculation
      uint32_t get_core1_image_size(void)
      {
        uint32_t core1_image_size;
      #pragma section = "__sec_core"
        core1_image_size = (uint32_t)__section_end("__sec_core") - 
                           (uint32_t)&core1_image_start;
        return core1_image_size;
      }
  6. В коде перед запуском ядра 1 скопировать прошивку из FLASH-памяти в SRAM.
    //  Copy CPU1 image to an other SRAM block
    memcpy(CORE1_BOOT_ADDRESS, (void *)CORE1_IMAGE_START, get_core1_image_size());
  7. Обновить стартовый адрес (эта строчка уже должна быть, поменять нужно только адрес).
    // Setup boot address
    SYSCON->CPBOOT = SYSCON_CPBOOT_CPBOOT(CORE1_BOOT_ADDRESS);
  8. Можно собрать прошивку ядра 0, запустить и убедиться, что всё работает.

Если на данном этапе возникли ошибки, нужно проверить что при сборке прошивки ведомого ядра генерируется бинарный файл и содержимое этого файла включается в прошивку ведущего ядра (шаги 2 и 7 из предыдущего раздела).


Организация доступа к общим ресурсам

Теперь модифицируем прошивки таким образом, чтобы светодиоды зажигались строго по очереди и в любой момент времени горел только один светодиод. Это можно сделать посредством мьютекс-регистра блока MAILBOX.

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

/**** Mailbox setup for mutex ****/
// Enable mailbox clocking
SYSCON->AHBCLKCTRLX[0] |=  SYSCON_AHBCLKCTRL0_MAILBOX_MASK;

По умолчанию мьютекс-регистр содержит единицу, что означает, что ресурс свободен.

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

// Try to acquire mutex
while (!(MAILBOX->MUTEX & MAILBOX_MUTEX_EX_MASK));

После выключения светодиода нужно не забыть вернуть мьютекс.

// Release mutex
MAILBOX->MUTEX = MAILBOX_MUTEX_EX_MASK;

Эти действия нужно сделать в прошивках сразу двух ядер.

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


Код прошивки

После всех модификаций исходный код прошивок принял следующий вид.


Прошивка ядра 0 (ведущее ядро)
#include "LPC55S69_cm33_core0.h"
#include "string.h"  // for memcpy

#define LED_GPIO_BLOCK 1
#define LED_GPIO_PIN 7

// Address of RAM, where the image for CPU1 should be copied
// According to UM11126 2.1.5 it is SRAM 3 on CM33 data bus
#define CORE1_BOOT_ADDRESS (void *) 0x20033000

// Sequence required to be written
// to SYSCON->CPUCTRL register ORed with other bits
// to applay settings
// See UM11126 4.5.70 for more details
#define SYSCON_CPUCTRL_ENABLING_SEQ 0xC0C40000

// Warning!!! IAR specific code. 
// core1_image_start is defined in 
// LPC55S69_cm33_core0_flash.icf linker config file
extern unsigned char core1_image_start[];
#define CORE1_IMAGE_START core1_image_start

static inline void Delay(void)
{
  for(int i = 0; i < 8000000; i++);
}

// Firmware image size calculation
uint32_t get_core1_image_size(void)
{
    uint32_t core1_image_size;
#pragma section = "__sec_core"
    core1_image_size = (uint32_t)__section_end("__sec_core") - 
                       (uint32_t)&core1_image_start;
    return core1_image_size;
}

int main()
{  
  /**** LED setup ****/
  // Enable clocking for GPIO1 (located on AHB bus)
  SYSCON->AHBCLKCTRLX[0] |= SYSCON_AHBCLKCTRL0_GPIO1_MASK;

  // Set direction for GPIO1 7 as output
  GPIO->DIR[LED_GPIO_BLOCK] |= GPIO_DIR_DIRP(1 << LED_GPIO_PIN);

  /**** Mailbox setup for mutex ****/
  // Enable mailbox clocking
  SYSCON->AHBCLKCTRLX[0] |=  SYSCON_AHBCLKCTRL0_MAILBOX_MASK;

  /**** CPU1 (slave) setup ****/
  //  Copy CPU1 image to an other SRAM block
  memcpy(CORE1_BOOT_ADDRESS, (void *)CORE1_IMAGE_START, get_core1_image_size());

  // Enable CPU1
  SYSCON->CPUCFG |= SYSCON_CPUCFG_CPU1ENABLE_MASK;

  // Setup firmware boot address
  SYSCON->CPBOOT = SYSCON_CPBOOT_CPBOOT(CORE1_BOOT_ADDRESS);

  // Enable clock and reset CPU1
  SYSCON->CPUCTRL = SYSCON->CPUCTRL | SYSCON_CPUCTRL_ENABLING_SEQ | 
                    SYSCON_CPUCTRL_CPU1RSTEN_MASK | SYSCON_CPUCTRL_CPU1CLKEN_MASK;

  // Release reset for CPU1
  SYSCON->CPUCTRL = (SYSCON->CPUCTRL | SYSCON_CPUCTRL_ENABLING_SEQ) & 
                    (~SYSCON_CPUCTRL_CPU1RSTEN_MASK);

  while(1)
  {
    /**** LED blinking ****/
    while (!(MAILBOX->MUTEX & MAILBOX_MUTEX_EX_MASK));  // Try to acquire mutex
    GPIO->B[LED_GPIO_BLOCK][LED_GPIO_PIN] = 0;          // Turn led On

    Delay();

    GPIO->B[LED_GPIO_BLOCK][LED_GPIO_PIN] = 1;  // Turn led off
    MAILBOX->MUTEX = MAILBOX_MUTEX_EX_MASK;     // Release mutex

    Delay();
  }
}


Прошивка ядра 1 (ведомое ядро)
#include "LPC55S69_cm33_core1.h"

#define LED_GPIO_BLOCK 1
#define LED_GPIO_PIN 4

static inline void Delay(void)
{
  for(int i = 0; i < 8000000; i++);
}

int main()
{  
  /**** LED setup ****/
  // Enable clocking for GPIO1 (located on AHB bus)
  SYSCON->AHBCLKCTRLX[0] |= SYSCON_AHBCLKCTRL0_GPIO1_MASK;

  // Set direction for GPIO1 7 as output
  GPIO->DIR[LED_GPIO_BLOCK] |= GPIO_DIR_DIRP(1 << LED_GPIO_PIN);

  /**** Mailbox setup for mutex ****/
  // Enable mailbox clocking
  SYSCON->AHBCLKCTRLX[0] |=  SYSCON_AHBCLKCTRL0_MAILBOX_MASK;

  while(1)
  {
    /**** LED blinking ****/
    while (!(MAILBOX->MUTEX & MAILBOX_MUTEX_EX_MASK)); //  Try to acquire mutex
    GPIO->B[LED_GPIO_BLOCK][LED_GPIO_PIN] = 0;         // Turn led On

    Delay();

    GPIO->B[LED_GPIO_BLOCK][LED_GPIO_PIN] = 1;  // Turn led off
    MAILBOX->MUTEX = MAILBOX_MUTEX_EX_MASK;     // Release mutex

    Delay();
  }
}

Весь проект с историей изменений можно посмотреть на GitHub.

При подготовке данной статьи использовались следующие документы:


© Habrahabr.ru