Пять лет использования C++ под проекты для микроконтроллеров в продакшене

    В данной статье я расскажу, как я в течение пяти лет переводил предприятия, на которых работал, с ведения проектов под микроконтроллеры на C на C++ и что из этого вышло (спойлер: все плохо).

    Немного о себе


    Я начал писать под микроконтроллеры на C, имея лишь школьный опыт работы с Pascal, потом изучил ассемблер и порядка 3 лет потратил на изучение различных архитектур микроконтроллеров и их периферии. Затем был опыт реальной работы на C# и C++ с параллельным их изучением, который занял несколько лет. После этого периода я вновь и надолго вернулся к программированию микроконтроллеров, уже имея необходимую теоретическую базу для работы над реальными проектам.

    Первый год


    Я ничего не имел против процедурного стиля C, однако предприятие, на котором началась моя реальная практика над реальными проектами использовало «программирование на Си в объектно-ориентированном стиле». Это выглядело примерно так.

    typedef const struct _uart_init {
    	USART_TypeDef *USARTx;
    	uint32_t baudrate;
    	...	
    } uart_cfg_t;
    
    int uart_init (uart_cfg_t *cfg);
    int uart_start_tx (int fd, void *d, uint16_t l);
    int uart_tx (int fd, void *d, uint16_t l, uint32_t timeout);

    Данный подход имел следующие преимущества:

    1. код продолжал оставаться кодом на C. Отсюда вытекают следующие достоинства:
      • проще контролировать «объекты», поскольку несложно проследить кто и где что вызывает и в какой последовательности (за исключением прерываний, но о них не в этой статье);
      • для хранения «указателя на объект» достаточно запомнить возвращенный fd;
      • если «объект» был удален, то при попытке его использовать вы получите соответствующую ошибку в возвращаемом значении функции;
    2. абстракция таких объектов над использовавшимся там HAL-ом позволяла писать настраиваемые под задачу из собственной структуры инициализации объекты (а в случае недостачи функционала HAL-а можно было прятать обращение к регистрам внутри «объектов»).

    Минусы:

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

    На вопрос о том, почему не был выбран C++ при формировании всей инфраструктуры, мне отвечали примерно следующее: — «Ну C++ ведет к сильным дополнительным расходам, неконтролируемым расходам памяти, а так же громоздкому исполняемому файлу прошивки». Возможно, они были правы. Ведь в момент начала проектирования был лишь GCC 3.0.5, который не блистал особым дружелюбием к C++ (до сих пор приходится работать с ним для написания программ под QNX6). Не было constexpr и C++11/14, позволяющих создавать глобальные объекты, которые по сути представляли собой данные в .data области, вычисленные на этапе компиляции и методы к ним.

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

    Осознав все это и поняв, что сейчас С++ уже не такой, каким был при GCC 3.0.5 я принялся переписывать основную часть функционала на C++. Для начала работу с аппаратной периферией микроконтроллера, затем периферию внешних устройств. По сути, эта была лишь более удобная оболочка над тем, что имелось на тот момент.

    Год второй и третий


    Я переписал все необходимое для своих проектов на C++ и продолжал писать новые модули уже сразу на C++. Однако это были всего лишь оболочки над C. Поняв, что я недостаточно использую C++ я начал использовать его сильные стороны: шаблоны, header-only классы, constexpr и прочее. Все шло хорошо.

    Год четвертый и пятый


    • все объекты глобальные и включают ссылки друг на друга на этапе компиляции (согласно архитектуре проекта);
    • всем объектам выделена память на этапе компоновки;
    • по объекту класса на каждый пин;
    • объект, инкапсулирующий все пины для их инициализации одним методом;
    • объект контроля RCC, который инкапсулирует все объекты, которые находятся на аппаратных шинах;
    • проект конвертера CAN<->RS485 по протоколу заказчика содержит под 60 объектов;
    • в случае, если что-то на так на уровне HAL-а или класса какого-то объекта, то приходится не просто «исправлять проблему», а еще и думать, как ее исправить так, чтобы это исправление работало на всех возможных конфигурациях данного модуля;
    • используемые шаблоны и constexpr невозможно просчитать до просмотра map, asm и bin файлов конечной прошивки (или запуска отладки в микроконтроллере);
    • в случае ошибки в шаблоне выходит сообщение длиной с треть конфигурации проекта от GCC. Прочитать и понять из него что-то — отдельное достижение.

    Итоги


    Сейчас я понял следующее:
    1. использование «универсальных конструкторов модулей» лишь без надобности усложняет программу. Куда проще оказывается поправить регистры конфигурации под новый проект, чем копаться в связях между объектами, а потом еще и в библиотеке HAL-а;
    2. не стоит бояться использовать C++ опасаясь того, что он «сожрет много памяти» или «будет менее оптимизирован чем C». Нет, это не так. Нужно бояться того, что использование объектов и множество слоев абстракции сделает код не читаемым, а его отладку — героическим подвигом;
    3. если не использовать ничего «усложняющего», как шаблоны, наследование и прочие манящие прелести C++, то зачем вообще использовать C++? Только ради объектов? А оно того стоит? Причем ради статических глобальных объектов без использования запрещенных на некоторых проектах new/delete?

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

    Подробнее
    Реклама

    Комментарии 129

      +5
      «по объекту класса на каждый пин»… так и живем (((
        0

        Эти объекты совсем не означают какой-то рантайм-оверхед, так что непонятно, что вас в такой жизни смущает.

          0
          Но ведь тогда это и не объекты? Во всяком случае в не в смысле объектов по стандарту C++.
            +2
            Почему же? constexpr объект может представлять из себя один get метод для const поля. Вполне себе. При этом переменная будет лежать во flash. Ровно как и метод. Если конечно опциями компоновщика не задано иное.
              +1
              В Стандарте пишут, что объект — это регион памяти, но не функция. Каких-то особых свойств от него не требуется.
                0
                тогда єто не объект, а структура данных, если без свойств.
                  0
                  Почему же объект не является структурой данных? Дайте тогда ваше определение объекта, если не согласы со стандартом)
                    0
                    Потому что набежали апологеты языков, в которых между компьтером и программистом стопицот слоев абстракций и рантаймов. )))
                      0
                      А давайте конструктивно дискутировать, а не делиться стереотипами?)
                        0
                        Опыт показывает, что конструктивно получается редко, но давайте.
                        Ваш ход
                          0
                          Начните с конструктивного ответа на мой вопрос выше, пожалуйста.
                  0

                  Регион памяти во флеше всё ещё регион памяти

                    0
                    Речь про оперативную память, разумеется.
                      0
                      Просто оперативная память — это «объект» совершенно особого типа: неинициализированная память. Послушайте разработчиков компиляторов от C до Go и Rust — они еще и в видах неинициализированной памяти разбираются.
                      А вот когда тем или иным способом в байты такой памяти помещается осмысленное значение — такая область памяти превращается в объект в смысле C, C++ и далее везде. Правда «объектно-ориентированное помешательство на классах» тут не при чем.
                        0
                        объект в смысле C, C++

                        Откуда вы черпаете это вдохновение? Можно ссылку?
                          0
                          В качестве ликбеза
                          Воспользовавшись прямой цитатой = «An object is a region of storage» — (C++11 draft n3290 §1.8).
                            0
                            И где там написано про инициализацию? Если я делаю reinterpret_cast, то это уже в любом случае не объект с точки зрения C++? Что такое осмысленное значение в контексте С++ и где почитать об этом термине?

                            А цитата вам не кажется уж слишком похожей на то, что я писал ранее, говоря как раз о том, что там ничего такого нет?)
                              0
                              Что такое осмысленное значение в контексте С++?
                              Инициализированное каким-либо конструктором. И да, у всяких целых чисел в C++ тоже есть конструктор.

                              Если я делаю reinterpret_cast, то это уже в любом случае не объект с точки зрения C++?
                              Зависит от того, что и куда вы кастите, но в общем случае — нет.
                                0
                                в общем случае — нет.

                                Это личное заблубеждение?
                                  0
                                  Это то, что говорит стандарт: вы можете рассмотреть любую область памяти как последовательность байт (signed char[], unsigned char[] или std::byte[]) — а вот с другими типами всё может быть уже куда сложнее.
                                    0
                                    Это неправда, стандарт такого не говорит.

                                    Зато он разрешает мне скастовать указатель на любую (условно) область памяти к указателю на любой другой тип и с точки зрения языка это будет вполне себе объект, на котором можно будет вызывать его функции-члены, изменять его состояние. Да, без инициализации и конструктор вызван не будет.
                                      0
                                      Это неправда, стандарт такого не говорит.
                                      Раздел 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++ и, соответственно, мы потеряем возможность говорить о том, что она будет делать вообще.
                                        0
                                        всё это можно делать, если вы сможете каким-то образом гарантировать, что вы получите валидный объект.

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

                                        Будет вызвад другой, «достаточно похожий».

                                        Не обязательно. Скажем, открываю файл и читаю заголовки, накладывая на сырые данные известную структуру.
                                          0
                                          Скажем, открываю файл и читаю заголовки, накладывая на сырые данные известную структуру.
                                          … чем вызываете, внезапно, неопределённое поведение — со всеми отсюда вытекающими.

                                          В «чистом» C++ такие вещи категорически запрещены, хотя некоторые другие стандарты и компиляторы (скажем POSIX) могут давать дополнительные гарантии.

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

                                          А вот завести объект сложной структуры на стеке и прочитать туда что-нибудь с помощью fread — это пожалуйста, это разрешено.

                                          Если мы говорим не про «чистый» C++, а, скажем, про POSIX — то там даются дополнительные гарантии сверх того, что разрешает «голый» C++, иначе использовать mmap и разделяемую память было бы невозможно.
                                            0
                                            В «чистом» C++ такие вещи категорически запрещены

                                            Бинарные файлы читать и данные по сети передавать?) Или корректная программа на «чистом» языке — это что-то вроде единорога?

                                            А вот завести объект сложной структуры на стеке и прочитать туда что-нибудь с помощью fread — это пожалуйста, это разрешено.

                                            У меня есть подозрение, что мы друг друга не поняли) Вы предлагаете выделять память под объект и заливать его состоянием? В чем принципиальное отличие, если мы в обоих случаях имеем дело с сырыми данными и предполагаем, что они принесут корректное состояние?
                                              0
                                              В чем принципиальное отличие, если мы в обоих случаях имеем дело с сырыми данными и предполагаем, что они принесут корректное состояние?
                                              В том, что этот вариант корректен, а предлагаемый вами — нет?

                                              Смотрите: в C и C++ запрещено (причём довольно-таки в жёсткой форме запрещено) менять тип объекта. Однако есть несколько исключений:
                                              1. К объекту любого типа можно обращаться как к последовательности байт.
                                              2. Если две структуры начинаются с одинаковой преамбулы, то можно обращаться к этим данным этой «преамбулы» через указатель на любую из них (но это только для standard layout типов — есть способ это проверить).

                                              За счёт этого вполне себе можно парсить файлы и делать другие манипуляции.

                                              Вы предлагаете выделять память под объект и заливать его состоянием?
                                              Это, собственно, единственный вариант, который C++ предлагает. Вы не можете просто так взять — и изменить или создать объект «за спиной» компилятора. Почитайте про std::launder как-нибудь на досуге.
            +7
            С++ использовать, безусловно, стоит, хотя бы ради улучшенного синтаксиса (namespace, auto, template).
            Но нужно предохраняться, чтобы не подхватить ООП головного мозга, когда «конвертер CAN<->RS485 по протоколу заказчика содержит под 60 объектов». Возможно, вы слишком сильно разбиваете задачу на объекты. Зачем на каждый пин по объекту? Делайте объекты крупнее.
            Например, для RS485 это будет 1 класс Rs485Port, который делает запросы через HAL к UART и GPIO, подписывается на события от отдельного класса таймера, и предоставляет API в виде «отправить пакет» и «пакет принят». И это никакой не God object, круг его обязанностей строго ограничен, он легко тестируется и отлаживается. Не нужно ему ни наследований, ни шаблонов.
              0
              Делайте объекты крупнее.

              Тогда нарушается принцип «один объект — одна область ответственности». А это уже не хорошо. Потому что никто не ждет от «функции с именем print запроса данных от пользователя». Я помню, как у меня горело, когда я обновил HAL, а там функция изменения частоты ядра (HAL_RCC..._Config) еще и инициализировала systic, что приводило к тому, что у меня прерывание срабатывало раньше, чем пройдет инициализация всех сущностей проекта и запустится планировщик FreeRTOS.
                +5
                В моем примере у объекта Rs485Port как раз одна область ответственности — передать на линию 485 и принять из нее блок данных.
                Он отслеживаеть все эти тайминги конца пакета, дергает пин DE и пишет/читает в UART.
                Не нужно создавать дополнительные объекты «Дергатель пина DE». Или «Читатель абстрактного потока», от которого потом наследуем класс «Читатель потока UART».
                Попробуйте нарисовать блок схему вашего устройства на бумажке. В ней должно быть 5-10 основных классов, не больше. Не нужно создавать дополнительный абстрактные классы просто из-за того что у нас ООП. Если у вас в системе чтение из UART нужно только в одном месте и оно реализуется простым вызовом функции HAL_ReadUart(void* data), то не нужны тут «Читатель потока UART» и «Абстрактный читатель».

                Вообще, по опыту, правильная архитектура складывается не сразу. Сначала нужно написать чтобы просто работало. Потом, через некоторое время смотрим на код и понимаем, что стоит его отрефакторить, создать новые классы или наоборот избавиться от лишних. После того как сделаешь 10 проектов, уже не захочеться создавать все эти «Абстрактные читатели» там где они не нужны на 100%.

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

                В моем проекте Ethernet-RS485 всего 18 классов. При этом он умеет ModbusTCP, UDP, есть внутри Web сервер, через который можно менять настройки.
                  +1
                  Конечная цель (еще до моего прихода на предприятие) было создание «конструктора». Когда можно «собрать устройство» из готовых блоков на проверенной архитектуре. Поэтому и классов много. Если бы я создавал устройство с нуля, то я бы делал так, как делаю на фрилансах: на Си пишу драйвер под архитектуру (если вдруг нужно что-то новое) и на Lua пишу логику. Которую, в случае чего, может править пользователь.
                    +3
                    Понятно, это и есть ООП головного мозга, когда пытаются делать «конструкторы». Вполне вероятно, это идея какого-нибудь менеджера — ускорить разработку будущих устройств, сделав универсальные блоки. Потом на это наложилось мировосприятие программиста, недавно прочитавшего про ООП умную книжку. Налепили абстракций и теперь чтобы дернуть пин и включить светодиод, им нужно не вызвать 1 функцию из HAL с записанными прямо в параметрах номерами пинов. Им нужно создать кучу структур описателей пинов, их положить в массив, массив положит в класс BSP, этот класс передать в драйвер устройства, и наконец объект «Светодиод» вызовет у драйвера функцию включения светодиода.

                    Если хочешь переиспользовать код, то проще скопировать его и переписать под задачу, чем городить дерево абстракций. Проще будет потом, когда будет отлаживать все это. Проще будет другому программисту вникнуть в незнакомый проект, если в нем количество сущностей небольшое.
                      +3
                      Еще была цель в будущем переложить задачу создания «однотипных устройств» на программистов, не имеющих опыта работы с железом. Например, когда меняется только протокол (для устройств сопряжения).
                      Идея звучала интересно. Но разбилась о практику. Такие дела.
                      Я не соглашусь с использованием HAL-а даже. Он дико избыточен. Инициализировать UART у меня занимает 5 строк кода (запись в регистры). А вот HAL просит структуру какую-то. Ведет свои состояния. Логику и прочее. Это ведь тоже по сути объектно-ореинтированный подход. Что уже по сути своей для вывода избыточно. Причем, еще и игнорируется аппаратная часть. Чтобы записать «1» в вывод — не нужно считывать регистр, накладывать маску и записывать обратно. Нужно просто знать про бит-ендинг. И записать число по нужному адресу. Абсолютная атомарность.
                        0
                        +1 за невозможность программировать МК без понимания железа. Или этого программиста нужно абстракциями так обложить, что он вздохнуть не сможет (не то что полезную работу делать). Или он без задней мысли так использует достаточно мощные абстракции, что вызовет глюки или зависание софта. И без шансов понять, что именно он такое сделал и как это можно было бы починить! :-)
                          +1
                          Я уже видел в своей практике (на прошлом предприятии) вот такой участок кода в проекте под stm32f1.
                          cout << "temp: " << sensor.get();

                          И вопросы «А почему он вдруг не вмещается в МК?! Я же только вывести хотел!». А то что stdout отжирает 300кб в мк с 128 кб flash — его не беспокоило.
                          Или snprintf внутри прерывания в буфер в 2 кб при стеке под прерывания в 1 кб…
                            0
                            А Вам не кажется, что это скорее проблема HR?
                              0
                              В том предприятии это было явно. Ведь это писал сварщик по образованию (хоть и оператор электронно-лучевой установки).
                              0
                              :-) Ну или еще бывает, что привыкший к «большой» системе разработчик искренне считает, что если через Serial сообщение о достижении контрольной точки не пришло — значит ошибка где-то до этого места. И без заглядывания в черный ящик UART попробуй объясни, почему это не так! На большой-то системе абстракция не течет: что в лог записал, то потом в файле (даже в случае крэша — библиотека и ОС же заботятся...) и увидел!
                          +1
                          Если хочешь переиспользовать код, то проще скопировать его и переписать под задачу, чем городить дерево абстракций. Проще будет потом, когда будет отлаживать все это.

                          Когда есть слова «скопировать» и «переписать», то отладки становится ровно в два раза больше, не говоря уже о дальнейшей параллельной жизни родственных версий. Это точно проще, чем оттестированные универсальные абстракции?
                              +1
                              Там только про опыт, обоснования нет.
                                0
                                Обоснование очень простое: откуда у вас возьмутся «оттестированные универсальные абстракции» пока вам нечего астрагировать?

                                Практически проще всего сделать так: «скопировать, переписать под задачу, потом добавить поддержку двух вариантов».

                                Как правило когда у вас появляется 2-3-4 варианта того, чего вы абстрагируете (пинов, UART'ов, etc — неважно), у вас появляется достаточно материала, чтобы понять, что то, что у вас в руках — таки реально «отлаженные универсальные абстракции». А без этого — велик шанс заложиться расширяемость вообще «не в ту сторону».
                                  0
                                  Практически проще всего сделать так: «скопировать, переписать под задачу, потом добавить поддержку двух вариантов».

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

                                  Как правило когда у вас появляется 2-3-4 варианта того, чего вы абстрагируете (пинов, UART'ов, etc — неважно), у вас появляется достаточно материала, чтобы понять, что то, что у вас в руках — таки реально «отлаженные универсальные абстракции».

                                  Речь же скорее была о том, что эти абстракции должны быть (не важно когда появившись) и размножаться они должны не копипастой.
                                    0
                                    Будете руками ходить и подправлять везде?
                                    Да, конечно. И при этом смотреть — точно ли мне приходится делать одинаковые правки… или нет?

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

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

                                    Попытки «вывосать абстракции из пальца», скорее всего, приведут только к тому, что вы разведёте кучу бессмысленых абстракций, которые решать конечные задачи вам не помогут ни разу…

                                    Речь же скорее была о том, что эти абстракции должны быть (не важно когда появившись)
                                    Нет. Их не должно быть до тех пор, пока они не нужны.

                                    и размножаться они должны не копипастой.
                                    А как? Как вы поймёте что у вас правильные абстракции не опробовав их на двух (а желательно — на большем числе) вариантах и не сравнив их?
                                      0
                                      зачем вам абстракции если конкретная реализация справляется не хуже?

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

                                      Нет. Их не должно быть до тех пор, пока они не нужны.

                                      Зачем так категорично? Абстракции — это не какая-то уникальная фича ООП. Вы исходники на функции/модули разбиваете?

                                      А как?

                                      Наследованием, агрегацией, композицией.

                                      Как вы поймёте что у вас правильные абстракции не опробовав их на двух (а желательно — на большем числе) вариантах и не сравнив их?

                                      Это навык планирования архитектуры приложения (= Чем меньше изменений потребует адаптация под новые условия — тем она лучше.
                                        0
                                        Это навык планирования архитектуры приложения (= Чем меньше изменений потребует адаптация под новые условия — тем она лучше.
                                        Вот только на практике, почему-то, выясняется, что адаптация-то изменений не требует, вот только кода приходится писать втрое больше, чем если бы всех этих абстракиций не было.
                                          0
                                          По этим законам живет и здравствует мир прикладной разработки. Не вижу объективных причин для изменения «законов физики» для embedded.

                                          Я предполагаю, что исторически больших кодобаз там не было, поэтому и такой консервативный подход сходил с рук. Но прогресс не стоит на месте, повсеместный IoT с толстыми клиентами все ближе.
                                            +1
                                            По этим законам живет и здравствует мир прикладной разработки.
                                            Мир прикладной разработки порождает монстров на сотни мегабайт, которые кое-как справляются с задачами, для решения которых и мегабайта-то много.

                                            Не вижу объективных причин для изменения «законов физики» для embedded.
                                            Очень просто: в случае с embedded за вычислительные ресурсы и потребляемую память тоже платит разработчик.

                                            Это принципиально меняет систему: решение, при котором вы экономите $100K в год на ещё одного разработчика, а ваши клиенты (суммарно) потом тратят $100M для того, чтобы вашего монстра запустить — больше «не канают».

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

                                            Но прогресс не стоит на месте, повсеместный IoT с толстыми клиентами все ближе.
                                            Он «на горизонте» уже лет десять, но пока ничего подобного в ценовом диапазоне в доли центов (где большая часть embedded исторически живёт) нету. Хотя всё может быть… Посмотрим.
                                              0
                                              в случае с embedded за вычислительные ресурсы и потребляемую память тоже платит разработчик.

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

                                              Он «на горизонте» уже лет десять

                                              Это дело времени. Пока внедряются «умные колонки», а там инфраструктуры будут только расширяться.
                                                0
                                                Копипаста сама по себе программы не ускоряет
                                                И ускоряет и уменьшает. Абстракции — штука дорогая. По меркам контроллеров — так очень дорогая.

                                                А compile-time zero-cost abstractions дороги в написании и окупаются тогда, когда вам нужно избавиться не от пары-тройки копий, а от сотни.

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

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

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

                                                Так что стоимость железа — по прежднему важнее «гибкости».
                                                  0
                                                  И ускоряет и уменьшает. Абстракции — штука дорогая. По меркам контроллеров — так очень дорогая.

                                                  Абстракции, которые нужны для компилятора — зачастую бесплатные. Опять же, вы пользуетесь препроцессором?) Ваши функции компилятор инлайнит?

                                                  А compile-time zero-cost abstractions дороги в написании

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

                                                  Не бывает бесплатных абстракций.

                                                  Вы себе противоречите)

                                                  Так что стоимость железа — по прежднему важнее «гибкости».

                                                  Apple смотрит на вас с недоумением.
                                                    0
                                                    Apple смотрит на вас с недоумением.
                                                    Просто вещи, которые продаются лучше, чем Apple Watch (типа Xiaomi Mi Band) — выносятся в другую категорию, что позволяет им «делать хорошую мину при плохой игре».

                                                    Чем дальше в лес, тем больше Apple приходится упирать именно на этот навык.

                                                    Не бывает бесплатных абстракций.
                                                    Вы себе противоречите)
                                                    Нет. За так называемые «zero-cost abstractions» тоже приходится платить. Временем компиляции, сложностью кода и так далее.
                                                0
                                                Очень просто: в случае с embedded за вычислительные ресурсы и потребляемую память тоже платит разработчик.

                                                Мог бы — дал бы +5 во все кармы!
                                        0
                                        А потом найдется баг в той реагизации, которую вы скопировали. Будете руками ходить и подправлять везде?

                                        Ну смотрите, вы сделали одно устройство, скажем, CAN-RS485. В нем вы применили универсальный драйвер UART, который оформили в виде внешней библиотеки, которая лежит в отдельном git репозитории. Все работает, устройство продается.

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

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

                                          Но в вашем случае нельзя быть увереным, что кто-то (ваш колега, например) не сделал пару фатальных изменений именно в n-ном скопипащенном куске кода, которая разрушит гомогенность интерфейса и фикс бага сделает другой баг.
                                          Не говоря уже о рутине и вероятности ошибиться.
                                            0
                                            Если бы всё было так чудесно, как вы говорите, то не приводил бы выход каждой новой версии Windows к «окирпичиванию» определённого количества железок.

                                            А ведь Windows PC — куда как более гомогенны, чем мир микроконтроллеров.

                                            В том-то и дело, что в случае с «железом» — вы никогда не можете быть до конца уверенным в том, что вы точно знаете что, где и для чего вы фиксите. И никакие абстракции тут не спасут — текут они безбожно.
                                              0
                                              Вы так говорите, будто в embedded багов не бывает. Так иногда и людей «окирпичивают».

                                              И никакие абстракции тут не спасут — текут они безбожно.

                                              Это скорее к вопросу о качестве и количестве кода. Вы же пользуетесь какими-то библиотеками?
                                                0
                                                Вы так говорите, будто в embedded багов не бывает. Так иногда и людей «окирпичивают».
                                                Всё бывает. Но традиционное решение (ничего не править без крайней меобходимости в уже вышедших прошивках) резко снижает вред от копи-пасты и фактически умножает на ноль выигрыш от абстракций.

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

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

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

                                                  Эти суждения нуждаются в аргументации.

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

                                                  Я баги фиксить собираюсь в core-логике, мы же с этого начинали. «Копии» не старые, а альтернативные, так же в ходу прямо сейчас.

                                                  да, конечно.

                                                  То есть универсальные абстракции вы используется, а другой рукой однозначно топите за их однозначный вред.
                                                  Ну, такое себе (=
                                                    0
                                                    «Копии» не старые, а альтернативные, так же в ходу прямо сейчас.
                                                    Это никого не волнует. Если они прошли испытания и запущены в производство — то менять их всё равно никто не будет.

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

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

                                                      Еще как волнует.

                                                      Но вот этот аргумент «Не нужно!» про поддержку софта отлично демонстрирует глубину глубин. Либо проекты маленькие и поддержка их действительно дешевая, либо…

                                                      Всё — яд, всё — лекарство; отличие лишь в дозе.

                                                      Внезапно вы противоречите своей категоричности выше.
                                                        0
                                                        Либо проекты маленькие и поддержка их действительно дешевая, либо…
                                                        … либо это таки embedded и никто поддержкой заморачиваться и не собирется.

                                                        Подавляющее большинство любимых вами IoT устройст также ничего и никогда не обновляют (даже если технически имеют возможноcть).

                                                        Внезапно вы противоречите своей категоричности выше.
                                                        Где именно? Как я сказал — абстракции всегда имеют цену. А вот отдача от них — гарантирована далеко не всегда.

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

                                                          У меня таки был опыт работы вокруг эмбеддеда в бытовой технике и там облака для этих целей разворачивали. Пылесосы зачастую USB-выходом оборудованы явно не для закачивания музыки.

                                                          Где именно?

                                                          Например, в предыдущем сообщении «никто».

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

                                                          Почитайте дальше, я вам ответил, что речь про абстракции вообще, а не про то, когда их нужно вводить. А потом началось, что абстракции — это дорого по производительности (прикладное — отстой криворуких, да), 0-cost — это вдруг сложно писать, копипаста рулит, а если не рулит, то это не нужно и вот мы здесь.
                                0
                                С другой стороны, писать номера пинов прямо в месте дерганья ноги — это значит сделать код плохо читаемым. В драйвере RS-485 можно написать transceiver.transmitEnable();, где transceiver определён как AD485<PORTE,5>, а transmitEnable — как pinDE.set(); Если параметры пина — это параметры шаблонов AD485 и OutPin — transmitEnable() разворачивается всего в 3 инструкции STM32 — запись константы в PORTE.BSRR.
                          0
                          Вы можете создать объект класса Rs485Port, который агрегирует UART, GPIO (для управления DE (направление передачи) выводом). Тогда это будет правильно с точки зрения архитектуры проекта. А над ним построить класс, предоставляющий API именно для взаимодействия по требуемому протоколу. Какой-нибудь ModBusContriller. И вот у вас уже 3 уровня абстракции (UART и GPIO, потом инкапсулятор 485 и контроллер протокола). А выше могут быть еще какие-то классы логики. Которые что-то высылают по протоколу при каких-то событиях.
                          0

                          Интересно было бы посмотреть на пример кода, чтобы составить впечатление о том, как вы C++ используете.

                            0
                            Ну вот краткая нарезка из одного из проектов (имена изменены, части приведенные ниже разбиты согласно архитектуре проекта по различным файлам в разных директориях. Тут приведено подряд для удобства).
                            Код
                            // 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 дается уже контроллеру приложения, в котором инкапсулирована бизнес-логика приложения.
                              0

                              Не понятно, зачем вам иметь отдельно объекты pin_cfg, отдельно объекты pin и отдельно вектор указателей на объекты pin, если вы могли изначально сделать что-то вроде:


                              pin project_name_pin_array[PROJ_NAME_PIN_COUNT] = {
                                 {PIN_SPI_1_MOSI},
                                 {PIN_SPI_1_MISO},
                                 ...
                              };
                                0
                                Не понятно, зачем вам иметь отдельно объекты pin_cfg

                                Для того, чтобы не передавать в конструктор класса 8 параметров. Вместо этого указатель на структуру с этими параметрами.
                                отдельно объекты pin

                                Ну вот, например, объект UART хочет себе при инициализации объект класса Pin для управления CS (если не используется аппаратный по какой-то причине. Например, если в МК один SPI на кучу устройств).
                                отдельно вектор указателей на объекты pin

                                Для объекта, который управляет всеми выводами разом одной командой.
                                  0
                                  Для того, чтобы не передавать в конструктор класса 8 параметров. Вместо этого указатель на структуру с этими параметрами.

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

                                    0
                                    Так было задумано для того, чтобы потом можно было изменить параметры вывода на лету (скорости, альтернативная функция). А потом, по надобности, восстановить начальные значения отдельной командой. До того, как это понадобилось, было как вы сказали. Передавалась структура и по ней constexpr метод заполнял HAL структуру внутри объекта. По которой в реальном времени инициализировался вывод.
                                      0
                                      Так было задумано для того, чтобы потом можно было изменить параметры вывода на лету (скорости, альтернативная функция).

                                      У вас же объекты pin_cfg константные. Как вы их меняете на лету?


                                      Думается, что вам нужно было иметь что-то вроде:


                                      class pin {
                                      public:
                                         pin(const pin_config & initial) ... {}
                                         ...
                                         // Набор сеттеров-геттеров.
                                         void set_seed_freq(...);
                                         void set_...();
                                         ...
                                         // Или даже так:
                                         void reset(const pin_cfg & updates) {...}
                                         ...
                                      }

                                      Так что ваше пояснение толком ничего не пояснило :(

                                        0
                                        Суть в том, что структура, которая передается объекту PIN — это не структура HAL Это собственная настройка. По ней в init методе заполнялась структура HAL-а… И когда вызывались методы изменения конфигурации, то менялась структура HAL-а, которая в RAM. А если вызывался метод reset, то это было равносильно удалению объекта и его созданию заново. Только без работы с памятью. Старая структура в RAM перезатиралась данными из структуры, указатель на которую был передан при создании.
                                          0

                                          И при чем здесь это? У вас есть HAL, который вы заполняете на основании параметров из конструктора. Затем вам нужно эту структуру изменить (частично или полностью). Но владеет же ей pin, если я вас правильно понял. Значит pin в своих методах может нужные преобразования выполнить, получая все необходимые параметры в виде аргументов своих методов.


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

                                            0
                                            У вас есть HAL, который вы заполняете на основании параметров из конструктора.

                                            Нет. Не в конструкторе. А в методе init. То есть, в реальном времени. Это нужно для того, повторюсь, чтобы после всех манипуляций по ходу работы с структурой HAL-а иметь возможность восстановить ее в исходное состояние.
                                              0

                                              Без разницы. Получается, что у вас есть нечто вроде:


                                              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-а. При этом вам не нужны внешние константные объекты.

                                                0
                                                А восстанавливать исходное состояние как будете? Если нужен метод reset_cfg?
                                                  0

                                                  Это зависит от того, как вы со своим pin-объектом работаете. Я же не вижу ни реализации pin, ни работы с ним. Вообще все выглядит пока так, что в pin-е как-то и конфиг хранить не нужно. А можно просто подсовывать конфиг прямо в init.

                                                    0
                                                    Вот. Вы предлагаете делать примерно как в HAL-е. Подсовывать структуру при каждом вызове. А в моем случае это скрыто. Внутри есть структура с текущим состоянием и с начальным. К которому можно откатиться, вызвав метод cfg_reset. Таком образом, после инициализации не нужно ничего передавать в вывод.
                                                    По поводу реализации. Есть у меня проект-песочница. Там я делал нечто подобное. Когда только отрабатывал все эти штуки:
                                                      0

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

                                      0
                                      Видимо, под этим лежит C-интерфейс HAL, который сделан так, как сделан, а код для него генерится инструментом от производителя чипА.
                                  +1
                                  А зачем вам экземпляры объектов для портов в/в? Гораздо эффективнее использовать только типы и списки типов:
                                  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>;
                                  
                                  Вроде еще Чижов активно пропагандировал такой подход.
                                    0
                                    Такой подход зародился и используется задолго до Чижова, так что это не аргумент. Но подход верный. Только есть проблема:
                                    кто-то должен написать и отладить весь этот метопрограммический DSL для разного железа. Для языка C этим достаточно эффективно занимаются производители железа. А вот mp-решения такой поддержки не имеют, и в конкретном случае их приходится допиливать «под себя» и часто уже не напильником, а расточно-шлифовальным станком. Что обесценивает code reuse и делает такие решения неинтересными.
                                    Но в теории, да, это верный путь.
                                      0
                                      Такой подход зародился и используется задолго до Чижова, так что это не аргумент.
                                      Это был не аргумент, а референс.
                                      этим достаточно эффективно занимаются производители железа.
                                      Это скорее исключение, чем правило, взять хотя бы приснопамятный HAL STM32.
                                      Что обесценивает code reuse и делает такие решения неинтересными.
                                      В моей практике ситуация обратная. Абстракции написаны и отлажены, а за последние 7 лет ни одной прошивки не написано на чистом C.
                                        0
                                        Вам повезло: удалось профинансировать написание и отладку абстракций. Отдачу от инвестиций получилось получить?
                                          0
                                          Отдачу от инвестиций получилось получить?
                                          Собственно разработка библиотек, всегда включалась в работу над прошивкой, поэтому каких-то отдельных средств на это не выделялось. Но контора в плюсах, сроки выдержаны, поэтому экономическую часть можно считать состоявшейся. Были небольшие затыки идеологического характера на начальных этапах, но на общую картину они не повлияли.
                                            0
                                            Хорошо Вам, удовлетворили творческие амбиции за счет заведения. ) А какой примерно коэффициент переиспользования от получившегося DSL?
                                              0
                                              Фактически за свой счет. Но вектор был выбран правильный. Повторно используется практически все. Единственный минус — потеряли программиста, нормально пишущего на C, но не смогшего в достаточной степени освоить метапрограммирование на шаблонах.
                                                0
                                                Класс! То есть компоненты используются для быстрого написания именно новых проектов? Не только для эволюции тех, в рамках которых созданы?
                                                Не уверен, но вроде бы Вы где-то уже писали, что туда входит: конфигурация периферии, GPIO, ADC/DAC, UART, SPI? Или я ошибаюсь? Что-то еще?
                                                  0
                                                  Конечно, все выносится в библиотеку модулей, которые имеют зависимости описанные средствами cmake. Вся периферия МК, особенности процессора, маппинг некоторых общих моментов RTOS, интерфейс к внешней периферии типа индикаторов, датчиков, алгоритмы и протоколы.

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

                                                  Конечно, чтобы не было неожиданностей, каждый проект содержит копию библиотеки, поэтому риск поломать старое практически отсутствует.
                                                    0
                                                    Мы используем для этого submodule в git. Можно просто не обновлять субмодуль — и ничего не сломается. Или обновиться на новую версию и посмотреть, что поменялась. Обычно просто производится проверка устройства тестами и в продакшен. Так во всех поддерживаемых устройствах одна база.
                                      0

                                      Списки пинов — это достаточно продвинутая тема, не каждый такое может написать, а использовать чужое не каждый хочет… А так да, я в класс 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 — это первая и единственная рантайм операция с пинами, все остальное делается на этапе компиляции, так что бесплатные абстракции очень даже бывают.

                                  +4
                                  Пожалуй, очень важная (и одновременно — редко встречающаяся) категория статей — рассказывающая о том, что не заработало. Обычно всем больше нравится рисовать истории успеха — и не только в программировании, к сожалению. Думаю, что стоит плюсовать автора!

                                  Поддержу еще несколько технических моментов:

                                  — Активное использование шаблонов и boost в малых микроконтроллерных проектах, скорее всего, не очень оправдано. По ощущениям — высоко-уровневые абстракции (вместо цикла по массиву делаем коллекцию и обрабатываем ее модными лямбдами) не уменьшают сложность разработки. И про простыни сообщений об ошибках где что не так с шаблоном — автор написал совершенно верно! В скобках замечаю: в больших проектах на Java, наоборот, без объектных фишек жить было бы тяжко. Значит где-то проходит водораздел, и должны быть признаки, по которым можно определять, где ООП дает потенциальные выгоды — а где не дает.

                                  — Чем ближе к аппаратуре работаешь — тем сложнее сделать эффективные абстракции, которые бы при этом не текли. Очень многое в ООП заложено на то, что у объекта есть некое состояние, которое может изменяться не иначе, как вызовом методов этого объекта. В случае с МК — это не всегда (и это мягко сказано) так. Поскольку большая часть объектов отражает свойства реального узла контроллера — вдруг оказывается, что либо состояние объекта меняется без вызова его методов, либо состояние программного объекта не адекватно состоянию реальной кучки транзисторов внутри микросхемы.

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

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


                                    Что для обработки лямбдами, то вроде как уже давно что простой цикл по массиву, что range-for, что std::for_each с лямбдой компиляторами разворачивается в один и тот же код. В релизной сборке, естественно.

                                      0
                                      В больших (не МК) проектах смысл есть. Если алгоритм обработки является первоклассным объектом (той же лямбдой), и спускается откуда-то из-за пределов собственно функции, которая обрабатывает данные, то:

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

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

                                      — Но самое главное — на энтерпрайзе всегда есть шанс что придется систему горизонтально масштабировать. Сегодня тебе на вход подают 10к данных, а через пять лет — 1Тб. При небольшом везении, код с лямбдами путем применения хитрой библиотеки сам распараллелит обработку на несколько ядер/GPU/кластеров, раздав куски данных и копии лямбды каждому worker-у.

                                      А вот внутри МК таких задач не наблюдается. Там обычно задача очень конкретная — и один раз хорошо написанная прошивка может годами работать без всякого развития и изменения. С третьей стороны — бывают и большие проекты под МК (а-ля управление 3Д-принтерами). Но даже там сейчас народ немного переосмысливает ситуацию: на МК оставляют только низкоуровневое дрыгание ногами, а на микрокомпьютер с Linux — высокоуровневые расчеты и логику. Это проект Klipper — я за ним последнее время все больше наблюдаю.
                                        0

                                        Вопрос все-таки был не про лямбды.


                                        Контейнер для данных выбирается под конкретные нужды и требования. Причем массив (как и std::array, как и std::vector) может быть наиболее эффективным вариантом в определенных условиях. Соответственно, если массив эффективное представление данных — то зачем его на что-то менять?


                                        А если нужно менять, то не суть, МК это или нет.

                                          0
                                          В больших проектах мы часто пишем Collection вместо указания конкретного типа (Java, конечно — но смысл будет тот же). И, поскольку в этой точке уже нет понимания что именно подадут на вход — уже надо или итератор или range-for. А потом уже как карты лягут: из тестов туда пойдет ArrayList. В продакшене какая-то backed-by-database коллекция. Потом еще что-то. Но опять, дело-то в том, что мы пишем нечто сейчас, стараясь не делать предположений о том, как его захочется использовать через пять лет. Но хотим чтобы работало — поэтому и извращаемся с абстрактными типами данных и прочей мощью ООП. В разработке для МК нет смысла это делать: задача известна сейчас, и при минимальном везении через пять лет будет ровно той же! И если динамическое распределение памяти запрещено (а часто это так!) — то вопрос в объявлении массива, или std::array как тонкой обертки над ним становится вопросом вкуса и привычек. Мне кажется (я могу ошибаться!) что большАя часть доработок C++ последних лет сделана в попытке «закопать» адресную арифметику и аллокацию/деаллокацию ресурсов, чтобы можно было (придерживаясь определенных правил) жить как в Java: завел себе объект (даже new писать не надо), потом бросил (или кому-то передал) не думая. В Java GC за тебя мусор убирает — тут умные указатели. Но в специфике МК — ни то ни другое не нужно, IMHO.
                                            +1
                                            В больших проектах мы часто пишем Collection вместо указания конкретного типа (Java, конечно — но смысл будет тот же)… Но хотим чтобы работало — поэтому и извращаемся с абстрактными типами данных и прочей мощью ООП.

                                            Т.е. по факту вы смотрите на C++ глазами Java-разработчика. Тогда как в C++ нет упоротого ООП вокруг коллекций. И, соответственно, в C++ вы не можете просто так заменить один тип коллекции на другой. Если только ваш код изначально не был написан на шаблонах.

                                              0
                                              Я на C++ кодил еще когда Java толком никому и известна-то не была. :-) Но вот реально так получается, что большие проекты мы делаем на Java, а прошивки для ICP-DAS на C++ (кстати, Borland C++ 3.1 в DosBox — раритет!). А вычислительную часть для задачи оптимизации — на C++v11. Где-то потихоньку на более новые стандарты C++ переползаем. Где-то на Java — все-таки разработчиков в стране дефицит, иметь возможность ввести человека в проект без стреляния себе в ногу разными способами — для бизнеса тоже ценность…
                                                +1

                                                Если вам интересно померяться, то я на C++ кодил когда Java еще даже не выросла из Oak project. Так что вопрос не в длине опыта.


                                                Суть в том, что не нужно подходы из Java, в которой кроме ООП ничего толком и не было (да и сам ООП там кастрированный по сравнению, например, c Eiffel-ем), переносить на C++. Если в C++ кто-то вместо простого массива берет какой-то хитрый контейнер и начинает с ним работать через лямбды, то либо задача такая, либо человек просто упоролся и сам не ведает, что творит. Специфика МК тут сильно сбоку.

                                                  0
                                                  Ну то есть если судить по времени — скорее мы смотрим на Java глазами C++, да? :-)

                                                  Но в целом, идея понятна. Разные языки, разные подходы, разные области применения.
                                    +1
                                    Пока не очень убедительно, впечатление такое, что проблема не в С++, а в программисте архитектуре и точно так же можно было написать на С, только вышло бы сложнее, длиннее, без RAII и с ветвистыми «псевдоклассами» из примера в начале статьи.
                                      +7
                                      Мы на фирме разрабатываем софт для преобразователей частоты. Жесткий реалтайм, всё на перываниях (без ОС), всё оптимизировано до изучения дизассемблера. Уже десяток лет пишем исключительно на «Си в объектно-ориентированном стиле». И, в отличие от примера автора, еще и запихиваем в структуру указатели на «методы» класса (его функции).
                                      Никакого динамического выделения памяти, «кучи» вообще нет — только так можно гарантировать отсутствие утечек.
                                      Объекты относительно крупные — «модуль ШИМ», «Драйвер RS485», «Модуль инкрементального энкодера». Внутри — всё на «регистрах» и дефайнах, вложенных классов нет (или они редки, типа модуля фильтра первого порядка). «HAL» делаем дефайнами пинов или отдельными функциями (init для этой конфигурации, init для той) — т.е. не делаем. Проще переписать пяток пинов, чем разгребать кучу слоёв абстракции.
                                      Каждый вызов функции на счету, и для пущей оптимизации кода иногда приходится делать функции inline или вообще макросами.
                                      Си++ балуемся периодически, но далеко от Си он не отходит (в силу статического выделения памяти на этапе компиляции). Ну объекты чуть поприятнее выглядят, и пару раз за проект можно наследование применить. Но дизассемблер смотреть менее наглядно становится, и я лично люблю чистый Си.
                                        +2
                                        А оптимизацию компилятора используете? А то оптимизированный C-код не так уж сильно отличается от оптимизированного C++-кода.
                                          0
                                          Отличается, когда дело доходит до inline и макросов, на Си экономится время на вызовы. Ну и количество слоёв абстракции на Си как-то само-собой меньше получается, поэтому и вызовов меньше. А оптимизация — давно и по-умолчанию везде включена.
                                        +2

                                        Такая же ерунда была, пока не сделали так: на С++ в основном бизнес логика, т. е никаких объектов на каждый пин. Только скажем объект драйвер Spi. Железо же в основном инициализируется в -_low_level_init, т. Е из документа, описывающего, какие модули и как должны быть настроены, просто все так и настраиваем. Эта функция выполняется ещё до инициализации всех объектов. Поэтому скажем драйвер Spi уже не надо заботиться об настройке модуля Spi. Драйвер отвечает только за приём, передачу…
                                        В итоге бизнес логика, была полностью переносима и очень понятна, а вот железо да приходилось везде переписывать. Но это все равно проще, чем придумать универсальное решение и запутать мозг :). Да есть такая проблема… подтверждаю те же грабли были.

                                          0
                                          сам написал, сам не смог поддерживать, а виноват яп. Хм…
                                            0
                                            Виноват подход. О чем ясно сказано. А подход идет от возможностей языка. Если полноценно не используются возможности языка, то он выбран не верно. Это все ИМХО, конечно же.
                                              +1
                                              как я понял проблема как раз в том, что автор пытался использовать как можно больше возможностей современного с++, не совладал с ним и получил переусложненное решение. То, что лук и стрелы устарели, не значит, что для охоты на оленя вам нужны «тополя».
                                              +3
                                              Вот из-за таких примерно комментариев в научных журналах избегают публиковать отрицательные результаты. Которые для прогресса тоже важны. Вообще, начались рассуждения о виновности — конструктивного решения уже можно, как правило, и не ждать… :-(
                                              0
                                              есть две категории embedder'ов
                                              1. Пишут на C/С++ в Keil/IAR. Получают жирный код. И не стесняясь заливают в МК. Благо что памяти сейчас много.
                                              2. Пишут на С в Keil/IAR. Получают жирный код. Дизассемблируют. Оптимизируют, уменьшая код на 30-70-100% ) — > получают быстрый маленький код.

                                              Для второго случая, конечно же требуется отличное знание МК, под который все и пишется.
                                                0
                                                Есть ещё одна категория: Пишут на C++ в Keil/IAR. Применяя новую конструкцию, дизассемблируют. Оптимизируют, уменьшая код на 30-70-100%. Дальше применяют оптимальную конструцию везде, и получают быстрый маленький код.
                                                  +1
                                                  А бывает категория, которая просто пишет быстрый маленький код по**р в чем и по**р на чем?
                                                    0
                                                    категория конечно бывает, только её представителей вряд ли получится найти %)
                                                  0
                                                  А чего вы забили категорию «пишут в Atmel Studio на C++»? Не кайлом единым жив человек.
                                                    0
                                                    Ну и про тех кто пишет в CLion + GCC тоже. Вариантов много :)
                                                  +3
                                                  закон Мерфи гласит:
                                                  сложность программы растёт, пока не превысит способности программиста
                                                    +1

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


                                                    Поскольку задача стояла не "написать одну систему, которую можно будет переносить на N других платформ", а "иметь одну-единственную систему под одну-единственную платформу, чтобы можно было её переконфигурировать/добавить фичу/убрать фичу за 5 минут", получилось довольно-таки удобно.

                                                      0
                                                      60 объектов — это сильно.
                                                      У меня вся работа с периферией идёт через драйвер (который настраивает пины, клоки и прочее). Причём драйвер это аппаратная сущность. Драйвер АЦП, УАРТ, CAN и прочая. Его вызывает эээ драйвер конечного устройства. Например, дисплей сидит SPI, у него своя инициализация, ну или внешние часы например. Ну и наверху сидит логика с планировщиком.
                                                      Аппаратный драйвер периферии имеет в основном следующие функции: Init, GetError, Receive, Transmit.
                                                      Драйвер конечных устройств зависит от устройства. (SetPixel, ClearScreen and so on).
                                                      Ну и как бы всё. Пока хватает на все случаи жизни.
                                                        0
                                                        Тут еще до меня использовался подход «поток должен решить все проблемны аппаратки». То есть да, ловятся прерывания, но их обслуживает поток. То есть что можно — в прерывании, а что-то значительное и серьезное (например, сбросить модуль заново) — в потоке. Мне остается лишь поддерживать эту архитектуру.
                                                        +1
                                                        зачем вообще использовать C++?
                                                        Выскажу непопулярное мнение — C++ стоит использовать для передачи по ссылке и кучи мелкого синтаксического сахара, типа нормального типа bool, «структур с методами» и так далее.

                                                        Иными словами — С++ использовать стоит, ООП — нет в некоторых случаях — нет.

                                                        Проблема ООП — очень узких объектах. В простых случаях — мы имеем всего одну иерархию объектов. В сложных (множественное наследование) — несколько иерархий и всё.

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

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

                                                        А там, где иерархия сходу не выстраивается — лучше недообъекты Си.

                                                        Что такое включение питания на UART? Это часть UART или часть процедуры подачи питания? А что такое подача тактирования? Она чья часть? Ладно, решили.

                                                        А как в SPI? Ровно так же, как в UART? А что делать, если это не удобно?

                                                        Вот если неудобно — значит нужны сишные недообъекты. Которые позволяют для SPI решать так, для UART — иначе, для CAN — третьим путем. Или вообще — совсем процедурно.

                                                        ООП — отличная штука, но лишь тогда, когда в задаче объекты выделяются естественным образом.

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

                                                            Реляционный подход крайне гибок, поэтому в 99.9% используется он. В реляционной СУБД мы вполне можем сделать запрос «продукты, включающие белок курицы» и получить торт. А можем сделать запрос иначе «продукты, включающие куриное мясо» — и торт не войдет.

                                                            И главное. В реляционной СУБД для этого не надо переделывать структуру СУБД — достаточно лишь поменять запрос. То есть данные отделены от иерархической структуры. И для гибкости и производительности подходит только он.

                                                            А если вы строгий вегетарианец или аллергик, то кремовый торт наследуется много от чего, но в том числе — и от курицы. И для аллергика принципиально, куриные там яйца или страусиные.

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

                                                            И SoC с ее устройствами — тоже плохо ложится на ООП.

                                                            P.S Чуть больше.20 лет назад я принимал участие в разработке объектной КИС на базе объектной СУБД в качестве руководителя группы тестирования. Так что все плюсы ООП в СУБД видел. Равно как и минусы.
                                                              +1
                                                              вы описали классический случай в котором наследование стоит заменить композицией.
                                                                +1
                                                                Вы не правы. Когда у вас десятки тысяч экземпляров и сотни классов — композиция помогает. А когда у вас 20 солитонов на 20 классов — нет.

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

                                                                Про SoC. Типичная картина такая — 20 солитонов, 20 классов, 50 кирпичиков. которые делегируются (обычно в одно, изредка в два места). То есть повторного использования кода — с гулькин нос. Поэтому и ООП (даже без наследования) — все равно лишние накладные расходы.

                                                                Вся выгода от ООП — повторное использование кода. Если его нет — ООП — лишь лишние расходы по сравнению с процедурным подходом. Поэтому в одних вещах ООП очень полезен, а в других — нет.
                                                                  0
                                                                  Обсуждая ООП в общем, вы акцентируетесь на одной конкретной реализации одной конкретной задачи. В которой, если я вас правильно понял, классы являются рантайм сущностями, а ооп в с++-ном его понимании даже не пахнет. Да еще и ссылаетесь на опыт двадцатилетней давности. При чем тут это всё?
                                                                    0
                                                                    Я бы сказал, что все наоборот. Обсуждая применение С++ ООП в очень частном случае (абстракции железа одного SoC на разных платах) я прихожу к выводу, что ООП там не нужен. И в процессе этого я ссылаюсь ещё на одну область (СУБД) где общепризнанно, что ООП не нужен.

                                                                    Причем, все очень-очень конкретно. Сила ООП — в повторном использовании кода. Если вы можете один раз написать объект и 10 раз его использовать — отлично, ООП применим. Если для каждого нового использования приходится переделывать объекты — значит в конкретном случае ООП хуже процедурного подхода.

                                                                    Вторая проблема ООП — у вас в любом случае «одна сущность — один объект». Это не гибко. В 99% случаев — это не мешает. Ну да, не гибко, но гибкость не нужна. Конкретно в SoC бывают ситуации, когда гибкость нужна. В итоге — получаем дробление на кучу мелких микрообъектов. Ну или отказ от ООП.

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

                                                                    Резюме — все очень-очень конкретно.

                                                                    P.S. Вообще, делая многоуровневые абстракции, прежде всего думайте, что вы собрались скрывать, от кого и зачем. И если у вас нет ответов на эти вопросы — не надо скрывать «потому, что все так делают».
                                                          0
                                                          Даже интересно, каким образом 22-летний пацан умудряется с 17 лет «переводить предприятия на плюсы в продакшене», не кажется ли вам это смешным, господа? Особенно учитывая то, что автор текста года 3 назад в чатиках за основы С и С++ спрашивал.
                                                            0
                                                            Спрашивал. Не соглашусь правда что за основы (можно ссылки, пожалуйста? Помню что не так давно спрашивал про constexpr. Даже потом статью запилил. Но там, ИМХО, были не очень очевидные вещи с неявным cast-ом объектов к структурам). А что смешного в том, что я относительно рано начал изучать эту предметную область (ну и нашел работу, потому что уже имел некоторый опыт)?
                                                              0
                                                              Что-то меня это сообщение задело и я даже пошел, проверил. Да, вот. Как раз в это время разбираться с constexpr. Тогда же примерно задавал вопросы по этой теме. Потому что тогда переходил с C++11 на C++14.

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

                                                            Самое читаемое