Comments 139
Регион памяти во флеше всё ещё регион памяти
А вот когда тем или иным способом в байты такой памяти помещается осмысленное значение — такая область памяти превращается в объект в смысле C, C++ и далее везде. Правда «объектно-ориентированное помешательство на классах» тут не при чем.
объект в смысле C, C++
Откуда вы черпаете это вдохновение? Можно ссылку?
Воспользовавшись прямой цитатой = «An object is a region of storage» — (C++11 draft n3290 §1.8).
А цитата вам не кажется уж слишком похожей на то, что я писал ранее, говоря как раз о том, что там ничего такого нет?)
Что такое осмысленное значение в контексте С++?Инициализированное каким-либо конструктором. И да, у всяких целых чисел в C++ тоже есть конструктор.
Если я делаю reinterpret_cast, то это уже в любом случае не объект с точки зрения C++?Зависит от того, что и куда вы кастите, но в общем случае — нет.
в общем случае — нет.
Это личное
signed char[]
, unsigned char[]
или std::byte[]
) — а вот с другими типами всё может быть уже куда сложнее.Зато он разрешает мне скастовать указатель на любую (условно) область памяти к указателю на любой другой тип и с точки зрения языка это будет вполне себе объект, на котором можно будет вызывать его функции-члены, изменять его состояние. Да, без инициализации и конструктор вызван не будет.
Это неправда, стандарт такого не говорит.Раздел 8.2.1:
If a program attempts to access the stored value of an object through a glvalue of other than one of theИ дальше там список.
following types the behavior is undefined.
Зато он разрешает мне скастовать указатель на любую (условно) область памяти к указателю на любой другой тип и с точки зрения языка это будет вполне себе объектНет. В том-то и дело, что даааалеко не «на любой другой тип». А на весьма ограниченное подмножество. Можно менять
signed
на unsigned
, добавлять/убирать const
, плюс ещё несколько манипуляций. Ну и, как я уже сказал, работать с «сырой» памятью как с массивом байт.Превратить
int
во float
, скажем, нельзя. Это можно через bit_cast сделать — но это не позволяет вам смотреть на ту же память по-другому, все биты объекта при этом копируются в другой объект. И, опять-таки, всё это можно делать, если вы сможете каким-то образом гарантировать, что вы получите валидный объект.и с точки зрения языка это будет вполне себе объект, на котором можно будет вызывать его функции-члены, изменять его состояние.«С точки зрения языка» это не будет валидной программой и может произойти всё, что угодно:
If any such execution contains an undefined operation,И да — слова в скобках это не моё добавление — это часть стандарта.
this International Standard places no requirement on the
implementation executing that program with that input
(not even with regard to operations preceding the
first undefined operation).
Да, без инициализации и конструктор вызван не будет.Будет вызвад другой, «достаточно похожий». А если вы так попытаетесь сменить тип «слишком сильно», то это перестанет быть програмой на C++ и, соответственно, мы потеряем возможность говорить о том, что она будет делать вообще.
всё это можно делать, если вы сможете каким-то образом гарантировать, что вы получите валидный объект.
О чем я и говорю. Логическая валидность — забота программиста, стандарт от этого открещивается заявлениями, которые мы обсуждали выше.
Будет вызвад другой, «достаточно похожий».
Не обязательно. Скажем, открываю файл и читаю заголовки, накладывая на сырые данные известную структуру.
Скажем, открываю файл и читаю заголовки, накладывая на сырые данные известную структуру.… чем вызываете, внезапно, неопределённое поведение — со всеми отсюда вытекающими.
В «чистом» C++ такие вещи категорически запрещены, хотя некоторые другие стандарты и компиляторы (скажем POSIX) могут давать дополнительные гарантии.
Логическая валидность — забота программиста, стандарт от этого открещивается заявлениями, которые мы обсуждали выше.Если бы он просто «открещивался». Положить в байтовый массив 4 байта, а потом прочитать оттуда
int
— вообще говоря запрещено. Этого само по себе достаточно для того, чтобы компилятор получил индульгенцию на то, чтобы отформатировать вам винчестер и устроить вообще что угодно, любую бяку.А вот завести объект сложной структуры на стеке и прочитать туда что-нибудь с помощью
fread
— это пожалуйста, это разрешено.Если мы говорим не про «чистый» C++, а, скажем, про POSIX — то там даются дополнительные гарантии сверх того, что разрешает «голый» C++, иначе использовать mmap и разделяемую память было бы невозможно.
В «чистом» C++ такие вещи категорически запрещены
Бинарные файлы читать и данные по сети передавать?) Или корректная программа на «чистом» языке — это что-то вроде единорога?
А вот завести объект сложной структуры на стеке и прочитать туда что-нибудь с помощью fread — это пожалуйста, это разрешено.
У меня есть подозрение, что мы друг друга не поняли) Вы предлагаете выделять память под объект и заливать его состоянием? В чем принципиальное отличие, если мы в обоих случаях имеем дело с сырыми данными и предполагаем, что они принесут корректное состояние?
В чем принципиальное отличие, если мы в обоих случаях имеем дело с сырыми данными и предполагаем, что они принесут корректное состояние?В том, что этот вариант корректен, а предлагаемый вами — нет?
Смотрите: в C и C++ запрещено (причём довольно-таки в жёсткой форме запрещено) менять тип объекта. Однако есть несколько исключений:
1. К объекту любого типа можно обращаться как к последовательности байт.
2. Если две структуры начинаются с одинаковой преамбулы, то можно обращаться к этим данным этой «преамбулы» через указатель на любую из них (но это только для standard layout типов — есть способ это проверить).
За счёт этого вполне себе можно парсить файлы и делать другие манипуляции.
Вы предлагаете выделять память под объект и заливать его состоянием?Это, собственно, единственный вариант, который C++ предлагает. Вы не можете просто так взять — и изменить или создать объект «за спиной» компилятора. Почитайте про std::launder как-нибудь на досуге.
Но нужно предохраняться, чтобы не подхватить ООП головного мозга, когда «конвертер CAN<->RS485 по протоколу заказчика содержит под 60 объектов». Возможно, вы слишком сильно разбиваете задачу на объекты. Зачем на каждый пин по объекту? Делайте объекты крупнее.
Например, для RS485 это будет 1 класс Rs485Port, который делает запросы через HAL к UART и GPIO, подписывается на события от отдельного класса таймера, и предоставляет API в виде «отправить пакет» и «пакет принят». И это никакой не God object, круг его обязанностей строго ограничен, он легко тестируется и отлаживается. Не нужно ему ни наследований, ни шаблонов.
Делайте объекты крупнее.
Тогда нарушается принцип «один объект — одна область ответственности». А это уже не хорошо. Потому что никто не ждет от «функции с именем print запроса данных от пользователя». Я помню, как у меня горело, когда я обновил HAL, а там функция изменения частоты ядра (HAL_RCC..._Config) еще и инициализировала systic, что приводило к тому, что у меня прерывание срабатывало раньше, чем пройдет инициализация всех сущностей проекта и запустится планировщик FreeRTOS.
Он отслеживаеть все эти тайминги конца пакета, дергает пин DE и пишет/читает в UART.
Не нужно создавать дополнительные объекты «Дергатель пина DE». Или «Читатель абстрактного потока», от которого потом наследуем класс «Читатель потока UART».
Попробуйте нарисовать блок схему вашего устройства на бумажке. В ней должно быть 5-10 основных классов, не больше. Не нужно создавать дополнительный абстрактные классы просто из-за того что у нас ООП. Если у вас в системе чтение из UART нужно только в одном месте и оно реализуется простым вызовом функции HAL_ReadUart(void* data), то не нужны тут «Читатель потока UART» и «Абстрактный читатель».
Вообще, по опыту, правильная архитектура складывается не сразу. Сначала нужно написать чтобы просто работало. Потом, через некоторое время смотрим на код и понимаем, что стоит его отрефакторить, создать новые классы или наоборот избавиться от лишних. После того как сделаешь 10 проектов, уже не захочеться создавать все эти «Абстрактные читатели» там где они не нужны на 100%.
Из вашего примера про функцию настройки частоты я не вижу никаких логических проблем. Таймеры тактируются от ядра. Значит, когда меняется частота ядра, нужно настроить и делители таймера, что логично. Вот что я тут не вижу, так при чем тут С++ или разбиение на объекты.
В моем проекте Ethernet-RS485 всего 18 классов. При этом он умеет ModbusTCP, UDP, есть внутри Web сервер, через который можно менять настройки.
Если хочешь переиспользовать код, то проще скопировать его и переписать под задачу, чем городить дерево абстракций. Проще будет потом, когда будет отлаживать все это. Проще будет другому программисту вникнуть в незнакомый проект, если в нем количество сущностей небольшое.
Идея звучала интересно. Но разбилась о практику. Такие дела.
Я не соглашусь с использованием HAL-а даже. Он дико избыточен. Инициализировать UART у меня занимает 5 строк кода (запись в регистры). А вот HAL просит структуру какую-то. Ведет свои состояния. Логику и прочее. Это ведь тоже по сути объектно-ореинтированный подход. Что уже по сути своей для вывода избыточно. Причем, еще и игнорируется аппаратная часть. Чтобы записать «1» в вывод — не нужно считывать регистр, накладывать маску и записывать обратно. Нужно просто знать про бит-ендинг. И записать число по нужному адресу. Абсолютная атомарность.
cout << "temp: " << sensor.get();
И вопросы «А почему он вдруг не вмещается в МК?! Я же только вывести хотел!». А то что stdout отжирает 300кб в мк с 128 кб flash — его не беспокоило.
Или snprintf внутри прерывания в буфер в 2 кб при стеке под прерывания в 1 кб…
Если хочешь переиспользовать код, то проще скопировать его и переписать под задачу, чем городить дерево абстракций. Проще будет потом, когда будет отлаживать все это.
Когда есть слова «скопировать» и «переписать», то отладки становится ровно в два раза больше, не говоря уже о дальнейшей параллельной жизни родственных версий. Это точно проще, чем оттестированные универсальные абстракции?
Практически проще всего сделать так: «скопировать, переписать под задачу, потом добавить поддержку двух вариантов».
Как правило когда у вас появляется 2-3-4 варианта того, чего вы абстрагируете (пинов, UART'ов, etc — неважно), у вас появляется достаточно материала, чтобы понять, что то, что у вас в руках — таки реально «отлаженные универсальные абстракции». А без этого — велик шанс заложиться расширяемость вообще «не в ту сторону».
Практически проще всего сделать так: «скопировать, переписать под задачу, потом добавить поддержку двух вариантов».
А потом найдется баг в той реагизации, которую вы скопировали. Будете руками ходить и подправлять везде?
Как правило когда у вас появляется 2-3-4 варианта того, чего вы абстрагируете (пинов, UART'ов, etc — неважно), у вас появляется достаточно материала, чтобы понять, что то, что у вас в руках — таки реально «отлаженные универсальные абстракции».
Речь же скорее была о том, что эти абстракции должны быть (не важно когда появившись) и размножаться они должны не копипастой.
Будете руками ходить и подправлять везде?Да, конечно. И при этом смотреть — точно ли мне приходится делать одинаковые правки… или нет?
Понимаете, весь смысл всех этих «оттестированные универсальных абстракций» — в том, чтобы часть повеления была одинаковой (если таких частей нет, то копи-паста уж точно лучше), а часть поведения — разной (если разницы нет, то зачем вам абстракции если конкретная реализация справляется не хуже?).
И вот пока у вас нет нескольких реализаций — вы понятия не имеете, что у вас там должно быть сделано одинаково, а что — по разному.
Попытки «вывосать абстракции из пальца», скорее всего, приведут только к тому, что вы разведёте кучу бессмысленых абстракций, которые решать конечные задачи вам не помогут ни разу…
Речь же скорее была о том, что эти абстракции должны быть (не важно когда появившись)Нет. Их не должно быть до тех пор, пока они не нужны.
и размножаться они должны не копипастой.А как? Как вы поймёте что у вас правильные абстракции не опробовав их на двух (а желательно — на большем числе) вариантах и не сравнив их?
зачем вам абстракции если конкретная реализация справляется не хуже?
Нечитабельный плохокод может работать не хуже (так же быстро, экономично по памяти) красивого и логичного. Но первый поддерживать сложнее. Так и здесь.
Нет. Их не должно быть до тех пор, пока они не нужны.
Зачем так категорично? Абстракции — это не какая-то уникальная фича ООП. Вы исходники на функции/модули разбиваете?
А как?
Наследованием, агрегацией, композицией.
Как вы поймёте что у вас правильные абстракции не опробовав их на двух (а желательно — на большем числе) вариантах и не сравнив их?
Это навык планирования архитектуры приложения (= Чем меньше изменений потребует адаптация под новые условия — тем она лучше.
Это навык планирования архитектуры приложения (= Чем меньше изменений потребует адаптация под новые условия — тем она лучше.Вот только на практике, почему-то, выясняется, что адаптация-то изменений не требует, вот только кода приходится писать втрое больше, чем если бы всех этих абстракиций не было.
Я предполагаю, что исторически больших кодобаз там не было, поэтому и такой консервативный подход сходил с рук. Но прогресс не стоит на месте, повсеместный IoT с толстыми клиентами все ближе.
По этим законам живет и здравствует мир прикладной разработки.Мир прикладной разработки порождает монстров на сотни мегабайт, которые кое-как справляются с задачами, для решения которых и мегабайта-то много.
Не вижу объективных причин для изменения «законов физики» для embedded.Очень просто: в случае с embedded за вычислительные ресурсы и потребляемую память тоже платит разработчик.
Это принципиально меняет систему: решение, при котором вы экономите $100K в год на ещё одного разработчика, а ваши клиенты (суммарно) потом тратят $100M для того, чтобы вашего монстра запустить — больше «не канают».
Абстракции, внезапно, получают вполне себе заметную цену — и оказывается, что смысла в них при таких раскладах — немного.
Но прогресс не стоит на месте, повсеместный IoT с толстыми клиентами все ближе.Он «на горизонте» уже лет десять, но пока ничего подобного в ценовом диапазоне в доли центов (где большая часть embedded исторически живёт) нету. Хотя всё может быть… Посмотрим.
в случае с embedded за вычислительные ресурсы и потребляемую память тоже платит разработчик.
Это немного не в ту степь. Копипаста сама по себе программы не ускоряет, а вот вероятность ошибки увеличивает, что действительно может быть больно в случае с embedded. Этой проблемы бесплатные абстракции лишены.
Он «на горизонте» уже лет десять
Это дело времени. Пока внедряются «умные колонки», а там инфраструктуры будут только расширяться.
Копипаста сама по себе программы не ускоряетИ ускоряет и уменьшает. Абстракции — штука дорогая. По меркам контроллеров — так очень дорогая.
А compile-time zero-cost abstractions дороги в написании и окупаются тогда, когда вам нужно избавиться не от пары-тройки копий, а от сотни.
Этой проблемы бесплатные абстракции лишены.Не бывает бесплатных абстракций. Забудьте. Вы за них неизбежно чем-нибудь платите. Всегда. Даже когда они вам нафиг не нужны. Об этом, собственно, обсуждаемая статья…
Пока внедряются «умные колонки»Я бы сказал, что они пока «хайпуются». Я знаю изрядное количество людей, которые с ними немного игрались, но куда как меньше людей, которые ими пользуются регулярно.
И то же самое и с разными другими «хайповыми» вещами типа часов и браслетов: покупаются, как раз, самые ограниченные и дешёвые вещи, а не самые умные.
Так что стоимость железа — по прежднему важнее «гибкости».
И ускоряет и уменьшает. Абстракции — штука дорогая. По меркам контроллеров — так очень дорогая.
Абстракции, которые нужны для компилятора — зачастую бесплатные. Опять же, вы пользуетесь препроцессором?) Ваши функции компилятор инлайнит?
А compile-time zero-cost abstractions дороги в написании
Это неправда, иначе бы ими никто не пользовался. Но, да, чтобы научиться ими пользоваться программист должен совершить усилие воли однажды.
Не бывает бесплатных абстракций.
Вы себе противоречите)
Так что стоимость железа — по прежднему важнее «гибкости».
Apple смотрит на вас с недоумением.
Apple смотрит на вас с недоумением.Просто вещи, которые продаются лучше, чем Apple Watch (типа Xiaomi Mi Band) — выносятся в другую категорию, что позволяет им «делать хорошую мину при плохой игре».
Чем дальше в лес, тем больше Apple приходится упирать именно на этот навык.
Нет. За так называемые «zero-cost abstractions» тоже приходится платить. Временем компиляции, сложностью кода и так далее.Не бывает бесплатных абстракций.Вы себе противоречите)
Очень просто: в случае с embedded за вычислительные ресурсы и потребляемую память тоже платит разработчик.
Мог бы — дал бы +5 во все кармы!
А потом найдется баг в той реагизации, которую вы скопировали. Будете руками ходить и подправлять везде?
Ну смотрите, вы сделали одно устройство, скажем, CAN-RS485. В нем вы применили универсальный драйвер UART, который оформили в виде внешней библиотеки, которая лежит в отдельном git репозитории. Все работает, устройство продается.
Потом вы сделали новое устройство — метеостанцию. Подключили тот же драйвер, нашли в нем баг и исправили, плюс написали еще функциональности, которая нужна именно под метеостанцию. Вы уверены, что старые устройства при этом не сломаются? Напоминаю, они работали со старым кодом нормально. Будете каждый раз проводить тестирование всех ваших устройств, когда меняете универсальные драйвера? Срочно пойдете менять всем прошивки? А сроки и так горят…
Вы уверены, что старые устройства при этом не сломаются?
Если абстракция есть, то пофиксив в одном месте, я буду уверен, что пофикшу везде, ибо все устройства работают с единым интерфейсом. Если даже потребуются специфичные изменения, они будут либо явно отражены, скажем, наследованием с переопределением метода, либо будут явные общие изменения.
Но в вашем случае нельзя быть увереным, что кто-то (ваш колега, например) не сделал пару фатальных изменений именно в n-ном скопипащенном куске кода, которая разрушит гомогенность интерфейса и фикс бага сделает другой баг.
Не говоря уже о рутине и вероятности ошибиться.
А ведь Windows PC — куда как более гомогенны, чем мир микроконтроллеров.
В том-то и дело, что в случае с «железом» — вы никогда не можете быть до конца уверенным в том, что вы точно знаете что, где и для чего вы фиксите. И никакие абстракции тут не спасут — текут они безбожно.
И никакие абстракции тут не спасут — текут они безбожно.
Это скорее к вопросу о качестве и количестве кода. Вы же пользуетесь какими-то библиотеками?
Вы так говорите, будто в embedded багов не бывает. Так иногда и людей «окирпичивают».Всё бывает. Но традиционное решение (ничего не править без крайней меобходимости в уже вышедших прошивках) резко снижает вред от копи-пасты и фактически умножает на ноль выигрыш от абстракций.
Какая вам разница что у вас там происходит в старых копиях вашего кода, которые вы не собираетесь менять?
Вы же пользуетесь какими-то библиотеками?В тех случаях, когда я чётко понимаю, что цена от их поддержки будет меньше, чем выигрыш от их использования — да, конечно.
На практике это происходит заметно реже, чем многим фанатам «переиспользования кода ради переиспользования кода» кажется.
Но традиционное решение… резко снижает вред от копи-пасты и фактически умножает на ноль выигрыш от абстракций.
Эти суждения нуждаются в аргументации.
Какая вам разница что у вас там происходит в старых копиях вашего кода, которые вы не собираетесь менять?
Я баги фиксить собираюсь в core-логике, мы же с этого начинали. «Копии» не старые, а альтернативные, так же в ходу прямо сейчас.
да, конечно.
То есть универсальные абстракции вы используется, а другой рукой однозначно топите за их однозначный вред.
Ну, такое себе (=
«Копии» не старые, а альтернативные, так же в ходу прямо сейчас.Это никого не волнует. Если они прошли испытания и запущены в производство — то менять их всё равно никто не будет.
То есть универсальные абстракции вы используется, а другой рукой однозначно топите за их однозначный вред.Я ещё и на самолётах летаю — а они, как известно, озоновый слой портит.
Всё — яд, всё — лекарство; отличие лишь в дозе. Вот и с абстракциями так же: разумеется бывают случаи, когда абстракции — полезны. Но эти случаи встречаются куда реже, чем фанатам фабрикфабрикфабрик кажется.
Это никого не волнует.
Еще как волнует.
Но вот этот аргумент «Не нужно!» про поддержку софта отлично демонстрирует глубину глубин. Либо проекты маленькие и поддержка их действительно дешевая, либо…
Всё — яд, всё — лекарство; отличие лишь в дозе.
Внезапно вы противоречите своей категоричности выше.
Либо проекты маленькие и поддержка их действительно дешевая, либо…… либо это таки embedded и никто поддержкой заморачиваться и не собирется.
Подавляющее большинство любимых вами IoT устройст также ничего и никогда не обновляют (даже если технически имеют возможноcть).
Внезапно вы противоречите своей категоричности выше.Где именно? Как я сказал — абстракции всегда имеют цену. А вот отдача от них — гарантирована далеко не всегда.
Потому я и рекомендую их вводить тогда, когда уже очевидно, что отдача — таки будет. Мы с этого начали.
либо это таки embedded и никто поддержкой заморачиваться и не собирется.
У меня таки был опыт работы вокруг эмбеддеда в бытовой технике и там облака для этих целей разворачивали. Пылесосы зачастую USB-выходом оборудованы явно не для закачивания музыки.
Где именно?
Например, в предыдущем сообщении «никто».
Потому я и рекомендую их вводить тогда, когда уже очевидно, что отдача — таки будет. Мы с этого начали.
Почитайте дальше, я вам ответил, что речь про абстракции вообще, а не про то, когда их нужно вводить. А потом началось, что абстракции — это дорого по производительности (прикладное — отстой криворуких, да), 0-cost — это вдруг сложно писать, копипаста рулит, а если не рулит, то это не нужно и вот мы здесь.
Интересно было бы посмотреть на пример кода, чтобы составить впечатление о том, как вы C++ используете.
// SPI
#define PIN_SPI_1_MOSI {GPIOA, {GPIO_PIN_7, GPIO_MODE_AF_PP, GPIO_NOPULL, GPIO_SPEED_FREQ_VERY_HIGH, GPIO_AF5_SPI1}}
#define PIN_SPI_1_MISO {GPIOA, {GPIO_PIN_6, GPIO_MODE_AF_PP, GPIO_NOPULL, GPIO_SPEED_FREQ_VERY_HIGH, GPIO_AF5_SPI1}}
#define PIN_SPI_1_CLK {GPIOA, {GPIO_PIN_5, GPIO_MODE_AF_PP, GPIO_NOPULL, GPIO_SPEED_FREQ_VERY_HIGH, GPIO_AF5_SPI1}}
#define PIN_SPI_1_CS {GPIOA, {GPIO_PIN_4, GPIO_MODE_OUTPUT_PP, GPIO_NOPULL, GPIO_SPEED_FREQ_VERY_HIGH, 0}}
...
// SPI
const pin_cfg pin_spi_1_mosi[] = {PIN_SPI_1_MOSI};
const pin_cfg pin_spi_1_miso[] = {PIN_SPI_1_MISO};
const pin_cfg pin_spi_1_clk[] = {PIN_SPI_1_CLK};
const pin_cfg pin_spi_1_cs[] = {PIN_SPI_1_CS};
...
// SPI
pin spi_1_mosi(pin_spi_1_mosi);
pin spi_1_miso(pin_spi_1_miso);
pin spi_1_clk(pin_spi_1_clk);
pin spi_1_cs(pin_spi_1_cs);
...
const pin *project_name_pin_array[PROJ_NAME_PIN_COUNT] = {
...
&spi_1_mosi,
&spi_1_miso,
&spi_1_clk,
&spi_1_cs,
...
}
...
GlobalProt gb_controller(project_name_pin_array);
...
struct project_name_bsp_controller_cfg {
const pin **initialization_pins;
uint32_t initialization_pins_count;
...
}
proj_name_bsp_controller bsp(&bsp_cfg);
prog_name_controller_cfg pr_cfg {
...
&bsp
};
prog_name_controller proj_name(pr_cfg);
...
int main () {
return proj_name.start();
}
Тут приведена «жизнь обычного вывода». Есть структура с описанием его конфигурации (для удобства, конфигурации описываются отдельными определениями в .h файле для каждого пина, а потом этот define используется для инициализации полей структур). Эта структура «скармливается» глобальному объекту пина в качестве параметра инициализации. Далее указатель на этот объект становится частью глобального массива на объекты используемых пинов. Этот массив является структурой инициализации для объекта GlobalProt, через который производится взаимодействие сразу со всеми выводами (инициализировать все, сбросить конфигурацию, изменить параметры порта целиком и прочее). Указатель на этот объект является частью структуры инициализации bsp контроллера (board support package), который управляет всей периферией и дает высокоуровневый интерфейс для взаимодействия с периферией (на уровне протоколов и нематериальных объектов (не таймер, работающий в режиме pwm,, а «подсветка с яркостью в диапазоне от А до Б»). После чего, для использования аппаратки из кода логики, указатель на объект bsp дается уже контроллеру приложения, в котором инкапсулирована бизнес-логика приложения.
Не понятно, зачем вам иметь отдельно объекты pin_cfg, отдельно объекты pin и отдельно вектор указателей на объекты pin, если вы могли изначально сделать что-то вроде:
pin project_name_pin_array[PROJ_NAME_PIN_COUNT] = {
{PIN_SPI_1_MOSI},
{PIN_SPI_1_MISO},
...
};
Не понятно, зачем вам иметь отдельно объекты pin_cfg
Для того, чтобы не передавать в конструктор класса 8 параметров. Вместо этого указатель на структуру с этими параметрами.
отдельно объекты pin
Ну вот, например, объект UART хочет себе при инициализации объект класса Pin для управления CS (если не используется аппаратный по какой-то причине. Например, если в МК один SPI на кучу устройств).
отдельно вектор указателей на объекты pin
Для объекта, который управляет всеми выводами разом одной командой.
Для того, чтобы не передавать в конструктор класса 8 параметров. Вместо этого указатель на структуру с этими параметрами.
Ну и в чем проблема передать эту структуру прямо в конструктор pin-а? Вам же экземпляр этой структуры больше не нужен, так зачем его вообще создавать как глобальный объект...
Так было задумано для того, чтобы потом можно было изменить параметры вывода на лету (скорости, альтернативная функция).
У вас же объекты pin_cfg константные. Как вы их меняете на лету?
Думается, что вам нужно было иметь что-то вроде:
class pin {
public:
pin(const pin_config & initial) ... {}
...
// Набор сеттеров-геттеров.
void set_seed_freq(...);
void set_...();
...
// Или даже так:
void reset(const pin_cfg & updates) {...}
...
}
Так что ваше пояснение толком ничего не пояснило :(
И при чем здесь это? У вас есть HAL, который вы заполняете на основании параметров из конструктора. Затем вам нужно эту структуру изменить (частично или полностью). Но владеет же ей pin, если я вас правильно понял. Значит pin в своих методах может нужные преобразования выполнить, получая все необходимые параметры в виде аргументов своих методов.
Отдельный объект pin_cfg выглядит просто избыточным здесь. А раз так, то у вас могут быть и другие объекты/классы, которые избыточны и не нужны в принципе для решения задачи.
У вас есть HAL, который вы заполняете на основании параметров из конструктора.
Нет. Не в конструкторе. А в методе init. То есть, в реальном времени. Это нужно для того, повторюсь, чтобы после всех манипуляций по ходу работы с структурой HAL-а иметь возможность восстановить ее в исходное состояние.
Без разницы. Получается, что у вас есть нечто вроде:
class pin {
const pin_cfg * current_config_;
public:
pin(const pin_cfg * config) : current_config_{config} {...}
void init() { ... /* что-то с использованием current_config_ */ }
...
void change_config(const pin_cfg * new_config) {
... /* что-то, например, current_config_ = new_config */
}
};
Собственно, ничего не мешало вам сделать так:
class pin {
pin_cfg current_config_;
public:
pin(... /* параметры для current_config*/) {...}
...
};
И при необходимости менять конфиг вашего pin-а делать это через методы самого pin-а. При этом вам не нужны внешние константные объекты.
Это зависит от того, как вы со своим pin-объектом работаете. Я же не вижу ни реализации pin, ни работы с ним. Вообще все выглядит пока так, что в pin-е как-то и конфиг хранить не нужно. А можно просто подсовывать конфиг прямо в init.
По поводу реализации. Есть у меня проект-песочница. Там я делал нечто подобное. Когда только отрабатывал все эти штуки:
using GreenLed = io::PinA1;
using YellowLed = io::PinA2;
using RedLed = io::PinA3;
using Leds = io::PinList<GreenLed, YellowLed, RedLed>;
...
Leds::set_mode(io::PinMode::PushPull);
// или
using MySvetofor = Svetofor<GreenLed, YellowLed, RedLed>;
Вроде еще Чижов активно пропагандировал такой подход.кто-то должен написать и отладить весь этот метопрограммический DSL для разного железа. Для языка C этим достаточно эффективно занимаются производители железа. А вот mp-решения такой поддержки не имеют, и в конкретном случае их приходится допиливать «под себя» и часто уже не напильником, а расточно-шлифовальным станком. Что обесценивает code reuse и делает такие решения неинтересными.
Но в теории, да, это верный путь.
Такой подход зародился и используется задолго до Чижова, так что это не аргумент.Это был не аргумент, а референс.
этим достаточно эффективно занимаются производители железа.Это скорее исключение, чем правило, взять хотя бы приснопамятный HAL STM32.
Что обесценивает code reuse и делает такие решения неинтересными.В моей практике ситуация обратная. Абстракции написаны и отлажены, а за последние 7 лет ни одной прошивки не написано на чистом C.
Отдачу от инвестиций получилось получить?Собственно разработка библиотек, всегда включалась в работу над прошивкой, поэтому каких-то отдельных средств на это не выделялось. Но контора в плюсах, сроки выдержаны, поэтому экономическую часть можно считать состоявшейся. Были небольшие затыки идеологического характера на начальных этапах, но на общую картину они не повлияли.
Не уверен, но вроде бы Вы где-то уже писали, что туда входит: конфигурация периферии, GPIO, ADC/DAC, UART, SPI? Или я ошибаюсь? Что-то еще?
Например, одним из самых красивых является полный аналог RIME от Данкелса, который совместим с ним по формату пакетов, сохраняет все уровни оригинала, позволяет их тасовать во время компиляции, но не содержит ни одного косвенного вызова внутри себя, из за чего многие функции компилятор успешно инлайнит. И все это на шаблонах, практически без define'ов.
Конечно, чтобы не было неожиданностей, каждый проект содержит копию библиотеки, поэтому риск поломать старое практически отсутствует.
Списки пинов — это достаточно продвинутая тема, не каждый такое может написать, а использовать чужое не каждый хочет… А так да, я в класс SPI передаю 4 пина, сам класс SPI передается в какой-нибудь класс дисплея или sd-карты, итого 2 строки, при этом сами пины существуют можно сказать виртуально, а не в массиве указывающем на реальные объекты. Как-то так:
using spi1 = Spi1<PA7, PA6, PA5, PA4>;
using lcd = LcdSpi<ST7735, LcdOrient::Landscape, spi1, PA8, PA10>;
lcd::init(SpiBaudRate::Presc_2);
Внутри класса SPI пины передаются в другой класс который проверяет их допустимость и если что получаем ошибку компиляции с конкретным указанием какая именно нога задана неверно, а возвращается оттуда уже список пинов с добавленными AF. Далее всему списку задается режим, в данном случае 3 разных режима и т.к. все 4 пина относятся к одному порту, то запись в регистры GPIOA будет объединена. Собственно запись констант в известный набор регистров GPIO — это первая и единственная рантайм операция с пинами, все остальное делается на этапе компиляции, так что бесплатные абстракции очень даже бывают.
Поддержу еще несколько технических моментов:
— Активное использование шаблонов и boost в малых микроконтроллерных проектах, скорее всего, не очень оправдано. По ощущениям — высоко-уровневые абстракции (вместо цикла по массиву делаем коллекцию и обрабатываем ее модными лямбдами) не уменьшают сложность разработки. И про простыни сообщений об ошибках где что не так с шаблоном — автор написал совершенно верно! В скобках замечаю: в больших проектах на Java, наоборот, без объектных фишек жить было бы тяжко. Значит где-то проходит водораздел, и должны быть признаки, по которым можно определять, где ООП дает потенциальные выгоды — а где не дает.
— Чем ближе к аппаратуре работаешь — тем сложнее сделать эффективные абстракции, которые бы при этом не текли. Очень многое в ООП заложено на то, что у объекта есть некое состояние, которое может изменяться не иначе, как вызовом методов этого объекта. В случае с МК — это не всегда (и это мягко сказано) так. Поскольку большая часть объектов отражает свойства реального узла контроллера — вдруг оказывается, что либо состояние объекта меняется без вызова его методов, либо состояние программного объекта не адекватно состоянию реальной кучки транзисторов внутри микросхемы.
— Вместе с тем, оказывается очень (!) продуктивным отлаживать высокоуровневые алгоритмы не внутри микроконтроллера — а на хост-системе (например на Linux), имитируя поступление данных из заранее подготовленных файлов. Поэтому некоторый (тонкий!) слой абстракции в средние-большие проекты на МК вводить все-таки стоит. По крайней мере, становится гораздо легче разбираться — то ли проблема с аппаратной частью МК (или — что чаще: нашим неправильным пониманием, как она ждет чтобы мы с ней работали), то ли проблема уровнем выше в алгоритмах опроса/управления.
По ощущениям — высоко-уровневые абстракции (вместо цикла по массиву делаем коллекцию и обрабатываем ее модными лямбдами) не уменьшают сложность разработки.
А где вот такая вот замена, т.е. коллекция вместо массива, может быть оправдана вообще?
Что для обработки лямбдами, то вроде как уже давно что простой цикл по массиву, что range-for, что std::for_each с лямбдой компиляторами разворачивается в один и тот же код. В релизной сборке, естественно.
— До некоторой степени облегчается построение сложных обработок, т.к. используется комбинация лямбд, а не постоянные изменения кода внутри цикла.
— По мере жизненного цикла проекта проще прикрутить кеширование результатов расчетов (буде таковое потребуется), или, например, ограничить одновременно выполняющееся количество «тяжелых» вычислений.
— Но самое главное — на энтерпрайзе всегда есть шанс что придется систему горизонтально масштабировать. Сегодня тебе на вход подают 10к данных, а через пять лет — 1Тб. При небольшом везении, код с лямбдами путем применения хитрой библиотеки сам распараллелит обработку на несколько ядер/GPU/кластеров, раздав куски данных и копии лямбды каждому worker-у.
А вот внутри МК таких задач не наблюдается. Там обычно задача очень конкретная — и один раз хорошо написанная прошивка может годами работать без всякого развития и изменения. С третьей стороны — бывают и большие проекты под МК (а-ля управление 3Д-принтерами). Но даже там сейчас народ немного переосмысливает ситуацию: на МК оставляют только низкоуровневое дрыгание ногами, а на микрокомпьютер с Linux — высокоуровневые расчеты и логику. Это проект Klipper — я за ним последнее время все больше наблюдаю.
Вопрос все-таки был не про лямбды.
Контейнер для данных выбирается под конкретные нужды и требования. Причем массив (как и std::array, как и std::vector) может быть наиболее эффективным вариантом в определенных условиях. Соответственно, если массив эффективное представление данных — то зачем его на что-то менять?
А если нужно менять, то не суть, МК это или нет.
В больших проектах мы часто пишем Collection вместо указания конкретного типа (Java, конечно — но смысл будет тот же)… Но хотим чтобы работало — поэтому и извращаемся с абстрактными типами данных и прочей мощью ООП.
Т.е. по факту вы смотрите на C++ глазами Java-разработчика. Тогда как в C++ нет упоротого ООП вокруг коллекций. И, соответственно, в C++ вы не можете просто так заменить один тип коллекции на другой. Если только ваш код изначально не был написан на шаблонах.
Если вам интересно померяться, то я на C++ кодил когда Java еще даже не выросла из Oak project. Так что вопрос не в длине опыта.
Суть в том, что не нужно подходы из Java, в которой кроме ООП ничего толком и не было (да и сам ООП там кастрированный по сравнению, например, c Eiffel-ем), переносить на C++. Если в C++ кто-то вместо простого массива берет какой-то хитрый контейнер и начинает с ним работать через лямбды, то либо задача такая, либо человек просто упоролся и сам не ведает, что творит. Специфика МК тут сильно сбоку.
Никакого динамического выделения памяти, «кучи» вообще нет — только так можно гарантировать отсутствие утечек.
Объекты относительно крупные — «модуль ШИМ», «Драйвер RS485», «Модуль инкрементального энкодера». Внутри — всё на «регистрах» и дефайнах, вложенных классов нет (или они редки, типа модуля фильтра первого порядка). «HAL» делаем дефайнами пинов или отдельными функциями (init для этой конфигурации, init для той) — т.е. не делаем. Проще переписать пяток пинов, чем разгребать кучу слоёв абстракции.
Каждый вызов функции на счету, и для пущей оптимизации кода иногда приходится делать функции inline или вообще макросами.
Си++ балуемся периодически, но далеко от Си он не отходит (в силу статического выделения памяти на этапе компиляции). Ну объекты чуть поприятнее выглядят, и пару раз за проект можно наследование применить. Но дизассемблер смотреть менее наглядно становится, и я лично люблю чистый Си.
Такая же ерунда была, пока не сделали так: на С++ в основном бизнес логика, т. е никаких объектов на каждый пин. Только скажем объект драйвер Spi. Железо же в основном инициализируется в -_low_level_init, т. Е из документа, описывающего, какие модули и как должны быть настроены, просто все так и настраиваем. Эта функция выполняется ещё до инициализации всех объектов. Поэтому скажем драйвер Spi уже не надо заботиться об настройке модуля Spi. Драйвер отвечает только за приём, передачу…
В итоге бизнес логика, была полностью переносима и очень понятна, а вот железо да приходилось везде переписывать. Но это все равно проще, чем придумать универсальное решение и запутать мозг :). Да есть такая проблема… подтверждаю те же грабли были.
1. Пишут на C/С++ в Keil/IAR. Получают жирный код. И не стесняясь заливают в МК. Благо что памяти сейчас много.
2. Пишут на С в Keil/IAR. Получают жирный код. Дизассемблируют. Оптимизируют, уменьшая код на 30-70-100% ) — > получают быстрый маленький код.
Для второго случая, конечно же требуется отличное знание МК, под который все и пишется.
сложность программы растёт, пока не превысит способности программиста
В аспирантуре пришлось писать под МК, при том что на тот момент у меня ни опыта, ни толком знаний не было, что о микроконтроллере, что о плюсах. Однако предложили, а я согласился. Навертел несколько абстракций, одна вокруг "железа", другая вокруг логики и её состояния.
Поскольку задача стояла не "написать одну систему, которую можно будет переносить на N других платформ", а "иметь одну-единственную систему под одну-единственную платформу, чтобы можно было её переконфигурировать/добавить фичу/убрать фичу за 5 минут", получилось довольно-таки удобно.
У меня вся работа с периферией идёт через драйвер (который настраивает пины, клоки и прочее). Причём драйвер это аппаратная сущность. Драйвер АЦП, УАРТ, CAN и прочая. Его вызывает эээ драйвер конечного устройства. Например, дисплей сидит SPI, у него своя инициализация, ну или внешние часы например. Ну и наверху сидит логика с планировщиком.
Аппаратный драйвер периферии имеет в основном следующие функции: Init, GetError, Receive, Transmit.
Драйвер конечных устройств зависит от устройства. (SetPixel, ClearScreen and so on).
Ну и как бы всё. Пока хватает на все случаи жизни.
зачем вообще использовать C++?Выскажу непопулярное мнение — C++ стоит использовать для передачи по ссылке и кучи мелкого синтаксического сахара, типа нормального типа bool, «структур с методами» и так далее.
Иными словами — С++ использовать стоит, ООП — нет в некоторых случаях — нет.
Проблема ООП — очень узких объектах. В простых случаях — мы имеем всего одну иерархию объектов. В сложных (множественное наследование) — несколько иерархий и всё.
А в реальном мире — немного не так. В С++ потомок курицы — всегда будет иметь свойства птицы и курицы. Да, он может стать омлетом, потом — омлетом по кубански, но вегетарианским блюдом ему никогда не стать. Даже если мы от него унаследуем крем для торта. Хотя в реальной жизни часть вегетарианцев едят яйца, а уж тем более — торты с кремом.
Поэтому Си, позволяющий выстраивать недобобъекты, как угодно — рулит в тех местах, где иерархии сложные.
А там, где иерархия сходу не выстраивается — лучше недообъекты Си.
Что такое включение питания на UART? Это часть UART или часть процедуры подачи питания? А что такое подача тактирования? Она чья часть? Ладно, решили.
А как в SPI? Ровно так же, как в UART? А что делать, если это не удобно?
Вот если неудобно — значит нужны сишные недообъекты. Которые позволяют для SPI решать так, для UART — иначе, для CAN — третьим путем. Или вообще — совсем процедурно.
ООП — отличная штука, но лишь тогда, когда в задаче объекты выделяются естественным образом.
Реляционный подход крайне гибок, поэтому в 99.9% используется он. В реляционной СУБД мы вполне можем сделать запрос «продукты, включающие белок курицы» и получить торт. А можем сделать запрос иначе «продукты, включающие куриное мясо» — и торт не войдет.
И главное. В реляционной СУБД для этого не надо переделывать структуру СУБД — достаточно лишь поменять запрос. То есть данные отделены от иерархической структуры. И для гибкости и производительности подходит только он.
А если вы строгий вегетарианец или аллергик, то кремовый торт наследуется много от чего, но в том числе — и от курицы. И для аллергика принципиально, куриные там яйца или страусиные.
А в части написания — реляционная структура данных в программе — попроще объектной. Так что когда у вас много параллельных иерархий или новые иерархии могут появится после написания кода — вспомните, что не обязательно жестко запихивать иерархию в код, а можно применить реляционный подход.
И SoC с ее устройствами — тоже плохо ложится на ООП.
P.S Чуть больше.20 лет назад я принимал участие в разработке объектной КИС на базе объектной СУБД в качестве руководителя группы тестирования. Так что все плюсы ООП в СУБД видел. Равно как и минусы.
Про СУБД. Мне кажется, что вы не работали с объектной СУБД. Но самое лучшее, что можно сделать с СУБД — это сделать её независимой от структуры данных, то есть реляционной. Тогда разные приложения, работающие с единой СУБД, имеют разную структуру. А попытка сделать единые объекты — это лебедь рак и щука в одной упряжке.
Про SoC. Типичная картина такая — 20 солитонов, 20 классов, 50 кирпичиков. которые делегируются (обычно в одно, изредка в два места). То есть повторного использования кода — с гулькин нос. Поэтому и ООП (даже без наследования) — все равно лишние накладные расходы.
Вся выгода от ООП — повторное использование кода. Если его нет — ООП — лишь лишние расходы по сравнению с процедурным подходом. Поэтому в одних вещах ООП очень полезен, а в других — нет.
Причем, все очень-очень конкретно. Сила ООП — в повторном использовании кода. Если вы можете один раз написать объект и 10 раз его использовать — отлично, ООП применим. Если для каждого нового использования приходится переделывать объекты — значит в конкретном случае ООП хуже процедурного подхода.
Вторая проблема ООП — у вас в любом случае «одна сущность — один объект». Это не гибко. В 99% случаев — это не мешает. Ну да, не гибко, но гибкость не нужна. Конкретно в SoC бывают ситуации, когда гибкость нужна. В итоге — получаем дробление на кучу мелких микрообъектов. Ну или отказ от ООП.
Как видите, применять или не применять ООП — зависит не только от предметной области, но и от того, сколько вы программ пишите. 20 уникальных солитонов, используемых в одном проекте скорее всего означают, что вы зря потратили силы на ООП. Повторное использование тут равно нулю. Так можно писать, лишь когда оверхед небольшой и плюсы от инкапсуляции перекрывают её минусы.
Резюме — все очень-очень конкретно.
P.S. Вообще, делая многоуровневые абстракции, прежде всего думайте, что вы собрались скрывать, от кого и зачем. И если у вас нет ответов на эти вопросы — не надо скрывать «потому, что все так делают».
Вот, вот. Начнут писать прошивки на c++, а потом с++ программист уходит в backend и ищине нового хипстера поддерживать весь этот абстракционизм Пикассо.
В предприятиях, где удалось поработать с момента написания статьи везде С++ кстати был на эмбеде. Чтобы конструктором прошивки собирать под достаточно мощные камни
А компилятор какой был?
Cкрипты сборки на чём писали?
Везде gcc, где-то фиксированный 10-й, где-то последний 13-й. Сборка Makefile умерла в целом. Давно не встречал. CMake + Makefile или Ninja
Сборка Makefile умерла в целом
Неправда.
Голый make отлично подходит для конфигурирования сборок для микроконтроллеров
Вот методичка
№1
Настройка ToolChain(а) для Win10+GCC+С+Makefile+ARM Cortex-Mx+GDB
https://habr.com/ru/articles/673522/
№2
Сборка firmware для CC2652 из Makefile
https://habr.com/ru/articles/726352/
Сборка из make хороша тем, что при сборке из make все микроконтроллеры выглядят одинаково.
Как вы в С++ решаете тот факт, что последовательность запуска конструкторов для глобальных объектов может быть в случайном порядке?
Ведь нельзя допустить, чтобы, например, класс SPI про инициализировался раньше класса GPIO.
Краткий ответ - никак. Длинный: инициализировать в конструкторах только данные-поля экземпляра класса. А уже в main в нужной последовательности init методы. Чтобы и порядок нужный и поведение, если чего не так
Пять лет использования C++ под проекты для микроконтроллеров в продакшене