Embedded software что это
Разработка встроенного ПО: введение
Привет, Хабр! Представляю вашему вниманию перевод статей Chris Svec, оригинал здесь. Публикуется с разрешения автора по лицензии CC-A-NC-ND.
Embedded software engineering 101: введение
Я запускаю цикл статей по обучению разработке встроенного программного обеспечения. Мы начнем с описания простого микроконтроллера, и после того, как Вы поймете, как он работает, разовьем это до понимания работы относительно сложных систем, таких как Fitbit или Nest.
Я назвал эту серию Embedded Software Engineering 101, и она начинается с этой недели прямо здесь в этом блоге.
Продолжайте читать для дальнейших объяснений и подробностей.
Одни строительные блоки на других строительных блоках.
Я работаю со встроенными системами и разрабатываю микросхемы уже более 14 лет. Мне нравятся встроенные системы — железо, софт и ограничения, которые связывают их вместе.
Любительская электроника и такие идеи, как Arduino, Adafruit и Sparkfun дали возможность легко накидать что-то из железа и софта за выходные (или месяц, или семестр), создав что новое, интересное и может быть даже полезное.
Это здорово! Предоставление людям возможности созидать — изумительная штука; если бы я хотел выражаться выспренно, то с придыханием назвал бы это «демократизирующей технологией».
Большая часть любительских проектов единовременные. Вы собираете нечто, делаете это настолько хорошим, насколько хватает времени или энергии, и двигаетесь дальше.
Я провел свою карьеру на противоположном конце спектра — создавая продукцию, которая выпускается в сотнях тысяч или миллионах или больше экземпляров — и это требует совсем другого образа мышления и системного подхода.
Я хочу учить людей, как писать встроенное ПО для такого рода систем. Я уже давно вынашивал эту идею курса/руководства/книги/блога «Embedded Software Engineering 101», и благодаря блогу Embedded.fm начинаю ее реализацию сейчас.
Я человек фундаментального типа, так что мой план — начать с основ, с простого описания простого микропроцессора, и развивать эту основу, пока вы не поймете, как работает относительно сложная встроенная система.
Моя цель — чтобы к концу этого цикла вы могли разобраться как работает Fitbit, термостат Nest или подобная встроенная система. Вы сможете начать работать со встроенными программными системами используя профессиональный опыт.
Embedded Software Engineering 101 предназначен для:
Так вот, я не Фейнман, но я уверен, что лучший способ понять систему — это начать с основ. Вооруженные этим пониманием, вы сможете создавать простые встроенные системы с простым софтом. И поняв сначала очень простую программу, вы сможете развивать это, создавая более сложное ПО по мере роста опыта.
Основы в первую очередь — это конечно только мое личное убеждение. Множество людей сделали полезные штуки с Ардуино без понимания чего бы то ни было из основ. Этот цикл статей для тех, кто все-таки хочет понимать основы и все, что на них построено.
Конечно мы должны задаться вопросом — где правильный уровень чтобы начать с этих самых «основ»? Транзисторы и логические вентили? Нет, это слишком низкий уровень для старта со встроенным ПО. Подключение к распространенным датчикам? Нет, это слишком высокий уровень, требуется слишком много знаний чтобы начать с этого.
Я думаю правильный уровень основ это встроенный микропроцессор. Не обязательно понимать физику или электронику чтобы использовать встроенный микропроцессор, также не обязательно быть экспертом в программировании.
Так что с этого мы и начнем в следующей статье.
Предупреждение о предвзятости: в прошлой жизни я был архитектором/разработчиком процессоров. Начать этот цикл с понимания как работает ЦПУ может быть не лучшим способом для понимания встроенных систем, но именно так работает мой мозг. Обязательно попробуйте другие курсы/руководства и т.д., если не станете понимать этот после нескольких статей.
Embedded software engineering 101: основы микроконтроллера
Мы начнем наше путешествие Embedded Software Egineering 101 со скромного микроконтроллера. Микроконтроллер (или микропроцессор) это основной строительный блок всех вычислительных систем, встроенных и прочих.
МК кажется довольно сложным, но он состоит из трех простых вещей: инструкции, регистры и память. Инструкции это те штуки, которые микроконтроллер знает как выполнять. Простой МК умеет выполнять не так уж много — у него может быть например 20 или 30 инструкций. В дальнейшем в этом цикле я буду использовать микроконтроллер MSP430 от Texas Instruments, у которого только 27 инструкций.
Просто фотография МК (TI MSP430F5529)
Эти 27 инструкций — единственное, что MSP430 умеет делать. Он может сложить два числа, вычесть из одного числа другое, переместить числа с одного места в другое или выполнить 24 другие простые операции. 27 операций может показаться недостаточно чтобы сделать что-либо полезное, но на самом деле их хватит с избытком, чтобы выполнить любую мыслимую программу.
Хорошо, значит у микроконтроллера есть инструкции, которые делают что-то с числами. Но где находятся эти числа? Регистры и память! Инструкции оперируют числами, которые хранятся в регистрах и памяти.
Регистры это очень быстрое хранилище, содержащее числа, которыми оперируют инструкции. Можно думать о них, как об используемом инструкциями блокноте. МК содержит немного регистров, обычно 8-32. Например, у MSP430 16 регистров.
Память это тоже хранилище для чисел, но она гораздо объемнее и медленнее чем регистры. У микроконтроллера может быть 64 кБ, 256 кБ или даже более 1 МБ памяти. У MSP430F5529 около 128 кБ памяти; это более чем в 8000 раз превосходит количество его регистров!
Прежде чем мы начнем рассматривать примеры, я призываю вас достать лист бумаги и ручку или карандаш и прорабатывать эти примеры по мере чтения. Прорабатывать их на бумаге сложнее, чем просто читать, что я написал. Таким образом вы внимательнее подойдете к процессу, и шансы на запоминание изученного будут выше.
Давайте рассмотрим вымышленный, но характерный пример микроконтроллера.
Пусть скажем у нашего МК 4 регистра и 8 ячеек памяти. Регистры обычно называют как-нибудь креативно, например «R0», «R1» и т.д., поступим и мы так же. На ячейки памяти обычно ссылаются по их номерам, также называемым адресами памяти, начиная нумерацию с 0. Вот так будут выглядеть наши регистры и память:
И теперь я помещу в них некоторые значения:
Теперь нашему вымышленному микроконтроллеру нужны какие-нибудь инструкции.
Совокупность инструкций, которые знает МК, называется его набором инструкций. Пусть скажем в наборе будет три инструкции: ADD (сложить), SUB (сокращение от «subtract» — вычесть) и MOVE (переместить). Инструкции должны получать откуда-то числа, которыми они оперируют, и также помещать куда-то свои результаты, так что некоторые из них содержат информацию о том, где находятся входные и выходные данные.
Пусть, например, у нашей инструкции ADD два источника и один приемник данных, и все они должны быть регистрами. Руководство может описывать эту инструкцию примерно так:
ADD регИст, регПрм
Инструкция ADD добавляет значение регистра «регИст» к значению регистра «регПрм» и сохраняет результат в регистре «регПрм»
Резюме: регПрм = регИст + регПрм
Пример: ADD R1, R2 выполняет операцию R2 = R1 + R2
Это общепринято в инструкциях — использовать один из источников также в роли приемника, как делает инструкция ADD, используя регПрм в качестве и источника и приемника данных.
«ADD R1, R2» — это язык ассемблер для микроконтроллера, это нативный язык программирования МК.
Давайте определим SUB в том же стиле:
SUB регИст, регПрм
Инструкция SUB вычитает значение регистра «регИст» из значения регистра «регПрм» и сохраняет результат в регистре «регПрм»
Резюме: регПрм = регПрм — регИст
Пример: SUB R3, R0 выполняет операцию R0 = R0 — R3
И наконец пусть у инструкции MOVE один источник и один приемник, и либо:
1. MOVE регИст, регПрм
2. MOVE памИст, регПрм
3. MOVE регИст, памПрм
Инструкция MOVE копирует данные из аргумента Ист в аргумент Прм.
Резюме: есть три типа инструкции MOVE
1. регПрм = регИст
2. регПрм = мемИст
3. мемПрм = регИст
Пример: я покажу примеры инструкции MOVE ниже в этом посте.
Одно замечание о слове «move», используемом для этой инструкции: большая часть наборов инструкций используют именно его, хотя в действительности данные копируются, а не перемещаются.
Название «move» может создать впечатление, что операнд-источник инструкции уничтожается или очищается, но на самом деле он остается в покое, модифицируется только приемник.
Давайте пройдемся по нескольким примерам используя наш вымышленный микроконтроллер.
На старте наши регистры и память выглядят так:
Теперь выполним на МК следующую инструкцию:
Она берет значение R1, складывает его со значением R2 и сохраняет результат в R2. Процессор выполняет большую часть инструкций за одну операцию, но я разобью выполнение каждой инструкции ADD, SUB и MOVE на несколько шагов стрелкой «=>» ведущей через замены (регистр/память => значение):
После выполнения этой инструкции память неизменна, но регистры теперь выглядят следующим образом, с изменившимся значением написанным красным:
Обратите внимание, что R1 неизменен; изменился только регистр-приемник R2.
Следующей давайте попробуем инструкцию SUB:
Она берет значение R3, вычитает его из значения R0, и сохраняет результат в R0:
После выполнения этой инструкции память неизменна, но регистры теперь выглядят таким образом:
И наконец давайте попробуем пару версий инструкции MOVE:
Эта инструкция MOVE копирует в R0 значение R2:
И теперь регистры выглядят так:
Дальше мы скопируем регистр в память:
Эта инструкция MOVE копирует в ячейку памяти 3 значение R3. Квадратными скобками в нашем наборе инструкций обозначаются ячейки памяти.
Регистры неизменны, но память меняется:
И для нашего последнего примера мы скопируем значение из памяти в регистр:
Здесь значение ячейки памяти 6 копируется в регистр R0:
Память неизменна, а регистры теперь выглядят следующим образом:
Верите или нет, но если вы поняли большую часть того, что мы только что обсудили насчёт инструкций, регистров и памяти, то вы понимаете основы микроконтроллеров и языка ассемблер.
Конечно я опустил множество деталей. Например, как МК получает инструкции для выполнения?
Есть ли более интересные инструкции, чем только простые математические и инструкции копирования? Память это то же самое, что RAM или флэш, или нет?
Мы ответим на эти вопросы в следующей статье.
Два подхода к проектированию ПО для embedded
Хочу немного рассказать о двух подходах проектирования ПО в embedded. Два подхода эти – c использованием суперцикла или же с использованием RTOS (Real-Time Operation System, операционная система реального времени).
Думаю, что по ходу рассказа будет понятно также, в каких именно случаях стоит применять первый, а в каких не обойтись без второго.
Надеюсь, будет интересно всем тем, кто хочет заглянуть в мир разработки для встраиваемых систем. Для тех, кто в embedded уже собаку съел, скорее всего, не будет ничего нового.
Совсем немного теории (для тех, кто делает самые первые шаги).
Есть у нас микроконтроллер, представляющий из себя собственно процессор, немного памяти и различную периферию, например: аналого-цифровые преобразователи, таймеры, Ethernet, USB, SPI – все это сильно зависит от контроллера и решаемых задач.
Ко входу АЦП можно, например, подключить какой-нибудь датчик, скажем, температурный сенсор, который при подаче на него питания преобразует температуру в напряжение, измеряемое этим АЦП.
А к выходу контроллера, называемому GPIO (General Purpose Input-Output) можно, к примеру, подключить светодиод (или же что-нибудь более мощное вроде моторчика, но уже через усилитель).
Через SPI, RS232, USB и т.п. контроллер может связываться с внешним миром более сложным способом – получая и отсылая сообщения по заранее заданному протоколу.
В 90% случаев ПО пишется на С, иногда может использоваться С++ или ассемблер. Хотя все чаще появляются возможности писать на чем-нибудь более высокоуровневом, если это не касается непосредственной работы с периферией и не требуется максимально возможное быстродействие.
Чтобы лучше представить, с чем приходится иметь дело, вот пара примеров окружений, с которыми приходится работать: размер FLASH контроллера (аналог жесткого диска) – 16-256 килобайт, размер RAM – 64-256 килобайт! И в таком окружении реально запустить не только приложение, а еще и операционную систему реального времени с полноценной поддержкой многозадачности!
Примеры ниже – на псевдокоде, местами очень похожем на С. Без подробностей реализации там, где это несущественно для понимания.
Итак, подход «суперцикл».
Программа в этом подходе выглядит проще простого:
Бесконечный цикл, в котором контроллер последовательно делает все, что он должен делать.
Самое интересное, конечно же, во встраиваемых системах – это работа с периферией (теми самыми АЦП, SPI, GPIO и т.д.). С внешней периферией контроллер может работать двумя способами: опрашивая или используя прерывания. В первом случае, если мы хотим, например, прочитать символ из RS232 консоли, то мы будем периодически проверять, нет ли там символа, до тех пор, пока его не получим. Во втором же случае мы настраиваем RS232 контроллер так, чтобы он генерировал прерывание в тот момент, когда появится новый символ.
Демонстрация первого подхода. Например, хотим мы следить за температурой, а если она превысит установленный лимит – зажечь светодиод. Выглядеть это будет как-то так:
Пока все должно быть просто и понятно. (Функции чтения температуры и манипуляций со светодиодом приводить не буду – это не цель данной статьи).
Но что, если нам нужно делать что-то с заданной периодичностью? В примере выше температура будет проверяться с максимально возможной частотой. А если нам, например, нужно мигать светодиодом раз в секунду? Или опрашивать датчик строго с интервалом в 10 миллисекунд?
Тогда на помощь приходят таймеры (которые есть практически у любого микроконтроллера). Работают они так, что генерируют прерывание с заданной частотой. Мигание светодиода тогда будет выглядеть как-то так:
Особенность работы с прерываниями такова, что обработчик прерывания (код, который будет вызван непосредственно в тот момент, когда прерывание произойдет) должен быть как можно более коротким. Поэтому наиболее часто встречаемое решение – в обработчике установить глобальную переменную-флаг (да-да, без глобальных переменных никуда, увы), а в основном цикле ее проверять, и когда она изменится, выполнить уже основную работу, требуемую для обработки возникшего события.
Эта самая глобальная переменная обязательно должна быть объявлена с идентификатором volatile – иначе оптимизатор может банально «выбросить» неиспользуемый с его точки зрения код.
А если нужно будет мигать двумя светодиодами, так чтобы один мигал раз в секунду, а второй – три раза? Можно, конечно, использовать два таймера, но с таким подходом таймеров нам надолго не хватит. Вместо этого сделаем так, чтобы таймер работал с гораздо более высокой частотой, а в программе будем использовать делитель.
Заметьте, нам не нужно следить за переполнением счетчика миллисекунд, так как применяется беззнаковый тип.
Представим теперь, что у нас есть отладочная консоль, реализованная поверх интерфейса RS232 (самое распространенное решение в мире embedded!). И мы хотим выводить туда отладочные сообщения (которые будут видны, если наш контроллер подключить к компу через COM-порт). А одновременно с этим нам нужно со строго заданной (при этом высокой) частотой опрашивать датчик, подключенный к контроллеру.
И вот здесь возникнет вопрос – как реализовать такую банальную вещь, как вывод строки в консоль? Очевидное решение вроде
будет в данном случае недопустимым. Строку-то оно выведет, но при этом необратимо нарушит требование опрашивать датчик со строго заданной частотой. Мы же все это делаем в одном большом цикле, где все действия выполняются последовательно, помните? А консоль – устройство медленное, и вывод строки может занять гораздо больше времени, чем требуемый интервал между последовательными опросами датчика. Пример ниже — то, как делать не надо!
Еще пример – захотите вы реализовать программную защиту от перегрузки. Добавите измеритель тока, подключите его к АЦП контроллера, управление предохранительным реле заведете на один из пинов входа-выхода. И естественно захотите, чтобы защита срабатывала как можно быстрее после наступления события перегрузки (а иначе все просто сгорит). А у вас – все тот же общий цикл, в котором все действия выполняются строго по порядку. И гарантированное время реакции на событие никак не может быть меньше, чем время выполнения одной итерации цикла. И если в этом цикле будут операции, требующие для своего завершения длительного времени – то собственно все, именно они и будут задавать время реакции системы на все остальное.
А если вдруг где-то в этом цикле закрадется ошибка – то «ляжет» вся система. В том числе и реакция на перегрузку (чего допускать очень не хотелось бы, не так ли?).
Хотя с первой проблемой теоретически можно еще что-то сделать. Например, заменить простейшую, но долгую функцию печати строки на что-то вроде:
А простой вызов этой функции на что-то вроде:
Мы в результате сократили время прохода одного цикла со времени, необходимого для печати целой строки, до времени, необходимого для печати одного символа. Но для этого нам пришлось вместо примитивной и всем понятной с первого взгляда функции вывода строки в консоль добавить в код две машины состояний – одну для печати (чтоб запоминать позицию), а вторую – собственно для печати, чтобы помнить, что мы сейчас печатаем строку на протяжении нескольких следующих циклов. Да здравствуют глобальные переменные, «грязные» функции, хранящие состояния, и тому подобные замечательные штуки, которые запросто и очень быстро способны превратить код в несопровождаемое спагетти.
А теперь представьте себе, что система должна одновременно опрашивать с десяток датчиков, реагировать на несколько критических событий, требующих немедленной реакции, обрабатывать команды, «прилетающие» от пользователя или компьютера, выводить отладочные сообщения, управлять десятком индикаторов или манипуляторов. И для каждого из действий заданы свои ограничения по времени реакции и частота опроса или управления. И попробуйте все это запихнуть в один последовательный общий цикл.
Безусловно, это все реально. Но вот тому, кому придется это все сопровождать спустя хотя бы год после написания, я не позавидую.
Еще одна проблема дизайна «общий цикл» — сложность измерения загруженности системы. Предположим, у вас есть код:
Система как-то реагирует на прерывание, приходящее извне. И вопрос – сколько таких прерываний в секунду система сможет обработать? Насколько будет занят процессор при обработке 100 событий в секунду?
Вам будет очень сложно измерить, сколько времени было потрачено на обработку событий, а сколько – на опрос переменной «А не произошло ли прерывание?». Ведь все выполняется в одном цикле!
И вот здесь на помощь приходит второй подход.
Применение операционной системы реального времени.
Проще всего ее применение проиллюстрировать на том же примере: одновременный опрос датчика с заданной частотой и вывод на консоль длинной отладочной строки.
Как видите, в главной функции больше нет одного главного бесконечного цикла. Вместо него – отдельный бесконечный цикл в каждой задаче. (Да-да, функция os_start_sheduler(); никогда не вернет управление!). И что самое главное – у этих задач есть приоритеты. Операционная система сама обеспечит то, что нам нужно – чтобы задача с высоким приоритетом выполнялась прежде всего, а с низким – только лишь тогда, когда ей остается время.
И если время реакции на, например, прерывание в дизайне с суперциклом будет равно в худшем случае времени выполнения всего цикла (прерывание-то случится, конечно же, сразу же, но далеко не всегда необходимые действия можно сделать непосредственно в обработчике), то время реакции в случае ОС реального времени будет равно времени переключения между задачами (которое достаточно мало, чтобы считать, что это происходит сразу же!). Т.е. прерывание произойдет в одной задаче, а сразу по его завершению мы переключимся на другую задачу, ожидающую события, «запущенного» из прерывания.
Что касается измерения загрузки процессора – то и эта задача с применением ОС становится тривиальной. По умолчанию каждая ОС имеет самую прожорливую (но и самую низкоприоритетную) задачу Idle, которая выполняет пустой бесконечный цикл и получает управление лишь тогда, когда все остальные задачи неактивны. И подсчет времени, проведенного в Idle, обычно тоже уже реализован. Остается лишь его вывести в консоль.
Также если вдруг вы «не заметите» какую-нибудь ошибку, то «упадет» только та задача, в которой будет присутствовать ошибка (возможно также, что и все задачи с более низким приоритетом тоже), но задачи с более высоким приоритетом продолжат выполняться, обеспечивая хотя бы минимальные жизненно важные функции устройства, например, защиту от перегрузки.
И подводя итог: если система очень простая и нетребовательная ко времени реакции, ее проще сделать по образцу «суперцикл». Если же система собирается стать большой, соединяющей в себе много разных действий и реакций, которые к тому же критичны ко времени – то альтернативы использования ОС реального времени нет.
Кроме этого, плюс использования ОС – более простой и понятный код (поскольку мы можем группировать код по задачам, избегая глобальных переменных, машин состояний и прочего мусора, необходимого при использовании дизайна с суперциклом).
Минус же использования ОС – для ее использования требуется больше места, памяти, опыта и знаний (хотя ничего сложного там и нет, все же многозадачность априори сложнее и непредсказуемее, чем последовательно выполняющийся код). Обязательно хорошее понимание принципов работы в многозадачной среде, принципов потокобезопасного кода, синхронизации данных и многого другого.
Для «поиграться» можно взять FreeRTOS – бесплатный проект с открытым кодом, при этом достаточно стабильно работающий и простой в освоении. Хотя не редкость и коммерческие проекты с использованием именно этой операционки.
Проектирование архитектуры embedded-приложения
Добрый день! Хотелось бы поговорить на тему архитектуры embedded приложений. К сожалению, книг по этой теме очень мало, а в связи с тем, что, в последнее время, интерес к embedded и IoT растет, хочется уделить внимание этому вопросу. В этой статье, я бы хотел описать один из возможных вариантов того, как можно проектировать такие приложения.
Вопрос этот дискуссионный! Поэтому предлагают поделиться своим виденьем в комментариях!
Для начала определимся с областью: в рамках данной статьи, под embedded разработкой будем понимать разработку ПО под микроконтроллеры (далее МК, напр. STM32) на языке C / Asm.
Проекты для систем на базе МК условно можно разделить на не требующие и требующие многозадачности. Что касается решений первого типа, они, как правило, не очень сложные (со структурной точки зрения). Например, простой проект, в рамках которого необходимо считывать данные с датчика и показывать их на экране, не требует многозадачности, здесь достаточно реализовать последовательное выполнение перечисленных операций.
Если же приложение более сложное: в рамках которого необходимо считывать данные как с цифровых датчиков, так и с аналоговых, сохранять полученные значения в память (например, на sd-карту), обслуживать пользовательский интерфейс (дисплей + клавиатура), предоставлять доступ к данным через цифровой интерфейс (например, RS-485 / Modbus или Ethernet / TCP/IP) и максимально быстро реагировать на определенные события в системе (нажатие аварийных кнопок и т.п.), то в этом случае будет тяжело обойтись без многозадачности. Существует два способа решения вопроса многозадачности: реализовывать ее самому, либо воспользоваться какой-то операционной системой (далее ОС). На сегодняшний день, одной из самых популярных ОС реального времени для встраиваемых систем является FreeRTOS.
Попробуем представить, как должна выглядеть архитектура “сложного” embedded приложения, выполняющего достаточно большое количество разнородных операций. Я допускаю, что можно предложить еще более сложный вариант, который предполагает решение вопросов обработки звука, криптографию и т.п., но остановимся на варианте, который был описан чуть выше.
Поставим задачу более четко, пусть в рамках нашего приложения необходимо:
Если проанализировать задачу, то можно увидеть, что разные компоненты системы используют одни и те же данные. Например: данные с датчиков необходимо получить, отобразить на экране, записать на носитель и предоставить внешним системам для считывания. Это наводит на мысль, что нужна какая-то база данных реального времени (RTDB) для хранения и для предоставления самых актуальных данных различным подсистемам.
Задачи, выполняющиеся в системе (считывание данных, запись, отображение и т.п.), могут иметь различные требования к частоте их вызова. Нет смысла обновлять данные на дисплее с частотой 1 раз в 100 мс, т.к. для человека это не критично, а вот считывать данные с датчиков (особенно, если необходимо выдавать по ним управляющие воздействия) нужно часто (хотя в зависимости от ТЗ может и нет). Еще один важный момент связан с решением задачи доступа к одним и тем же данным на чтение и запись. Например: поток, опрашивающий датчики записывает полученные значения в RTDB, а в этот момент поток, отвечающий за обновление информации на дисплее, их считывает. Здесь нам помогут механизмы синхронизации, которые предоставляет операционная система.
Начнем проектировать архитектуру нашего приложения!
База данных реального времени
В качестве такой базы может выступать обычная структура, содержащая необходимый набор полей или массив. Для доступа к “RTDB” будем использовать API, который позволит записывать и считывать данные из базы. Синхронизацию доступа к данным внутри функций API можно построить на мьютексах, предоставляемых ОС (либо использовать какой-то другой механизм).
Работа с датчиками на шинах
Работа с датчиками предполагает следующее:
“Port” — реальный порт МК;
“Protocol driver” — драйвер протокола (например, Modbus). К такому драйверу желательно сделать свой интерфейс и работать через него. В рамках такого интерфейса можно реализовать управление доступом к ресурсу через мьютексы, так как это было сделано для “RTDB”. Некоторые разработчики предлагают это делать на уровне порта, чтобы быть уверенным в том, что никто другой в этот порт записывать ничего не будет, пока мы через него передаем свои Modbus пакеты.
“Sensor reader” — задача (task), которая опрашивает датчики, приводит в порядок полученную информацию и записывает ее в ”RTDB”.
“RTDB” — база данных реального времени, описанная выше, в соответствующем разделе.
Надпись “Pr: 1” над задачей означает приоритет, суть в том, что у каждой задачи может быть приоритет, если у двух задач, ожидающих процессорное время, разный приоритет, ресурс получит та, у которой приоритет выше. Если у задач приоритет одинаковый, то запустится та, у которой дольше время ожидания.
Работа с дискретными входами
В общем случае работу с дискретными входами можно организовать точно также как и с цифровыми датчиками. Но может возникнуть необходимость в быстром реагировании на изменение состояния входов. Например, по нажатию кнопки как можно быстрее замыкать релейный выход. В таком случае, лучше применить следующий подход: для обработки релейного выхода мы создаем специальную отдельную задачу с более высоким приоритетом, чем у остальных. Внутри этой задачи находится семафор, который она пытается захватить. На срабатывание конкретного дискретного входа заводится прерывание, в котором сбрасывается упомянутый выше семафор. Т.к. приоритет прерывания максимальный, то связанная с ним функция выполнится почти мгновенно, в нашем случае, она сбросит семафор, и, после этого, следующей задачей в очереди на выполнение будет как раз та, в рамках которой осуществляется управление реле (т.к. у нее приоритет выше, чем у остальных задач и блокировка по ожиданию семафора снята).
Вот так может выглядеть схема данной подсистемы.
Помимо быстрого срабатывания на изменение состояния конкретного входа, дополнительно можно поставить задачу “DI reader” для считывания состояния дискретных входов. Эта задача может быть как самостоятельной, так и вызываться по таймеру.
Работа “Interrupt handler’а” и “Relay 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 точно).
На этом все, надеюсь было интересно.