В 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).
Будем считать, что проверка работы алгоритма имитационным моделированием на первой нашей итерации разработки выполнена успешно.
Теперь нужно сгенерировать Си код. Если в собранной модели нет ошибок, то в той же директории файлового браузера (ФБ) Engee, в которой расположена модель, создаётся папка <имя модели>_code (Рисунок 3). В этой папке создаются файлы – шаблон main.c, заголовочный файл <имя модели>.h и файл источника кода <имя модели>.c. Последние два содержат в себе полноценный и независимый код из модели.
Работу нашей модели описывают три функции
-
инициализация
<имя модели>_init()
, -
пошаговый расчёт
<имя модели>_step()
, -
терминация
<имя модели>_term()
.
Их мы и будем вызывать в теле основной программы main.c.
В модели сейчас нет специфических блоков для целевого устройства, которые сконфигурируют его периферию и передадут в неё нашу последовательность. Следовательно, работу с периферией пока что организуем в виде текста, кодом в файле 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.
Тестирование на устройстве первой итерации выполнено, что дальше?
Переносим код периферии МК в Engee
При желании и необходимости мы можем теперь сократить количество кода в main.c, перенеся код для работы с периферией в блоки с кодом Си в модели Engee.
Со временем можно собрать целую библиотеку блоков специально для периферии МИК32 в Engee. Так мы избавим себя от необходимости писать или переносить из прошлых проектов код для работы с периферией.
Чтобы это провернуть, мы воспользуемся классным блоком из библиотеки Engee, он называется C Function. Суть работы с ним проста: мы представляем операции/функцию/набор функций на Си в виде функционального блока, который связывает получаемые набором функций переменные с возвращаемыми. Такие переменные, соответственно, становятся входами и выходами данного блока. Кроме этого, в блоке могут быть реализованы сложные механизмы параметризации, статические рабочие переменные, можно подключить файлы Си с готовым кодом, статические и динамические библиотеки.
Внутри самого блока – 4 вкладки для добавления кода. Три из них соответствуют трём функциям, генерируемым из модели – инициализация, пошаговый расчёт и терминация. Во вкладку инициализации вставляем код для конфигурирования и инициализации периферии, во вкладку пошагового расчёта – функции для циклической работы с периферией. Если в программе требуется ещё и терминация модели, во вкладку терминации C Function можно, например, добавить функции для отключения периферии. Четвертая вкладка блока используется для кода, который будет добавлен в файл сгенерированного кода, но не будет включен ни в одну из функций, получаемых из предыдущих вкладок. Здесь будет удобно, например, объявить глобальные переменные, определить имена структур или функции.
По итогу блок в Engee с кодом цифрового выхода может выглядеть так, как показано на рисунке 5.
Обращу внимание на несколько деталей, о которых я не успел ещё упомянуть.
-
Имя порта блока – это не обязательно имя переменной, используемой в коде.
-
Блок имеет 2 входа: seq – порт, переменная которого во встраиваемом коде не используется. Он применяется только для последовательного соединения блоков в Engee и, таким образом, достоверного определения последовательности вхождения кода из блоков C Function в результирующий код модели.
-
Эта модель работает и в Engee, и на целевой платформе. То есть код блока будет не только встраиваться в сгенерированный код, но и исполняться в Engee. Чтобы использовать добавленный код только для встраивания в код целевой платформы, можно ограничить его конструкцией с условными директивами
#ifdef MIK32V2 ... #else ... #end
. МакросMIK32V2
, как правило, уже определён внутри библиотеки HAL МИК32, и встраиваемый код будет компилироваться в IDE, но не в Engee. -
Во вкладке 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.
Моделирование проходит аналогично предыдущему шагу, так как добавленные блоки не вносят выполняемых операций в процессе моделирования. После успешной генерации кода можно перейти к работе с пользовательской программой. Теперь наш 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).
После генерации кода эта последовательность также автоматически окажется в функции пошагового расчёта модели, а загруженный на контроллер код позволит МИК32 весело приветственно мигать светодиодом (Рисунок 8).
Следующие шаги
Мы обещали обсудить фишки генератора кода. То есть, конечно, следующие шаги состоят в том, чтобы сделать более крутую модель или реализовать настоящее реальное время. Но пока обсудим оптимизацию.
Для улучшения модели и программы, оптимизации их под свои задачи можно воспользоваться следующими возможностями Engee:
-
собрать пользовательскую библиотеку периферийных блоков МИК32;
-
добавить маски периферийных блоков – интерактивные (например, для задания номера порта прямо из маски) или неинтерактивные (просто с красивыми картинками на лицевой стороне блоков);
-
добавить в процесс разработки этап верификации сгенерированного кода;
-
использовать программное управление моделью для автоматизации конфигурирования модели, моделирования, генерации кода и его верификации и прочего;
-
автоматизировать конфигурирование модели при помощи обратных вызовов;
-
подключить специализированные библиотеки Julia реализовать с их помощью автотесты или генерацию части кода ;
-
и многое другое.
Сейчас на платформе и в Cообществе выложены несколько интересных примеров для работы с цифровыми и аналоговыми входами/выходами МИК32. А еще совсем скоро в открытом доступе появится и пример с тестированием нечёткого регулятора (НР) (Рисунки 9, 10).
Резюмируем
В целом, мы прошлись по процессу разработки программ для МИК32 в Engee и рассмотрели некоторые особенности процесса проектирования модели. Этот пример уже может служить отправной точкой для моделирования в Engee вашей модели с последующим встраиванием кода на внешнюю аппаратную платформу, в том числе МИК32.
Давайте пообщаемся в комментариях, а еще можем продолжить диалог вживую (хоть и онлайн) на бесплатном вебинаре от ЦИТМ Экспонента. Уже 3 декабря с 10:00 я буду рад рассказать всем желающим о работе с генератором кода в Engee для МК МIK32V2 и STM32F4.
Спасибо за внимание, до встречи!
И ещё обязательно подписывайтесь на телеграм-канал Engee, чтобы быть в курсе обновлений этой среды.
ссылка на оригинал статьи https://habr.com/ru/articles/862122/
Добавить комментарий