Добрый день! Хотелось бы поговорить на тему архитектуры embedded приложений. К сожалению, книг по этой теме очень мало, а в связи с тем, что, в последнее время, интерес к embedded и IoT растет, хочется уделить внимание этому вопросу. В этой статье, я бы хотел описать один из возможных вариантов того, как можно проектировать такие приложения.
Вопрос этот дискуссионный! Поэтому предлагают поделиться своим виденьем в комментариях!
Для начала определимся с областью: в рамках данной статьи, под embedded разработкой будем понимать разработку ПО под микроконтроллеры (далее МК, напр. STM32) на языке C / Asm.
Проекты для систем на базе МК условно можно разделить на требующие и не требующие многозадачности. Что касается решений первого типа, они, как правило, не очень сложные (со структурной точки зрения). Например, простой проект, в рамках которого необходимо считывать данные с датчика и показывать их на экране, не требует многозадачности, здесь достаточно реализовать последовательное выполнение перечисленных операций.
Если же приложение более сложное: в рамках которого необходимо считывать данные как с цифровых датчиков, так и с аналоговых, сохранять полученные значения в память (например, на sd-карту), обслуживать пользовательский интерфейс (дисплей + клавиатура), предоставлять доступ к данным через цифровой интерфейс (например, RS-485 / Modbus или Ethernet / TCP/IP) и максимально быстро реагировать на определенные события в системе (нажатие аварийных кнопок и т.п.), то в этом случае будет тяжело обойтись без многозадачности. Существует два способа решения вопроса многозадачности: реализовывать ее самому, либо воспользоваться какой-то операционной системой (далее ОС). На сегодняшний день, одной из самых популярных ОС реального времени для встраиваемых систем является FreeRTOS.
Попробуем представить, как должна выглядеть архитектура “сложного” embedded приложения, выполняющего достаточно большое количество разнородных операций. Я допускаю, что можно предложить еще более сложный вариант, который предполагает решение вопросов обработки звука, криптографию и т.п., но остановимся на варианте, который был описан чуть выше.
Поставим задачу более четко, пусть в рамках нашего приложения необходимо:
- Считывать данные с датчиков на шине RS-485/Modbus.
- Считывать данные с датчиков на шине I2C.
- Считывать данные с дискретных входов.
- Управлять релейным выходом.
- Обслуживать пользовательский интерфейс (дисплей + клавиатура).
- Предоставлять доступ к данным по шине RS-485/Modbus.
- Сохранять данные на внешний носитель.
Т.к. нам необходимо реализовать достаточно большое количество различных подзадач, в качестве базы будем использовать операционную систему реального времени (например, уже указанный выше FreeRTOS). Потоки в ОС иногда будем называть задачами — по аналогии с FreeRTOS. Сразу хочу предупредить: исходного кода в статье не будет, интересен именно архитектурный аспект данного вопроса.
Если проанализировать задачу, то можно увидеть, что разные компоненты системы используют одни и те же данные. Например: данные с датчиков необходимо получить, отобразить на экране, записать на носитель и предоставить внешним системам для считывания. Это наводит на мысль, что нужна какая-то база данных реального времени (RTDB) для хранения и для предоставления самых актуальных данных различным подсистемам.
Задачи, выполняющиеся в системе (считывание данных, запись, отображение и т.п.), могут иметь различные требования к частоте их вызова. Нет смысла обновлять данные на дисплее с частотой 1 раз в 100 мс, т.к. для человека это не критично, а вот считывать данные с датчиков (особенно, если необходимо выдавать по ним управляющие воздействия) нужно часто (хотя в зависимости от ТЗ может и нет). Еще один важный момент связан с решением задачи доступа к одним и тем же данным на чтение и запись. Например: поток, опрашивающий датчики записывает полученные значения в RTDB, а в этот момент поток, отвечающий за обновление информации на дисплее, их считывает. Здесь нам помогут механизмы синхронизации, которые предоставляет операционная система.
Начнем проектировать архитектуру нашего приложения!
База данных реального времени
В качестве такой базы может выступать обычная структура, содержащая необходимый набор полей или массив. Для доступа к “RTDB” будем использовать API, который позволит записывать и считывать данные из базы. Синхронизацию доступа к данным внутри функций API можно построить на мьютексах, предоставляемых ОС (либо использовать какой-то другой механизм).
Работа с датчиками на шинах
Работа с датчиками предполагает следующее:
- считывание данных;
- обработка данных (если это необходимо), которая включает:
- проверку на достоверность;
- масштабирование;
- фильтрацию;
- проверку на допустимые значения;
- запись полученных данных в RTDB.
Всю эту работу можно сделать в рамках одной задачи.
“Port” — реальный порт МК;
“Protocol driver” — драйвер протокола (например, Modbus). К такому драйверу желательно сделать свой интерфейс и работать через него. В рамках такого интерфейса можно реализовать управление доступом к ресурсу через мьютексы, так как это было сделано для “RTDB”. Некоторые разработчики предлагают это делать на уровне порта, чтобы быть уверенным в том, что никто другой в этот порт записывать ничего не будет, пока мы через него передаем свои Modbus пакеты.
“Sensor reader” — задача (task), которая опрашивает датчики, приводит в порядок полученную информацию и записывает ее в ”RTDB”.
“RTDB” — база данных реального времени, описанная выше, в соответствующем разделе.
Надпись “Pr: 1” над задачей означает приоритет, суть в том, что у каждой задачи может быть приоритет, если у двух задач, ожидающих процессорное время, разный приоритет, ресурс получит та, у которой приоритет выше. Если у задач приоритет одинаковый, то запустится та, у которой дольше время ожидания.
Работа с дискретными входами
В общем случае работу с дискретными входами можно организовать точно также как и с цифровыми датчиками. Но может возникнуть необходимость в быстром реагировании на изменение состояния входов. Например, по нажатию кнопки как можно быстрее замкать релейный выход. В таком случае, лучше применить следующий подход: для обработки релейного выхода мы создаем специальную отдельную задачу с более высоким приоритетом, чем у остальных. Внутри этой задачи находится семафор, который она пытается захватить. На срабатывание конкретного дискретного входа заводится прерывание, в котором сбрасывается упомянутый выше семафор. Т.к. приоритет прерывания максимальный, то связанная с ним функция выполнится почти мгновенно, в нашем случае, она сбросит семафор, и, после этого, следующей задачей в очереди на выполнение будет как раз та, в рамках которой осуществляется управление реле (т.к. у нее приоритет выше, чем у остальных задач и блокировка по ожиданию семафора снята).
Вот так может выглядеть схема данной подсистемы.
Помимо быстрого срабатывания на изменение состояния конкретного входа, дополнительно можно поставить задачу “DI reader” для считывания состояния дискретных входов. Эта задача может быть как самостоятельной, так и вызываться по таймеру.
Работа “Interrupt handler’а” и “Rele controller’а” в виде диаграмм представлена ниже.
Запись данных на внешний носитель
Запись данных на внешний носитель идеологически очень похожа на чтение данных с цифровых датчиков, только движение данных осуществляется в обратную сторону.
Мы читаем из “RTDB” и записываем через “Store driver” во внешний носитель — это может быть SD карта, USB-флешка или что-нибудь ещё. Опять-таки, не забываем в функции интерфейса помещать мьютекс-обертки (или какие-либо другие инструменты для организации доступа к ресурсу)!
Предоставление доступа к данным реального времени
Важным является момент предоставления данных из “RTDB” для внешних систем. Это могут быть практически любые интерфейсы и протоколы. В отличии от ряда рассмотренных подсистем, ключевым отличием этой является то, что некоторые из протоколов, широко применяемых в системах автоматизации, предъявляют особые требования ко времени ответа на запрос, если ответ не приходит в течении определенного времени, то считается, что с данным устройством нет связи, даже если он (ответ) придет через некоторое время. А т.к. доступ к “RTDB” в нашем примере может быть временно заблокирован (по мьютексу) необходимо предусмотреть защиту внешнего master-устройства (master — это устройство, которое пытается прочитать данные из нашего) от такой блокировки. Также стоит предусмотреть защиту самого устройства от того, что master будет опрашивать его с большой частотой, тем самым тормозя работу системы постоянным чтением из ”RTDB”. Один из вариантов решения — это использовать промежуточный буфер.
“Data updater” читает данные из “RTDB” с заданной периодичностью и складывает, то, что прочитал в “Protocol cache”, из которого “Protocol handler” будет данные забирать. В данном случае возникает проблема блокировки на уровне протокольного кэша, для ее решения можно завести ещё один кэш, в котором “Protocol handler” будет хранить данные на случай, если не смог прочитать из заблокированного “Protocol cache”, дополнительно можно:
— сделать для “Protocol handler” более высокий приоритет;
— увеличить период чтения из “RTDB” для “Data updater” (что так себе решение).
Работа с пользовательским интерфейсом
Работа с пользовательским интерфейсом предполагает обновление данных на экране и работу с клавиатурой. Архитектура этой подсистемы может выглядеть так.
UI worker занимается тем, что считывает нажатия клавиш, забирает данные из “RTDB” и обновляет дисплей, который видит пользователь.
Общая структура системы
Теперь взглянем на то, что получилось в итоге.
Для того чтобы балансировать нагрузку можно ставить дополнительные кэши, так, как мы это сделали в подсистеме, отвечающей за предоставление доступа к данным внешним системам. Часть задач по перебросу данных можно решить с помощью очередей, благо они, как правило, поддерживаются операционными системами реального времени (во FreeRTOS точно).
На этом все, надеюсь было интересно.
P.S.
В качестве литературы я бы посоветовал “Making Embedded Systems: Design Patterns for Great Software” Elecia White и статьи Андрея Курница “FreeRTOS — операционная система для микроконтроллеров”
Автор: marat_ab