Pull to refresh

Comments 181

К сожалению, ошибки доступа к портам — это только малая часть ошибок, которые можно совершить, взаимодействуя с физическим миром средствами МК, и эти ошибки (в отличие от архитектурных) ловятся более-менее вдумчивым code-review в статике.

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

То же можно сказать и про чтения — «разрушающие» чтения регистров статуса — превалирующая практика внутри SoC и частая снаружи. Методы работы с такими полями и фактический порядок обращения к битам регистра должен быть абсолютно прозрачен для программиста, его использующего.

В целом, наверное, полезно для простых задач, но для хитрого bitbang'а например с совмещением линий ВВ для индикации и ввода, а также для применений, требующих прозрачности управления линиями, применение может быть спорно
К сожалению, ошибки доступа к портам — это только малая часть ошибок, которые можно совершить, взаимодействуя с физическим миром средствами МК, и эти ошибки (в отличие от архитектурных) ловятся более-менее вдумчивым code-review в статике.

Насчет code review согласен. По-моему опыту да действительно большинство ошибок ловятся, но к сожалению не все. Это сильно зависит от того, кто проводит ревью, и какое у него настроение.

По поводу псевдопараллельных групповых методов — порядок установки групп битов — одновременно или по очереди — может иметь критическое значение для правильной работы с подключенной через GPIO к МК системой. Метод, который скрывает последовательную установку внутри своей реализации, следовательно, потенциально опасен
.
Да есть такое, поэтому и не хотел использовать, просто распаковку параметров шаблона. Если ставить по очереди, то лучше пользоваться конкретным пином, а если группу, это я в этой статье не описал.
То же можно сказать и про чтения — «разрушающие» чтения регистров статуса — превалирующая практика внутри SoC и частая снаружи. Методы работы с такими полями и фактический порядок обращения к битам регистра должен быть абсолютно прозрачен для программиста, его использующего.

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

но для хитрого bitbang'а например с совмещением линий ВВ для индикации и ввода

Согласен, у нас просто запрещены трюки и слишком «smart» код. Как раз в целях поддержки и надежности. Т.е. использовать нужно только стандартные подходы, код должен быть читаемым и понятным. Кроме того, переносимым еще с контроллера на контроллер.
Для специфических приложений, конечно согласен. Но я и не говорю, что это пилюля от всех проблем, это просто один из подходов. Вообще считаю, что в каждом случае подходы могут разные, и должны выбираться исходя из разных критериев, время, качество, ресурсы, поддерживаемость, быстродействие. В общем согласен с вами.
Т.е. использовать нужно только стандартные подходы, код должен быть читаемым и понятным. Кроме того, переносимым еще с контроллера на контроллер

Это работает, пока задача близка к тривиальной и/или нетребовательна к ресурсам, специфичным именно для выбранного SoC-чипа. Условно говоря — этикет за столом присутствует, пока еды на всех хватает.
Как только начинает ощущаться недостаток ресурсов — фантики абстракций разворачиваются и выбрасываются в корзину первыми, и на поверхности появляются сами конфеты
Совершенно верно, и часть таких проблем мы решили с помощью, «убрать всю настройку в одно место, low_level_init». Понятно, что иногда бывают такие тонкости, что нужно ухитриться сделать финт ушами, но очень редко и это порицается всеми в команде, а особенно аудиторами из сертификации на SIL2/3. Поэтому у нас даже в стандарте по дизайну написано, не будь слишком умным, чтобы за тобой потом могли другие люди это поддерживать.

я, в частности, для переносимости взял Ivory и сделал свой фреймворк над ним

И при чем тут «надёжность софта»? Куча абстракций, какие-то шаблоны. Чем это лучше обычного процедурного подхода CMSIS?
И при чем тут «надёжность софта»?

В данном случае, пользователь Pin типа не сможет сделать то, что может потенциально привести к проблеме, например, перенастроить Pin на выход, если он должен быть настроен только на вход. Это вам не позволит сделать компилятор. С регистрами также, те регистры, для что предназначены только для чтения не дадут вам возможность что-то попытаться в них записать, компилятор руганется. И вообще позволят записать только, то, что можно. RCC::AHB1ENR::GPIOAEN::Enable::Set() запишет значение Enable в поле GPIOAEN регистра AHB1ENR модуля RCC, и вы просто не промахнетесь никак ни с регистром, ни с полем, ни со значением… вы же можете написать, используя CMSIS так?: RCC->APB1ENR |= RCC_AHB1ENR_GPIOAEN, а на выше показанном примере не сможете.

Куча абстракций, какие-то шаблоны.

А чем шаблоны вам не нравятся? передача параметров в <> вместо в ()?

Чем это лучше обычного процедурного подхода CMSIS?

Ну только надежнее и код оптимальнее (как с точки зрения ОЗУ, так и ПЗУ), а так в принципе идеи одинаковые — абстрагироваться от аппаратуры.

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

Вступлюсь, пожалуй, за коллегу. Хотя и не очень согласен с его постановкой вопроса.


Дело ведь не в скобочках. И непривычная семантика легко становится привычной. Дело в другом. Хорошо, допустим уровень абстракции портов получился красивым. Оно действительно так — тут ничего не скажу. Но порты одна из самых простых вещей. А как быть с остальной периферией? И ладно бы только с периферией.


Мне кажется, что такой подход выпускает джина. Если раньше приходилось воевать на привычном поле битов и адресов, то теперь мы воюем на поле конструкторов и наследования. А это уже поле непривычное, а в случае с контроллером ещё и довольно сложно поддающееся отладке. Простого дампа памяти наверняка окажется мало.


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


И да, все написанное только мое мнение. Я совсем не утверждаю что так нельзя или неправильно. Мир большой и каждому решению найдется свое применение.

А что с конструкторами не так? Это же как раз очень просто, понятно и удобно. Как и наследование впрочем, если оно вдруг понадобится. Возможно вы привели неудачное сравнение, но я не вижу тут объективного противопоставления битам и байтам.


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

Я же не говорю что здесь хоть что-нибудь "не так". Как раз наоборот — здесь всё довольно красиво. Во всяком случае в рамках обозначенного объёма.


Давайте я озвучу свое понимание проблематики. Мне крайне редко (чтоб не сказать никогда) встречались системщики, которые находились бы ближе к прикладникам, нежели к схемотехникам. А те, ко то из нас, которые "от железа" к языкам уровня выше C относятся очень настороженно. Если хотите, то могу и причину назвать — все они так и норовят по каждому чиху использовать динамическое выделение памяти. А это затраты времени, и, что хуже снижение надёжности и отказоустойчивости кода. Отчасти поэтому мы и не любим работать парой, а ещё хуже тройкой над одним проектом. У каждого свои приоритеты и дольше будет договориться как именно мы дружить будем, чем одному написать.


Дело не в том, что мы ленивы и не способны выучить другой язык. Дело в том, что у нас аж две священных коровы: скорость и надёжность. Не можем мы их резать в угоду удобства, а тем паче моды. Для нас и C высоковат. Очень часто приходится бороться с его неоправданными оптимизациями. Или спускаться на уровень ниже, там где волшебник C странно трактуется директивы register. А наследование мы и на C можем. Быстро и в достаточном объеме. Вон CodeRush рядом пример приводил. Да и весь драйверный код Linux тому хорошее подтверждение.


Но вернёмся к статье. По мне тут вилочка отрисовывается. Одно дело, когда у автора глобальная задача требует плюсов и ему этот "котел" роднее и известные. Не вопрос. Сел, продумал, написал — просто красавец. И другое, когда над этой "отвязкой от железок" окажется прикладник, привыкший к богатству ресурсов ПК.


Но современный мир сильно отличается от того, с которого я начинал. Сотни мегагерц тактов и сотни килобайт оперативки, мегабайты флеш-памяти. В принципе, добавь MMU и контроллером оно уже не будет. Поэтому стоит воспринимать меня как старого брюзгу. Возможно за подобными решениями будущее. Вон та же Apple давно и довольно успешно переписала ядро BSD на плюсах. По мне весьма недурно получилось. Так что поживем — увидим. В конце концов когда я начинал ряд старичков так же брюзжал "зачем здесь контроллер и программа — тремя логическими микросхемами и пятком RC-цепочек обойдёмся".

Мне крайне редко (чтоб не сказать никогда) встречались системщики, которые находились бы ближе к прикладникам, нежели к схемотехникам


Ну, это по-разному бывает, а вот чего не отнять, так это того, о чем я написал выше — в эмбеде часто три с половиной землекопа, на которых все подряд. Особенно in Soviet Russia. Приходится выбирать между тем, чем нельзя не заниматься, и тем, что не необходимо. И это нормально, просто не надо ни гордиться этим, ни обличать это, ни устраивать холиваров вокруг. Часто еще в качестве ограничения бывает тулчейн такой, какой есть.

Дело в том, что у нас аж две священных коровы: скорость и надёжность


Как будто у плюсов с этим хуже, чем у pure C.

А те, ко то из нас, которые «от железа» к языкам уровня выше C относятся очень настороженно. Если хотите, то могу и причину назвать — все они так и норовят по каждому чиху использовать динамическое выделение памяти.


Я тоже «от железа», и никакой настороженности не испытываю. В том числе и в отношении new/delete, если просматривается горизонт.

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


Смотрите как быстро мы скатываемся в политику. На ровном вроде бы месте.

Еще раз — мир большой. Раз какие-то решения возникают, значит в них есть необходимость и потребность. Я ничего не имею против плюсов, раста, питона, да того же бедуиского языка от Arduino и много чего еще. И даже во встраиваемых системах.

Но сильно хочу, чтоб системы перевода стрелок и управление светофорами на железной дороге оставались аналоговым, на поляризованных реле с несимметричным отказом. А код для ЭБУ автомобиля был не просто написан на C, но проходил сертификацию по MISRA. А в самолетах и того круче. И свои код я хочу писать так, чтоб «как для самолета», в самом крайнем случае «как для автомобиля». И язык, на котором работаю, выбираю исключительно из этих соображений. Возможно, на других языках можно не хуже. Но моим заказчикам нужно вчера. И даже если нужно было бы завтра заменить многолетний опыт работы с одним языком неглубоким знакомством с другим — решение крайне сомнительное.

Собственно все. Еще раз — пусть расцветают сто цветов, пусть соперничают сто школ. Но не надо меня силком тащить в светлое завтра. По мере надобности и интереса я сам все осилю. Как в свое время осилил плюсы, как сейчас разбираюсь с Rust. Но пока мнение осталось тем же — pure C оптимум по соотношению сложности к гибкости для всего, что «непосредственно шуршит по регистрам»

В том числе и в отношении new/delete, если просматривается горизонт.


Вот именно это «если» меня и не устраивает. Я знаю чуть ли не с точностью до такта сколько времени выполняется та или иная часть программы. Я знаю максимальную интенсивность событий, на которую железка сможет прореагировать, я знаю как она поведет себя в случае перекосов. Увы, я всего этого не знаю, если есть динамическая память.

Да, еще раз — сейчас уже не приходится «ксорить указатели» ради экономии пары байт. Но я-то еще помню как и зачем это делается. И если теперь не надо экономит пару байт, это еще не говорит о том, что можно потратить сотню-другую тактов на то, что просто поиметь буфер для записи. Ибо такты тоже не бесконечны, а вместе с ростом производительности и объемов памяти у микроконтроллеров растут и хотелки, которые на них вешаются. И тут начинается… FreeRTOS или ее аналоги с ее системным таймером и временем реакции кратном системному таймеру, или голое железо с логикой внутри обработчиков прерываний и перекидыванием длительных кусков кода между разными обработчики и тупой сон в фоне.

Ну, или бьем комаров атомными бомбами (а чего мелочиться — они нынче сильно подешевели). И подумаешь на каждого комарика по бомбе — мы ж не на нищебродов работаем… Впрочем, простите, накипело… Давайте считать, что весь этот абзац ограничен тегом «сарказм»
Смотрите как быстро мы скатываемся в политику


При чем тут политика? Это скорее про экономику — такая вот экономика в эмбеде.

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


Да пожалуйста, непосредственно сами схемы управления стрелками и сигналами могут оставаться релейными, как и схемы рельсовых цепей. В конце концов там совсем немного аппаратуры, к тому же реализация оконечных устройств на полупроводниках всегда сложна, поэтому многие современные системы РПЦ и МП ДЦ используют готовую инфраструктуру напольных устройств. Но вы же наверное не предлагаете вернуть в строй скрежещущих монстров системы БМРЦ и ПЧДЦ? Если нет, то без сотен тысяч строк различного кода никак не обойтись, и их надо на чем-то писать. Так что это все лишняя патетика, бросьте.

А код для ЭБУ автомобиля был не просто написан на C, но проходил сертификацию по MISRA


Отлично, MISRA C++ 2008 же. :)
Я вас разумеется не уговариваю, просто для справедливости дискуссии.

Давайте считать, что весь этот абзац ограничен тегом «сарказм»


Говорю же, написанные вами слова мне хорошо знакомы, и это именно что контратака верхом на самооправдании — зачем мне ваше вот это вот, оно медленное/ненадежное/somethingelse. А настоящий ответ очень простой — всего лишь некогда. И нечего тут стесняться и незачем оправдываться. Самая короткая дорога — та, которую ты знаешь.

Впрочем при современном состоянии тулчейнов в большинстве случаев почти нет поводов писать на чистом Си. Всегда можно использовать какой-то сабсет С++.
Отлично, MISRA C++ 2008 же. :)
Я вас разумеется не уговариваю, просто для справедливости дискуссии.


Дык я и не хочу сводить дискуссию к любым проявлениям священной войны. Лишнее это. Поэтому давайте ограничимся тем, что:

Самая короткая дорога — та, которую ты знаешь.


Впрочем, не могу не признать Вашу правоту. MISRA C++ я, к стыду своему, даже не смотрел. Пожалуй стоит наверстать.

P.S.
Единственный момент — реле дороже проводников, но у него три четких состояния включено, выключено и неисправно. Его любят именно за это.
Отлично, MISRA C++ 2008 же. :)

А MISRA C++ 2008 поощряет TPL?!
Что вы в данном случае подразумеваете под TPL?

Не отвергает точно, честно говоря, там 80%, а то и больше правил просто копия MISRA C.

Шаблонные классы и функции вполне себе разрешены. Без них было бы совсем грустно.

Ясно, не дотянулись еще, значит… )))
Ну а чего бы и нет, если не упарываться в магию, то это же хорошо.

Я слегка запоздал на огонек, но по поводу спонтанных аллокаций могу посоветовать:
1) Ставить размер кучи равным 0 — все компилируется, но встает до входа в main на инструкции BKPT
2) В Keil можно сказать #pragma import(__use_no_heap), тогда будет ошибка линковки при попытке юзать кучу
3) В том же Keil после компиляции генерируется htm-файл, в котором можно проследить, кто вызывает malloc.

Ну, что сказать по этому поводу…

Мое мнение — HEAP нельзя ставить в ноль. Даже если я его не использую. В моих проектах это не куча, а резерв для переполнения стека. Больше того, я люблю ее каким-нить шаблоном забить типа 0xdeadbeaf и после ресурсных нагрузок проверить все ли хорошо, а то увеличить стек.

Ну и выбор компилятора — личное дело каждого разработчика (или корпоративного стандарта). Многие любят IAR, но IHMO, его время ушло. Все, что там осталось хорошего — это серьезная возможность настройки компонентов библиотеки языка. Возможность отключить поддержку локалей, юникода и редких printf/scanf форматеров дорогого стоит. Все эти моменты во встраиваемых системах не интересны, а размер итогового файла раздувают. Сейчас я пишу код так, чтоб работало с IAR и GCC. Чтоб или пользоваться благами IAR, или не иметь проблем с лицензиями. Так вот — GCC, как компилятор, куда как более предсказуем и беспроблемен. Ибо IAR'овская трактовка -Onone… «Мои вкусы весьма специфичны» (с). А остальные уровни оптимизации еще интереснее. Мне даже иногда начинает казаться, что те или иные оптимизации он рандомно выбирает. Впрочем, мало в последнее время с ним работать приходится. И не с последними версиями. Может быть все давно починено, просто я не знаю.

Но, собственно, отступление про компиляторы — это только к тому, что как только появляется компиляторно-специфичный кусок, так этот кусок надо срочно выжигать каленым железом. Даже если тебе поют песни о том, что «это стандарт предприятия и никогда ничего другого не будет». Будет. Вопрос только во времени. Есть, конечно, альтернатива — не будет предприятия. Но разработчик ПО тоже должен заботиться о том, чтоб до такой крайности не доходило. Он же программист…

По поводу переполнения стека могу бессовестно посоветовать мою статью на эту тему :) Хотя IAR я в ней не рассматриваю, поскольку не использовал, но, полагаю, что подход можно и на него натянуть.


Но "нецелевое" использование кучи — имхо ну такое себе решение. Я бы рекомендовал просто маркерный массив выделять по фиксированному адресу, если такой способ вам больше нравится, а кучу все-таки занулить. Иначе получается, что вы рассчитываете на конкретное расположение кучи и стека, которое стандартом языка не гарантировано; а если вы его сами контролируете через линкер, то можно и обычный массив разместить после стека или что-то более веселое проделать.


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


Для gcc можно еще вот так сделать.

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


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

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

Нецелевое использование… Ну да, не здорово. С другой стороны — а чем куча будет отличаться от массива? Я ее принципиально в своем коде не использую. Вся память у меня распределяется на этапе компиляции. Пока, во всяком случае, удавалось справиться так.

Если вы контролируете размещение стека и кучи, то можно воспользоваться таким трюком — разместить в самом начале ОЗУ стек, а кучу убрать. Тогда при переполнении стека будет HardFault из-за доступа к несуществующей памяти (при желании, там же можно и запас разместить).


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

Согласен, тоже так делаю. А не подскажете, как вы в коде узнаете размер таблицы векторов?


Нецелевое использование… Ну да, не здорово. С другой стороны — а чем куча будет отличаться от массива? Я ее принципиально в своем коде не использую. Вся память у меня распределяется на этапе компиляции. Пока, во всяком случае, удавалось справиться так.

В С++ с этим несколько сложнее :) Стандартная библиотека очень любит аллокации, приходится ее сдерживать.
Вообще, если мне память не изменяет, в С тоже есть несколько стандартных функций, которые выделяют память по-тихому, что-то из <string.h> этим грешило.


Соответственно, я предпочитаю перекладывать проверку отсутствия аллокаций на компилятор, а не помнить самому, что там может проаллоцировать, а что нет.


Плюс иногда стандарт не оговаривает, нужно кучу использовать или нет.
Например, у Keil'a вспоминается два прикола такого рода — alloca, которая должна выделять массив на стеке, использует кучу для каких-то внутренних нужд. Или если создать статический объект класса, у которого есть виртуальный деструктор (а виртуальный он просто чтобы warning убрать), то Keil тоже резко захочет кучу.


Короче, только на себя полагаться мне уже страшно.

А это и не надо. Просто структуру с идентификационными данными располагаем в определенной секции (.prod_id_data, например). А потом эту секцию располагаем сразу за векторами прерываний.

В IAR это так:
...
define block PROG_SPECIAL with alignment = 16, size = 0x80 { section .prog_id_data };
...
place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec, block PROG_SPECIAL };
...


Для GCC так
...
/* Define output sections */
SECTIONS
{
/* The startup code goes first into FLASH */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH

.prog_id_data :
{
. = ALIGN(16);
KEEP(*(.prog_id_data)) /* PROG identification data */
. = ALIGN(16);
} >FLASH

/* The program code and other data goes into FLASH */
.text :
...
}


И в коде программы как-то так:

#ifdef __GNUC__
#define __root __attribute__(( section(".prog_id_data")))
#endif

#pragma location = ".prog_id_data"
__root const prog_config_data prog_config = {
...
};


Все. Секция всегда помещается сразу за векторами. А размер таблицы векторов — уже дело компилятора.

Про остальное — ну, вот именно этот момент меня и задерживает от перехода на плюсы. Грамотный аллокатор — штука не самая простая. К тому же довольно компромисная в части скорость-фрагментация. Очень не хочется терять такты и/или в нужный момент оказываться без необходимой памяти. Но еще раз — я согласен, это подход динозавров. Потому пока динозавры хоть кому-то нужны я предпочту «закатывать солнце вручную». Потом придется переучиваться, но это же потом…
А это и не надо. Просто структуру с идентификационными данными располагаем в определенной секции (.prod_id_data, например). А потом эту секцию располагаем сразу за векторами прерываний.

Все. Секция всегда помещается сразу за векторами. А размер таблицы векторов — уже дело компилятора.

Спасибо, такое мне в голову не приходило.
Жаль только, что линкер придется трогать.


Я пока что просто держу константный массив с инфо-строкой, а открывая прошивку просто ищу его поиском.


Про остальное — ну, вот именно этот момент меня и задерживает от перехода на плюсы. Грамотный аллокатор — штука не самая простая. К тому же довольно компромисная в части скорость-фрагментация. Очень не хочется терять такты и/или в нужный момент оказываться без необходимой памяти. Но еще раз — я согласен, это подход динозавров. Потому пока динозавры хоть кому-то нужны я предпочту «закатывать солнце вручную». Потом придется переучиваться, но это же потом…

Ну так аллокатор-то использовать совершенно необязательно; я ведь тоже без кучи живу и прекрасно себя чувствую.


Часть стандартной библиотеки использовать не получается — ну и пёс с ней, это в основном контейнеры. Если очень хочется — есть ETL, где все контейнеры фиксированной длины.


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


Стандартная библиотека более богатая и с более приятным интерфейсом (местами); банально есть std::nth_element, чтобы медиану посчитать.


Last but not least — да, на С можно писать объектно-ориентированный код, но выглядит это отвратительно, столько работы, которую взял бы на себя плюсовый компилятор, приходится делать руками.


Конечно, С++ — это не страна сплошных медовых рек, но конкретно потребность в динамической памяти, имхо, далеко не главный минус.

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


Я же и не спорю. Просто привык к тому, что мои задачи не требуют особой тригонометрии или медиан. Но крайне критичны ко времени реакции системы. Да и начинал учиться у ассемблерщиков. А те ребята занятные. С одной стороны кроссплатформенный ассемблер на сплошных макросах, с другой жутко платформо-специфические оптимизации. До сих пор не понимаю как это могло уживаться в одном человеке. Но так и пошло — то, что переносимо, то на С, что сильно критично ко времени, то… тоже на С, за очень редким исключением. Как правило нет нужды в трюках с платформой, достаточно применить более толковый (но более непонятный) алгоритм. Ну и прагмами или ключами компиляции побороться с излишне умным компилятором.

Наверное, когда-нить, на других задачах я возьму и плюсы. Но пока у меня «мышление на С». Больше 10 лет работы непосредственно программистом, и еще 15 в учениках, да над очень сходными задачами даром не проходят… Вон, даже тот же Rust с трудом воспринимается. Хотя, казалось бы — те же яйца, только в профиль.

Да я ж тоже не настаиваю :) Если вас все устраивает и вам удобно, то почему нет?
К тому же, опыта у вас заметно больше, чем у меня, вам наверняка виднее.


А Rust — штука хорошая, но на С он слабо похож, на мой взгляд, такое сравнение только мешает.

Я довольно долго думал, стоит ли отвечать. Хабр уже многому научил. Но решил, что все же отвечу. Правда не обещаю что дискуссия продлится долго. Считайте, что это не приглашение к разговору, а затравка «на подумать и обсудить в курилке с коллегами». Ибо вопрос крайне спорный, более того, всегда есть возможность узнать больше и диаметрально поменять свое мнение. Как в одну, так и в другую сторону.

Ладно, отмазу написал, теперь можно и по делу…

Да по сути все языки одинаковые. Особенно низкоуровневые. Присвоения, циклы, математические и логические операции, управление памятью. Ничего нового ни один язык не преподносит. По сути они и отличаются только библиотекой и очень незначительными деталями в реализации базового функционала. Привет Алану Тьюрингу.

И нет принципиальной разницы, чем он «ворочает». Хоть объектами, хоть физическими байтами. Все равно все в конечном итоге скатывается лишь к тем операциями и с теми ограничениями, на которые способен процессор. Да и все процессоры, при всем их богатстве: от AVR и 8051 до ARM и MIPS, через Эльбрус и SPARC мало чем друг от друга отличаются.

Практически, даже не важно для физической или для виртуальной машины язык плодит код тот или иной язык. Виртуальная машина практически ничем не отличается от физической. Во всяком случае со времен Кнутовского MIX'а мало что поменялось. И между ним и JAVA больше сходства, чем отличий.

Через некоторое время приходит понимание простой истинны — знаешь один язык, знаешь все. Ладно, не все. Языки класса Prolog и Lisp стоят в сторонке, но к счастью они не столь часто встречаются «в дикой природе». А остальные практически близнецы.

А почему же их так много? Да все просто — каждый язык находит свои задачи. Обратное тоже верно — каждая крупная задача производит для себя язык. Нужна была обработка текстов и анализ на спам — родился Perl (Raku?). Нужна была генерация страниц — появился PHP. Нужно моделирование реального мира — C++ с сотоварищами. Да мало ли примеров — они у всех на устах.

И с таким подходом Rust реально мало чем отличается от C. Давайте исключим синтаксис (он, как известно, дело вкуса разработчиков языка и только) и посмотрим что останется. А останется… Ну да, безопасная работа с массивами данных. А за счет чего? Правильно, за счет усложнения кода стандартной библиотеки языка. Она вобрала в себя все Best Practice и прочие наработки по данному вопросу. А по другому и быть не может — никаких новых аппаратных блоков, нацеленных на безопасность обработки блоков данных в процессорах не появилось. Надеюсь, теперь мне простят сравнение Rust и C.

А нужен ли при таком раскладе C (тот, который без плюсов)? По мне нужен. Он как раз является тем самым «кросплатформенным ассемблером» и при этом как и положено ассемблеру не имеет никакой специализации. Я не вижу на горизонте языков, которые могли бы так же. Да, был в свое время Pascal. Но он сильно сдал.

Вот так как-то. Не судите строго. Еще раз повторюсь — это скорее для обсуждения в курилке или под пиво, чем для Хабра.

Я, в целом, согласен, с парой не слишком существенных оговорок.


  • Конечно, почти все языки по Тьюрингу полны, а потому в каком-то смысле равномощны. Но лезть грязными лапами в неуправляемую память могут (или легко могут) не все, по факту остались — С, С++, Rust, D, Go (наверное?) и Swift какой-нибудь. Так что в этом смысле, да, Rust похож на С.
  • В Rust'e нет классов с конструкторами, а только структуры с прямой инициализацией полей; опять же, как в С.
  • Безопасная работа с памятью в Rust достигается все-таки не только силами стандартной библиотеки, а в первую очередь силами borrow checker'a, который статически проверяет заимствования. Стандартная библиотека — это уже надстройка для большего удобства.
  • Синтаксис у Rust и C разный. Вроде бы не сильно (хотя заметно), но во многом "синтаксис определяет сознание" и отделяет т.н. "идиоматические" конструкции от "чужеродных".

Вот этой самой "идиоматикой" Rust на С не очень похож, на мой взгляд. Скажем, на С невозможно сделать RAII, потому что нет деструкторов — поэтому подсчет ссылок может быть только ручной.
Всякой функциональщины в Rust тоже больше.


Ну и просто, как сказать? Философия иная, что ли. Безопасность на первом месте, а не "программист не должен ошибаться".
Поэтому от кода на Rust создается совсем другое ощущение в целом, что ли.


Отмажусь тоже, что я с вами не спорю, просто делаю ремарку, так сказать. Да на Rust я продакшен не пишу, так, играюсь. Поэтому все вышеперечисленное — эт просто мое мнение, которое я никому не навязываю :)

… Философия иная, что ли. Безопасность на первом месте, а не «программист не должен ошибаться».
Поэтому от кода на Rust создается совсем другое ощущение в целом, что ли.


Увы, языка на котором может быстро, безопасно и грамотно писать обезьяна еще не изобретено. И крайне маловероятно, что он хоть когда-нить появится. Потому «мастер не должен ошибаться» парадигма вечная и не зависящая от сферы деятельности и используемых инструментов. И в равной степени относится как к строителям и продавцам в Пятерочке, так и к IT'шникам вообще и программистам любых направлений в частности.

А ощущения… Так они субъективны. У каждого мастера свои предпочтения в части инструмента. Кто-то фанатеет от Makita, кто-то от Bosch, а кому-то и Hummer «не настолько хуже, насколько дешевле». Стоит ли непосредственно ассоциировать качество итогового продукта с инструментом, которым он сделан?

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

Как всегда — есть множество крайностей. Встречаются люди, свято уверенные в том, что мастеру надо выдать веревку. А уж камень для топора и деревяху для ручки он и сам найдет. А дальше сам себе со временем все инструменты сделает. Увы, такое тоже не редкость.

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

Потому и к данной статье вопросов нет. Мастер выбрал инструмент, мастер сделал. И ведь не скажешь что некрасиво сделал. Наоборот. А в учебных целях так просто здорово. Мало ли мы в школе на черчении загогулин чертили, в реальной жизни не встречающихся.
Скажем, на С невозможно сделать RAII, потому что нет деструкторов — поэтому подсчет ссылок может быть только ручной.

Вот не разу не соглашусь. Почему-то RAII принято связывать именно с обязательностью вызова деструктора. На мой взгляд, это неоправданное сужение: RAII — это концепция, а обязательность вызова деструктора (заметьте, конструктор не обязателен, требуется только инициализация тем или иным способом) просто языковый сахар, позволивший популяризовать подход, повысить его надежность и снизить его цену для программиста. А в C RAII — это best practice, которой надо сознательно следовать, прикладывая интеллектуальные усилия.
И мы говорим, что в этом отношении C++ «сильнее» C и предоставляет лучшие языковые средства.

Ну, если судить по буквальному значению аббревиатуры — то да, вы правы.
Но по сути эта идиома и словосочетание появилось в С++ благодаря деструкторам и подразумевает создание локального объекта.
Если нет деструкторов, то и пару fopen-fclose можно назвать RAII, но так, вроде бы, не говорят?


Вопрос терминологии, так что спорить не хочется.

Ну нет, э, употребление термина «RAII» имеет смысл только в контексте автоматического освобождения ресурса после выхода из области видимости. И тут кроме __cleanup__ нет никаких причин говорить про RAII в Си (да и это так себе причина).
UFO just landed and posted this here
Нет. За счёт усложнения системы типов.


Хорошо. Но согласитесь — по сути мы говорим об одном и том же. Просто мое понимание более общее, а Ваше — более каноническое.

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

… borrow checker'a, который статически проверяет заимствования ...


Это не аппаратный блок процессора, а только надстройка над существующими (если хотите дырявыми) аппаратными решениями. И делает ее часть языка.

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

А то легко привести примеры языков без присвоения, без циклов, без управления памятью.


Ну, я поправился на тему Prolog и Lisp. Хотя и тот и другой имеет и циклы, и присваивания, и сравнения, и массивы в каком-то виде. Да что там — всеми любимый brainfuck имеет все необходимое. Правда тут вопрос что считать полномасштабными операциями, но… Я склонен считать, что они есть и тут.

Попробуйте. Я буду рад узнать язык, который этого не содержит.

P.S.
Патефон и музыкальную шкатулку не предлагать. Согласен, там тоже в некотором смысле программа, но она изначально линейная и ветвлений впринципе не предусматривает.
UFO just landed and posted this here
отсутствие ерунды в коде на расте опирается на компилятор раста, а не на квалификацию авторов стандартной библиотеки раста.


Хорошо, но ведь компилятор Rust'а не Моисей на скрижалях принес. Т.е. в конечном счете все так или иначе упирается в квалификацию авторов. Хорошо, не библиотеки, а компилятора. Но разве это что-то принципиально меняет?

Хаскель тот же.


Не готов судить прямо сейчас. Но ознакомлюсь. Все может быть…
UFO just landed and posted this here
Не знаю. Давайте я пожму плечами и соглашусь.

Мы вступаем на скользкую дорожку противоборства веры. Вашей веры в то, что стройная система типов — главная часть компилятора. против моей веры в то, что компилятор ценен весь, включая runtime. Я даже не буду вспоминать про то, от чего зависит прочность цепи. Веру нельзя перебить аргументами.

В конце-концов я описывал всего-то свое впечатление от Rust. При чем пока оно следствие крайне поверхностного знакомства с этим языком. Другое дело, что и подход к нему был как «к еще одному из рода», а не как к «универсальному способу писать безопасный код».

Потому я просто соглашусь.
«Циклов тоже нет. Есть рекурсия.»
Вот, функц. языки не имеют циклов, а реальная рекурсия запрещена для безопасных систем, прямо жёстко стоит NR для SIL2 уже по-моему. Так же как, скажем, и сборщик мусора, и что делать?
Там из всех рекомендаций подходят получается только Ada и Rust, а C и C++ только со статический анализатором, причём с жёсткими настройками по стронг типу, такими, что мне уже тут в комментам Lint зависимость и параною приписывают.
UFO just landed and posted this here
Угу, причем от синтаксиса Раста просто трясет, так что остается одна Ада, а там перекати-поле и гулкий свист (хотя конечно кто-то утверждает, что это совсем не так, но даже этих людей найти не так просто).
А если куча не за стеком расположена?
Многие любят IAR, но IHMO, его время ушло

Ну не скажите, один из немногих у кого есть сертификат на безопасность, т.е. компилятор действительно делает все правильно с учетом errata :). Да он немного отстает от GCC по внедрению фишек, но С++17 уже в 8.40 есть.
Оптимизация у него кстати неплохая, не знаю, что вы имели ввиду по «Мои вкусы весьма специфичны» (с)
Если следовать и писать по стандарту, то код будет везде компилироваться и в GCC и в IAR и на Clang, на MS только может не компилиться из-за его «особенностей» но он во встроенном и ни особо нужен.
Больше всего меня напрягает его манера бить четкий и последовательный код на функции. Вот с этой функцией из одного из своих ранних проектов (еще для AVR) я с таким поведение столкнулся впервые.
void Rotate ( unsigned char* in, unsigned char* out ) // IN & OUT 8 bytes
{
 ////////////////////////////////////////////////////////////////
 // http://membres.multimania.fr/amycoders/sources/c2ptut.html //
 //                                                            //
 // Source   Step1    Step2 Fast clockwise rotation algorithm  //
 // A B  =>  D B  =>  C A   based on linear  algebra combina-  //
 // C D      C A      D B   toric methods.                     //
 ////////////////////////////////////////////////////////////////
  
 register unsigned char a,b,c,d,e,f,g,h; // working matrix
 
 a = in[0]; b = in[1]; c = in[2]; d = in[3];
 e = in[4]; f = in[5]; g = in[6]; h = in[7];
 // 4x4 swap
 a ^= ( ( e & 0x0F ) << 4 ); e ^= ( ( a & 0xF0 ) >> 4 ); a ^= ( ( e & 0x0F ) << 4 );
 b ^= ( ( f & 0x0F ) << 4 ); f ^= ( ( b & 0xF0 ) >> 4 ); b ^= ( ( f & 0x0F ) << 4 );
 c ^= ( ( g & 0x0F ) << 4 ); g ^= ( ( c & 0xF0 ) >> 4 ); c ^= ( ( g & 0x0F ) << 4 );
 d ^= ( ( h & 0x0F ) << 4 ); h ^= ( ( d & 0xF0 ) >> 4 ); d ^= ( ( h & 0x0F ) << 4 );
 // 2x2 swap
 a ^= ( ( c & 0x33 ) << 2); c ^= ( ( a & 0xCC ) >> 2 ); a ^= ( ( c & 0x33 ) << 2 ); 
 b ^= ( ( d & 0x33 ) << 2); d ^= ( ( b & 0xCC ) >> 2 ); b ^= ( ( d & 0x33 ) << 2 ); 
 e ^= ( ( g & 0x33 ) << 2); g ^= ( ( e & 0xCC ) >> 2 ); e ^= ( ( g & 0x33 ) << 2 ); 
 f ^= ( ( h & 0x33 ) << 2); h ^= ( ( f & 0xCC ) >> 2 ); f ^= ( ( h & 0x33 ) << 2 ); 
 // 1x1 swap
 a ^= ( ( b & 0x55 ) << 1); b ^= ( ( a & 0xAA ) >> 1); a ^= ( ( b & 0x55 ) << 1);	
 c ^= ( ( d & 0x55 ) << 1); d ^= ( ( c & 0xAA ) >> 1); c ^= ( ( d & 0x55 ) << 1);	
 e ^= ( ( f & 0x55 ) << 1); f ^= ( ( e & 0xAA ) >> 1); e ^= ( ( f & 0x55 ) << 1);	
 g ^= ( ( h & 0x55 ) << 1); h ^= ( ( g & 0xAA ) >> 1); g ^= ( ( h & 0x55 ) << 1);	

 out[0] = h; out[1] = g; out[2] = f; out[3] = e;
 out[4] = d; out[5] = c; out[6] = b; out[7] = a; 
}  


Уже давно ссылка на описание не работает, но IAR даже на -Onone упорно выделял сдвиги и маски в функции, чем сильно ухудшал производительность. Потеря 20% на абсолютно ровном месте. GCC таких вольностей себе не позволял не только на -Onone, но и на более высоких уровнях. Позже этот же код был портирован на ARM (с поправкой на 32-разрядность) и там IAR повел себя так же паскудно. С тех пор я ему не верю и регулярно проверяю что именно он делает «на выходе». Если очень кратко — не нравится мне его работа. Местами шибко умный, а местами тупой как пробка. Посмотрите сами на свой код — думаю много интересного обнаружите.

Впрочем, у Вас плюсы… Вам сложнее… Или проще… Даже не знаю.
Думаю ваш собеседник имеет в виду его интерфейс. Это ж реально фасепалм.жпг…
Да, и про интерфейс. В меньшей степени, конечно, но не работающие Ctrl+Insert и Shift+Insert я ему до сих пор простить не могу. Это надо так неуважительно относиться к людям обе руки держащим на клавиатуре.

Про остальное — да не очень-то и хотелось. В конце-концов я больше думаю, чем пишу и фишки типа автопереименования переменных или автодополнения мне не нужны. Больше того — мешают, ибо почти всегда работают неправильно. В том же Atmel Studio. Уж на что в основе Microsoft Visual Studio, а такая лажа со всеми этими фантиками.
Ну, гм, правильно работающее автодополнение — сильная вещь. Хз что там у вас не работало… Я работал в Атмел Студии очень давно, и на ассемблере.
Вот я не могу вспомнить что именно, но вы что-то более элегантное предлагали, хотя и более сложное. Надо поискать по комментам…

Расскажите, если найдете, мне тоже самому интересно, что же я такое предлагал.

UFO just landed and posted this here
Доказательное программирование… Математически доказать устойчивость системы… Да, это сложно. Но если на голом C это хотя бы теоретически выполнимо, то плюсы…

Я предпочитаю «доказывать» сертификационными испытаниями итоговой железки. При чем меня всегда интересует не момент работает/не работает, а на каком превышении интенсивности событий от рассчетного (заданного в ТЗ) начинается пропадение данных. И как ведет себя железка, имеющая мусор по одному или нескольким (всем) входным каналам. Не будет ли перезапусков. Больше того, как правило я прошу поломать мне память. Есть у нас некоторые, хм, воздействия, которые гарантированно вносят сбои в SRAM'ы. Всегда стремимся к том, чтоб после окончания воздействия помехи был максимум один перезапуск. С таким программно бороться точно не получится.

Но еще раз — я не настаиваю на том, что всего этого можно достичь только связкой pure C + Assembler. Просто мне проще именно так.

И да, простите, но бизнес-логика… Во встраиваемых системах? Нет, конечно, четкие и понятные алгоритмы работы, наверное, частный случай той самой бизнес-логики. Но мне все же привычнее, когда алгоритмы называются алгоритмами. А бизнес-процессы — это смежная область.

Да и в целом — меня коробит сама постановка вопроса в стиле «как защититься от выставления логического уровня на пине, сконфигурированном как вход?» Это вообще что такое? Если реализация позволяет такое, надо срочно менять реализацию а не колдовать с конфигурацией пинов. Или я что-то не так понял?
UFO just landed and posted this here
доказывать, что вы никогда не пишете в пин в тот момент, когда он сконфигурирован на чтение?


Я не понимаю до конца вопроса, потому простите — отвечу как понимаю.

Контроллеры, как правило, однопроцессорные и однопоточные решения. В варианте голого железа эффект конкурирования за ресурсы может возникнуть либо между обработчиками прерываний (если разрешена вложенная обработка), любо между прерыванием и фоном. Таким образом вопрос доказательства сводится к алгоритму реализации. Если фон позволяет себе не задумываясь писать в пин, направление которого меняет обработчик (или наоборот, что совсем не принципиально), то это проблема алгоритма решения задачи. А такие проблемы всегда должны решаться заменой алгоритма и никак иначе.

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

Но повторюсь, я не уверен что вопрос понял правильно.
UFO just landed and posted this here
А если мамой поклясться?)))
Давайте я просто спасую. Не готов далее продолжать беседу за отсутствием требуемой квалификации.

Замечу лишь, что странно. И с необходимостью каким-то образом доказывать этот момент я никогда не сталкивался. Но вопрос скоре к функции doSomethingElse(). Должна она менять направление, не должна, и в каком оно должно оставаться по выходу. Исключительно от этого зависит правильность или неправильность предложенного кода. Универсальный вариант, впрочем, тоже известен:
void doSomething()
{
    configurePinForReading();
    int num = readInt();
    doSomethingElse();
    configurePinForReading();
    int otherNum = readInt();
}


Впрочем, от ситуации когда doSomethingElse() может вызываться и из обработчика прерываний он не спасет. Но опять же — вопрос скорее в логике работы.
Контроллеры, как правило, однопроцессорные и однопоточные решения.

XXI век на дворе. А мужики-то не знают…
А мужики-то не знают…


И хорошо что не знают… А то «каждый суслик агроном».

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

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

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

А вот массовое появление в контроллерной среде тех, кто считает контроллер слабым компьютером приводит к крайне интересным последствиям. Например замене контроллеров на ПЛИС'ы. Т.е. возврату к «жесткой логике»на современном этапе развития. А от вопросов в стиле «У нас же ARM на 200МГц с SD-картой и USB, так почему мы не можем воткнуть в него USB свисток для Wi-Fi?» я уже отбиваться замучался.

Так что да. Мужики-то не знают…
главное требование к которым максимально быстрое реагирование на «внешние раздражители»

Далеко не всегда это нужно. В моей практике не было ни одного проекта, где бы требовалось "считать такты".


FreeRTOS это просто переключалка задач по сути, на время реакции (если речь о прерываниях) она не влияет, там накладные расходы — память (стек у каждой задачи свой) и процессорное время в момент переключения контекста. Зато она позволяет очень сильно упростить код.

В моей практике не было ни одного проекта, где бы требовалось «считать такты».


И Вы считаете, что раз Вам не надо — значит и никому не надо?

Мне, по сути, больше и спрашивать нечего. Разве что пожелать вам серьезного проекта, где накладные расходы на FreeRTOS окажутся неприемлемыми. И дай бог при этом еще и не оказаться в ситуации кода RAM'а и FLASH'а в обрез. Ибо массовость такая, что экономия каждого цента критична.

Теперь про FreeRTOS. Если вся логика сделана на обработчиках прерываний, то большой вопрос — а зачем вам FreeRTOS? Чтоб ядро не простаивало и планировщик гоняло? Так может проще NOP'ами обойтись или порадовать зеленых и воспользоваться наконец энергосберегающими режимами?

А если нет, то время реакции — это не только время обработчика. Это время IPC (те же очереди с почтовыми ящиками) и время за которое нужная задача увидит приход данных, обработает их и отреагирует. Вот и вся недолга.

Да и упрощение кода… Господа, блин, неужели я единственный кого коробит xTaskCreate(...) и типы с именами TaskFunction_t? CamelCase и безумная многословность. Как будто индусам код за килобайты оплачиваются. Впрочем, ладно. Тут мои тараканы, наверное. Даже embOs от Segger, которую я одно время очень любил сегодня к практически POSIX'овским сигнатурам своих функций зачем-то добавила префикс OS_. В итоге не столько с кодом (алгоритмом) разбираешься, сколько в дебрях оси правду выискиваешь. Те же примеры от Texas наглядный пример. Толку с них — ноль без палочки. По документации писать проще, чем горы такого кода разбирать.
И Вы считаете, что раз Вам не надо — значит и никому не надо?

Нет, это вы считаете, что раз вам надо, значит всем надо. Это же вы озвучили главное требование к МК. Я просто сказал, что это не всегда верно.


Если вся логика сделана на обработчиках прерываний

Я вроде не писал такого


А если нет, то время реакции — это не только время обработчика. Это время IPC (те же очереди с почтовыми ящиками) и время за которое нужная задача увидит приход данных, обработает их и отреагирует. Вот и вся недолга.

Да. И когда задач нужно выполнять много "одновременно", кто-то должен следить за переключением. Либо программист, либо система. Когда ресурсов хватает, я за второй вариант. Легче писать, легче поддерживать, легче тестировать отдельно от других задач.


Да и упрощение кода… Господа, блин, неужели я единственный кого коробит xTaskCreate(...) и типы с именами TaskFunction_t?

Это уже вкусовщина, да и к простоте кода не относится. Но лично у меня многое из FreeRTOS было спрятано внутри классов-обёрток.

Вот слушаю я Вас и не понимаю.

Я просто сказал, что это не всегда верно.


Да. Но ровно это же можно написать и «Я просто сказал, что это не всегда не верно.» Задачи разные. Решения разные. Люди разные. Умения и навыки разные. Правда и результаты разные. Но раз в современном мире гигабайтные дистрибутивы никого не пугают, то почему бы на контроллере и ОС не запустить. Тем более, что любой из современных контроллеров по возможностям превосходит старые ПК. Но…

Не знаю — по мне это путь динозавров, гигантских деревьев, сверхтяжелых танков и прочего. Эта ветвь эволюции всегда заводила в тупик. По мне высокую производительность современных контроллеров стоит пускать на другие цели. Впрочем, если посмотреть на тот же MiBand, то там почти наверняка стоить какая-то ОС. И там она точно оправдана. А для управления станком с ЧПУ или автомобильном ЭБУ… Сомневаюсь. Скорее элемент ненадежности.

Когда ресурсов хватает, я за второй вариант


А я за грамотное проектирование. Если крайне важно удобство разработки и сопровождения, и ради этого можно пожертвовать стоимостью и производительностью, не возьмите SOM с тем же Linux'ом. И отстаньте от контроллеров. Не плодите очередного монстра «типа легко поддерживаемого». Это не так. Совсем. Этот код только кажется простым и понятным (а соответственно легко поддерживаемым). Причем только Вам. Контроллерный код делает понятным и поддерживаемым совсем не наличие или отсутствие ОС.

Это уже вкусовщина


Абсолютно согласен. Просто к слову пришлось.
Да. Но ровно это же можно написать и «Я просто сказал, что это не всегда не верно.» Задачи разные. Решения разные.

Да. Именно поэтому главное требование к МК, озвученное вами, не главное. Оно всего лишь одно из возможных.


А для управления станком с ЧПУ или автомобильном ЭБУ… Сомневаюсь. Скорее элемент ненадежности.

Элемент ненадёжности — сложный код. Да, от сложного кода тоже можно добиться заданной надёжности, и я не сомневаюсь, что у вас это получается. Но сил на это нужно тратить больше, чем можно было бы.


А я за грамотное проектирование. Если крайне важно удобство разработки и сопровождения, и ради этого можно пожертвовать стоимостью и производительностью

А не всегда это жертва. Я вот разработкой железа не занимался, на выбор МК, а соответственно и на цену железа, повлиять не мог. Работал с тем, что дают. Ресурсов для FreeRTOS не хватало (а может и хватило бы, но ужиматься очень не хотелось) только на одной плате, где был MSP с 512 байтами ОЗУ.


Не плодите очередного монстра «типа легко поддерживаемого». Это не так. Совсем. Этот код только кажется простым и понятным (а соответственно легко поддерживаемым). Причем только Вам.

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


Я писал и без FreeRTOS. И с нуля, и дорабатывал чужие проекты. И возвращаться к этому я бы не хотел. Может, у меня просто не было учителя, обладающего вашей мудростью =)

Элемент ненадёжности — сложный код. Да, от сложного кода тоже можно добиться заданной надёжности...


Хорошая фраза, только произнесена не к месту. Уровень сложности кода должен соответствовать сложности системы. Тогда все хорошо. В остальных случаях — плохо. Сложная система, накрытая простым кодом не имеет или функционала, или стабильности, или необходимого уровня удобства. А простая система, реализованная с излишне сложным кодом — сложна в поддержке и расширении.

А тут тот самый момент, который не является константой и изменяется с опытом. Определить сложность системы и выбрать оптимальное решение. И именно здесь ответственность разработчика. Просто задумайтесь в таком ключе. Всегда надо помнить не только о том, что завтра дедлайн и срочно надо сделать, но и о том, что через пол года отпуск. И лучше бы, чтоб во время отпуска тебя не искали. Особенно по причине того, что в коде твоем не разобраться, параметры не поменять, фичу новую не дописать — для всего этого непосредственно ты нужен. Или кто-то срочно фичу добавил (не очень грамотно) и опа… ресурсы кончились и основной функционал разваливается. Боюсь, однако, что тут нет предела совершенству. Сегодня многие из своих ранних проектов я бы сделал совсем по другому. И, в частности, во многих отказался бы от осей. А другие сделал как раз с ними.

Что до «подсчета тактов», то к счастью времена 8051 прошли и острой необходимости в этом уже давно нет. Впрочем, трюки из тех времен актуальны и поныне. И будут актуальны как минимум до тех времен, пока не появятся квантовые микроконтрооллеры. Т.е. еще очень долго.

Я вот разработкой железа не занимался, на выбор МК, а соответственно и на цену железа, повлиять не мог.


Плохо. Опять же ситуации разные бывают. Одно дело пришел «спасать проект», потому как разработчик по каким-то причинам слился. И тогда считайте что повезло, если эти причины не связаны с его профессиональной деятельностью. Иначе гарантированно предстоит сложная работа. При чем не важно был ли уровень вашего предшественника ниже вашего или наоборот. Если ниже — значит почти наверняка полная переработка. Просто физически не сможете сопровождать такой код. Валерьянки не напасешься, да и ошибки всплывают в самых неожиданных местах. А остроты добавят подсмотренные в интернете трюки. Если наоборот — вы приходите на замену большому профи, на готовое железо — тут хуже. Как правило есть какая-то скудная документация, но хороший самодокументируемый код. И, в принципе понятно как править ошибки когда они начнут возникать. Но вот добавление фичь… Вам некомфортно без осей. А я их практически нигде не использую. За то у меня гора callback'ов и асинхронный код. Писать в таком стиле крайне приятно и просто, но… Этому не учат в институтах. И до такого похода дорасти надо.

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

И давайте я оговорюсь — я не DIY. Я о производстве. DIY — это хобби. Делай что хочешь и как хочешь. Только технику безопасности соблюдай чтоб током не убило и лазером глаз не выжечь. Ну и литий не взорвать ненароком и пожар не устроить.

… иметь общий бесконечный цикл, откуда делается управление вообще всем...


Сочувствую. Идеальная main() всегда должна стремиться к следующему
void main(void)
{
  init();
  while(1) {
    enter_sleep();
  }
}


Про все остальное — ну да состояний лишних быть не должно. Это очевидно. Только ведь это не от оси зависит. А бить на мелкие части и решать по отдельности каждую элементарную задачу — так это ж просто обязательно. Это основной навык. И опять же — от наличия или отсутствия оси он не зависит.
Но меня от этого названия тоже коробит, сразу начинает пахнуть митингами, аджайлом, стендапами и презентациями в пауэрпоинте. Увы, другого названия не знаю.


Да нормальное название, именно термин «бизнес-логика» внятно раскрывает суть дела. Конечно термин «алгоритм работы» звучит привычнее, но с точки зрения семантики русского языка он тоже несколько странный.
Пофлеймим?

А чем алгоритм работы хуже (или непонятнее) чем бизнес-логика? Давайте посмотрим на определения:

АЛГОРИ́ТМ (Мужской род)
1.СПЕЦИАЛЬНОЕ
Система последовательных операций (в соответствии с определёнными правилами) для решения какой-н. задачи.
«Теория алгоритмов»

2. КНИЖНОЕ
Совокупность последовательных шагов, схема действий, приводящих к желаемому результату.
«А. поиска»


Попробуем получить настолько же внятные расшифровки для «бизнес-логики»? Сначала отметем все сугубо коммерцеское от слова бизнес, вспомнив, наконец, что с английского это всего-то «дело». Потом так-же пошерстим логику, под которую последовательность и взаимосвязь действий тоже напрямую не подходят…

Впрочем, ну пусть будет бизнес-логика. Я же перед программированием все равно алгоритмы прорисую. Хотя бы в голове и для себя персонально.
Не очень понимаю, зачем очередной велосипед, посмотрите работу с портами в mcucpp Константина Чижова — там все есть и очень хорошо сделано, на стоит придумывать свое, заведомо худшее.
Смотрел… да, согласен. Я добавил про надежность вариант с SFINAE. Тут я больше на надежность уклон делаю и способ как вообще конфигурировать аппаратуру. Сами Пины, понятно по другому не сделать :)
    static constexpr auto mask = T::MODER::FieldValues::Input::Mask ; 
    const ModerType offset = static_cast<ModerType>(pinNum * 2U) ;


А в чем смысл того, что в одном случае static constexpr, а в другом просто const?
А в чем смысл того, что в одном случае static constexpr, а в другом просто const?

mask может вычисляется на этапе компиляции, и по сути компилятор её заменит на число.
А pinNum передается в функцию, offset на стеке создался.
А pinNum передается в функцию, offset на стеке создался.

А auto const mask = T::MODER::FieldValues::Input::Mask; не во время компиляции будет вычислен? Это из-за шаблонного параметра T?
Да это из шаблонного параметра.
Но как оказалось const не всегда может быть const и его можно убрать, ну правда не в этом случае. Поэтому надежнее писать везде constexpr, чтобы точно быть уверенным, что все всегда будет хорошо для любого случая.
Хм, MODER же это просто целочисленный литерал, нет? И задача — «протащить» правильный литерал из описания платформы (неважно, в файлах с расширением .h или .xml) в желательное место. Мне кажется, тут лучше бы подошел using, да олдскульный #define бы тоже сработал. А то возникает подозрение про метапрограммирование ради метапрограммирования и обфускации кода…

MODER это тип. Mask это значение маски для полей MODER — 0b11. Да задача такая, вытащить из регистра MODER значение маски для его полей. Она там просто хранится.

Хм, неясно выразился. Я так понимаю, что T в T::MODER::FieldValues::Input::Mask — это GPIOxx? И тогда T::MODER::FieldValues::Input::Mask — это литеральная константа, где-то там прописанная. Просто по сути описания регистров и правил их использования, нет?
А «затянуть» куда-то именованное литеральное значение, на мой взгляд, логичнее через using, а не через constexpr. Как-то так…
Да T это «GPIOxx», но Mask, хранится в каждом таком типе в классе FieldValue, она там как раз constexpr, просто я вытаскиваю ее оттуда.
template<typename Field, typename Base, typename Field::Register::Type value>

struct FieldValue: public FieldValueBase<Field, Base, value>
{
  using Type = typename Field::Register::Type ;
  // вот она
  constexpr static auto Mask = static_cast<Type>((1U << Field::Size) - 1U) ; 
  
  constexpr static auto Value = value ;
  constexpr static auto Offset = Field::Offset ;
  using BaseType = Base ;
  using Access = typename Field::Access ;
} ;

Ну вот это:
constexpr static auto Mask = static_cast<Type>((1U << Field::Size) - 1U) ;

Если Field::Size — целочисленная константа времени компиляции, то и Mask целочисленная константа времени компиляции. Или я брежу?
Кстати, 1U можно смело сократить до просто 1:)))
А насчет constexpr здесь я тоже не уверен, пусть меня поправят, но насколько это необходимо?
Очень сильно похоже на мои изыскания — github.com/no111u3/stm32l476_examples, правда я так не нашёл в себе силы довести всё это до логического финала (уж слишком много труда требуется на всю экосистему).
UFO just landed and posted this here
Через типы-состояния, может быть?
Т.е. действие «сконфигурировать на запись» возвращает тип, у которого нет функции чтения.
UFO just landed and posted this here
Даже на чистом С можно выразить, выглядит страшно, но работает.
Можно сделать структуру вроде такой:
typedef struct {
    const uint8_t state[STATE_SIZE];
    WRITE* configure_for_writing(...);
    READ* configure_for_reading(...); 
    //UNINITIALIZED* reset(...);
    void* reserved_do_not_use_0(...);
    //STATUS write(...);
    STATUS reserved_do_not_use_1(...);
    //STATUS read(...);
    STATUS reserved_do_not_use_2(...)
} UNINITIALIZED;

Т.е. в момент ее создания там есть состояние (структура которого скрыта, и писать в него напрямую тоже не стоит), и функции, которые возвращают указатель на ту же самую структуру, только уже не с reserved_do_not_use_*(...), а с функциями, которые доступны на данный момент.
typedef struct {
    const uint8_t state[STATE_SIZE];
    void* reserved_do_not_use_0(...);
    void* reserved_do_not_use_1(...);
    UNINITIALIZED* reset(...);
    STATUS reserved_do_not_use_2(...);
    STATUS read(...);
    ...
} READ;

typedef struct {
    const uint8_t state[STATE_SIZE];
    void* reserved_do_not_use_0(...);
    void* reserved_do_not_use_1(...);
    UNINITIALIZED* reset(...);
    STATUS write(...);
    STATUS reserved_do_not_use_2(...)
    ...
} WRITE;

Получается такой себе фасад, за которым на конкретном этапе доступны только те функции, которые имеют на нем смысл. Понятно, что можно вручную кастовать что угодно к чему угодно, и все порушить, но даже такая защита сильно лучше, чем никакой, потому что случайно уже вызвать не ту функцию не получится.
UFO just landed and posted this here
Никак, к сожалению, тут кроме как на себя надеятся больше не на кого. Ну или писать на Расте обертки и работать с ними уже…

Еще можно попробовать брать указатель на указатель первым параметром всех функций, и ставить его в NULL при успехе, т.е. если все сработало, вот вам указатель нового типа, а старый мы вам занулили автоматически, спасибо.

Да, именно так и хотелось бы делать, но на С++ пока :). Но я, как уже говорил, подсаживаюсь на вещества по вашему совету. И ещё Rust тоже…. Проблема только в программистах и доступных сертифицированных компилятора.

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

UFO just landed and posted this here

Искренне надеюсь, что вам все-таки мешает NDA, а не DNA. ;)

Очень хочется спросить… А зачем вы везде где попало "лепите" постфикс "U"?

Это литерал unsigned, без него число будет просто signed. и где-то там будет происходить неявное преобразование signed в unsigned. Lint постоянно ругается… поэтому я просто на автомате везде так пишу теперь.

Я знаю что это такое. Вопрос был в другом. Просто это "смахивает" на какую-то паранойю...

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

Ну и пишет же человек — укушен линтером (и слава Богу).

А зачем там вообще беззнаковый литерал?
Я к этому вёл.

Чтобы потом линтер не ругался на знаковые типы в битовых операторах, наверное.
А какой литерал по-вашему характерен для этих операций?

Уточните, пожалуйста, о каких операциях вы говорите?
Какой литерал по-вашему характерен для static_assert?

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

Вы изначальный мой вопрос читали вообще?
Зачем вы вообще влезли в чужой диалог неразобравшись в чём дело?

Читал, вам не понравился беззнаковый литерал в ассерте. Только при чем тут ассерт, я еще раз спрашиваю? Разве это не должно быть везде сделано правильно?

Плохо читали. Видать плохо поняли. Когда окончательно поймёте — тогда и продолжим.

Ну, вы разъясните, тогда может и вопросы отпадут, зачем же сразу истерить.

#define A 0x03
...
uint16_t value = ...;
...
*PORT = (value << A); // <= вот здесь будет замечание линтера

потому что параметр шаблона uint8_t, а передаем 5 — которое на самом деле signed int. Чтобы линт не ругался вообще-то надо вот так делать uint8_t{5}. Но когда пишешь 5U, то как минимум половина warningов убирается.
uint8_t{5}


Хм, и такие инициализаторы бывают? Типа со встроенным кастованием?
Ещё чисто теоритечески же ошибки могут, если на автомате не лепить.
Студенты на AVR, вот так написали… примерно.
auto data = 40000; потом долго мучились, почему они его посылают в UART, а прилетает какая-то бяка.
А есть идеи, как вызвать ошибку компиляции или линковки, если я один и тот-же пин задействовал(создал) в двух разных местах?

В данном случае Pin статический класс, но действительно настроить разные типы можно на один и тот же порт и пин. Как от этого избавиться, идей пока нет. Если бы был просто класс, то можно было бы синглтон сделать. Но пришлось бы создавать столько синглтонов, сколько пинов.
В данном же случае, да проблемка. Может быть, как то регистрировать Pin в списке, и потом по списку пробегать и смотреть, есть ли там такой уже… Но как автоматом в списке зарегить? А самому вызывать метод для регистрации, можно и забыть вызвать, если только через constexpr конструктор, но тогда объект надо делать.

Не понял, что значит "разные типы можно настроить на один и тот же пин"? Типы чего?

using Led1Pin = Pin<Port<GPIOC>, 1, PinReadable> ;
using Led2Pin = Pin<Port<GPIOC>, 1, PinWriteable> ;


Два типа, указывающих на один и тот же пин. И нужно как то придумать, как сделать так, чтобы запретить такое делать

В бусте есть компайл тайм списки, словари и т.п., хотя в последних плюсах уже тоже много чего constexpr. Думаю можно через это сделать проверку.

А, ну это не тип, это алиас же. Впрочем неважно, суть я теперь понял.

Собственно решение вы сами выше написали — список зареганных пинов, и проверять его в конструкторе. И собственно перестать уже зацикливаться на компилтайме, пусть будет полноценный объект. Ну или через constexpr, если так уж хочется.

Скажите, а почему вы используете SFINAE для ограничения способов доступа к пину? Я тоже экспериментировал с C++ для доступа к регистрам, и делал примерно так:


struct mode_read {};
struct mode_write {};
struct mode_read_write : mode_read, mode_write {};

template<typename mode>
struct reg
{
  static inline void write(int value)
  {
    static_assert(std::is_base_of<mode_write, mode>::value, 
                  "Register is not writable");
    //...
  }

  static inline int read()
  {
    static_assert(std::is_base_of<mode_read, mode>::value,
                  "Register is not readable");
    //...
  }
};

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

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

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

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

Немножко про свой опыт напишу. На прошлых работах несколько раз делал свои шаблонные библиотечки для работы с периферией МК. Использовался IAR, как-то ограниченно поддерживавший c++ (для Renesas 78K0R и в 2017 всё было ещё очень плохо). Сейчас наверное то же самое делать было бы гораздо удобней.


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


В первых версиях (для LPC2xxx от NXP) я всю работу с периферией реализовывал самостоятельно и был полностью доволен результатом. Всё оборудование описывалось в одном условном "hardwarecfg.cpp" как глобальные объекты и инициализировалось в конструкторах. Ещё помню, сначала даже использовал дурацкий хак, чтоб размещать эти объекты за пределами ОЗУ дабы сэкономить место. Были реализованы классы для gpio, uart, spi, adc, pll, таймеров, но доступны они были только через тайпдефы внутри класса cpu и зависели от его настроек. Ну то есть при смене процессора могло получиться так, что новый не имеет нужного порта, и компилятор сразу об этом скажет. Разные константы считались в компайлтайме (например для настройки скорости uart нужно было три константы выбрать просто полным перебором, без этого можно было вычислить лишь очень грубое приближение), а ещё проверки всякие вроде ограничения на частоту процессора при включенной поддержке usb. А классы для внешнего оборудования использовали в свою очередь объекты внутреннего.
Первый раз я понял, что что-то не учёл, когда пришлось добавлять поддержку МК того же семейства, но с совершенно по-другому устроенной частью периферии. Пришлось переписать библиотеку.


На другой работе пришлось работать с упомянутыми в начале 78K0R. Уж не помню почему, может из-за особенностей использования прерываний, а может просто потому что постепенно переделывал старые проекты, но я сделал библиотечку гораздо проще. Теперь уже ничего не зависело от проца и его настроек, а мои классы использовали функции, генерируемые конструктором библиотек от производителя. Большой минус — если поменять проц, компиляция могла сломаться просто оттого, что в моей библиотеке инстанцировались шаблоны для несуществующего оборудования =( Но основной упор был сделан на то, чтоб вместо реального железа можно было очень легко подсунуть что-нибудь другое. В итоге последние проекты отлаживались по большей части полностью на ПК. Особенно это помогло отлаживать устройства, общающиеся с соседними и перемещающиеся в пространстве сообща — в железе такое отлаживать больно. И вот этому, я считаю, нужно уделять внимание при разработке подобных библиотек в первую очередь.

Видимо у всех почти одинаковые проблемы. У нас еще особенность есть, что по-мимо переносимости нужна еще надежность, чтобы минимизировать ошибки программиста. Потому что хорошего программиста днем с огнем не отыщешь в нашем регионе из-за зарплат, они раза в 2.5 ниже Московских, все хорошие программисты уже там :(

Спасибо за статью! Я, в целом, за любой движ в embed, который позволит уйти от чистого С, поэтому всегда рад подобным статьям :)


С другой стороны, в своей практике я ни разу не встречал проблему, которую (вроде бы) решает предложенный подход — "чтобы у Pinа, настроенного на вход не было возможности вызвать метод Set(), и наоборот для Pinа, настроенного на выход, не было даже намека на метод Get()."
Или же встречал, но она очень быстро решалась, потому что уровень на пине не изменялся, хотя должен был.


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


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


Но, на мой вкус, ваше решение просто слишком "сложное" — очень много шаблонного кода ради относительно простой вещи. И кода станет больше, если попытаться сделать его переносимым хотя бы в рамках STM32, поскольку регистры портов на разных сериях немножко разные.


Чем меня пугает шаблонный код? В основном тем, что его отлаживать тяжело. И читать, на самом деле, тоже, особенно, если он обильно перемазан enable_if'ами.
Ну и можно спровоцировать нехилый code-bloat на ровном месте, но это, в целом, решаемо.


Я лично сию проблему решил примерно таким классом:


Кусок кода
    struct GpioPin
    {
        GPIO_TypeDef * raw_port;
        uint16_t raw_pin;

        void initOutPP() const;
        void initOutOD() const;
        void initInFloating() const;
        void initInPullUp() const;
        void initInPullDown() const;
        void initClock() const;

        void set() const;
        void reset() const;
        void toggle() const;

        bool read() const;
    };

Отмазки

Да, тут просто структура и у нее просто методы. Да, оверхед на вызов, на хранение указателей в рантайме и т.д. В подавляющем числе случаев — пофиг: если надо дергать пином сверхбыстро, то можно и по-старинке через CMSIS'ный указатель. Но зачем такое может быть надо — ума не приложу, скорее всего, что-то спроектировано неправильно. Типа, надо было ШИМ генерировать, а взяли ногу, не подключенную к таймеру.


Если хочется сделать порт из пинов, можно просто сложить их в массив.


Оверхед по памяти относительно невелик, если начинаем каждый байт считать, то под нож первым пойдет что-то потолще.


Зато код короткий, простой и понятный; любой мой коллега или приблудный студент в нем разберется. Плюс я на разработку потратил совсем немного времени.


Я все еще считаю, что на настройку и использование периферии в реальных программах уходит ну процентов 10 кода и может быть процентов 15 рабочего времени. Все остальное — логика, на которую уходит гораздо больше сил. Соответственно, на нее эти силы и следует тратить в первую очередь.


Чисто формально глаз цепляется еще вот за это (прошу прощения за непрошенное code-review)
using Led1Pin = Pin<Port<GPIOA>, 5U, PinWriteableConfigurable> ;
using Led2Pin = Pin<Port<GPIOC>, 5U, PinWriteableConfigurable> ;
using Led3Pin = Pin<Port<GPIOC>, 8U, PinWriteable> ;
using Led4Pin = Pin<Port<GPIOC>, 9U, PinWriteable> ;
using ButtonPin = Pin<Port<GPIOC>, 10U, PinReadable> ;

Зачем нужен шаблон Port, который тут все время повторяется? Не проще ли сразу принимать указатель на GPIO_TypeDef?


И вот в таких фрагментах от количества шаблонных параметров становится не по себе


  SPI2::CR1Pack<
    SPI2::CR1::MSTR::Master,   //SPI2 master
    SPI2::CR1::BIDIMODE::Unidirectional2Line,
    SPI2::CR1::DFF::Data8bit,
    SPI2::CR1::CPOL::Low,
    SPI2::CR1::CPHA::Phase1edge,
    SPI2::CR1::SSM::NssSoftwareEnable,
    SPI2::CR1::BR::PclockDiv64,
    SPI2::CR1::LSBFIRST::MsbFisrt,
    SPI2::CR1::CRCEN::CrcCalcDisable
    >::Set() ;

И повторы SPI2::CR1 опять-таки царапают глаз; наверное, от этого можно избавиться, если сложить битовые маски в namespace, а не в класс, и написав using.


С третьей стороны, если rust'овые обертки над регистрами генерируются из svd-файлов, то, может, и ваши тоже можно?

Спасибо за развернутое комментарий, со многим согласен.
По вопросам, думаю вот так:
Зачем нужен шаблон Port, который тут все время повторяется? Не проще ли сразу принимать указатель на GPIO_TypeDef?

Так нельзя делать. Там будет используется reinterpret_cast<>, что сразу же ограничивает использование как в шаблонах, так и в constexpr конструкторах.

В вашем случае, объект GpioPin попадет в ОЗУ, даже если вы сделаете его константным, а это нехорошо для безопасности.

Датчик работает 5 лет до поверки. За это время с ОЗУ все что угодно может случится, чтобы в ОЗУ не было таких «постоянных данных», как раз лучше их кинуть в ПЗУ и делать проверку контрольной суммы ПЗУ, если с ПЗУ что-то произойдет, то выставить ошибку — это проще, чем проверять все объекты ОЗУ или пихать их в специальный сегмент и проверять этот сегмент.

Я за то, чтобы как можно больше за счет самого языка, а не специальных pragm, дефайнов и так далее. А сделать это можно с помощью шаблонов и статических классов или обычных классов и constexpr конструктора.

Я эту проблему описал вот тут habr.com/ru/post/459204,
см Способ 1. Очевидный и, очевидно, не самый лучший.

С третьей стороны, если rust'овые обертки над регистрами генерируются из svd-файлов, то, может, и ваши тоже можно?

Так я и сделал: регистры генерятся из svd файла habr.com/ru/post/459642. Даже для Milandrа очень хорошо все сгенеририлось.

И вот в таких фрагментах от количества шаблонных параметров становится не по себе

Думаю это дело вкуса, на самом деле, это просто установка битов в регистре CR1, можно было бы написать и последовательно:
    SPI2::CR1::MSTR::Master::Set() ;   //SPI2 master
    SPI2::CR1::BIDIMODE::Unidirectional2Line::Set() ;
    SPI2::CR1::DFF::Data8bit::Set() ;
    SPI2::CR1::CPOL::Low::Set() ;
    ...
    SPI2::CR1::CRCEN::CrcCalcDisable::Set() ;

А так строчка идентична
SPI2->CR1 = 
  SPI2_CR1_MSTR | 
  SPI_CR1_BIDIMODE | 
  ...
  SPI_CR1_BR_0 |
  SPI_CR1_BR_2  ;
 

Только еще смысл значения придан, например: SPI2::CR1::MSTR::Master — режим мастера. Здесь значение SPI2::CR1::MSTR::Master тождественно равно (1 << SPI_CR1_MSTR_Pos);

И да согласен, можно заменить SPI2::CR1 на
using CR1= SPI2::CR1
Там будет используется reinterpret_cast<>, что сразу же ограничивает использование как в шаблонах, так и в constexpr конструкторах

Тооочно, совсем забыл про это. Ужасно раздражает.


Так я и сделал: регистры генерятся из svd файла

Оу. Тогда это круто! К генеренному коду у меня заниженные требования :)
В таком случае могу только поапплодировать вашему упорству, меня на такое не хватило бы.


Думаю это дело вкуса, на самом деле, это просто установка битов в регистре CR1, можно было бы написать и последовательно

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

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


Он и проверяет на уникальность, и еще считает маску и записывает одной командой :)
//Класс для работы с регистром, можно передавать список Битовых полей для установки и проверки
template<uint32_t address, size_t size, typename AccessMode, typename FieldValueBaseType,  typename ...Args>
class Register
{
public:
  using Type = typename RegisterType<size>::Type;
  //Метод Set устанавливает битовые поля, только если регистр может использоваться для записи
  __forceinline template<typename T = AccessMode,
          class = typename std::enable_if_t<std::is_base_of<ReadWriteMode, T>::value>>
  static void Set()
  {
    Type newRegValue = *reinterpret_cast<volatile Type *>(address) ; //Сохраняем текущее значение регистра
    
    newRegValue &= ~GetMask() ; //Сбрасываем битовые поля, которые нужно будет установить
    newRegValue |= GetValue() ; //Устанавливаем новые значения битовых полей
    *reinterpret_cast<volatile Type *>(address) = newRegValue ; //Записываем в регистра новое значение
  }

    //Метод устанавливает значение битового поля, только в случае, если оно достпуно для записи
    __forceinline template<typename T = AccessMode,
      class = typename std::enable_if_t<std::is_base_of<ReadWriteMode, T>::value>>
    static void SetAtomic()
    {
      AtomicUtils<Type>::Set(
        address,
        GetMask(),
        GetValue,
        0U
      ) ;
    }

  //Метод Write устанавливает битовые поля, только если регистр может использоваться для записи
  __forceinline template<typename T = AccessMode,
          class = typename std::enable_if_t<std::is_base_of<WriteMode, T>::value>>
  static void Write()
  {
    *reinterpret_cast<volatile Type *>(address) = GetValue() ; //Записываем в регистра новое значение
  }
  
  
  //Метод IsSet проверяет что все битовые поля из переданного набора установлены
  __forceinline template<typename T = AccessMode,
          class = typename std::enable_if_t<std::is_base_of<ReadMode, T>::value ||
                                            std::is_base_of<ReadWriteMode, T>::value>>
  static bool IsSet()
  {
    Type newRegValue = *reinterpret_cast<volatile Type *>(address) ;
    return ((newRegValue & GetMask()) == GetValue()) ;
  }

private:
  //Вспомогательный метод, возвращает маску для конктретного битового поля на этапе компиляции.
  //Метод определен только в случае, если тип битового поля и базовый тип битового поля для регистра совпадают.
  //Т.е. нельзя устанвоить набор битов не соотвествующих набору для для данного регистра.
  __forceinline template<typename T,
          class = typename std::enable_if_t<std::is_same<FieldValueBaseType, typename T::BaseType>::value>>
  static constexpr auto GetIndividualMask()
  {
    Type result = T::Mask << T::Offset ;
    return result ;
  }
  
  //Вспомогательный метод, расчитывает общую маску для всего набора битовых полей на этапе компиляции.
  static constexpr auto GetMask()
  {
    const auto values = {GetIndividualMask<Args>()...} ;  //распаковываем набор битовых полей через список инициализации
    Type result = 0UL;
    for (auto const v: values)
    {
      result |= v ;  //для каждого битового поля устанавливаем битовую маску
    }
    return result ;
  }
  
  //Вспомогательный метод, возвращает значение для конктретного битового поля на этапе компиляции.
  //Метод определен только в случае, если тип битового поля и базовый тип битового поля для регистра совпадают.
  //Т.е. нельзя устанвоить набор битов не соотвествующих набору для для данного регистра.
  __forceinline template<typename T,
          class = typename std::enable_if_t<std::is_same<FieldValueBaseType, typename T::BaseType>::value>>
  static constexpr auto GetIndividualValue()
  {
    Type result = T::Value << T::Offset ;
    return result ;
  }
  
  //Вспомогательный метод, расчитывает значение которое нужно установить в регистре для всего набора битовых полей
  static constexpr auto GetValue()
  {
    const auto values = {GetIndividualValue<Args>()...};
    Type result = 0UL;
    for (const auto v: values)
    {
      result |= v ;
    }
    return result ;
  }
};

#endif //REGISTERS_REGISTER_HPP


Но это один раз сделал и забыл и скрыто от пользователя.

Тогда вопросов более не имею, все круто :)

Вы не промахнулись комментарием случайно? Или я вопрос не понял.

Да есть такое, там ещё и неточности есть, я пару запросов отправил им… "Thank you for your request. I have forwarded those questions to the team responsible for SVD files maintenance"


Приходится пока руками потом немного поправлять.

Датчик работает 5 лет до поверки. За это время с ОЗУ все что угодно может случится, чтобы в ОЗУ не было таких «постоянных данных», как раз лучше их кинуть в ПЗУ и делать проверку контрольной суммы ПЗУ, если с ПЗУ что-то произойдет, то выставить ошибку — это проще, чем проверять все объекты ОЗУ или пихать их в специальный сегмент и проверять этот сегмент.


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

Надёжное применение означает только то, что устройство гарантирует в случае отказа его выявление и соответственно каким то образом оповещение о нем пользователей.
Для устройств с токовой петлёй это выставление тока ниже 3.6 мА или выше 23 мА, например. Целостность ПЗУ обнаружить просто, читаем ее и считаем контрольную сумму — не совпал ставим 3.6 мА в петлю или ниже.
А вот с целостность ОЗУ проблема.
Если с регистрами все понятно, настроил их, потом переодически проверяй, что конфигурация ещё та или просто пиши снова ту же. Сама конфигурация в ПЗУ обычно, то с постоянными данными в ОЗУ проблема, они где угодно и как угодно линкеру лежат. Не будешь же каждый объект контрольной суммой защищать. Поэтому и проще их закинуть в ПЗУ. А сам тест ОЗУ, что она вобще фурычит ещё, писать, читать можно, ячейки не залипли уже отработан + во многих контроллера есть аппаратная проверка. На самом деле проверяется все, ALU, например, что все команды делают то, что надо… я видел отказ АЛУ на меге, датчик пришёл, не поверили вначале. Периодичность выполнения задач, если задача не выполниласъ в отведённое ей время, выставленный в петлю ток, что он примерно соответствует тому, что мы поставили и выход ещё фурычит, что сенсор не отвалился, не деградировал, данные в NV целые…

Не будешь же каждый объект контрольной суммой защищать… А сам тест ОЗУ, что она вобще фурычит ещё, писать, читать можно, ячейки не залипли уже отработан + во многих контроллера есть аппаратная проверка


Ну и прекрасно, не надо каждый объект защищать контрольной суммой. Всего лишь ЕСС и скраббер, частоту работы которого вы подбираете исходя из <something_reason>. У вас же все равно есть изменяемые данные — так и защищайте все разом.

Ну и если так уж хочется положить почему-то именно работу с пинами в ПЗУ… Хз, я бы обошелся минимально шаблонизированным кодом, не таким навороченным. Лично я.

На самом деле проверяется все, ALU, например, что все команды делают то, что надо


Круто, если без локстепа. И как проверяете кстати?
Во, вот это по мне! Я бы сделал точно так же.

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

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


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

Да да да, старая песня, всю жизнь слышу. Только ИРЛ такие ограничения редко встречаются, и то это чаще всего косяк бизнеса (а то и архитектора системы).

Ну, косяки тоже нельзя со счетов сбрасывать. Я лично сталкивался с таким, что
а) Платы уже изготовлены в количестве 30 штук
б) На ней стоит контроллер с 4 Кб ОЗУ, потому что косяк, не заметили
в) Корпус у него такой, что ничего другого не поставить
г) Плату переделать уже не успеваем, придется жить с тем, что есть


Т.е. уметь экономить все-таки приходится, хотя как часто это пригодиться — заранее, конечно, не угадаешь.


Не стоит забывать и про кхм-кхм отечественные МК, где просто очень ограниченный модельный ряд. Хочется контроллер где ОЗУ или флеша побольше? А их нет. И не будет.

Я конечно не хочу сказать, что это вообще не нужно, просто значимость преувеличена. И тонкое место в таком случае имхо будет не в способе хранения класса Pin, а в используемых либах, качестве протоколов связи и изощрённости бизнес-логики. Первое весьма важно, поскольку ценность С++ без стандартной библиотеки резко снижается.


В шаблонной магии автора, несмотря на ее объективную эффективность, меня ещё смущает ее недоступность для применения. Систему обычных классов любой программист слепит на раз-два, а всю вот эту вот красоту надо где-то брать, сам не напишешь. А негде, чтобы и лицензия МИТ, и под любые платформы.

Да, про отечественные МК — у того же миландра многие контроллеры имеют интерфейс внешней памяти. Явно с намеком, поскольку в остальном они не выглядят "мощными" (а у остальных производителей это скорее привилегия для "мощных" контроллеров, чем для всех подряд).

Боже, не хочу даже представлять тот день, когда мне придется еще и с внешней памятью на Миландрах возиться т_т


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

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


В шаблонной магии автора, несмотря на ее объективную эффективность, меня ещё смущает ее недоступность для применения. Систему обычных классов любой программист слепит на раз-два, а всю вот эту вот красоту надо где-то брать, сам не напишешь. А негде, чтобы и лицензия МИТ, и под любые платформы.

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

Боже, не хочу даже представлять тот день, когда мне придется еще и с внешней памятью на Миландрах возиться


О боже, вам вообще приходится с ними возиться? :-(

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


Ммм, поясните? Я человек, далекий от шаблонной магии, поэтому не все сходу понимаю, сорри.

Ну, учитывая, что код автора генерируется, а не руками пишется


Как же это он генерируется? Там данные из CMSIS SVD берутся, как я понял (видимо каким-то парсером, тоже пропреитарным), а сам-то код не генерируется, а пишется человеком с объемной головой.
Ммм, поясните? Я человек, далекий от шаблонной магии, поэтому не все сходу понимаю, сорри.

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


Как же это он генерируется? Там данные из CMSIS SVD берутся, как я понял (видимо каким-то парсером, тоже пропреитарным), а сам-то код не генерируется, а пишется человеком с объемной головой.

Ну, главное ж что не надо все биты и все регистры руками заполнять. Ссылка на исходники парсера вроде в другой статье была. Или это другой парсер?

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


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

Ну, главное ж что не надо все биты и все регистры руками заполнять. Ссылка на исходники парсера вроде в другой статье была. Или это другой парсер?


Хм, не видел эту ссылку. Ну ок, парсер SVD есть, но меня больше беспокоил собственно сам код классов.
Довольно неожиданно. Я был полностью уверен, что уж одинаковый код точно должен быть в единственном варианте, как в случае с перегрузкой методов. И я не понял тогда, как с этим борются, в самых общих чертах?

Насколько я понимаю, тут сложность в том, что разные инстансы шаблонов могут быть созданы в разных единицах компиляции (т.е. вразных срр-файлах), потому что шаблоны всегда в хедерах, которые могут быть подключены в куче разных мест.
Поэтому чтобы провести такую оптимизацию, нужна т.н. "link-time code generation". Некоторые тулчейны вроде бы это умеют, например, у MSVC такой ключ есть.


Как еще борются — даже не знаю, если честно. Я лично просто одинаковый код из шаблонных классов выделяю в обычные, а из шаблонов только вызываю. Или наследую шаблонный класс от обычного. Пока что вроде хватало. Наверно, есть способы и получше.

Из разных единиц компиляции копии одинаковых инстанциированных функций удаляются компановщиком, см. жадную инстанциацию шаблонов начиная с Borlan C++ 4.0.
Но если есть хотя бы один отличный параметр шаблона, то инстанциации имеют разный тип, и как автоматически удалять/сливать методы разных классов?
Но если есть хотя бы один отличный параметр шаблона, то инстанциации имеют разный тип, и как автоматически удалять/сливать методы разных классов?

Если у них одинаковый машинный код, то почему бы не слить?
Например, метод в шаблонном классе, который шаблонные параметры вообще не использует.


Вроде бы в стандарте для функций нет правила, что "разные функции обязаны иметь разные адреса", которое есть для объектов.

За счет декорации имен у них имена разные, что в одном объектном файле, что в разных. И как Вы представляете: компилятор по своему разумению сливает две разноименные сущности в одну? А имя для нее какое должно быть, по которому компановщик правильный адрес вызова запишет? Теоретически идея кажется здравой, но я не берусь просчитать все последствия и варианты.(

Я особых проблем не вижу (хотя, опять-таки, компиляторов не пишу, а только вслух рассуждаю :).


Ну разные имена и разные — почему бы линкеру не создать два имени, которые ссылаются на одну функцию?


По-моему, проблемы могут быть только, если кто-то берет у метода адрес, а потом по адресу сравнивает методы на равенство. Это очень странный юз-кейс, но защититься вроде просто — если у метода берут адрес, то оставлять его уникальным.


Наверное, если код бинарный и библиотечный — тоже могуть быть проблемы, но шаблоны в бинарном виде вроде не экспортируют все равно.


С другой стороны, раз компиляторы не делают эту оптимизацию по-умолчанию, какие-то грабли на этом пути все-таки есть :)

Хммм, я видимо их с чем-то путал; какую-то похожую фишку вроде бы реализовывал только borland сто лет назад.
Спасибо, надо будет потыкать.


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


Но на итоговый бинарник это вроде бы никак не влияет, одни и те же инстансы в нем не должны быть ни так, ни эдак; просто без extern template эта работа перекладывается на линкер.


Но как с помощью этого "объяснить" компилятору, что foo абсолютно идентичен foo — я не знаю.

Borland имеет очень опосредованное отношение к extern template). Borland изобрел жадную инстанциацию шаблонов: все инстанциации сваливаются в особый сегмент, а потом компановщик мержит одинаковые инстанциации в одну, две разные инстанциации под одним символом — ошибка линковки.
extern template велит не инстанциировать в единице компиляции шаблон, так объявленный. Обычно используется с условной компиляцией в заголовках: все инстанциации «включаются» с помощью макро-определения перед включением заголовка в конкретном исходном файле или вообще при сборке динамической библиотеке — и нужные инстанциации оказываются в ней.

Во, вспомнил! Не extern, а export, и не Борланд, а EDG, и не выиграл, а проиграл.

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


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

Поэтому чтобы провести такую оптимизацию, нужна т.н. «link-time code generation»


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

Или наследую шаблонный класс от обычного


А точно ли при этом не создается новых копий и идет сугубо ссылка на методы базового класса?
Не, ну если шаблоны инстанциированы в разных единицах компиляции, то почти очевидно, что и вложенные методы будут скопированы соответствующее число раз. Хотя странно, что компоновщик не оптимизирует это, для перегруженных методов обычных классов это же делается и таблица виртуальных методов одна (по крайней мере я так был всегда уверен). Хотя вот ниже пишут, что все-таки удаляет, но это видимо только полностью идентичных инстансов касается, а не вложенных методов.

Если шаблонный параметр одинаковый, то очевидно, а если он разный — то, видимо, не очень.


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


А точно ли при этом не создается новых копий и идет сугубо ссылка на методы базового класса?

Невиртуальный метод обычного класса — это просто свободная функция с неявным параметром this. Класс один — ну и метод тоже один. Тут я причин для появления копий не вижу.


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


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

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


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

Но если по размеру прошивки явно видно отсутствие этого, то конечно спорить не о чем.

А вообще вся переписка, если на нее поглядеть слегка со стороны, выглядит как «не используйте шаблоны, у меня от этого брат умер там какой-от треш и угар, и станет только хуже»
Я имел в виду вот что — если шаблонный класс отнаследован от обычного, то теоретически компилятор мог бы сказать «да ну нахрен, перегружу-ка я все методы теневым образом, у меня ж тут шаблоны кругом, боюсь некорректного инстанцирования» и сделать копию тех же самых методов, но под видом перегрузки (даже если исходники невиртуальные).

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


А вообще вся переписка, если на нее поглядеть слегка со стороны, выглядит как «не используйте шаблоны, у меня от этого брат умер там какой-от треш и угар, и станет только хуже»

Ну это ж С++, все как обычно. Каждым инструментом нужно уметь пользоваться, больше силы — больше ответственности, стрелять себе в ногу и вот это вот все.

Еще раз: жадная инстанциация шаблонов в Borland C++ 4.0 (собственно, в tlink) открыла дорогу широкому использованию шаблонов на практике и header-only подходу. До этого похожие вещи встречались в виде нестандартной оптимизации времени компановки (привет, optlink!), но общей практикой было запрещать инстанциацию шаблонов в большинстве исходных файлов и явно разрешать ее в конкретных, обычно для этой цели созданных, исходниках.
По личным воспоминаниям о Zortech C++ 3.1 и Borland C++ 3.1 было не очень удобно;)
но общей практикой было запрещать инстанциацию шаблонов в большинстве исходных файлов и явно разрешать ее в конкретных, обычно для этой цели созданных, исходниках


Это как конкретно? Организационно, макрос-магией, или просто шаблоны в cpp-файл прятали?
Ключи компилятора, для gcc, например, -fno-implicit-templates. Для всех единиц компиляции или только для избранных.
А в каком-либо файле/файлах делаются явные инстанциации. Если забыть что-либо инстанциировать — ошибка линковки. Как-бы реализация решающая проблему SDR для шаблонов модель CFront с репозиторием шаблонов. В отличии от подхода Borland больше возни, но зато и контроля больше.
Начиная с C++11 появилось extern template… Можно конкретную инстанциацию подавить на уровне исходного кода.
(ушел читать про явную и неявную инстанциацию шаблонов)
Каждым инструментом нужно уметь пользоваться


Ну или пользоваться совсем чуть-чуть. Описанный вами подход мне подходит.
UFO just landed and posted this here
Вы уже рассказывали про это. Скорее всего то была ATTiny2313 (в те годы), хз как вы тулчейн собрали (в те годы), не понял в чем именно заключалось управление LM317 (квази-ЦАП на GPIO?), ну и самое главное — тут дело ж не в эмбеде, оно и под ПК выглядит как треш и угар, если голова не как у вас.
UFO just landed and posted this here
Стоило ли делать плату? Можно же было на монтажке собрать, схема вроде несложная… Это сейчас DIY-платы типа просто делать, раньше конечно было цирковато.

Я только вот о чем подумал — если в 2008 году вы были еще в школе, то откуда шаблонная магия? Это что за школа такая? Да и даже если самому изучать, то хз как, интернета почти не было, книги в институтской библиотеке все старые… Ну, по крайней мере в библиотеке моего института в те же годы был треш и угар, писанный наверное еще лично Керниганом и Виртом.
UFO just landed and posted this here
А как сейчас делают? Разве не так же печатаете на фотобумаге на лазернике, гладите утюгом, макаете в хлоржелезо на сколько-то там и радуетесь жизни?


Ну, лично я уже давно этим не занимаюсь даже в личных целях, время дороже, чем заказ на фабрике. А вообще народ уже лет десять как вовсю внедряет нормальные УФ-фоторезисты, ну и химия чуть иная бывает.

Началось всё с того, что один тамошний препод мне классе в восьмом посоветовал Стивена Прату, а потом я уже узнал про всяких Саттеров, Мейерсов и прочих Александреску


Господи, Александреску в восьмом классе… Надеюсь учителя посадили потом? :)

Вот смотрите 100 000 датчиков, микро с 32 кбайт ОЗУ, стоит 0.5 центов, а с 48 уже 0.72. Таким образом экономится 22 000 долларов — Полноценный инженер. А если датчиков под 1 000 000. Из-за этого у нас постоянный Cost Reduction для датчиков.
Ещё такая загвостка, ошибка в коде и 100 000 датчиков коту под хвост. Цена ошибки очень высока.

Господь с вами, знаю я все эти расчеты ))

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

Да согласен, но у нас очень консерватиный рынок, мы с петли уйти не можем, а вы про новые фичи, они не нужны никому. Нужна надёжность, на неытеперабатыващем заводе или АЭС, новые фичи внедряются, когда уже они лет 20 проверены на рынке широкого потребления и отлично отлажены. До сих пор микроконтроллерная техника в некоторых местах считается узким местом… Это так побаловаться, да ради красоты можно датчики с Lora на завод вставить, но когда они вам весь канал засрут и данные с них перестанут идти и система откажет, по головке не погладят. Там китайского ещё долго не будет. Цикл жизни датчика 10-15 лет, после чего электронные компоненты устаревают и можно новый делать, такой же, но с графическим индикатором или версией протола FF новым, но существенно ниче не меняется уже лет 30, а то и 70 :}. Аналоговая петля всем рулит на всех заводах во всем мире. Можно даже соотношение посчитать, там все цифровые FF, PF, Modbus к петле. Дай бог соотношение бет 1:10 по миру. А так в мире даже без петли до фига датчиков, больше, чем цифровых. На которые надо ходить и глазами смотреть. Я в Самаре был, девахи по 4 человека в бригаде ходят целый день уровень сжиженного газа в танках смотрят, где выше отметочки, вентиль открывают и сливают… И так весь день. А вы про фичи. Это дома, Алиса включи свет, выключи, а на производствах если оно работает, никто трогать не будет без нужды. Главное, чтобы надёжно было и временем проверено.

Все эти вещи я тоже прекрасно понимаю, честное слово, сам киповец же (в прошлом). И даже в некотором роде отношусь к той категории киповцев, которые скорее за аналоговые преобразователи, пусть и со стандартным выходным сигналом (я не знаю, какие именно датчики вы делаете, вполне возможно в вашем случае операционниками не обойтись). Потому что, да, простота, надежность, монотонность отказов, вот это вот все. И так же точно люблю петлю 4-20 мА. Но есть следующие соображения: во-первых цифровых интерфейсов и функций все больше, во-вторых я уверен, что вы не только датчики делаете и смотрите в другие ниши, и в-третьих — китайцев я упомянул скорее в качестве некоего собирательного образа конечно же. Современному российскому производителю оказывается необходимым конкурировать по цене даже с итальянцами (а это уже не китайцы по качеству, хотя и китайцы бывают конечно очень разные). Кое-где и американцы по цене давят, то есть тут вопрос не в сравнении конкретно с китайцами, а вообще в бесперспективности конкуренции только по цене. И если кроме цены остается в активе только привязанность пользователей, то это неуверенная позиция, даже если речь про рынок переработки.
Можно без повторения cr1, использовав ещё немного шаблонной магии, когда компилятор сам будет знать куда писать значения для регистров. А генерацию из SVD это прям тоже вариант (я в комментарии выше кидал как это прям будет выглядеть).
При использовании этого подхода есть какая-то защита, если попытаться использовать один и тот же пин и как gpio, и как, например, TIO?
Если только все Pin еще добавить в список. То у списка можно проверить что все Pin уникальны…

using Pin0A = Pin<Port<GPIOA>, 0U, PinWriteableConfigurable> ;
using Pin1A = Pin<Port<GPIOA>, 1U, PinWriteableConfigurable> ;
using Pin2A = Pin<Port<GPIOA>, 2U, PinWriteable> ;
...
using Pin15A = Pin<Port<GPIOA>, 15U, PinWriteable> ;
using Pin15 = Pin<Port<GPIOA>, 15U, PinReadable> ;

//using GPIOAPins = PinsPack<Pin1A, Pin2A, Pin3A,...Pin15A, Pin15> ; //При вызове любого метода класса PinsPack компилятор выдаст ошибку, так как Pin15A, Pin15 ссылаются на один и тот же порт и номер пина.
using GPIOAPins = PinsPack<Pin1A, Pin2A, Pin3A,...Pin15A> ; //Ok


int main()
{
  GPIOAPins<0>::Set() ;     // Устанавливаем Pin0 порта А
  GPIOAPins<1>::Toggle() ; // Переключаем Pin1 порта А
  return 0 ;
}


Саму проверку списка можно сделать как-то так:

  template<typename ... Types> struct Collection {};

  template<typename QueriedType, typename T, typename ... Types>
  constexpr static auto IsUniqueType(Collection<T, Types...>)
  {
    auto result = false ;
    constexpr bool match = std::is_same<T, QueriedType>::value;
    if constexpr (sizeof...(Types) != 0)
    {
      result =   match ? false : IsUniqueType<QueriedType>(Collection<Types...>());
    }
    else
    {
      result =  match ? false : true;
    }
    return result ;
  }

  template <typename T, typename... Ts>
  constexpr static void Check(Collection<T, Ts...>)
  {
    if constexpr (sizeof...(Ts) != 0)
    {
      static_assert(IsUniqueType<T>(Collection<Ts...>()), "Беда, одинаковые пины") ;
      Check(Collection<Ts...>()) ;
    }
  }
}
Спасибо, но я несколько более ленивый и невнимательный :) Чтобы получить ошибку компиляции, если мы сначала пин отключили от GPIO при настройке порта, а потом попробовали его обозначить выводом, посоветуете логику мультиплексирование перенести в шаблона пина?

lamerok Сергей, мы тут подумали и решили (ц), что в разделе про атомарные операции написана полная фигня.


Код работает правильно (ну… почти… Работает пока тип этого атомика 32-битный), а вот с объяснением беда…
Подробности у производителя.

Да это цитата с электроникса (я и оформил её в виде цитаты), немного переделал текст. Убрал про прерывания. Хотя я с вами и не согласен до конца. Я полагаю так, что если между LDREX и STREX было прерывание, то STREX не сработает, потому что мое понимание такое, что при каждом входе и выходе из прерывания выполняется CLREX

Я полагаю так, что если между LDREX и STREX было прерывание, то STREX не сработает

Да, всё верно. Это подтверждается и экспериментами (см. на форуме) и документацией (ссылка выше).


А вот про любое обращение к памяти neiver на easyelectronics написал зря, и его там в комментариях поправляли. А Вы это перепечатали...


Ну и код у Вас сильно избыточный.В большинстве случаев достаточно один раз прочитать LDREX'ом и один раз записать, а не перечитывать по три раза...

Простите мя, чайника, но возможно будет лучше если просто парсить комментарии и по ним генерировать код?! Чем смотреть на использование шаблонов и прочих ухищрений чтобы написать:

GPIOB::MODERPack<
GPIOB::MODER::MODER1::Output,         //CS
...
>::Set() ;

два раза gpiob, три раза moder и все проясняющий комментарии - CS.


Или уж как оно должно быть изначально:

    MOV     R1, #0x1            ; Initialize the ‘lock taken’ value
try
    LDREX   R0, [LockAddr]      ; Load the lock value
    CMP     R0, #0              ; Is the lock free?
    ITT     EQ                  ; IT instruction for STREXEQ and CMPEQ
    STREXEQ R0, R1, [LockAddr]  ; Try and claim the lock
    CMPEQ   R0, #0              ; Did this succeed?
    BNE     try                 ; No - try again
    ....                        ; Yes - we have the lock.


PS Я про читабельность, наглядность и простоту использования для всех.

Можно без комментариев, можно руками самому еще определить все вещи для конкретно вашего случая. И оно прямо по вашей спецификации может быть. Например, что CS находится на порту CPIOB.1. Тогда код очень просто валидировать со спецификацией.

using CS_Output = GPIOB::MODER::MODER1::Output;

GPIOB::MODERPack<
CS_Output,         
...
>::Set() ;

Парсится файл от производителя, который точно достоверный, а комментарий это такое себе - может достоверный, может нет.

Sign up to leave a comment.

Articles