Как мы МИК32 «Амур» подружили с Engee

от автора

В 2024 году в продаже появился первый российский микроконтроллер с RISC-V архитектурой – МИК32 Амур (К1948ВК018). Наша команда не могла пройти мимо такой новинки, учитывая интерес профессиональной общественности к RISC-V. Мы поучаствовали и в программе раннего доступа к RISC-V на отладочной плате MIK32 Nuke, и в техническом тренинге от АО «Микрон», чтобы в контакте с производителем наладить программирование контроллера кодом, сгенерированным из среды модельно-ориентированного проектирования Engee.

Меня зовут Алексей Евсеев, я инженер Экспоненты, и я хочу поделиться с вами опытом разработки моделей в Engee для МИК32, показать наш типовой workflow, а также осветить некоторые фишки и особенности работы с генератором кода Engee. Надеюсь, материал будет интересен и разработчикам встраиваемого ПО, и специалистам в моделировании.

Общие принципы разработки

В парадигме модельно-ориентированного проектирования (МОП) разработка ведётся над одной обобщенной моделью – «единым источником правды», объединяющего модели конечного устройства, процесса и алгоритма.

В первую очередь, мы упрощаем задачу, «декомпозируем» её на функциональные и архитектурные части, далее итеративно наращиваем нашу модель, тестируя её на каждом шаге. В процесс тестирования мы стараемся включить необходимый минимум – имитационное моделирование и проверку работы кода, полученного из модели, на целевом устройстве. Если все сделать правильно, то автоматическое тестирование кода должно происходить уже при генерации Си кода. Однако здесь наше участие минимально – в Engee часть проверок происходит по одному нажатию на кнопку «Сгенерировать Си код».

При необходимости в процесс тестирования можно включить и верификацию кода в модельном окружении, но на целевом процессоре (PIL-тестирование), и отладку в режиме реального времени с оператором (HIL-тестирование). Последняя, нужно заметить, в Engee доступна (пока что) только для комплекса полунатурного моделирования КПМ РИТМ. Теперь пойдём по порядку выполнения шагов.

Программирование по-русски

(+ немного профессионального жаргона)

Рассмотрим простейшую модель (Рисунок 1): блок Repeating Sequence Stair циклически передаёт заданную последовательность нулей и единиц на выходной порт. Выбранный в модели решатель – дискретный с фиксированным шагом, длительность шага циклического расчёта модели – 100 мс. Результат работы модели – наблюдаемый график изменения переменной signal (Рисунок 2).

Рисунок 1. Модель Engee для циклического формирования нулей и единиц

Рисунок 1. Модель Engee для циклического формирования нулей и единиц
Рисунок 1. Модель Engee для циклического формирования нулей и единиц

Рисунок 2. График циклического формирования нулей и единиц

Будем считать, что проверка работы алгоритма имитационным моделированием на первой нашей итерации разработки выполнена успешно.

Теперь нужно сгенерировать Си код. Если в собранной модели нет ошибок, то в той же директории файлового браузера (ФБ) Engee, в которой расположена модель, создаётся папка <имя модели>_code (Рисунок 3). В этой папке создаются файлы – шаблон main.c, заголовочный файл <имя модели>.h и файл источника кода <имя модели>.c. Последние два содержат в себе полноценный и независимый код из модели. 

Работу нашей модели описывают три функции

  • инициализация <имя модели>_init(),

  • пошаговый расчёт <имя модели>_step() ,

  • терминация <имя модели>_term().

Их мы и будем вызывать в теле основной программы main.c.

Рисунок 3. Автоматическая генерация кода из модели

Рисунок 3. Автоматическая генерация кода из модели

В модели сейчас нет специфических блоков для целевого устройства, которые сконфигурируют его периферию и передадут в неё нашу последовательность. Следовательно, работу с периферией пока что организуем в виде текста, кодом в файле main.c. Вот пример работы с цифровым выходом (мигающий светодиод), основанный на коде, который дают производители МИК32:

#include "stdint.h" #include "mik32_hal_pcc.h" #include "mik32_hal_gpio.h"  const uint32_t tickInMs = 3200; // константа числа тактов в миллисекунде  void delay(uint16_t ms);        // прототип функции задержки  int main()                      // основная программа {   // Настройка подсистемы тактирования и монитора частоты МК   PCC_InitTypeDef PCC_OscInit = {0};   PCC_OscInit.OscillatorEnable = PCC_OSCILLATORTYPE_ALL;   PCC_OscInit.FreqMon.OscillatorSystem = PCC_OSCILLATORTYPE_OSC32M;   PCC_OscInit.FreqMon.ForceOscSys = PCC_FORCE_OSC_SYS_UNFIXED;   PCC_OscInit.FreqMon.Force32KClk = PCC_FREQ_MONITOR_SOURCE_OSC32K;   PCC_OscInit.AHBDivider = 0;   PCC_OscInit.APBMDivider = 0;   PCC_OscInit.APBPDivider = 0;   PCC_OscInit.HSI32MCalibrationValue = 128;   PCC_OscInit.LSI32KCalibrationValue = 128;   PCC_OscInit.RTCClockSelection = PCC_RTC_CLOCK_SOURCE_AUTO;   PCC_OscInit.RTCClockCPUSelection = PCC_CPU_RTC_CLOCK_SOURCE_OSC32K;    HAL_PCC_Config(&PCC_OscInit);   GPIO_InitTypeDef GPIO_InitStruct = {0};   GPIO_InitStruct.Pull = HAL_GPIO_PULL_NONE;   GPIO_InitStruct.Mode = HAL_GPIO_MODE_GPIO_OUTPUT;    // включение тактирования GPIO_0   __HAL_PCC_GPIO_0_CLK_ENABLE();   GPIO_InitStruct.Pin = GPIO_PIN_9;   HAL_GPIO_Init(GPIO_0, &GPIO_InitStruct);    uint8_t state = true; // объявление переменной состояния светодиода    while (1)   // бесконечный цикл   {     HAL_GPIO_WritePin(GPIO_0, GPIO_PIN_9, state); // вкл/выкл светодиода    state=!state;// инвертируем переменную состояния светодиода    delay(500);// формируем задержку 500 мс   } }  void delay(uint16_t ms) // функция задержки в миллисекундах {          for (volatile uint32_t i = 0; i < (tickInMs * ms); i++); }

Итог работы этой программы – мигание встроенного светодиода МИК32 с частотой ~1 Гц.

 Модифицируем этот пример, встраивая сгенерированный из Engee код. Для этого:

  • подключим сгенерированный заголовочный файл: #include "mik32_test.h"

  • перед бесконечным циклом инициализируем модель: mik32_test_init();

  • сам же бесконечный цикл перепишем таким образом:

while (1)                       // бесконечный цикл {   mik32_test_step();   HAL_GPIO_WritePin(GPIO_0, GPIO_PIN_9, mik32_test_Y.Out1);   delay(100); }

Здесь mik32_test_Y.Out1 – переменная, несущая результат вычисления нашей модели, который оказывается в выходном порте (блок Out1). В неё в итоге передаётся текущее состояние на выходе блока-формирователя повторяющейся последовательности.

Вызывая delay(100);, мы имитируем шаг расчёта модели. Почему имитируем? Ну, потому, что цикл выполнения программы здесь будет явно больше шага моделирования. Так что для лучшего соответствия программы и модели стоит рассчитывать и контролировать внутри цикла его длительность, а в идеале – вообще запустить FreeRTOS и включить функцию расчёта шага модели в таск.

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

Сгенерированный код включён в основную программу, а значит, осталось собрать проект и загрузить его в МК. В Engee пока нет готовых решений для полноценной коммуникации с внешними устройствами. Поэтому сгенерированный код и основную программу можно, например, добавить в проект во внешней IDE. Рекомендованная для МИК32 среда разработки – MikronIDE на базе Eclipse. Кроме того, в свободном доступе находится пакет поддержки МИК32 для VSCode+PlatformIO. Последним мы и воспользуемся в этом примере.

После сборки проекта, компиляции и загрузки кода в МК можно наблюдать мигание светодиода по смоделированной ранее последовательности. На рисунке 4 показана осциллограмма на цифровом пине 9.

Рисунок 4. Осциллограмма того, что и требовалось сделать

Рисунок 4. Осциллограмма того, что и требовалось сделать

 Тестирование на устройстве первой итерации выполнено, что дальше?

Переносим код периферии МК в Engee

При желании и необходимости мы можем теперь сократить количество кода в main.c, перенеся код для работы с периферией в блоки с кодом Си в модели Engee.

Со временем можно собрать целую библиотеку блоков специально для периферии МИК32 в Engee. Так мы избавим себя от необходимости писать или переносить из прошлых проектов код для работы с периферией.

Чтобы это провернуть, мы воспользуемся классным блоком из библиотеки Engee, он называется C Function. Суть работы с ним проста: мы представляем операции/функцию/набор функций на Си в виде функционального блока, который связывает получаемые набором функций переменные с возвращаемыми. Такие переменные, соответственно, становятся входами и выходами данного блока. Кроме этого, в блоке могут быть реализованы сложные механизмы параметризации, статические рабочие переменные, можно подключить файлы Си с готовым кодом, статические и динамические библиотеки.

Внутри самого блока – 4 вкладки для добавления кода. Три из них соответствуют трём функциям, генерируемым из модели – инициализация, пошаговый расчёт и терминация. Во вкладку инициализации вставляем код для конфигурирования и инициализации периферии, во вкладку пошагового расчёта – функции для циклической работы с периферией. Если в программе требуется ещё и терминация модели, во вкладку терминации C Function можно, например, добавить функции для отключения периферии. Четвертая вкладка блока используется для кода, который будет добавлен в файл сгенерированного кода, но не будет включен ни в одну из функций, получаемых из предыдущих вкладок. Здесь будет удобно, например, объявить глобальные переменные, определить имена структур или функции.

По итогу блок в Engee с кодом цифрового выхода может выглядеть так, как показано на рисунке 5.

Рисунок 5. Вкладки блока C Function для цифрового выхода МИК32. Слева направо: настройка портов блока, подключение файлов библиотеки HAL МИК32, вкладка Start code, вкладка Output code

Рисунок 5. Вкладки блока C Function для цифрового выхода МИК32. Слева направо: настройка портов блока, подключение файлов библиотеки HAL МИК32, вкладка Start code, вкладка Output code

Обращу внимание на несколько деталей, о которых я не успел ещё упомянуть.

  1. Имя порта блока – это не обязательно имя переменной, используемой в коде.

  2. Блок имеет 2 входа: seq – порт, переменная которого во встраиваемом коде не используется. Он применяется только для последовательного соединения блоков в Engee и, таким образом, достоверного определения последовательности вхождения кода из блоков C Function в результирующий код модели.

  3. Эта модель работает и в Engee, и на целевой платформе. То есть код блока будет не только встраиваться в сгенерированный код, но и исполняться в Engee. Чтобы использовать добавленный код только для встраивания в код целевой платформы, можно ограничить его конструкцией с условными директивами #ifdef MIK32V2 ... #else ... #end. Макрос MIK32V2, как правило, уже определён внутри библиотеки HAL МИК32, и встраиваемый код будет компилироваться в IDE, но не в Engee.

  4. Во вкладке Build options нужно указать путь расположения в ФБ Engee и имена заголовочных файлов из библиотеки HAL МИК32 для работы с данной периферией, а также имена заголовочных файлов используемой стандартной библиотеки. Это позволяет нам в результирующем коде получить такие строки:

#include "stdint.h" #include "math.h" #include "mik32_hal_pcc.h" #include "mik32_hal_gpio.h"

Итак, собрав функции для работы с GPIO в один блок, а для инициализации библиотеки HAL МИК32, настройки подсистемы тактирования и монитора частоты МК – в другой, мы получим модель, как на рисунке 6.

Рисунок 6. Модель Engee  с периферийными блоками

Рисунок 6. Модель Engee с периферийными блоками

Моделирование проходит аналогично предыдущему шагу, так как добавленные блоки не вносят выполняемых операций в процессе моделирования. После успешной генерации кода можно перейти к работе с пользовательской программой. Теперь наш main.c будет выглядеть так:

#include "mik32_test.h" // подключение сгенерированного файла  int main()                          // основная программа {     mik32_test_init();     // функция инициализации модели     while (1)                       // бесконечный цикл     {         mik32_test_step(); // функция пошагового расчёта модели         delay(100);     // «шаг» расчёта модели     } }

Такое сокращение main.c позволяет нам масштабировать модель, не возвращаясь больше к редактированию пользовательской программы в сторонней IDE. Результат работы программы на этой итерации аналогичен предыдущему, как на рисунке 4.

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

Автоматизируем расчёт последовательности

Следующая небольшая ознакомительная задача – задать более сложную, например, кодирующую последовательность. Мы не будем вручную вписывать нули и единицы в блок Repeating Sequence Stair. В него вместо последовательности мы впишем переменную Код_Морзе. В эту переменную далее передадим последовательность, которую сейчас с вами сформируем.

Для технических расчётов в Engee есть несколько удобных инструментов – терминал, редактор скриптов, область переменных и область функций, обратные вызовы моделей. Язык среды вычислений – Julia, очень удобный и понятный в том числе, и для пользователей MATLAB и Python. Кстати говоря, в Julia можно встраивать вычисления на других языках – С, Python, MATLAB и некоторых других. Но вернёмся к нашей задаче.

Закодируем приветствие для радиопередачи «ЗДР, ENGEE! «. Принципы кодирования описаны в примере из сообщества Engee . Следующая часть кода на Julia описывает функцию посимвольного кодирования сообщения по азбуке Морзе.

Символы = collect("ЗДР, ENGEE! ") Код_Морзе = (Int64)[];  for Итерация in 1:size(Символы, 1)       if (Символы[Итерация] == 'D')||(Символы[Итерация] == 'Д')             Код_символа = [1,1,1,0,1,0,1,0,0,0];       elseif (Символы[Итерация] == 'E')||(Символы[Итерация] == 'Е')             Код_символа = [1,0,0,0];       elseif (Символы[Итерация] == 'G')||(Символы[Итерация] == 'Г')             Код_символа = [1,1,1,0,1,1,1,0,1,0,0,0];       elseif (Символы[Итерация] == 'N')||(Символы[Итерация] == 'Н')             Код_символа = [1,1,1,0,1,0,0,0];       elseif (Символы[Итерация] == 'R')||(Символы[Итерация] == 'Р')             Код_символа = [1,0,1,1,1,0,1,0,0,0];       elseif (Символы[Итерация] == 'Z')||(Символы[Итерация] == 'З')             Код_символа = [1,1,1,0,1,1,1,0,1,0,1,0,0,0];       elseif Символы[Итерация] == ' '             Код_символа = [0,0,0,0];       elseif Символы[Итерация] == ','             Код_символа = [1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,1,1,0,0,0];       elseif Символы[Итерация] == '!'             Код_символа = [1,1,1,0,1,1,1,0,1,0,1,0,1,1,1,0,1,1,1,0,0,0];       end   Код_Морзе = vcat(Код_Морзе, Код_символа); end

Выполнив этот код в Engee, мы получим вектор из 116 значений. После запуска модели с новой последовательностью можно увидеть желаемую последовательность из нулей и единиц (Рисунок 7).

Рисунок 7. График закодированного сообщения

Рисунок 7. График закодированного сообщения

После генерации кода эта последовательность также автоматически окажется в функции пошагового расчёта модели, а загруженный на контроллер код позволит МИК32 весело приветственно мигать светодиодом (Рисунок 8).

Рисунок 8. МИК32 явно хочет нам что-то сказать

Рисунок 8. МИК32 явно хочет нам что-то сказать

Следующие шаги

Мы обещали обсудить фишки генератора кода. То есть, конечно, следующие шаги состоят в том, чтобы сделать более крутую модель или реализовать настоящее реальное время. Но пока обсудим оптимизацию.

Для улучшения модели и программы, оптимизации их под свои задачи можно воспользоваться следующими возможностями Engee:

  • собрать пользовательскую библиотеку периферийных блоков МИК32;

  • добавить маски периферийных блоков – интерактивные (например, для задания номера порта прямо из маски) или неинтерактивные (просто с красивыми картинками на лицевой стороне блоков);

  • добавить в процесс разработки этап верификации сгенерированного кода;

  • использовать программное управление моделью для автоматизации конфигурирования модели, моделирования, генерации кода и его верификации и прочего;

  • автоматизировать конфигурирование модели при помощи обратных вызовов;

  • подключить специализированные библиотеки Julia реализовать с их помощью автотесты или генерацию части кода ;

  • и многое другое.

Сейчас на платформе и в Cообществе выложены несколько интересных примеров для работы с цифровыми и аналоговыми входами/выходами МИК32. А еще совсем скоро в открытом доступе появится и пример с тестированием нечёткого регулятора (НР) (Рисунки 9, 10).

Рисунок 9. Модель Engee и график тестирования НР

Рисунок 9. Модель Engee и график тестирования НР
Рисунок 10. Осциллограмма на аналоговом выходе МИК32 в ходе тестирования НР

Рисунок 10. Осциллограмма на аналоговом выходе МИК32 в ходе тестирования НР

Резюмируем

В целом, мы прошлись по процессу разработки программ для МИК32 в Engee и рассмотрели некоторые особенности процесса проектирования модели. Этот пример уже может служить отправной точкой для моделирования в Engee вашей модели с последующим встраиванием кода на внешнюю аппаратную платформу, в том числе МИК32.

Давайте пообщаемся в комментариях, а еще  можем продолжить диалог вживую (хоть и онлайн) на бесплатном вебинаре от ЦИТМ Экспонента. Уже 3 декабря с 10:00 я буду рад рассказать всем желающим о работе с генератором кода в Engee для МК МIK32V2 и STM32F4.

Спасибо за внимание, до встречи! 

И ещё обязательно подписывайтесь на телеграм-канал Engee, чтобы быть в курсе обновлений этой среды.


ссылка на оригинал статьи https://habr.com/ru/articles/862122/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *