В данной статье я расскажу, как я в течение пяти лет переводил предприятия, на которых работал, с ведения проектов под микроконтроллеры на 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++ оказалась лишь поводом многократно увеличить сложность проекта без какого-то выигрыша по скорости или памяти.

Комментарии (120)


  1. Ostrovv
    24.07.2019 07:38
    +1

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


    1. 0xd34df00d
      24.07.2019 14:37

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


      1. Ryppka
        24.07.2019 15:48

        Но ведь тогда это и не объекты? Во всяком случае в не в смысле объектов по стандарту C++.


        1. Vadimatorikda Автор
          24.07.2019 16:03

          Почему же? constexpr объект может представлять из себя один get метод для const поля. Вполне себе. При этом переменная будет лежать во flash. Ровно как и метод. Если конечно опциями компоновщика не задано иное.


        1. Free_ze
          24.07.2019 16:09
          +1

          В Стандарте пишут, что объект — это регион памяти, но не функция. Каких-то особых свойств от него не требуется.


          1. site6893
            24.07.2019 17:38

            тогда єто не объект, а структура данных, если без свойств.


            1. Free_ze
              24.07.2019 17:50

              Почему же объект не является структурой данных? Дайте тогда ваше определение объекта, если не согласы со стандартом)


              1. Ryppka
                25.07.2019 08:45

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


                1. Free_ze
                  25.07.2019 08:50

                  А давайте конструктивно дискутировать, а не делиться стереотипами?)


                  1. Ryppka
                    25.07.2019 08:55

                    Опыт показывает, что конструктивно получается редко, но давайте.
                    Ваш ход


                    1. Free_ze
                      25.07.2019 09:13

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


          1. Goron_Dekar
            25.07.2019 07:35

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


            1. Free_ze
              25.07.2019 08:45

              Речь про оперативную память, разумеется.


              1. Ryppka
                25.07.2019 08:54

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


                1. Free_ze
                  25.07.2019 09:12

                  объект в смысле C, C++

                  Откуда вы черпаете это вдохновение? Можно ссылку?


                  1. Ryppka
                    25.07.2019 09:23

                    В качестве ликбеза
                    Воспользовавшись прямой цитатой = «An object is a region of storage» — (C++11 draft n3290 §1.8).


                    1. Free_ze
                      25.07.2019 09:35

                      И где там написано про инициализацию? Если я делаю reinterpret_cast, то это уже в любом случае не объект с точки зрения C++? Что такое осмысленное значение в контексте С++ и где почитать об этом термине?

                      А цитата вам не кажется уж слишком похожей на то, что я писал ранее, говоря как раз о том, что там ничего такого нет?)


                      1. khim
                        25.07.2019 09:51

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

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


                        1. Free_ze
                          25.07.2019 10:00

                          в общем случае — нет.

                          Это личное заблубеждение?


                          1. khim
                            25.07.2019 10:24

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


                            1. Free_ze
                              25.07.2019 11:38

                              Это неправда, стандарт такого не говорит.

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


                              1. khim
                                25.07.2019 14:40

                                Это неправда, стандарт такого не говорит.
                                Раздел 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++ и, соответственно, мы потеряем возможность говорить о том, что она будет делать вообще.


                                1. Free_ze
                                  25.07.2019 14:59

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

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

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

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


                                  1. khim
                                    25.07.2019 15:26

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

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

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

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

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


                                    1. Free_ze
                                      25.07.2019 15:38

                                      В «чистом» C++ такие вещи категорически запрещены

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

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

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


                                      1. khim
                                        25.07.2019 19:37

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

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

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

                                        Вы предлагаете выделять память под объект и заливать его состоянием?
                                        Это, собственно, единственный вариант, который C++ предлагает. Вы не можете просто так взять — и изменить или создать объект «за спиной» компилятора. Почитайте про std::launder как-нибудь на досуге.


  1. ittakir
    24.07.2019 08:05
    +2

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


    1. Vadimatorikda Автор
      24.07.2019 09:16

      Делайте объекты крупнее.

      Тогда нарушается принцип «один объект — одна область ответственности». А это уже не хорошо. Потому что никто не ждет от «функции с именем print запроса данных от пользователя». Я помню, как у меня горело, когда я обновил HAL, а там функция изменения частоты ядра (HAL_RCC..._Config) еще и инициализировала systic, что приводило к тому, что у меня прерывание срабатывало раньше, чем пройдет инициализация всех сущностей проекта и запустится планировщик FreeRTOS.


      1. ittakir
        24.07.2019 09:40

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

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

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

        В моем проекте Ethernet-RS485 всего 18 классов. При этом он умеет ModbusTCP, UDP, есть внутри Web сервер, через который можно менять настройки.


        1. Vadimatorikda Автор
          24.07.2019 10:43

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


          1. ittakir
            24.07.2019 10:53

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


            1. Vadimatorikda Автор
              24.07.2019 11:05
              +2

              Еще была цель в будущем переложить задачу создания «однотипных устройств» на программистов, не имеющих опыта работы с железом. Например, когда меняется только протокол (для устройств сопряжения).
              Идея звучала интересно. Но разбилась о практику. Такие дела.
              Я не соглашусь с использованием HAL-а даже. Он дико избыточен. Инициализировать UART у меня занимает 5 строк кода (запись в регистры). А вот HAL просит структуру какую-то. Ведет свои состояния. Логику и прочее. Это ведь тоже по сути объектно-ореинтированный подход. Что уже по сути своей для вывода избыточно. Причем, еще и игнорируется аппаратная часть. Чтобы записать «1» в вывод — не нужно считывать регистр, накладывать маску и записывать обратно. Нужно просто знать про бит-ендинг. И записать число по нужному адресу. Абсолютная атомарность.


              1. ruomserg
                24.07.2019 11:22

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


                1. Vadimatorikda Автор
                  24.07.2019 11:26
                  +1

                  Я уже видел в своей практике (на прошлом предприятии) вот такой участок кода в проекте под stm32f1.

                  cout << "temp: " << sensor.get();

                  И вопросы «А почему он вдруг не вмещается в МК?! Я же только вывести хотел!». А то что stdout отжирает 300кб в мк с 128 кб flash — его не беспокоило.
                  Или snprintf внутри прерывания в буфер в 2 кб при стеке под прерывания в 1 кб…


                  1. Ryppka
                    24.07.2019 11:28

                    А Вам не кажется, что это скорее проблема HR?


                    1. Vadimatorikda Автор
                      24.07.2019 11:35

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


                  1. ruomserg
                    24.07.2019 11:41

                    :-) Ну или еще бывает, что привыкший к «большой» системе разработчик искренне считает, что если через Serial сообщение о достижении контрольной точки не пришло — значит ошибка где-то до этого места. И без заглядывания в черный ящик UART попробуй объясни, почему это не так! На большой-то системе абстракция не течет: что в лог записал, то потом в файле (даже в случае крэша — библиотека и ОС же заботятся...) и увидел!


            1. Free_ze
              24.07.2019 11:35

              Если хочешь переиспользовать код, то проще скопировать его и переписать под задачу, чем городить дерево абстракций. Проще будет потом, когда будет отлаживать все это.

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


              1. ittakir
                24.07.2019 12:04

                1. Free_ze
                  24.07.2019 12:19

                  Там только про опыт, обоснования нет.


                  1. khim
                    24.07.2019 17:28

                    Обоснование очень простое: откуда у вас возьмутся «оттестированные универсальные абстракции» пока вам нечего астрагировать?

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

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


                    1. Free_ze
                      24.07.2019 17:52

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

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

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

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


                      1. khim
                        24.07.2019 18:04

                        Будете руками ходить и подправлять везде?
                        Да, конечно. И при этом смотреть — точно ли мне приходится делать одинаковые правки… или нет?

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

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

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

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

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


                        1. Free_ze
                          24.07.2019 18:15

                          зачем вам абстракции если конкретная реализация справляется не хуже?

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

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

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

                          А как?

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

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

                          Это навык планирования архитектуры приложения (= Чем меньше изменений потребует адаптация под новые условия — тем она лучше.


                          1. khim
                            24.07.2019 18:57

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


                            1. Free_ze
                              24.07.2019 19:05

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

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


                              1. khim
                                24.07.2019 19:20

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

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

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

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

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


                                1. Free_ze
                                  24.07.2019 20:05

                                  в случае с embedded за вычислительные ресурсы и потребляемую память тоже платит разработчик.

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

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

                                  Это дело времени. Пока внедряются «умные колонки», а там инфраструктуры будут только расширяться.


                                  1. khim
                                    25.07.2019 08:43

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

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

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

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

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

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


                                    1. Free_ze
                                      25.07.2019 08:57

                                      И ускоряет и уменьшает. Абстракции — штука дорогая. По меркам контроллеров — так очень дорогая.

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

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

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

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

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

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

                                      Apple смотрит на вас с недоумением.


                                      1. khim
                                        25.07.2019 10:07

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

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

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


                                1. Ryppka
                                  25.07.2019 08:49

                                  Очень просто: в случае с embedded за вычислительные ресурсы и потребляемую память тоже платит разработчик.

                                  Мог бы — дал бы +5 во все кармы!


                      1. ittakir
                        24.07.2019 18:58

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

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

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


                        1. Free_ze
                          24.07.2019 19:14

                          Вы уверены, что старые устройства при этом не сломаются?

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

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


                          1. khim
                            24.07.2019 19:24

                            Если бы всё было так чудесно, как вы говорите, то не приводил бы выход каждой новой версии Windows к «окирпичиванию» определённого количества железок.

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

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


                            1. Free_ze
                              24.07.2019 20:15

                              Вы так говорите, будто в embedded багов не бывает. Так иногда и людей «окирпичивают».

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

                              Это скорее к вопросу о качестве и количестве кода. Вы же пользуетесь какими-то библиотеками?


                              1. khim
                                25.07.2019 08:48

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

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

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

                                На практике это происходит заметно реже, чем многим фанатам «переиспользования кода ради переиспользования кода» кажется.


                                1. Free_ze
                                  25.07.2019 09:04

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

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

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

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

                                  да, конечно.

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


                                  1. khim
                                    25.07.2019 10:13

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

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

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


                                    1. Free_ze
                                      25.07.2019 11:40

                                      Это никого не волнует.

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

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

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

                                      Внезапно вы противоречите своей категоричности выше.


                                      1. khim
                                        25.07.2019 14:43

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

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

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

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


                                        1. Free_ze
                                          25.07.2019 15:14

                                          либо это таки embedded и никто поддержкой заморачиваться и не собирется.

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

                                          Где именно?

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

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

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


            1. Sun-ami
              24.07.2019 13:23

              С другой стороны, писать номера пинов прямо в месте дерганья ноги — это значит сделать код плохо читаемым. В драйвере RS-485 можно написать transceiver.transmitEnable();, где transceiver определён как AD485<PORTE,5>, а transmitEnable — как pinDE.set(); Если параметры пина — это параметры шаблонов AD485 и OutPin — transmitEnable() разворачивается всего в 3 инструкции STM32 — запись константы в PORTE.BSRR.


    1. Vadimatorikda Автор
      24.07.2019 09:21

      Вы можете создать объект класса Rs485Port, который агрегирует UART, GPIO (для управления DE (направление передачи) выводом). Тогда это будет правильно с точки зрения архитектуры проекта. А над ним построить класс, предоставляющий API именно для взаимодействия по требуемому протоколу. Какой-нибудь ModBusContriller. И вот у вас уже 3 уровня абстракции (UART и GPIO, потом инкапсулятор 485 и контроллер протокола). А выше могут быть еще какие-то классы логики. Которые что-то высылают по протоколу при каких-то событиях.


  1. eao197
    24.07.2019 09:32

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


    1. Vadimatorikda Автор
      24.07.2019 09:58

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

      Код
      // 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();
      }


      1. eao197
        24.07.2019 10:18

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


        pin project_name_pin_array[PROJ_NAME_PIN_COUNT] = {
           {PIN_SPI_1_MOSI},
           {PIN_SPI_1_MISO},
           ...
        };


        1. Vadimatorikda Автор
          24.07.2019 10:20

          Не понятно, зачем вам иметь отдельно объекты pin_cfg

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

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

          Для объекта, который управляет всеми выводами разом одной командой.


          1. eao197
            24.07.2019 10:28

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

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


            1. Vadimatorikda Автор
              24.07.2019 10:37

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


              1. eao197
                24.07.2019 11:40

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

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


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


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

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


                1. Vadimatorikda Автор
                  24.07.2019 11:43

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


                  1. eao197
                    24.07.2019 11:50

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


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


                    1. Vadimatorikda Автор
                      24.07.2019 11:52

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

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


                      1. eao197
                        24.07.2019 11:58

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


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


                        1. Vadimatorikda Автор
                          24.07.2019 12:20

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


                          1. eao197
                            24.07.2019 12:37

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


                            1. Vadimatorikda Автор
                              24.07.2019 13:28

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


                              1. eao197
                                25.07.2019 09:36

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


            1. Ryppka
              24.07.2019 10:51

              Видимо, под этим лежит C-интерфейс HAL, который сделан так, как сделан, а код для него генерится инструментом от производителя чипА.


      1. borisxm
        24.07.2019 19:32
        +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>;
        
        Вроде еще Чижов активно пропагандировал такой подход.


        1. Ryppka
          25.07.2019 09:05

          Такой подход зародился и используется задолго до Чижова, так что это не аргумент. Но подход верный. Только есть проблема:
          кто-то должен написать и отладить весь этот метопрограммический DSL для разного железа. Для языка C этим достаточно эффективно занимаются производители железа. А вот mp-решения такой поддержки не имеют, и в конкретном случае их приходится допиливать «под себя» и часто уже не напильником, а расточно-шлифовальным станком. Что обесценивает code reuse и делает такие решения неинтересными.
          Но в теории, да, это верный путь.


          1. borisxm
            25.07.2019 10:48

            Такой подход зародился и используется задолго до Чижова, так что это не аргумент.
            Это был не аргумент, а референс.
            этим достаточно эффективно занимаются производители железа.
            Это скорее исключение, чем правило, взять хотя бы приснопамятный HAL STM32.
            Что обесценивает code reuse и делает такие решения неинтересными.
            В моей практике ситуация обратная. Абстракции написаны и отлажены, а за последние 7 лет ни одной прошивки не написано на чистом C.


            1. Ryppka
              25.07.2019 20:12

              Вам повезло: удалось профинансировать написание и отладку абстракций. Отдачу от инвестиций получилось получить?


              1. borisxm
                26.07.2019 06:29

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


                1. Ryppka
                  26.07.2019 08:11

                  Хорошо Вам, удовлетворили творческие амбиции за счет заведения. ) А какой примерно коэффициент переиспользования от получившегося DSL?


                  1. borisxm
                    26.07.2019 10:09

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


                    1. Ryppka
                      26.07.2019 13:38

                      Класс! То есть компоненты используются для быстрого написания именно новых проектов? Не только для эволюции тех, в рамках которых созданы?
                      Не уверен, но вроде бы Вы где-то уже писали, что туда входит: конфигурация периферии, GPIO, ADC/DAC, UART, SPI? Или я ошибаюсь? Что-то еще?


                      1. borisxm
                        26.07.2019 14:19

                        Конечно, все выносится в библиотеку модулей, которые имеют зависимости описанные средствами cmake. Вся периферия МК, особенности процессора, маппинг некоторых общих моментов RTOS, интерфейс к внешней периферии типа индикаторов, датчиков, алгоритмы и протоколы.

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

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


                        1. Vadimatorikda Автор
                          26.07.2019 14:57

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


        1. Reflector
          26.07.2019 10:17

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


  1. ruomserg
    24.07.2019 09:55
    +2

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

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

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

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

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


    1. eao197
      24.07.2019 10:20

      По ощущениям — высоко-уровневые абстракции (вместо цикла по массиву делаем коллекцию и обрабатываем ее модными лямбдами) не уменьшают сложность разработки.

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


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


      1. ruomserg
        24.07.2019 10:46

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

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

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

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

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


        1. eao197
          24.07.2019 13:16

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


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


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


          1. ruomserg
            24.07.2019 13:46

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


            1. eao197
              24.07.2019 13:55

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

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


              1. ruomserg
                24.07.2019 14:06

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


                1. eao197
                  24.07.2019 14:13
                  +1

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


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


                  1. ruomserg
                    24.07.2019 14:48

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

                    Но в целом, идея понятна. Разные языки, разные подходы, разные области применения.


  1. dipsy
    24.07.2019 10:15

    Пока не очень убедительно, впечатление такое, что проблема не в С++, а в программисте архитектуре и точно так же можно было написать на С, только вышло бы сложнее, длиннее, без RAII и с ветвистыми «псевдоклассами» из примера в начале статьи.


  1. BelerafonL
    24.07.2019 10:54
    +2

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


    1. Ryppka
      24.07.2019 11:26
      +1

      А оптимизацию компилятора используете? А то оптимизированный C-код не так уж сильно отличается от оптимизированного C++-кода.


      1. BelerafonL
        24.07.2019 11:29

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


  1. lamerok
    24.07.2019 11:38
    +2

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


  1. Antervis
    24.07.2019 12:15
    -1

    сам написал, сам не смог поддерживать, а виноват яп. Хм…


    1. Vadimatorikda Автор
      24.07.2019 12:19

      Виноват подход. О чем ясно сказано. А подход идет от возможностей языка. Если полноценно не используются возможности языка, то он выбран не верно. Это все ИМХО, конечно же.


      1. Antervis
        24.07.2019 13:47

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


    1. ruomserg
      24.07.2019 12:23

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


  1. IvanhoeLab
    24.07.2019 13:18
    +1

    есть две категории embedder'ов
    1. Пишут на C/С++ в Keil/IAR. Получают жирный код. И не стесняясь заливают в МК. Благо что памяти сейчас много.
    2. Пишут на С в Keil/IAR. Получают жирный код. Дизассемблируют. Оптимизируют, уменьшая код на 30-70-100% ) — > получают быстрый маленький код.

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


    1. Sun-ami
      24.07.2019 13:57

      Есть ещё одна категория: Пишут на C++ в Keil/IAR. Применяя новую конструкцию, дизассемблируют. Оптимизируют, уменьшая код на 30-70-100%. Дальше применяют оптимальную конструцию везде, и получают быстрый маленький код.


      1. Ryppka
        24.07.2019 14:09

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


        1. skymorp
          24.07.2019 14:13

          категория конечно бывает, только её представителей вряд ли получится найти %)


    1. DrGluck07
      24.07.2019 16:51

      А чего вы забили категорию «пишут в Atmel Studio на C++»? Не кайлом единым жив человек.


      1. Vadimatorikda Автор
        24.07.2019 16:55

        Ну и про тех кто пишет в CLion + GCC тоже. Вариантов много :)


  1. eshirshov
    24.07.2019 14:10
    +2

    закон Мерфи гласит:

    сложность программы растёт, пока не превысит способности программиста


  1. deitry
    24.07.2019 16:37
    +1

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


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


  1. EGregor_IV
    25.07.2019 07:50

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


    1. Vadimatorikda Автор
      26.07.2019 10:18

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


  1. Jef239
    26.07.2019 22:51

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

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

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

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

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

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

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

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

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

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