Заслуженно распространена точка зрения, что типичный разработчик высокоуровневого прикладного ПО настолько свыкся с доступностью системных ресурсов и мягкостью требований реального времени, что ожидать от него оптимизации кода в угоду снижения ресурсоёмкости приложения можно лишь в крайних случаях, когда этого прямо требуют интересы бизнеса. Это и логично, ведь в задачах прикладной автоматизации самым дорогим ресурсом остаётся ресурс человеческий. Более того, снижение когнитивных затрат на возню с байтами оставляет внимание разработчика свободным для задач первоочередной важности, таких как обеспечение функциональной корректности программы.
Редко когда речь заходит об обратной проблеме, имеющей место в куда более узких кругах разработчиков встраиваемых систем, включая системы повышенной отказоустойчивости. Есть основания полагать, что ранний опыт использования MCS51/AVR/PIC оказывается настолько психически травмирующим, что многие страдальцы затем продолжают считать байты на протяжении всей карьеры, даже когда объективных причин для этого не осталось. Это, конечно, не относится к случаям, где жёсткие ценовые ограничения задают потолок ресурсов вычислительной платформы (микроконтроллера). Но это справедливо в случаях, где цена вычислительной платформы в серии незначительна по сравнению со стоимостью изделия в целом и стоимостью разработки и верификации его нетривиального ПО, как это бывает на транспорте и сложной промышленной автоматизации. Именно о последней категории систем этот пост.
Обычно здесь можно встретить упрёк: "Ты чё пёс А MISRA? А стандарты AUTOSAR? Ты, может, и руководства HIC++ не читал? У нас тут серьёзный бизнес, а не эти ваши побрякушки. Кран на голову упадёт, совсем мёртвый будешь." Тут нужно аккуратно осознать, что адекватное проектирование ПО и практики обеспечения функциональной корректности в ответственных системах не взаимоисключающи. Если весь ваш софт проектируется по V-модели, то вы, наверное, в этой заметке узнаете мало нового хотя бы уже потому, что ваша методология содержит пункт под многозначительным названием проектирование архитектуры. Остальных эмбедеров я призываю сесть и подумать над своим поведением.
Не укради
Что, в конечном итоге, говорят нам вышеупомянутые стандарты в кратком изложении? Примерно вот что:
-
Не своевольничай. Наличие каждой строчки кода должно быть однозначно обосновано низкоуровневыми техническими требованиями и проектной документацией.
-
Пиши простой код. У простого меньше шансов сломаться или выстрелить тебе в ногу, чем у сложного.
-
Делай свои намерения явными и избегай неявных предположений. Это касается проверки инвариантов, исключения платформно-зависимых конструкций, исключения UB, unsafe и схожих граблей, заботливо разложенных языком программирования и средой исполнения.
-
Не забывай об асимптотической сложности. Ответственные системы обычно являются системами реального времени. Адептов C++ призывают воздержаться от злоупотреблений RTTI и использования динамической памяти (хотя последнее к реальному времени относят ошибочно, потому что подобающим образом реализованные
malloc()
иfree()
выполняются за постоянное время и даже с предсказуемой фрагментацией кучи). -
Не игнорируй ошибки. Если что-то идёт не так, обрабатывай как следует, а не надейся на лучшее.
Это всё применимо в тех случаях, где непредвиденный сбой обходится бизнесу дороже, чем инвестиции в качественный софт и адекватную платформу. Про тестирование тут, кстати, говорится сравнительно мало, потому что это несколько обособленная дисциплина.
Видите пункт "игнорируй широко известные принципы проектирования сложных информационных систем"? Я тоже нет. Но подавленные воспоминания о том, как кросс-компилятор под дохлые однокристалки унижает разработчика в его собственном доме не дают человеку в полной мере осмыслить свои действия и их долгосрочные последствия. В итоге мы получаем спагетти без архитектуры, которое невозможно поддерживать и тестировать, но при этом оно носит гордую метку MISRA-совместимости как (сомнительное) свидетельство качества.
Я имел несчастье ознакомиться с некоторым количеством встраиваемого ПО реального времени, к надёжности которого предъявляются повышенные требования, и в пугающем числе случаев я ощущал, как у меня шевелятся на голове волосы. Меня, например, сегодня уже не удивляет старая байка об ошибках в системе управления Тойоты Приус, или байка чуть поновее про Boeing 737MAX (тот самый самолёт, который проектировали клоуны под руководством обезьян). В нашем новом дивном мире скоро каждая первая система станет программно-определяемой, что (безо всякой иронии) здорово, потому что это открывает путь к решению сложных проблем затратой меньших ресурсов. Но с повальной проблемой качества системоопределяющего ПО нужно что-то делать.
Если взять типичную встраиваемую систему достаточно высокой сложности, можно выделить примерно следующие сгустки логики:
-
Класс-бог, отвечающий за всё сущее.
-
Несколько драйверов периферии и коммуникационных протоколов, ко внутренним состояниям которых зачастую класс-бог обращается непосредственно, минуя хилые абстракции.
-
Utils или helpers, без них никуда.
Что в итоге: модульное тестирование невозможно, потому что нет модулей; толковая поддержка невозможна, потому что всё сложно; о гарантиях функциональной корректности трудно рассуждать всерьёз, нам бы для начала научиться выполнять вменяемую декомпозицию задачи, а не решать все проблемы в одном цикле сразу. Это зачастую преподносится как неотвратимая данность, нельзя по-другому, потому что это же не десктоп система с неограниченными ресурсами, вы не понимаете, это другое.
Инфоцыгане
Косвенным образом масла в огонь подливают некоторые поставщики программных инструментов для разработчиков встраиваемого ПО: Mbed, Arduino, и т.п. Их маркетинговые материалы вполне могут заставить начинающего специалиста поверить, что суть этой работы заключается в низкоуровневом управлении железом, потому что именно на этом аспекте диспропорционально фокусируются упомянутые поставщики ПО. Вот у меня на соседнем рабочем столе открыт в CLion проект ПО для одной встраиваемой системы; проект собирается из чуть более чем ста тысяч строк кода. Из этой сотни примерно три тысячи приходятся на драйверы периферии, остальное приходится на бизнес-логику и всякий матан. Моя скромная практика показывает, что за исключением простых устройств сложность целевой бизнес-логики приложения несопоставима с той его частью, что непосредственно работает с железом.
В один прекрасный день я обсуждал детали своего опенсорсного проекта (тесно связанного с эмбедами) с разработчиком из сторонней компании; назовём его Ильёй (в целях деанонимизации имя не было изменено). Илья работал над фичей для моего проекта, которая требовалась его работодателю. Илья работал внимательно и неспешно в силу своей аккуратности. Илья регулярно созванивался со мной, и мы обсуждали, как лучше сделать то да сё. Илья однажды сообщил, что пришёл к нему его начальник, отверз уста и изрёк:
Смотри, что я нашёл! Есть крутая новая система, Mbed называется, значит, для эмбедеров. Гляди, как можно быстро прототипы лепить! Клац, клац, и мигалка готова! Вот же, на видео. А ты, Илья, свой алгоритм оптимизации CAN фильтров пилишь уже неделю, не дело это, давай переходить на Mbed.
Я, разумеется, не хочу сказать, что права на существование сии продукты не имеют. Конечно, имеют, и более того: они чрезвычайно полезны, когда бизнес-логика проста и сосредоточена вокруг интеграции нескольких компонентов. За примерами ходить не надо, просто откройте любой их рекламный проспект — это то, для чего они созданы. Но у меня подгорает, когда я слышу о менеджере, продвигающем эмбеддед-фреймворк для проекта, где работа с периферией и её отладка едва ли займёт 1% внимания программиста.
Весь этот горький катаклизм знаком многим, кто вхож в отрасль. Но ещё хуже обстоят дела, когда разработчик низкоуровневого ПО без надлежащей подготовки берётся за проектирование распределённых систем.
Когда один бэкэндер лучше двух эмбедеров
Ранее я публиковал большую обзорную статью о нашем открытом проекте UAVCAN (Uncomplicated Application-level Vehicular Computing And Networking), который позволяет строить распределённые вычислительные системы (жёсткого) реального времени в бортовых сетях поверх Ethernet, CAN FD или RS-4xx. Это фреймворк издатель-подписчик примерно как DDS или ROS, но с упором на предсказуемость, реальное время, верификацию, и с поддержкой baremetal сред.
Для организации распределённого процесса UAVCAN предлагает предметно-ориентированный язык — DSDL — с помощью которого разработчик может указать типы данных в системе и базовые контракты, и вокруг этого затем соорудить бизнес-логику. Это работает примерно как REST эндпоинты в вебе, XMLRPC, вот это вот всё. Если взять одного обычного бэкендера — человека, измученного сервис-ориентированным проектированием и поддержкой сложных распределённых комплексов — и объяснить ему суть реального времени, то он в короткие сроки начнёт выдавать хорошие, годные интерфейсы на UAVCAN.
Хрестоматийный пример — интеграция системы воздушных сигналов, или хотя бы одного лишь датчика воздушной скорости. Наученный тяжким опытом разработки и сопровождения продакшена, бэкендер тщательно осмыслит следующий вопрос: "какую бизнес-задачу мы решаем?"
Допустим, ответ подопытного является вариацией на тему "измерение воздушной скорости, барометрической высоты и статического давления". Тогда на свет появляются примерно следующие строки DSDL:
# Calibrated airspeed
uavcan.time.SynchronizedTimestamp.1.0 timestamp
uavcan.si.unit.velocity.Scalar.1.0 calibrated_airspeed
float16 error_variance
# Pressure altitude
uavcan.time.SynchronizedTimestamp.1.0 timestamp
uavcan.si.unit.length.Scalar.1.0 pressure_altitude
float16 error_variance
# Static pressure & temperature
uavcan.time.SynchronizedTimestamp.1.0 timestamp
uavcan.si.unit.pressure.Scalar.1.0 static_pressure
uavcan.si.unit.temperature.Scalar.1.0 outside_air_temperature
float16[3] covariance_urt
# The upper-right triangle of the covariance matrix:
# 0 -- pascal^2
# 1 -- pascal*kelvin
# 2 -- kelvin^2
Мы получаем законченный сетевой сервис, который предоставляет данные системы воздушных сигналов (конечно, этот пример не претендует на звание законченного сервиса, но суть вы поняли). Если потребитель хочет, например, знать барометрическую высоту, он просто берёт и подписывается на соответствующий топик.
Знакомый с физикой полёта читатель здесь уместно поинтересуется: откуда оконечному устройству (что поставляет сервис воздушных сигналов) знать калибровочные параметры датчика воздушной скорости? Ведь это предполагает осведомлённость датчика о собственном положении на летательном аппарате и об его аэродинамических свойствах. Принципы инкапсуляции и разделения ответственности предписывают, что соответствующие параметры настраиваются на самом поставщике сервиса воздушных сигналов (т.е. сетевом узле датчика) с целью сокрытия деталей реализации сервиса от его потребителей.
В БПЛА некоторых типов практикуется автокалибровка, где выборка измерений с приёмника воздушного давления за значительный промежуток времени сопоставляется с наземной скоростью, чтобы таким образом определить калибровочные параметры эмпирически. В рамках сервис-ориентированного подхода это решается превращением системы воздушных сигналов в потребителя данных о наземной скорости. Проще говоря, наш сетевой узел, что измеряет воздушную скорость, подписывается на топик, в котором есть данные о наземной скорости летательного аппарата, получая таким образом доступ к необходимому контексту для выполнения автокалибровки.
"Очевидные вещи говоришь" — скажет матёрый архитектор информационных систем — "Тут сервис, тут зависимость, соединили и полетели". Но в кругу встраиваемых систем эти очевидные вещи, судя по моему удручающему опыту, являются откровением, потому что опытный эмбедер подходит к решению той же проблемы совершенно иным образом: не от целей, стоящих перед системой, а от средств их достижения. Или, короче говоря: первым вопросом является не что мы делаем, а как мы это делаем. В итоге вместо сервиса мы получаем один-единственный топик следующего типа:
uint16 differential_pressure_reading
uint16 static_pressure_reading
uint16 outside_air_temperature_reading
Это, конечно, нельзя использовать непосредственно, поэтому наше конечное устройство превращается в пассивный сенсор, который сообщает измерения на центральный узел, и тот затем выполняет вычисления и публикует их результаты в сеть в узкоспециализированных форматах, по одному под конечного потребителя. Например, если гироподвес и привод предкрылков нуждаются в воздушной скорости, то к каждому будет приделан отдельный топик своего типа. Своими глазами видел.
В итоге мы получаем всё то же спагетти с классом-богом, только вместо класса у нас центральный узел, а вместо спагетти — сотня топиков без архитектуры. Нетрудно заметить, что этот подход также может увеличить время доставки данных и загрузку сети, а заодно и, возможно, снизить отказоустойчивость ввиду централизации процессов.
Художника каждый может обидеть
Не следует думать, что я хочу определить всех эмбедеров в биореактор. Я, в конце концов, и сам один из них. Но я склонен к мысли, что легче из хорошего прикладного программиста сделать толкового эмбедера, чем дождаться вменяемого кода от последнего.
Коллеги, одумайтесь.
Я вижу, как нашим микроскопом заколачивают ржавые гвозди, и представляю, сколько ещё подобного происходит за пределами моего поля зрения. В прошлом году уровень отчаяния в нашей скромной команде был столь высок, что мы опубликовали наноучебник, где объясняется, как выглядит сетевой сервис здорового человека: UAVCAN Interface Design Guidelines. Это, конечно, капля в море, но в один прекрасный день я всё-таки переведу его на русский язык ради подъёма уровня профессиональной грамотности.
Непонимание основ организации распределённых вычислений затрудняет внедрение новых стандартов на замену устаревших подходов. Наши наработки в рамках стандарта DS-015 (созданного в коллаборации с небезызвестными NXP Semiconductors и Auterion AG) встречают определённое сопротивление ввиду своей непривычности для целевой аудитории, в то время как ключевые принципы, на которых они основаны, известны индустрии информационных технологий уже не одно десятилетие. Этот разрыв должен быть устранён.
Желающие принять участие в движении за архитектурную чистоту и здравый смысл могут причаститься в телеграм-канале uavcan_ru или на форуме forum.uavcan.org.
Автор: Павел Кириенко