Comments 36
В принципе рабочий метод, но сложный и очень трудоемкий (требуется реализация низкоуровневого взаимодействия) и хорошего понимание работы самой библиотеки в случае необходимости частичной реализации низкоувровненго взаимодействия.
Для подобный целей я использую другой, более простой подход - реализую библиотеку на уровне обработки потока данных (в виде нескольких функций или отдельного класса) без привязки к аппаратным интерфейсам. Это позволяет не зависеть от сложностей взаимодествия с железом, когда оно не требуется, тогда как коннектор можно сделать на любой интерфейс. Дополнительно гораздо проще все это разработывать и покрывать все это тестами, так как не требуется держать в голове или эмулировать взаимодействие с железом.
Все это так же можер работать без изменений на любом микроконтроллере, HAL или без него, и даже на обычном компе (например, для эмуляции при тестировании)
а ведь 2024 год(
А что 2024? В этом самом 2024 в baremetal на С у меня нет возможности асинхронно опросить stdin и узнать нажали кнопку или нет. Ну то есть абстрактно есть, куча способов "разного уровня совместимости", а в конкретной реализации - хрен тебе. Или отсутствует в библиотеке или собирается, но молча не работает. А инженеры производителя рекомендуют пойти и взять их собственный драйвер для работы с UART. Который работает, но, естественно, не совместим со стандартыми функциями С. ARM A53 / Xilinx, если что.
Это хорошо, что в статье предложили - но по моим наблюдениям, жизнь сложнее. То несколько устройств сидят на одной шине, и нельзя из другого места вызывать SPI или I2C процедуру пока предыдущая транзакция не завершилась. При приеме данных, хочется использовать прерывания, а не зависать синхронно в ожидании (чем грешит ардуина). Еще хуже, если вам надо режим ноги периодически переключать между digital output и digital input (или analog input), и при этом желательно делать это в синхронизации с устройством на той стороне (чтобы вы вдвоем не драйвили одну и ту же линию вверх с одной стороны и вниз - с другой). И вот когда все это начинает предусматриваться в библиотеке - вы уже не обойдетесь универсальными указателями на функции чтобы одно и то же одинаково работало на разных контроллерах. А если все это обмазать слоями абстракции чтобы можно было теоретически сделать что угодно и где угодно - оно начинает быть таким сложным в разработке и отладке, что проще разработать отдельную прошивку для каждой целевой платформы...
Слой абстракции железа с виртуальным SPI, буферами, приоритетами, балансировщиком, всё по взрослому. Так незаметно и напишите кусок rtos.
Но есть ли смысл ? У меня одна половина жизни - это большие энтерпрайз системы с кучей слоев абстракции, write once run anywhere и вот это все... И там оно имеет смысл потому что бизнес меняется, требования меняются, и с нуля этих монстров не перепишешь. А другая - когда что-то делается на Tiny13 или ESP32. Это обычно конкретная задача которую надо один раз сделать, и она годами будет что-то регулировать или включать/выключать. С моей точки зрения, тут нет смысла гнаться за переносимостью. В крайнем случае - можно сделать двухуровнвую систему типа как сейчас сделан Klipper в 3d-печати: сложная логика и красивый UI на полноценном линуксе на встраиваемом ПК, а real-time задачи вытащены на тонкий слой логики в микроконтроллере. Захотите сменить контроллер - ну перепишете тонкий слой, да и все. Один черт, каждый производитель лепит свое в тактировании, инициализации, прерываниях, DMA - и под новый тип контроллера хоть так, хоть этак писать прилично приходится...
Я бы сказал больше - даже в ERP, спроектированных полностью изолированно-абстрактно по слоям, с полной примитивизацией возможностей СУБД до CRUD, замена RDBMS - это авантюра с недетерминированным исходом - то есть на деле все эти задекларированные преимущества (привинтим двигатель от Мерседеса к коробке БМВ) не работают, или требуют (зачастую неочевидной и кропотливой) доработки - особенно, если мы хотим получить от информационной системы гарантированно высокую отдачу.
Смотрите на Природу - она использует все частные возможности биологических созданий для внутривидовой и межвидовой борьбы. Наследование - только ссылка назад, но никак не ограничитель движения вперед :)
Слой абстракции железа с виртуальным SPI, буферами, приоритетами, балансировщиком, всё по взрослому. Так незаметно и напишите кусок rtos.
И будете "мигать светодиодом" - то есть выполнять задачи, с которыми когда-то справлялся PIC16 - на STM32 с мегабайтом памяти.
Так беда не в том чтобы предусмотреть это всё, а в том чтобы сделать правильную абстрактную модель.
ИМХО, почти все кейсы использования дефолтных интерфейсов уже изучены в доль и поперёк. Если тебе нужно что-то специфическое, то нанимай деда по серьёзнее, а для остальных 99% хватит и дефолтных либ.
Особенно я не понимаю почему не сделают нормальные либы для новых плюсов(С++20 хотяб). Был бы у тебя условный dev::ina219
в котором тупо карта регистров забита и пару функций для удобной работы, а инициализировался бы он от hal::i2c[0]
драйвера который вообще на этапе компиляции считается. И всё были бы счастливы. (Эх... может когда-нибудь и раскрою свой фреймворк)
ЗЫ Я вообще к тому, что слишком абстрактные модели не нужны. Они должны делать ровно то что написано в документации, если клас делает что-то ещё этот клас не имеет права называться аппаратной абстракцией, а скорее клас для обычных пользователей, так сказать.
А что будет, если у вас, например таймеры-счетчики в одном кристалле 16-разрядные, а в другом - 8? И prescaler где-то есть, где-то - нет? Это я к тому, что универсальная абстракция либо имеет свойства наихудшей имплементации, либо сложность, равную наименьшему общему кратному свойств покрываемых имплементаций.
Неистово плюсую и хочу развернуть ответ. Даже такая простая вещь, как GPIO, у каждого производителя имеет свои особенности; у кого-то вход только PULLUP, у кого-то и PULLUP, и PULLDOWN , где-то выходы push-pull и открытый коллектор, а где-то ещё можно включить высокотоковый драйвер, но только на некоторых пинах.
В итоге универсальная библиотека будет либо иметь самый простой интерфейс и не уметь никаких расширенных возможностей, либо будет покрыта ifdef'ами и все равно код, использующий возможности одного МК, не будет работать на других.
В этом смысле представляет интерес event-driven архитектура, где все процессы внутри микроконтроллера представляют собой разновидности конечных автоматов, и единственным нормальным способом взаимодействия является запуск события в очередь. Тогда только КА нижнего уровня надо будет переписывать под железо, а логика будет плюс-минус переносима. Был тут цикл статей на хабре под общим заголовком "Автоматное программирование". Но тут свои заморочки - сложный процесс разложить в автомат может быть дурная работа! Ну или генерировать автоматы из промежуточного императивного языка, что тоже такое себе - особенно если оно не работает и это надо как-то отладить... Но зато в event можно включать информацию вне зависимости от того, поддерживается ли она конкретным железом или нет. По принципу - аппаратный уровень постарается выполнить наилучшим образом то, что он умеет и прогинорирует то - что не умеет. Ибо если не дал бог на ноге высокотоковый выход, никакими программными ухищрениями его туда посадить нельзя... Но хотя бы мы не обязаны писать под "наименьший общий знаменатель" аппаратуры, который неизбежно получается очень убогим...
Полгода назад библиотеку для Arduino для SPI устройства портировал для запуска на Линукс на CH341A. Просто написав хидеры имитирующие API из хидеров к Arduino.
К чему это кокетство с указателями на функции? В С++ это называется виртуальной функцией. Зачем писать на неООП-языке в ООП стиле? Или вам удобней чесать за правым ухом левой ногой? Если нет, то используйте С++.
Ещё одно крупное упущение. Как только вы замахиваетесь на что-то более-менее универсальное, сразу возникает вопрос синхронизации доступа, иначе всё, что вы тут написали, будет не возможно использовать в многозадачной среде. Упоминаний об этом, как и примитивов синхронизации доступа, я в статье не заметил.
Плюсы в дефолтном состоянии тянут за собой нефиговый такой рантайм, который в мелкие контроллеры не влезет. Да, его можно уменьшить, но это уже не уровень ардуино, порог входа таки отрастает на понимание работы тонны опций сборки...
C++ на микроконтроллерах надо использовать очень аккуратно, и по моей практике - как его задумал Страуструп: "С с классами". Ибо любое использование темплейтов (а на них построен весь std) - запускает неистовую кодогенерацию, которая потом то-ли вырежется оптимизатором, то-ли нет...
Для того же Arduino предостаточно библиотек, написанных с использованием шаблонов. Все ими просто пользуются, не задумываясь о каком-то неистовстве кодогенерации. И что значит "то-ли вырежется, то-ли нет"? После того, как шаблонный код выведен, компилятор обрабатывает его, как код без шаблонов. В чем проблема?
На arduino mega, или STM32 или ESP32 - это проходит. А вот написать с arduino-библиотеками под avr tiny (2313, 13, etc) - тот еще цирк. То не хватает флэша под код, то SRAM под переменные. А на C - прекрасно программируется...
Возникает вопрос, так ли уж нужны avr tiny. Периодически в обсуждениях возникает вопрос, зачем нужны Arduino, если есть STM32 или Pi Pico, для которых можно на питоне писать, и цена которых сравнима со всем перечисленным. А мощность несравнимо выше.
Допустим, для "помигать светодиодами" достаточно и tiny, а в более сложных случаев лично мне куда проще взять что-то чуть подороже, но для чего можно писать на C++. Если бы для меня это было работой, а не хобби, и была бы экономическая необходимость, я бы писал на ассемблере, но пока что никакой необходимости в этом не вижу.
Может вы просто плохо понимаете современный С++...
Ну Arduino достаточно жирный фреймворк. Да, он портирован чуть ли не все что с ножками. Но жирный
Ну вот только не надо про " нефиговый такой рантайм"! Если вы не используете потоковый ввод-вывод и исключения, то ничего лишнего в память не заносится. И про тонны опций сборки тоже не надо ля-ля. Сразу видно человека, который не владеет вопросом, а начитался бредней малограмотных погромистов. Даже реализация поддержки С++ в лице g++ на всяких Атмегах требует реализации нескольких функций обработки внутренних ошибок размером в несколько десятков байт кода.
Впредь не говорите того, чего не знаете.
Тогда может стоит сначала договориться, что мы понимаем под C++ ? Если вы убираете исключения, шаблоны, стандартные коллекции, RTTI - то это "С с классами", а не C++... И да, на "C с классами" можно прекрасно писать под любые контроллеры...
Если есть шаблоны, то это уже C++. Для тех же Arduino достаточно библиотек, в которых шаблоны ещё как используются. Например, EncButton от AlexGyver для работы с кнопками и энкодерами. Библиотека выдаёт весьма компактный и быстрый код. Или, например, библиотеки для работы с экранами - SSD1306Ascii. Также весьма компактная библиотека, в которой ничего лишнего.
А почему вы считаете, что если не использовать исключения, RTTI и стандартную библиотеку, то это не С++? С++ это большой набор возможностей, и каждую можно использовать, а можно не использовать. И ещё хорошо понимать, какую цену имеет каждая. Так-то в С++ есть куча фич, которые работают на этапе компиляции и абсолютно бесплатны в рантайме. Как пример, constexpr, namespaces, enum classes, перегрузка операторов, дефолтные значения для аргументов функций.
Об этом куча статей написана, вот например: http://fjrg76.com/2023/04/22/7-tips-noo-cpp-en/
Я об этом и говорю - надо договориться, что такое тогда C++. Я его видел еще с компилятора Borland C++ 3.1 - и по сравнению с тем, что было тогда, из языка сделали того еще монстрика, который вроде хочет пойти сразу во все стороны, но ни в какую из них не идет так, чтобы это было удобно. Для меня C++ без коллекций и std:: становится именно "C с классами" который когда-то через cfront компилировал программу на C++ в программу на чистом "C" делая this явным параметром функций. Что касается вычислений и мета-программирования на этапе компиляции - то этого я вообще стараюсь не касаться. В моем уютном мире, разработчик пишет код - компилятор транслирует - процессор выполняет. Идея запрограммировать хитрым инпутом компилятор чтобы он сгенерировал код, который при компиляции чего-то там сделает - мне кажется ущербной. Развитием этой идеи является мета-мета программирование: давайте напишем хитрый промпт для LLM, чтобы она сгенерировала хитрый код для компилятора, чтобы он (и далее по-тексту). Это, безусловно, очень интересно и занимательно - но я в таком не участвую.
В сухом итоге - если вы хотите программировать близко к железу или в условиях сильно ограниченных ресурсов - есть "C", если не нравятся явные указатели на функции - расширьте до "C с классами".
Если у вас много ресурсов - поставьте ОС и пишите на чем-нибудь безопасном типа джавы или питона...
А C++ со всем его могуществом и UB - для программирования микроконтроллеров подходит не очень...
C++ это в первую очередь реализация принципа "не платить за то, что не используется", что позволяет с одной стороны расти языку в монстроподобную кашу всяких новомодных фишек, и одновременно игнорить их, если они вам не требуются.
Поэтому С++ вполне себе нормальный язык для микроконтроллеров, а какие его возможности вы будете использовать, это исключительно ваше дело, так как не используемые вами фичи не добавляют в исполняемый код программы никакого лишнего оверхеда.
Это какое-то заблуждение про рантайм С++. Ардуино-ядро для attiny13 с 1Кб флэша компилируется с помощью g++ и ничего, никакие тонны рантайма не добавляются.
Скорее наоборот, чтобы подтянуть, например, исключения (которые добавляют несколько десятков Кб) нужны знания уровнем выше, чем у среднего ардуино-программиста
Вместо указателей на функции использовать ifdef __название_камня, и будет совсем хорошо.
Кажется, видел такой подход в библиотеке для W5500, предлагаемой производителем.
Уже реализовано в проекте modm.
Создание аппаратно-независимых библиотек для микроконтроллеров