Портируемость
Встраиваемые системы делают портируемость очень важной темой: каждый производитель и даже каждая серия от одного производителя предлагает различные периферийные устройства и возможности, а способы взаимодействия с этими периферийными устройствами также различаются.
Общий способ устранения таких различий — использование слоя, называемого уровнем абстракции оборудования или HAL.
Абстракции оборудования — это наборы программных процедур, которые эмулируют некоторые специфические для платформы детали, предоставляя программам прямой доступ к аппаратным ресурсам.
Они часто позволяют программистам писать независимые от устройства высокопроизводительные приложения, предоставляя стандартные вызовы операционной системы к оборудованию.
Википедия: Уровень абстракции оборудования
Встраиваемые системы в этом отношении немного особенные, поскольку обычно у них нет операционных систем и программного обеспечения, устанавливаемого пользователем, а вместо этого используются образы прошивки, которые компилируются целиком, а также существуют другие ограничения. Таким образом, традиционный подход, определенный Википедией, потенциально может работать, но, вероятно, не является наиболее продуктивным для обеспечения портируемости.
Как это делается в Rust? Встречайте embedded-hal...
Что такое embedded-hal?
Вкратце, это набор трейтов, которые определяют контракты реализации между реализациями HAL, драйверами и приложениями (или прошивками). Эти контракты включают как возможности (т.е. если трейт реализован для определенного типа, реализация HAL предоставляет определенную функциональность), так и методы (т.е. если вы можете создать тип, реализующий трейт, гарантируется наличие методов, указанных в этом трейте).
Типичная структура уровней может выглядеть следующим образом:
Некоторые из определенных трейтов в embedded-hal включают:
- GPIO (пины ввода и вывода)
- Последовательная связь
- I2C
- SPI
- Таймеры/обратные отсчеты
- Аналогово-цифровое преобразование
Основная причина использования трейтов embedded-hal и крейтов, их реализующих и использующих, — это контроль сложности. Если учесть, что приложение должно реализовать использование периферийного устройства в оборудовании, а также само приложение и, возможно, драйверы для дополнительных аппаратных компонентов, становится понятно, что возможности повторного использования весьма ограничены. Математически, если M — это количество реализаций HAL для периферийных устройств, а N — количество драйверов, то без использования embedded-hal сложность реализации может достигать M×N. Использование трейтов embedded-hal снижает сложность реализации до уровня, близкого к M+N. Конечно, есть и дополнительные преимущества, такие как меньшее количество проб и ошибок благодаря хорошо определенным и готовым к использованию API.
Пользователи embedded-hal
Как упомянуто выше, есть три основных пользователя HAL:
Реализация HAL
Реализация HAL обеспечивает взаимодействие между оборудованием и пользователями трейтов HAL. Типичные реализации состоят из трех частей:
- Один или несколько типов, специфичных для оборудования
- Функции для создания и инициализации таких типов, часто предоставляющие различные параметры конфигурации (скорость, режим работы, используемые пины и т.д.)
- Одна или несколько реализаций (
impl) трейтов embedded-hal для этого типа
Такая реализация HAL может быть представлена в различных вариантах:
- Через низкоуровневый доступ к оборудованию, например, через регистры
- Через операционную систему, например, с использованием
sysfsв Linux - Через адаптер, например, заглушки типов для модульного тестирования
- Через драйвер для аппаратных адаптеров, например, мультиплексор I2C или расширитель GPIO
Драйвер
Драйвер реализует набор пользовательских функций для внутреннего или внешнего компонента, подключенного к периферийному устройству, реализующему трейты embedded-hal. Типичные примеры таких драйверов включают различные датчики (температуры, магнитометр, акселерометр, освещенности), устройства отображения (светодиодные матрицы, ЖК-дисплеи) и исполнительные механизмы (двигатели, передатчики).
Драйвер должен быть инициализирован экземпляром типа, реализующего определенный трейт embedded-hal, что обеспечивается через ограничение трейта, и предоставляет собственный экземпляр типа с пользовательским набором методов, позволяющих взаимодействовать с управляемым устройством.
Приложение
Приложение объединяет различные части и обеспечивает достижение желаемой функциональности. При переносе между различными системами именно эта часть требует наибольших усилий по адаптации, поскольку приложение должно правильно инициализировать реальное оборудование через реализацию HAL, а инициализация различного оборудования может существенно отличаться. Кроме того, выбор пользователя часто играет большую роль, поскольку компоненты могут быть физически подключены к разным терминалам, шины оборудования иногда требуют внешнего оборудования для соответствия конфигурации, или существуют различные компромиссы в использовании внутренних периферийных устройств (например, доступно несколько таймеров с разными возможностями, или периферийные устройства конфликтуют друг с другом).