Azure RTOS. Часть 1: обзор и запуск (STM32 + CubeIDE + HAL)

На недавно прошедшем Microsoft Build 2020 многократно упоминалась Azure RTOS как специализированная ОС жесткого реального времени для микроконтроллеров.

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

Кому не интересен обзор, а нужна практическая часть — переходите сразу к ней.

Что это вообще такое?

Микроконтроллер – это специализированная микросхема, объединяющая микропроцессор, память и периферийные устройства в одном корпусе. В отличие от "большого" компьютера имеет ограниченные объемы собственно этой самой памяти: типовые значения и для RAM, и для ROM – десятки-сотни килобайт.

Как правило, микроконтроллер не имеет MMU (хотя есть и исключения, но это именно исключения, которые правильнее будет уже отнести к совершенно другой категории систем-на-кристалле), то есть отсутствует аппаратная поддержка механизма виртуальной памяти, что не позволяет использовать "полновесные" ОС даже при расширении объема встроенной памяти внешними микросхемами.

В связи с перечисленным, код под микроконтроллеры разрабатывается особым образом, в специализированных IDE, а операционные системы вообще выделены в особый класс. Основной функцией ОС для микроконтроллера является реализация многозадачности, а бонусом обычно идут разные стеки сети, файловых систем и т.д. Ни о каком окружении и вспомогательных утилитах, как в настольных ОС, здесь речи не идет. Так, например, в ОС для микроконтроллеров нет процессов, есть только задачи = нити = потоки, а сама ОС, как правило, компонуется с пользовательским кодом в единую микропрограмму ("прошивку"). Для понимания особенностей таких ОС рекомендуем статью. Отметим, что в ThreadX, несмотря на прямое отсутствие процессов, есть их аналог — модули.

Впрочем, ограниченные объемы ресурсов никак не мешают использовать микроконтроллеры для решения узкоспециализированных задач. Более того, по меркам микроконтроллера, 128 КБ ROM и 64 КБ RAM – уже довольно внушительные цифры. Микроконтроллер, несмотря на отсутствие "большой" ОС, успешно может записывать файлы на USB флешку, обмениваться данными по сети, а некоторые реализации содержат специальные инструкции для цифровой обработки сигналов, то есть могут решать достаточно "тяжелые" задачи.

Зачем вообще нужна ОС в микроконтроллере, ведь есть альтернативные варианты архитектуры типа "суперцикла"? Вопрос не очень простой и до сих пор вызывающий что-то типа религиозных войн. Достаточно подробный ответ на него дан в этой статье. Если совсем вкратце, то это упрощение кода в целом, что позволяет не только разработать, но и успешно модифицировать уже написанный код. Конечно, есть и минусы в виде потребления ресурсов "железа" на саму ОС и необходимости понимания основ написания потокобезопасного кода.

Все изложенное описывает картину достаточно укрупненно, так как везде есть исключения и оговорки. Например, на микроконтроллерах без MMU можно запустить ucLinux – порт "большого" Linux специально для микроконтроллеров без MMU (без защиты памяти, естественно, со всеми вытекающими последствиями). Как правило, для этого потребуются дополнительные микросхемы памяти, так как встроенной хватит только для загрузчика этой самой ucLinux.

Что есть у Microsoft?

Microsoft традиционно занимается "большими" ОС, среди которых тоже есть специализированные решения в виде Windows 10 IoT Enterprise LTSC, значительно дешевле настольных систем и со специальными возможностями встраивания. Windows 10 IoT Enterprise требует практически полноценного (хоть и промышленного и малогабаритного) компьютера для запуска. Впрочем, есть редакция Windows 10 IoT Core, ориентированная только на приложения UWP, где требования к системе ниже: она успешно запускается на Raspberry Pi 2.

Здесь же нельзя не упомянуть класс операционных систем Windows Embedded Compact, которые могут работать на системах, по вычислительным возможностям находящимся где-то между полноценными компьютерами и микроконтроллерами. Compact – отдельный класс ОС, не совместимых с "настольной" Windows, требующих особых средств разработки. Последний выпуск датируется 2013-м годом, далее ОС развития не получила, но все еще продается и поддерживается, как и несколько предыдущих версий.

С развитием Интернета вещей Microsoft постепенно стал предлагать решения и для систем с ограниченными ресурсами, таких, как микроконтроллеры. Предполагается, что именно на микроконтроллерах будут создаваться именно сами устройства Интернета вещей, отправляющие данные в облако и принимающие команды от него.

Первым таким решением был .Net Micro Framework, который позволял разрабатывать для микроконтроллеров на языке C#. Вводную информацию можно найти в статье, а репозитории проекта – по ссылке. К сожалению, на всех репозиториях стоит метка "Archive", а последние изменения датируются 2018-м годом. .Net Micro Framework достаточно интересен именно реализацией C#, что позволяет применить все преимущества данного языка на таких ограниченных системах, как микроконтроллеры. Реализация C# с его механизмами управления памятью представляет собой значительный "оверхед" для систем с ограниченными ресурсами, хотя из личного опыта — работает достаточно хорошо и надежно (несмотря на часто встречающиеся едкие комментарии к тематическим статьям). Существуют и коммерческие проекты на .Net Micro Framework.

На данный момент доступны и сторонние реализации среды выполнения для C#: https://www.nanoframework.net/, https://www.wildernesslabs.co/. Отметим, что последняя аппаратная платформа вполне подходит и для запуска ucLinux, так что к выбору ОС следует относиться, как к выбору инструмента для решения задачи: что удобнее, то и применяем.

В 2019 году Microsoft поглощает Express Logic, и среди решений для микроконтроллеров от Microsoft появляется Azure RTOS, которая раньше называлась X-WARE IoT Platform. В Azure RTOS входит ядро ThreadX вместе с дополнительными компонентами, а также добавлены средства подключения к Azure IoT Hub и Azure IoT Central. Само название Azure RTOS подчеркивает применение совместно с сервисами Azure для устройств Интернета вещей.

В состав Azure RTOS входят:

  • сама ОС ThreadX, а именно, ядро, планировщик, реализующий многозадачность и синхронизацию задач;
  • стек TCP/IP NetX/NetX Duo;
  • стек FAT FileX;
  • стек USB Host/Device/OTG USBX;
  • реализация графического интерфейса GUI: GUIX и инструмент разработки (GUIX Studio);
  • реализация равномерного износа флеш-памяти для FileX: LevelX;
  • система трассировки событий TraceX;
  • SDK для Azure IoT поверх NetX Duo – готовые средства для подключения устройства к службам Azure.

Нельзя не отметить одно из специализированных решений высокой готовности: Azure Sphere. Это — безопасная платформа для приложений интернета вещей со встроенными механизмами коммуникаций и безопасности. Она представляет собой микроконтроллер (скорее даже SoC) с установленным ядром Linux, а также готовыми облачными сервисом для доставки обновлений безопасности.

Реальное время

Этот термин используется повсеместно, без уточнения, в нашем же случае важно разобраться, что это такое.

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

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

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

  • Все семейство Windows Embedded Compact — ориентировано на промышленные компьютеры;
  • Azure RTOS/ThreadX — ориентирована на микроконтроллеры;
  • Azure Sphere — специализированное решение, в котором операционная система (Azure Sphere OS) не является системой реального времени, но тем не менее предоставляются механизмы запуска пользовательских приложений реального времени.

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

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

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

Что делает ThreadX системой реального времени? Время реакции на внешние события в ThreadX строго определено: поток с высоким приоритетом начинает обработку внешнего события за гарантированное время. Например, время переключение контекста всегда гарантированно меньше 100 циклов.

Таким образом, для микроконтоллеров на данный момент Azure RTOS — единственное решение жесткого реального времени от Microsoft. В принципе, сюда же (с некоторыми оговорками) можно было бы отнести и Azure Sphere, но это уникальный продукт с уникальными возможностями, поэтому его мы обсудим в отдельной статье.

Чем уникальна Azure RTOS

Исследование показывает, что данная ОС является одной из наиболее часто применяемых (более 6 миллионов инсталляций). В основном она используется в специализированном оборудовании, таком, как устройства беспроводной связи, принтеры, модемы, устройства хранения данных, медицинские устройства, интеллектуальные датчики.

Но помимо Azure RTOS, существует большое количество других ОС, выполняющих те же типовые функции. Естественно, возникает вопрос о том, какие уникальные возможности есть именно в этой ОС?

  • Малый размер. Минимальная система занимает 2 КБ ROM. Размер увеличивается автоматически по мере использования возможностей ОС.
  • Поддерживаются различные методы реализации многопоточности, как вытесняющая, так и кооперативная многопоточность.
  • Детерминированное время переключения контекста (меньше 100 циклов), быстрая загрузка (меньше 120 циклов), опциональная проверка ошибок, пикоядро без "слоев".
  • Поддержка большого количества микроконтоллеров и IDE для разработки.
  • Порог вытеснения (Preemption threshold) — порог вытеснения N означает, что данный поток может быть вытеснен только потоками с приоритетом выше N, т.е. от 0 до (N — 1) включительно, а потоки с приоритетом ниже N (т.е. больше N включительно) не могут вытеснять данный поток. Правильное использование данной возможности уменьшает количество переключений контекста, а также уменьшает время реакции на внешние события. Подробную информацию можно найти в статье.
  • Сцепление событий (Event chaining) — позволяет объединить несколько событий в единый сигнал синхронизации для потока, что позволяет синхронизироваться сразу по нескольким событиям, причем в разных комбинациях (И, ИЛИ).
  • Наследование приоритета (Priority inheritance) — позволяет избежать негативных последствий ситуации инверсии приоритетов. Описание ситуации инверсии приоритетов — тема для целой статьи, отдельно с данной проблемой многозадачных систем можно ознакомиться здесь.
  • Оптимизированная обработка прерываний от аппаратных таймеров;
  • Модули (Modules). ThreadX позволяет "обернуть" один или несколько потоков приложения в "модуль", который может быть динамически загружен и запущен на целевом устройстве. Модули позволяют производить обновление "в полях" с целью исправления ошибок. Также при помощи модулей можно разбить микропрограмму на сегменты и динамически определять набор выполняемых потоков, чтобы сэкономить память.
  • Встроенная трассировка событий и аналитика стека. Подбор размера стека потока является одной из самых важных задач при разработке с использованием ОС для микроконтроллера. Нельзя сделать слишком маленький стек, т.к. в отсутствие защиты памяти при переполнении стека — произойдет порча областей памяти других задач. Слишком большой стек также недопустим, т.к. приведет к излишнему расходованию памяти, а она ограничена.

Также рекомендуем интересное сравнение ThreadX с FreeRTOS от инженера, работающего с обеими ОС, а также данную книгу.

Лицензирование

Azure RTOS — коммерческая ОС с соответствующими требованиями к применению в производстве. Однако в ряде случаев платить за ее использование не понадобится.

  • Вам не требуется лицензия, если вы используете код не для производства, а для изучения, разработки, тестирования, портирования, адаптации системы для вашего решения;
  • Лицензия на использование в производстве включена автоматически при развертывании ОС на любой из микроконтроллеров из данного списка. На август 2020 года список еще не заполнен в связи с тем, что процедуры лицензирования еще не завершены, но уже есть соответствующий issue, в котором упомянуты микросхемы Microchip, NXP, Renesas, ST, и Qualcomm;
  • В ином случае вам нужно приобрести платную лицензию.

Во всех случаях ОС поставляется с исходным кодом.

Запуск ThreadX на STM32

Для понимания зависимостей между компонентами Azure RTOS приводим соответствующий рисунок из документации:

 

Как видим, ThreadX является основой для всего остального. Ядро ThreadX обеспечивает многопоточность, переключение контекста, синхронизацию потоков. Поддерживаются таймеры, очереди сообщений, семафоры, мьютексы, флаги событий, пулы байтов и блоков (в каком-то смысле аналог кучи в C++, но с потокобезопасным управлением).

В практической части данной статьи мы будем рассматривать именно ThreadX, чтобы понять, с чего начать работу с ним. И хотя разработчики предоставляют большое количество примеров для готовых средств разработки, интерес представляет именно настройка "с нуля" на каком-то микроконтроллере, для которого нет примера, ведь к этому при разработке своего устройства мы рано или поздно придем.

Будем использовать относительно недорогую и популярную плату STM32F4Discovery, но весь процесс можно с успехом повторить на любом микроконтроллере, например, на сверхдешевом и доступном STM32F103C8T6.

STM32F4Discovery удобна тем, что уже имеет встроенный отладчик ST-Link, большое количество периферии для экспериментов, а все выводы микроконтроллера (STM32F407VGT6) выведены на контакты.

 

Инструменты, которые нам понадобятся

Эксперименты будем проводить на Windows 10 (подойдет также любая, начиная с 7).

Будем также использовать STM32 HAL — набор универсальных API для микроконтроллеров STM32. Есть много мнений и "за", и "против" использования HAL. На наш взгляд, HAL, внося некоторый "оверхед", все же позволяет получить хорошо читаемый и модифицируемый код. HAL не требует скачивания, все необходимые библиотеки будут загружены автоматически при создании проекта.

Скачиваем и устанавливаем STM32CubeIDE — бесплатная IDE от STMicroelectronics на базе открытых инструментов.

Загружаем исходный код ThreadX c GitHub. Существуют, конечно, "правильные" способы использования репозитория с исходным кодом в виде клонирования репозитория или создания форка, но для простоты описания просто скачиваем его как архив: зеленая кнопка "Clone", затем "Download zip". UPD (см. комментарии): для того, чтобы структура исходного кода полностью соответствовала данному руководству, необходимо использовать определенный коммит. Для того, чтобы его получить, сначала установите клиент Git для командной строки, затем откройте командную строку, создайте временную директорию, перейдите в нее и выполните команды:

git clone https://github.com/azure-rtos/threadx.git
git checkout f8e91d4

Затем переименуйте результирующую директорию из threadx в threadx-master.

Теперь подключаем плату STM32F4Discovery через разъем Mini-USB к компьютеру, проверяем наличие устройства "ST Link" в диспетчере устройств. Плата питается по этому же кабелю.

 

Создание и правка проекта

Запускаем STM32CubeIDE. При запуске нас попросят указать директорию для хранения Workspace — можно оставить директорию по умолчанию.

Создаем новый проект, выбрав на главном экране Start New STM32 Project.

 

В появившемся окне в поле 1 набираем STM32F4DISCOVERY, выбираем плату в списке плат 2 (она там будет одна) и нажимаем Next (3):

 

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

 

Далее выбираем "Copy only necessary files..." и нажимаем Finish.

 

На появляющиеся вопросы отвечаем Yes:

 

Открывается окно Project Explorer, в нем находится иерархия файлов проекта. Находим файл threadx_test.ioc и переходим к нему (двойной клик):

 

В открывшемся окне переходим на вкладку Clock Configuration и убеждаемся, что система тактирования настроена следующим образом:

 

Заметим, что SYSCLK = 168 МГц, это нам понадобится далее для настройки таймера SysTick.

Вернемся на закладку Pinout & Configuration, где развернем пункт Connectivity, выберем USB_OTG_FS (1) и выключим его (2), затем в группе выводов (3) всем выводам выставим состояние Reset_State (4).

 

Мы отключаем USB OTG, так как в соответствующем коде используется функция HAL_Delay, которая, выполняя задержку, не позволяет планировщику ОС правильно переключать потоки. Код нужно адаптировать для использования с ThreadX, создав отдельный поток и заменив функцию задержки из HAL на функцию задержки из ThreadX, которая на время задержки передает управление другим потокам. Но для простоты примера мы этого не делаем, а просто отключаем USB OTG.

Аналогично в группе System Core перейдите к SYS и выберите таймер TIM7 в качестве Timebase Source для Serial Wire Debug:

 

SysTick Timer нам понадобится для ядра ThreadX.

Нам также нужно активировать прерывание на выводе PA0, к которому на плате подключена кнопка. Смотрим в User Manual к плате, как подключена кнопка:

 

Соответственно, нам понадобится прерывание по восходящему фронту. Подтягивающий вниз резистор на плате уже есть, встроенная подтяжка не потребуется. Соответствующие настройки делаем в группе System Core — GPIO:

 

В группе System Core — NVIC включаем прерывание EXTI0:

 

 На этой же странице в группе Priority group установите значение "4 bits...":

Важно! В этом же окне выделите строку EXTI line0 interrupt и ниже в поле Preemption priority установите значение 15 (самый низкий приоритет прерывания). Такое изменение связано с тем, что приоритет пользовательских прерываний, из обработчиков которых используются функции RTOS, не должен превышать приоритет, с которым работают служебные обработчики прерывания самой RTOS. В данном случае для простоты мы устанавливаем самый низкий приоритет.

Обратное чревато пропадающими, тяжело отлаживаемыми сбоями. Дополнительно отметим, что настройка служебных по отношению к ThreadX прерываний в данном окне не требуется, так как все необходимые действия выполняются в коде инициализации.

Там же, но на вкладке Code Generation, отключим генерацию кода для Pendable request for system serivice, System tick timer, так как соответствующий код уже есть в порте ThreadX для ядра Cortex-M4, причем обработчики определены уже с нужными именами:

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

Далее копируем полученный нами ранее каталог threadx-master в каталог threadx_test\Middlewares нашего workspace. В Project Explorer нажимаем F5 и видим, что все необходимые файлы появились в дереве:

 

Нажимаем на название проекта threadx_test правой кнопкой мыши и выбираем Properties. Переходим к разделу C/C++ General — Paths and Symbols. Нажимаем кнопку Add и добавляем путь

Middlewares/threadx-master/ports/cortex_m4/gnu/inc

Не забыв установить все флажки, как на рисунке:

 

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

Аналогично добавляем путь

Middlewares/threadx-master/common/inc

Важно! Перейдите на вкладку Source Location и убедитесь, что в поле Source folders on build path присутствуют следующие директории:

  • /threadx_test/Core
  • /threadx_test/Drivers
  • /threadx_test/Middlewares

Если какая-либо из них отсутствует — добавьте ее.

Нажимаем Apply and Close и соглашаемся с перестроением индекса.

Теперь по пути Middlewares/threadx-master/ports в Project Explorer исключим из сборки:

  • весь каталог cortex_m0
  • весь каталог cortex_m3
  • весь каталог cortex_m7
  • cortex_m4/gnu/src/tx_vector_table_sample.S (таблица векторов прерываний уже есть в нашем стартовом коде)

Для этого кликаем по каждому каталогу/файлу правой кнопкой мыши, выбираем Properties и устанавливаем галочку Exclude resource from build:

 

На исключенных таким образом из сборки файлах и директориях появляется значок-перечеркивание.

Также исключите из сборки (или можете просто удалить):

  • Middlewares/threadx-master/cmake
  • Middlewares/threadx-master/docs
  • Middlewares/threadx-master/samples

И убедитесь, что следующие каталоги НЕ исключены из сборки:

  • Middlewares/threadx-master/common
  • Middlewares/threadx-master/ports/cortex_m4

Перейдем к скрипту компоновщика STM32F407VGTX_FLASH.ld и найдем строчки

._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM

и после второй (последней, нижней) строки

. = ALIGN(8);

добавляем строку

__RAM_segment_used_end__ = .;

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

Если планируете запуск из RAM, можете то же самое сделать в файле STM32F407VGTX_RAM.ld.

Теперь в Project Explorer разворачиваем ветку Middlewares/threadx-master/ports/cortex_m4/gnu/src и открываем файл tx_initialize_low_level_sample.S.

Находим строку

SYSTEM_CLOCK = 6000000

и меняем значение 6000000 на 168000000 в соответствии с частотой SYSCLK.

В следующей строке

SYSTICK_CYCLES = ((SYSTEM_CLOCK / 100) -1)

меняем значение 100 на 1000. Тем самым мы изменим частоту системных тиков со 100 до 1000 Гц: удобнее будет задавать задержки для соответствующих функций ThreadX, задержка в тиках будет равна задержке в миллисекундах.

Конкретное значение частоты тиков планировщика подбирается в зависимости от решаемой задачи.

В Core/Src/main.c в начале файла включим

/* USER CODE BEGIN Includes */
#include "tx_api.h"
/* USER CODE END Includes */

а до кода

/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

добавим вызов

tx_kernel_enter();

тем самым передав управление планировщику.

Перейдем к Core/Startup/startup_stm32f407vgtx.s

После

.global Default_Handler

Добавим

.global _vectors

А после

g_pfnVectors:

Также добавим

_vectors:

Тем самым скомпонуем нашу таблицу векторов с кодом ThreadX.

В Core/Src/ создайте файл demo_threadx.c и скопируйте в него код ниже.

#include "tx_api.h"
#include "main.h"

/* Размер стека каждого демо-потока в байтах */
#define DEMO_STACK_SIZE 1024

/* Размер пула памяти, из которого будет выделяться память для демо-потоков, в байтах */
#define DEMO_BYTE_POOL_SIZE 10240

/* Количество демо-потоков */
#define THREAD_COUNT 4

/* Массив структур, каждая из которых хранит информацию о потоке (thread control block) */
TX_THREAD thread_x[THREAD_COUNT];

/* Структура, хранящая информацию о пуле памяти */
TX_BYTE_POOL byte_pool_0;

/* Группа флагов событий */
TX_EVENT_FLAGS_GROUP evt_group;

/* Область памяти для пула TX_BYTE_POOL */
UCHAR memory_area[DEMO_BYTE_POOL_SIZE];

/* Флаг события (маска) для потока N */
#define EVT_KEYPRESS_THREAD(n) (1 << n)

/* Время задержки после переключения режима светодиода */
#define LED_PAUSE_AND_DEBOUNCE_TIME_MS 100

/* Описание светодиодов для каждого потока */
static struct
{
GPIO_TypeDef* GPIOx;
uint16_t GPIO_Pin;
uint32_t blink_delay_ms;
char thread_name[10];
} BoardLedsSettings[THREAD_COUNT] =
{
{ LD3_GPIO_Port, LD3_Pin, 250, "orange" },
{ LD4_GPIO_Port, LD4_Pin, 500, "green" },
{ LD5_GPIO_Port, LD5_Pin, 750, "red" },
{ LD6_GPIO_Port, LD6_Pin, 1000, "blue" }
};

/* Callback внешнего прерывания (кнопки) */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
for (int i = 0; i < THREAD_COUNT; i++) tx_event_flags_set(&evt_group, EVT_KEYPRESS_THREAD(i), TX_OR);
}
}

/* Функция-worker каждого из 4 потоков */
void thread_entry(ULONG thread_input)
{
/* Поток управления одним светодиодом на плате */
uint8_t led_state = GPIO_PIN_RESET;
uint32_t cur_delay_ms = BoardLedsSettings[thread_input].blink_delay_ms;
ULONG actual_flags_ptr;
while(1)
{
HAL_GPIO_WritePin(BoardLedsSettings[thread_input].GPIOx, BoardLedsSettings[thread_input].GPIO_Pin, led_state);
led_state = !led_state;
if (TX_SUCCESS == tx_event_flags_get(&evt_group, EVT_KEYPRESS_THREAD(thread_input),
TX_AND_CLEAR, &actual_flags_ptr, cur_delay_ms))
{
/* Установлен флаг события "кнопка нажата". Выключаем светодиод */
HAL_GPIO_WritePin(BoardLedsSettings[thread_input].GPIOx, BoardLedsSettings[thread_input].GPIO_Pin, GPIO_PIN_RESET);
/* Пауза, что было видно, что светодиод погас */
tx_thread_sleep(LED_PAUSE_AND_DEBOUNCE_TIME_MS);
/* Дополнительно очистим флаг события, на случай, если оно произошло еще раз за время задержки (антидребезг) */
tx_event_flags_get(&evt_group, EVT_KEYPRESS_THREAD(thread_input), TX_AND_CLEAR, &actual_flags_ptr, TX_NO_WAIT);
/* Светодиод будет включен в следующей итерации цикла */
led_state = GPIO_PIN_SET;
/* Изменяем задержку */
cur_delay_ms = (cur_delay_ms == BoardLedsSettings[thread_input].blink_delay_ms) ?
cur_delay_ms * 2 : BoardLedsSettings[thread_input].blink_delay_ms;
}
}
}

/* Инициализация приложения */
void tx_application_define(void *first_unused_memory)
{
CHAR *pointer = TX_NULL;

/* Создаем byte memory pool, из которого будем выделять память для стека каждого потока */
tx_byte_pool_create(&byte_pool_0, "byte pool", memory_area, DEMO_BYTE_POOL_SIZE);

/* Создаем группу событий */
tx_event_flags_create(&evt_group, "event group");

/* Создаем в цикле 4 потока, каждый из которых получает в качестве параметра индекс данных из структуры BoardLedsSettings */
for (int i = 0; i < THREAD_COUNT; i++)
{
/* Выделяем стек для потока i */
tx_byte_allocate(&byte_pool_0, (void **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
/* Создаем поток i */
tx_thread_create(&thread_x[i], BoardLedsSettings[i].thread_name, thread_entry, i, pointer, DEMO_STACK_SIZE,
1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);
}
}

Данный код создает 4 потока, по одному для каждого светодиода, и каждый светодиод мигает со своей периодичностью, причем можно задавать некратные значения задержек. При нажатии на кнопку частота моргания уменьшается в два раза. При повторном нажатии — возвращается к исходному значению. Также реализован примитивный антидребезг.

Выберите Project — Build All и убедитесь, что сборка проекта прошла успешно.

После этого выберите Run — Debug Configurations, кликните правой кнопкой мыши на STM32 Cortex-M C/C++ и выберите New Configuration:

 

Оставьте значения по умолчанию (там выбран ST-LINK уже с нужными параметрами) и нажмите кнопку Debug. Согласитесь с переключением перспективы.

Отладка остановится на строке

HAL_Init();

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

Что происходит в данном примере

Приведенное приложение — классический пример "лампочки и кнопочки" для RTOS. На плате распаяно 4 светодиода, и задача приложения — мигать ими, причем у каждого из них должна быть своя частота этого мигания. Без RTOS это сделать будет достаточно сложно и неудобно (UPD: см. комментарии).

Также на плате имеется кнопка, и ее мы используем для демонстрации обработки внешнего прерывания в RTOS. Очень плохой практикой является обработка непосредственно в обработчике прерывания (наша функция-callback HAL_GPIO_EXTI_Callback() выполняется непосредственно в контексте прерывания), поэтому в самом обработчике мы устанавливаем флаг соответствующего события. В дальнейшем по этому флагу оно будет обработано в потоке.

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

Для каждого из четырех потоков используется один и тот же код потока (thread_entry), который на "вход" в качестве параметра получает индекс светодиода, а соответствующая информация (порт, вывод, время задержки, имя потока) будет получена потоком из соответствующей структуры BoardLedsSettings. Это очень удобно: нам не понадобилось писать по функции для каждого потока, вместо этого мы используем единую функцию, просто передавая ей параметр.

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

Выделенная область стека передается в функцию tx_thread_create() в виде указателя и размера области памяти в байтах. Обратите внимание, что в нашем примере достаточно было просто объявить массив нужной длины и передать указатель на массив в эту функцию, что означало бы статическое выделение памяти для стека. Но мы пошли более сложным путем, чтобы показать, как в ThreadX устроено динамическое управление памятью. Мы статически создали массив для пула байтов (byte_pool_0), создали сам пул в строке

tx_byte_pool_create(&byte_pool_0, "byte pool", memory_area, DEMO_BYTE_POOL_SIZE);

Затем выделили из этого пула память для стека каждого потока в строке

tx_byte_allocate(&byte_pool_0, (void **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);

И передали соответствующий указатель (pointer) в функцию создания потока:

tx_thread_create(&thread_x[i], BoardLedsSettings[i].thread_name, thread_entry, i, pointer, DEMO_STACK_SIZE,
1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);

Обратим внимание на следующее:

  • Поскольку мы выделяли память динамически, мы также можем ее и освободить, например, после уничтожения потока. Память вернется в пул и может быть в дальнейшем использована повторно. В ThreadX уже решена проблема фрагментации возвращенной в пул памяти, поэтому проблем с повторным выделением не будет.
  • Все созданные потоки запускаются автоматически (параметр TX_AUTO_START).
  • Параметр TX_NO_TIME_SLICE отключает механизм time-slice для создаваемого потока. Это означает, что квант времени на исполнение процесса мы не задаем, а вместо этого полагаемся на планировщик.
  • Данный код не подходит для производства, поскольку для упрощения примера не производится анализ возвращенного значения функций на предмет возникновения ошибок.
  • ThreadX достаточно гибко конфигурируется путем применения директив препроцессора. Для упрощения примера мы их не рассматривали. Подробная информация доступна здесь.

Инициализация потоков производится в теле функции tx_application_define(), которая в качестве параметра получает указатель на первый адрес неиспользуемой памяти. Мы также могли использовать этот адрес для организации пула байтов, вместо статического выделения.

Выводы

Мы рассмотрели лишь базовую часть Azure RTOS и (пока) не использовали расширенных функций, а также стеков FileX, NetX и т.д. Это — тема следующих статей.

Мы убедились, что работу с ThreadX можно начать достаточно быстро, буквально в пределах рабочего дня. Microsoft также предлагает руководство пользователя к ОС, а если у вас еще остались вопросы по Azure RTOS или другим встраиваемым операционным системам Microsoft — обращайтесь к нам в Кварта Технологии.


Автор статьи — Сергей Антонович, ведущий инженер Кварта Технологии. Связаться с ним можно по адресу sergant (at) quarta.ru. Статья опубликована в блоге компании на Хабр.

  • Чат с менеджером
  • Отправить запрос