Что такое встраиваемое программное обеспечение: руководство для инженеров-разработчиков

Чем встраиваемое ПО отличается от универсального ПО на инженерном уровне

Команды, выпускающие свой первый встраиваемый продукт, часто относятся к программному обеспечению так же, как к настольному приложению: добавляют функции, тестируют на стенде, выпускают, когда оно работает. Затем устройство работает три недели в производственной среде и зависает. Нет журнала сбоев. Нет трассировки стека. Просто зависшая система, требующая перезагрузки. Эта модель повторяется в разработке встраиваемых продуктов, потому что правила, регулирующие встраиваемое ПО, отличаются от правил, регулирующих прикладное ПО, и эти различия проявляются только при длительной эксплуатации в реальных условиях.

Ограничения ресурсов как первоклассный конструкторский ограничитель

Встраиваемое ПО работает в жестких пределах, установленных кремнием: фиксированный ОЗУ, ограниченная флэш-память, отсутствие виртуальной памяти и, в контексте критически важных систем безопасности, полное отсутствие динамического выделения памяти. Это не целевые показатели производительности, которых нужно достичь после завершения проектирования. Это границы, которые формируют каждый выбор дизайна с самого начала.

Анализ глубины стека — один из примеров. В настольной системе переполнение стека является восстанавливаемым исключением. На микроконтроллере с общим объемом ОЗУ 8 КБ более глубокая, чем ожидалось, цепочка вызовов молчаливо повреждает соседнюю память и приводит к сбоям, которые никак не похожи на проблему стека. Инженеры выделяют место в стеке для каждой задачи, измеряют глубину вызовов в худшем случае с помощью инструментов статического анализа и рассматривают любое нарушение как блокирующую проблему, а не как предупреждение.

Статическое выделение памяти следует той же логике. Выделение всех буферов во время компиляции означает, что компоновщик может проверить, укладывается ли общий объем в доступную ОЗУ до того, как прошивка вообще начнет работать на оборудовании. Эта детерминированность стоит той негибкости, которую она влечет за собой.

Цель — не в оптимизации на позднем этапе, а в том, чтобы сделать ограничения ресурсов видимыми на этапе проектирования, когда их исправление обходится дешево.

Модель выполнения, привязанная к оборудованию

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

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

Детерминизм и режим реального времени как критерии корректности

50-миллисекундный всплеск задержки в веб-приложении — это проблема пользовательского опыта. В контроллере двигателя тот же всплеск может повредить оборудование. Корректность встроенного программного обеспечения включает временную корректность — соблюдение сроков является частью спецификации, а не отдаленной целью.

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

Структура встраиваемого программного обеспечения в работающей системе

Многоуровневая модель программного обеспечения: HAL, промежуточное ПО и приложение

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

  • HAL (Hardware Abstraction Layer): Отвечает за доступ ко всем регистрам периферийных устройств. Ничто выше этого уровня не обращается напрямую к адресу оборудования.
  • Промежуточное ПО: Стеки протоколов, службы RTOS, файловые системы. Не зависят от конкретного оборудования или продукта.
  • Уровень приложений: Реализует поведение продукта, вызывая API промежуточного ПО и HAL.

Более глубокая абстракция снижает затраты на портирование при изменении оборудования. Однако граница каждого уровня добавляет накладные расходы на вызовы функций и глубину стека. На Cortex-M0, работающем на частоте 48 МГц с 16 КБ ОЗУ, эти накладные расходы измеримы. Инженеры, работающие с ограниченными ресурсами, иногда объединяют HAL и промежуточное ПО в один слой, чтобы восстановить этот запас. Правильный ответ зависит от того, как часто ожидается изменение оборудования и насколько жестко ограничен бюджет ресурсов.

Архитектура выполнения: Bare-Metal против RTOS

Две модели времени выполнения охватывают большинство встраиваемых систем. Прошивка Bare-Metal выполняет суперцикл — while(1), который опрашивает состояние и вызывает обработчики — или полностью полагается на прерывания. Система на базе RTOS выполняет несколько задач под управлением вытесняющего планировщика, а управление параллельной работой осуществляют уровни приоритетов, очереди между задачами и семафоры.

Фактор Bare-Metal На базе RTOS
Накладные расходы Минимальные Планировщик + стоимость переключения контекста
Параллелизм Единый путь выполнения Множество приоритетных задач
Разнообразие сроков выполнения Работает, когда сроки одинаковы Требуется, когда сроки сильно различаются
Сложность отладки Ниже Выше (состояния гонки, инверсия приоритетов)

Выбор определяется количеством задач и разнообразием сроков, а не предпочтениями. Регистратор данных с одного датчика с одним каналом связи работает чисто на bare-metal. Устройство, которое одновременно управляет дисплеем, шиной CAN, стеком USB и сторожевым таймером безопасности, нуждается в RTOS для разделения и планирования этих задач. Конкретные шаблоны программирования для целевых платформ RTOS см. в разделе шаблоны программирования встраиваемых систем для целевых платформ RTOS.

Загрузчик как структурный компонент, а не дополнение

SWD programming cable connected to PCB debug header on electronics assembly bench

Загрузчик запускается перед приложением. Он инициализирует основное оборудование, проверяет образ приложения и передает управление только в случае успешной проверки. Инженеры, которые добавляют загрузчик на поздних этапах проекта — или вовсе пропускают его для «простых» продуктов — часто обнаруживают проблему, когда обновление на месте повреждает устройство, а пути восстановления нет.

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

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

Стартовый код и системная инициализация до main()

Приложение не запускается в main()Перед вызовом этой функции выполняется начальный код: он устанавливает указатель стека, копирует инициализированные переменные из флэш-памяти в ОЗУ, обнуляет сегмент BSS и настраивает системные часы. На многих микроконтроллерах сторожевой таймер (watchdog) также включается по умолчанию аппаратно и должен быть либо обслужен, либо отключен до завершения настройки часов — иначе система сбросится до того, как main() будет достигнуто.

Поставляемые производителем начальные файлы корректно обрабатывают большую часть этого для стандартных конфигураций. Они дают сбой, когда проект использует нестандартную компоновку памяти, внешний генератор, требующий больше времени для стабилизации, чем позволяет стандартный тайм-аут, или пользовательский скрипт компоновщика, который неправильно размещает секцию BSS. Инженеры, которые относятся к начальному коду как к «черному ящику», теряют диагностическое время, когда сбои инициализации вызывают жесткие сбои (hard faults) без очевидной причины. Прочтение начального файла один раз, понимание его работы и проверка древовидной структуры тактирования перед добавлением прикладного кода сэкономят значительное время отладки в дальнейшем.

Проектирование обработчиков прерываний (ISR) и опасности совместного использования данных

Firmware engineer reading oscilloscope ISR timing waveform on embedded development bench

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

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

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

Стратегия управления памятью для целевых платформ с ограничениями

Динамическое выделение с помощью malloc и free доступно на большинстве встраиваемых платформ. Производственное встраиваемое ПО часто избегает его полностью. Фрагментация кучи в системе, работающей месяцами, может привести к сбоям выделения, которые проявляются только после недель безотказной работы — именно такого рода сбои трудно воспроизвести и еще труднее диагностировать в полевых условиях. Время выделения также недетерминировано, что противоречит требованиям жесткого реального времени.

Три стратегии заменяют динамическое выделение в производственных системах:

  • Статическое выделение: Все буферы объявляются во время компиляции. Компоновщик проверяет соответствие. Нулевые накладные расходы во время выполнения.
  • Пул памяти: Блоки фиксированного размера, выделяемые из предварительно выделенной области. Время выделения постоянно. Фрагментация внутри пула невозможна.
  • Стековое выделение: Локальные переменные в стеке задачи или функции. Автоматически освобождаются при возврате. Подходит для кратковременных данных ограниченного размера.

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

Встроенное программное обеспечение: Ответы на инженерные вопросы напрямую

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

Может ли встраиваемое программное обеспечение работать без операционной системы?
Да — см. обсуждение bare-metal против RTOS в разделе «Архитектура системы» выше. Многие серийные встраиваемые системы, включая контроллеры, критически важные для безопасности, и простые узлы датчиков, работают в режиме bare-metal по дизайну, поскольку накладные расходы RTOS не оправданы сложностью системы.

Что делает отладку встраиваемого программного обеспечения сложнее, чем отладку прикладного программного обеспечения?
Сложность усугубляется тремя факторами: ограниченный или отсутствующий вывод на дисплей целевого устройства; поведение в реальном времени, которое изменяется при вставке отладочных выводов; и сбои, зависящие от оборудования, которые не воспроизводятся в симуляторе. Интерфейсы отладки JTAG/SWD и логические анализаторы являются основными инструментами — подробно описаны в руководстве по инструментам отладки и трассировки для встраиваемых целевых устройств.

Требует ли встраиваемое программное обеспечение пользовательского компилятора или инструментария?
Это не универсальный компилятор, а всегда компилятор, специфичный для целевой платформы. Встраиваемое программное обеспечение компилируется кросс-компиляцией на хост-машине для другой целевой архитектуры — ARM Cortex-M, RISC-V и аналогичных. Скрипт компоновщика должен точно соответствовать карте памяти целевой платформы. Универсальный компилятор без правильного скрипта компоновщика создает бинарный файл, который не загрузится, поскольку код запуска и таблица векторов прерываний окажутся по неправильным адресам.

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