Современные микроконтроллеры имеют достаточно большую производительность и это дает многим программистом возможность думать в примерно следующем ключе: — «Ничего страшного, если 1-5% производительности уйдут на обслуживание операционной системы. Зато мой код будет легко отлаживаемый и явный!». Эти мысли подкрепляются большим количеством энергонезависимой (flash) памяти для хранения кода операционной системы и оперативной (RAM/SRAM) памяти для выделения под каждую задачу своего стека. Однако в большинстве случаев эта мысль ошибочна. И в данной статье я расскажу, почему.
О проектах, с которыми я работаю
В своей практике мне часто приходится работать с «конструктором». Я подробно описывал такой подход в своей предыдущей статье, посвящённой использованию C++ в микроконтроллерах. Тогда я не рассказал самое главное. Большинство «блоков» данного «конструктора» так или иначе завязаны на операционной системе реального времени. Большинство «блоков» имеют по собственному потоку (task, в терминах используемой операционной системы реального времени FreeRTOS). И того, в среднем, на проект приходится порядка 10-15 задач. Иногда это значение доходит до 35-40.
Куда столько?
Вот краткий перечень задач, которые встречаются в каждом проекте:
- обслуживание АЦП (каждый модуль обслуживается своим потоком);
- обслуживание wdt (если ОС упала, то задача его не сбросит и устройство перезагрузится);
- работа со страницами настроек (отдельный поток контролирует работу с flash памятью);
- обслуживание протокола взаимодействия с внешним миром (по потоку на интерфейс. Например, uart);
Далее идут уже специфические штуки для каждого устройства, такие как поток для обслуживания термисторов (получение данных от потока измерения АЦП и преобразование этих данных в температуру), опрос внешней периферии и прочее.
Кажущаяся простота
Несмотря на то, что в проекте много задач, каждая из них «спрятана» внутри объекта соответствующего класса (помним, что конструктор на C++, однако это можно сымитировать и на C, используя «программирование на C в объектно ориентированном стиле». Но лучше не надо). Так как объекты этого «конструктора» глобальны и в проектах используется FreeRTOS 9, поддерживающий создание собственных сущностей в буферы, выделенные пользователем, то использование памяти можно проконтролировать еще на этапе компоновки. Так что с точки зрения контроля утечек памяти — все более или менее нормально. Но есть следующие нюансы:
- требуется четко осознавать, сколько потребуется стека для каждого потока. При этом:
- нужно учитывать критичные случаи (например, вложенность при определенном поведении);
- если используются функции из стандартных библиотек, тогда еще и знать как они устроены, или хотя бы иметь представление о том, сколько они будут потреблять стека;
Не считая этого факта кажется, что использование операционной системы только улучшит логику кода и сделает его понятнее.
Злоупотребление функционалом операционной системы
Основные же проблемы начинаются в тот момент, когда вы начинаете забывать, что пишете именно под микроконтроллер. ОС накладывает свои расходы на работу с собственными сущностями (такими как семафоры, мутексы, очереди). Вот пример UART класса для реализации функции терминала. В прерывании идет прием байта, после чего, в случае, если он проходит диапазон на допустимые входные символы, он добавляется в очередь с соответствующими заменами (например 'n' меняется на последовательность "nr"). Так было сделано для того, чтобы обезопасить порт на отправку (поскольку порт может работать не только как терминал. Через него могут отправляться еще и log-данные). С одной стороны это дает гарантию того, что ответ будет отправлен при первой же возможности и не будет мешать отправке более приоритетных данных (к тому же пока отправляются более приоритетные данные — идет накопление в буфер, что позволяет потом задействовать DMA для отправки ответа). Однако уже начиная с этого момента вы встаете на скользкую дорожку. Вместо того, чтобы писать связку через очередь, можно было бы просто верно настроить прерывание по непустому буферу, не работающему в данный момент UART-у и окончании работы DMA. Такой подход требует чёткого понимания того, как работает периферия. Однако снижает издержки до абсолютного минимума, делая надобность в подобном решении нулевой.
Игнорирование аппаратного функционала микроконтроллера
В своей практике я встречал проект с 18-ю программными таймерами операционной системы, настроенными на одну и ту же частоту. При этом в микроконтроллере было порядка 10 таймеров, из которых использовался только systic. Для тактирования планировщика операционной системы. Такое решение объяснялось отсутствием желания «возиться с аппаратной частью» микроконтроллера. При этом под стек для функции, вызываемой программным таймером отводилось порядка 10 кб. На деле же использовалось порядка 1 кб (не доходя). Это объяснялось «неясностью происходящего внутри вызываемых библиотек».
В данном случае можно было смело выделить TIM6 (в случае использования stm32f4), который бы генерировал прерывание с заданной частотой и внутри просто вызывались бы требуемые функции.
Использование бесконечного цикла вместо конечного автомата
Отдельной графой я бы выделил неумение некоторых программистов писать компактные конечные автоматы, а вместо этого создавать поток, в котором находится бесконечный цикл, начинающий свою работу с получения чего-то из очереди. О том, как создавать компактные конечные автоматы средствами самого языка интересно написано в этой статье.
Игнорирование «аппаратного планировщика»
Во многих тридцати двух битных микроконтроллерах есть продуманный контроллер прерываний с настраиваемой системой приоритетов. В случае stm32f4 он имеет название NVIC, и имеет возможность настраивать приоритеты прерываний с 16 уровнями (не рассматривая еще и подуровни).
Большинство приложений под FreeRTOS, с которыми мне пришлось столкнуться могли бы быть написаны как конечные автоматы, вызываемые в прерываниях с верно настроенными приоритетами. А в случае возвращения процессора к «нормальному выполнению» — отправляться «спать». При этом отпала бы необходимость в блокировании доступа к большинству ресурсов (переменным и прочему). Приложения лишились бы лишнего уровня абстракции. Причем в данном случае — далеко не бесплатного. Однако такой подход требует вдумчивого планирования архитектуры для каждого проекта. В проектах «конструкторах» — все прерывания имеют один приоритет и, по сути, нужны для того, чтобы «отфильтровать» данные. После чего положить остатки в очередь, откуда их заберет поток объекта соответствующего класса.
Итоги
В данной статье я рассказал о базовых проблемах, с которыми приходится сталкиваться при использовании операционной системы в проектах под микроконтроллеры, а так же рассмотрел часто встречающиеся случаи применения операционной системы, когда этого можно было бы избежать, не потеряв при этом в читаемости и логичности кода.
Автор: Вадим