Pull to refresh

Comments 29

Было бы неплохо ещё затронуть тему корутин, раз разговор зашёл про 20 стандарт(кстати, в расте асинки не требуют хипа).

"Потеря безопасности" это конечно эпично. Установка операционной системы на микроконтроллер и есть потеря безопасности. Дрыгание ногами через функции это потеря безопасности. Безумный уровень абстракций это потеря безопасности. А управление контроллером через регистры это и есть безопасность. Гвонокодинг добрался до микроконтроллеров. Я никогда не думал, что это случится. Но вот, дожили.

Аргументы будут? Как тогда сделать правильно?

Вы на правильном пути. Compile time error checking намного лучше защищает код от сложных ошибок. Более того использование c++ часто разрешает оптимизации которые компилятор не делает даже когда весь код в макросах..Я уже не говорю про полную потерю error checking с макросами

Напрямую в регистр можно записать случайно бит где вместо Open Drain поставится Push-Pull и сгорит вход. А теперь смотрим за руками: пишем компайл-тайм функцию записи в регистр, проверяем внутри допустимость записи для того или иного регистра. Вуаля - вход не спален. Ну и где безопаснее?

Не подскажете ли, как указать компилятору, подключен ли к выходу светодиод или вход от периферии или может быть монтажное ИЛИ на диодах? Лучше с примером исходника.

А кто вам сказал, что компилятор должен проверять еще и схемотехнику?

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

Подчеркиваю - для embedded-программиста!!!

Вы случайно embedded-программистов с embedded-говнокодерами не путаете?

И вообще, что такого нечитаемого в строчке, допустим, led1->turn_on() (раз уж вы тут про светодиоды заговорили)?

Почему возможность указать в одном месте какая периферия куда подключена - вдруг плохо, и "embedded-программист" должен держать в уме эту информацию? Кстати, что ваши "embedded-программисты" делают, если возникла необходимость изменить схему?

С говнокодерами уж точно не путаю.

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

"Нормальная абстракция" в моем понимании:

Один (ровно один, 1) заголовочный файл "hw_defines.h" с описанием целевой платы, какой GPIO на светодиод, какой на кнопку, какой канал I2C на какую периферию, какой канал DMA на прием данных по какому SPI и т.п.

И несколько файлов вида "hw_gpio.c", "hw_uart.c" и т.п. для работы с аппаратно зависимой периферией на регистрах.

И пишутся эти файлы под каждую целевую платформу, взяв в одну руку даташит на проц, во вторую-еррату, в третью - SDK от производителя, а в четвертую правила codestyle, принятые в Вашей компании, с Вашими блэкджеками и женщинами.

На выходе - абстрации вида

led_on(LED_STATUS), который дергает функцию из hw_gpio.c;

button_getstate(BUTTON_LEFT), который тоже дергает функцию из hw_gpio.c;

flash_write(*data, ...), который в свою очередь дергает функции из hw_spi.c

и т.п.

Все абстракции имеют четко оговоренные в том самом codestyle имена, правила вызовов и возврата и т.п. И тем самым покрытые один раз написанными юнит-тестами, не зависимыми от железа.

В результате мигрирование с платы на плату и даже с изделия на изделие в рамках одного типа процессора - это переписывание одного файла defines.h и дописывание файлов hw_xxx.c для периферии, которой раньше в проектах не было.

Мигрирование с проца на проц - переписывание тех самых файлов hw_xxx.c.

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

Меньше кода - меньше ошибок. Меньше специфичных для конкретной реализации конкретного языка (или упаси Б.г компилятора) - меньше проблем с совместимостью. Хотел бы я посмотреть на портирование ПО на проц, под который нет компилятора С++20 (а такие в Китае водятся, поверьте).

Короче - прицип бритвы Оккама, которому примененный в статье подход противоречит.

Если что, у меня опыт в embedded 25+ лет и изделия, к которым я руку приложил, работают по всей стране сотнями тысяч в режиме 24\7 (не бытовуха).

А какой фрагмент статьи заставил вас думать, что нужно размазывать работу с регистрами ровным слоем по всем исходникам? Или городить безумные слои абстракции?

Я вообще не 'показал подход' - я лишь показал, как добавить безопастность и не потерять ничего.

Compile time - это прекрасно. Мощь современного C++ с его шаблонами и constexpr (которые даже на CUDA-ядрах работают, например) сложно игнорировать. Проверки и защиты на этапе компиляции - это круто. Идентичный результирующий объем инструкций на выходе из компилятора - великолепно. Дополнительные (неочевидные) возможности для оптимизатора.

Автор проделал колоссальную работу. Спасибо ему.

На мой взгляд проблема в том, что не существует универсального подхода, нет общепризнанного стандарта. Есть бесконечное число разработок разного уровня и качества. Каждый по-своему решает однотипные задачи. И погружаться в эти решения нет желания и возможности. Здесь применили одно, в другом проекте иное. Где-то ошибка (да, она тоже может быть в этом слое абстракции).

И похоже, что понятный всем стандарт на сегодняшний день выглядит примерно так:

RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;

Хорошо, если когда-нибудь появится что-то вроде STL (с настолько же глубокой проработкой) для мира MCU для решения задач "подергать ножку" или "установить таймер".

Библиотека автора пока еще далека от CMSIS даже, т.к. нет работы с полями регистров (имена, R/W и R/O режимы, битовые маски). И совсем не сравнить с библиотеками на Rust, где для полей регистров на уровне типов указывается, есть ли вторичные эффекты при чтении/записи поля.

Но главное, что вендоры пока даже не пытаются исправлять SVD файлы, т.к. ими никто не пользуется. Перепутанные имена регистров, отсутствующая периферия, или наоборот несуществующая, RO поля, помеченные как RW и т.д.

Мало кто готов спуститься до этого уровня и поправить SVD файлы для нужного чипа. А вендоры не готовы принимать исправления. И пока это не поменяется, не возможно сделать автогенерацию кода регистров, и 99% разработчиков будут продолжать писать на С библиотеках вендоров.

В Rust кстати эту проблему решили тем что написали свой стандарт SVD. Ну и пишут с 0 SVD файлы, что довольно трудоемко, хотя они и выходят раз в 8 меньше файлов вендоров.

Библиотека автора пока еще далека от CMSIS даже, т.к. нет работы с полями регистров (имена, R/W и R/O режимы, битовые маски).

Не могли бы вы пояснить, что вы имеете ввиду?

Посмотрел подробнее примеры в конце статьи, поля регистров всё-таки есть (тут я был не прав).

А запутали меня строки:
GPIOA->ODR &= GPIO_ODR::ODR[NUM_4] | GPIO_ODR::ODR[NUM_10];

Из которых не следует что поля регистров вообще где-то описаны, или генерятся из SVD файлов.

Думаю это просто неудачный пример, т.к. регистры GPIO не подразумевают нормальных имен полей. Предлагаю эту часть статьи переписать, взяв например регистры и поля RCC. Тогда плюсы автогенерации из SVD будут всем очевидны.

P.S. Теперь ясно, что оно гораздо лучше CMSIS, при условии что вы сами поправите/напишите SVD файлы нужной перифирии.

Я согласен с вами. Не понял только один момент:

И похоже, что понятный всем стандарт на сегодняшний день выглядит примерно так:

RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;

Но ведь это же и есть одна из главных фишек моего решения

// Аналогичная строчка на cpp_register
RCC->AHB1ENR |= RCC_AHB1ENR::GPIODEN;

Или имеется ввиду то, что под капотом?

Проблема не в записи RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;

Проблема в том что стоит за этими AHB1ENR, GPIODEN. Туда ли они указывают куда надо, и можно ли именно в это время туда обращаться.
Мегабайтные хидеры с описанием регистров микроконтроллеров никто не обязывался делать абсолютно безошибочными.
На своей практике я пришел к мысли что лучший способ писать так:

  reg8 = 0
         + LSHIFT(1, 7) // TIE      | Transmit Interrupt Enable
         + LSHIFT(1, 6) // RIE      | Receive Interrupt Enable
         + LSHIFT(1, 5) // TE       | Transmit Enable
         + LSHIFT(1, 4) // RE       | Receive Enable
         + LSHIFT(0, 3) // MPIE     | Multi-Processor Interrupt Enable
         + LSHIFT(1, 2) // TEIE     | Transmit End Interrupt Enable
         + LSHIFT(0, 0) // CKE[1:0] | Clock Enable. 0 0: On-chip baud rate generator
  ;
  R_SCI1->SCR = reg8;

Потому что только так можно соотнести что вы написали и что на самом деле гласит даташит. Да даже даташиты не безгрешны.

Запись RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; - это чтение-модификация-запись. А можно ли так делать в конкретной ситуации? Что тут произойдет в случае прерывания? Как С++ ответит на такой вопрос? А как в отладчике будет выглядеть проход по шаблонам. Можно ли будет просто найти соответствие между ассемблерным кодом и кодом шаблона?
При работе с регистрами периферии есть проблемы посерьезнее простой путаницы в именах битов. Там проблема просто понять работу этих битов и их зависимости. Поэтому лучший путь никак не маскировать и не шаблонизировать, а использовать прямые однозначные записи по явным адресам с подробными коментариями.

Если я правильно понял, что своим примером вы обозначили, что заменяете 'чтение-модификация-запись' на 'запись'. Никто не запрещает так делать и с моим решением.

Отвечая на вопросы:

А можно ли так делать в конкретной ситуации? Что тут произойдет в случае прерывания?

А как на это отвечает CMSIS? Не всегда возможно заменить 'чтение-модификация-запись' на запись 'запись' Думаю, что тут программист может за это отвечать.

Как С++ ответит на такой вопрос?

Также как и C

А как в отладчике будет выглядеть проход по шаблонам?

Я особой разницы не вижу.

 Можно ли будет просто найти соответствие между ассемблерным кодом и кодом шаблона?

Можно, активно это делал во время разработки.

Просто ваше решение опирается на сомнительные имена. Уделяете много внимания незначительной проблеме и упускаете более серьезные и трудно отлаживаемые.

Можно, активно это делал во время разработки.

Вот это и было бы интересно посмотреть. И сравнить с отладкой без избыточных текстовых конструкций.

Просто ваше решение опирается на сомнительные имена.

Честно говоря, я вообще не понимаю, что это значит

Уделяете много внимания незначительной проблеме и упускаете более серьезные и трудно отлаживаемые.

Приведите, пожалуйста, пример таких проблем, которые я упускаю (как это вообще можно понять из данной статьи?).

Я повторю, как и для комментатора выше, если я посвятил время теме регистров, это не значит что я только и я только этому и посвящаю свое время. И если я тчо-то не описал, это не значит что этого нет.


Вопрос в том, какие проблемы значительные тогда? Вы привели кусок кода (без контекста), по которому не особо и понятно, что это и для чего. При этом, выглядит еще и непонятнее того, что я предлагаю.

Там проблема просто понять работу этих битов и их зависимости. Поэтому лучший путь никак не маскировать и не шаблонизировать, а использовать прямые однозначные записи по явным адресам с подробными коментариями.

Я и сам уважаю принцнцип Keep It Simple Stupid. Однако, по такой логике, вообще ничего нельзя использовать, если не уверен до конца как это работает внутри, ни одной библиотеки, вообще.

Может немного не в тему.

Недавно довелось разбираться с кодом, в котором управление периферией (драйверы) были реализованы через классы (и прочие инструменты С++).

Претензии у меня были следующие:

  • Стремление к абстрагированию и обобщению лишь «захламляет» код. Не получится сделать обобщённые подходы к настройке АЦП и таймера высокого разрешения, надо будет всё равно погружаться в документацию на МК и вникать в назначение аппаратных регистров и битовых полей.

  • Вводит дополнительный барьер для понимания кода. То есть, если бы управление аппаратурой осуществлялось напрямую через регистры (либо небольшой набор простых функций/макросов) для понимания целей манипуляции достаточно документации на МК. В случае с реализованным подходом нужно: либо качественная документация на класс драйвера (по объёму соизмеримая с описанием на аппаратные регистры в документации на МК), либо лезть разбираться в код (без комментариев, с несколькими наследованиями и тд, за что я даже не брался).

То, что вы перечислили, это же не проблемы C++

На самом деле, с точки зрения концепции, при высоко абстрагированном подходе, С и С++ не сильно и отличаются. Взять хотя-бы тот-же 'объектно-ориентированный' C, с указателями на функции и композицией. На нем ведь написано очень много чего, тот же Linux например.

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

Однако, правда и в том, что C++ программы склонны к переусложнению, просто из-за объема языка и его возможностей (довольно часто попадался такой код). Если именно это главная проблема, то я бы посмотрел на Rust.

Похожий проект, тоже от русского автора, тоже старается использовать шаблоны и возможности новых стандартов C++.
Из интересного:
* Вычисление итогового значения регистров при стартовой конфигурации периферии в compile time
* Объединение нескольких GPIO причем с разных физических портов, в виртуальный порт.

Вы приводите в пример опечатки. Их так же можно заложить в ваш промежуточный слой. А при смене архитектуры - мучиться и перетаскивать его. Хотя, все придумано до нас - есть LL, с которым чтение куда выше, чем прямое тыканье в регистры, хотя часто там тоже просто макросы поверх прямого тыканья, только они худо-бедно от производителя.

Операции с несколькими полями за раз? Тут тоже может прилететь, когда за раз делаете init и start. Это тоже частая ошибка, как и описки с именами шин, или натягивание чужих enum'ов.

Открывая статью, я хотел увидеть порядок в коде, а увидел снижение читаемости с переносом вероятности ошибки в другое место.

И оно так и будет, пока авторов "if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)" не начнут бить за код в стиле "змея съела верблюда и, быть может, подавилась макросом"

Для системности нужен хороший статический анализатор, а не прокладка. Но если вам подходит, то ок.

Отвечу то же, что отвечал уже несколько раз под этой статьей.

Я НЕ призываю писать все на регистрах. Этот слой нужен для написания драйверов, и только в случае если вам нужно написать драйвера.

Расскажите мне пожалуйста, как статический анализотор поможет диагностировать ошибки, упомянутые в статье.

...после ленивой годичной практики языка, я все же был вынужден заключить, что в разрезе embedded программирования (где можно и нужно активно использовать программирование времени компиляции) Rust не только не делает качественно скачка вперед, но и даже в чем-то уступает C++ в 2024 году.

А вот про это можно поподробнее? В чём уступает? Мне на ум только меньший охват целевых платформ приходит.

Это. Просто. Офигенно) Спасибо) Поиграюсь на досуге)

Sign up to leave a comment.

Articles