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

Загрузчик (BootLoader)

Загрузчик нужен для обновления прошивки без специализированного оборудования типа программаторов. Это очень важно для пользователей. Загрузчик обязательно должен уметь обновлять по UART так как драйвер UART занимает мизерное количество On-Chip NorFlash памяти. Остальные интерфейсы обновления по обстоятельствам или поддержка этих тяжеловесных интерфейсов (CAN, USB, LoRa, TCP, WiFI) должна быть в приложении так как там больше Flash памяти. Задача минимального загрузчика это найти в NorFlash приложение и прыгнуть в него. А если приложения нет, то просить прошивку в UART, чтобы совсем в тыкву не превращаться. Тем более, что переходники с USB-UART (например на основе чипа CP2102) самые дешевые из всех возможных интерфейсов. Всего порядка 6 EUR.

Компонент управления логированием

Должен быть программный компонент для управления логированием в UART. Раскраска логов в TeraTerm/Putty, выбор/отмена TimeStamp выбор отмена логирования для конкретного компонента. Это поможет анализировать лог загрузки устройства и следить за событиями внутри прошивки в run-time(е), просто глядя в UART.

фрагмент цветного лога загрузки в UART
фрагмент цветного лога загрузки в UART

CLI (Command Line Interface) он же Shell он же консоль он же TUI (Text User Interface)

CLI(шка) обязательный компонент для отладки и тестирования прошивок. С этим компонентом можно общаться с устройством на человеческом языке. Многие успешные продукты обладают CLI (Flipper-Zero, NanoVNA V2, U-Blox ODIN C099-F9P).

Универсальная FIFO

В любом Firmware проекте рано или поздно понадобится FIFO(шка). Например, FIFO нужна для предварительного хранения данных приходящих из UART Rx, для зранения кнопок с HID клавиатуры, для хранения принятых пакетов из LoRa или TCP. FIFO нужна для того же CLI. Причем реализация FIFOшки должна подстраиваться под любой тип данных: char, uint16_t, array[N].

Вычислители контрольных сумм CRC

При реализации протоколов понадобится целая куча разнообразных контрольных сумм (CRC8, CRC16, CRC24, CRC32). Также CRC нужны для энергонезависимых файловых систем.

BSP (Board Support Package) для каждого семейства микроконтроллеров

Кодовая база должна быть универсальной так в AUTOSAR, однако для каждого семейства микроконтроллеров будет своя уникальная реализация BSP. В BSP надо прописать реализации универсального API. Например для управления GPIO, прописи OnChip FLASH, запуска таймеров, пуляния пакетов в I2C/SPI/I2S/UART, вычитывания ADC и прочего.

Циклический массив

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

Драйверы периферийных чипов с управлением по I2C/SPI/MDIO

В каждой плате есть множество умных навороченных чипов с конфигурацией по I2C/SPI/MDIO. Я видел чип у которого 936 32-битных регистров конфигурации. В репозитории должна быть папка с драйвером для каждого такого чипа и отдельная папка с модульными тестами для чипа. Напишите в комментариях драйверы каких умных SPI/I2C/MDIO чипов писали вы.

Компонент для поддержки каждого конкретного процессорного ядра

Для каждого конкретного процессорного ядра (Cortex-M3/Cortex-M33/PowerPC/Tensilica Xtensa и про) должна быть папка с сорцами описания этого ядра. Это функции вкл/откл прерываний, проверка прерывание ли сейчас, перезагрузка ядра, печать таблицы прерываний, управление системным таймером и прочее.

Компонент для поддержки каждого конкретного микроконтроллера

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

Компонент для поддержки каждой конкретной платы (platform code)

Для каждой платы должна быть создана отдельная папка в репозитории. Там должны быть исходники конфигурации, которые говорят сколько в этой конкретной плате кнопок, LED(ов), SPI, I2C, SDIO. Какие там есть драйверы чипов и прочее.

Отдельная папка для каждой конкретной сборки

Все сборки должны делить общую кодовую базу. Поэтому в папке с проектом в принципе не должно быть *.с файлов. Максимум один Makefile, парочка конфигов в *.h, и еще пара скриптов.

Программный Таймер

Программный таймер это способ из одного аппаратного таймера сделать сотни программных таймеров. Очень полезно, если все аппаратные таймеры исчерпаны или их настройка на неизвестном SoC(е) очень трудна и непонятна. Хороший программный таймер позволяет настраивать период и фазу счета и полностью конфигурируется в run-time из CLI.

Синтаксический разбор строчек

При реализации CLI нужны функции для синтаксического разбора чисел всех типов данных из текстовых ASCII строк. Поэтому для каждого типа данных (float, int32_t, string, hexArray) следует реализовать парсеры типов данных. Своего рода интерпретаторы чисел. Это очень большая часть кода порядка 2k строк.

Компонент компрессии-декомпрессии

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

Драйвер подавление дребезга контактов

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

Программный Триггер Шмитта

Это звено для реализации гистерезиса. Очень полезно для подавления аналогового шума с датчиков при релейном управлении чем-либо.

Компонент limiter

Это функция, которая следит за тем, чтобы конкретная функция не вызывалась чаще чем установлено в параметрах. Очень полезно при реализации примитивов RTOS. Можно прямо в суперцикле пропускать все вызовы через limiter и таким образом выставлять период срабатывания каждой функции.

Реализация механизма выделения и освобождения памяти

Весьма вероятно, что придется накропать собственную надежную детерминированную и переносимую версию функций malloc и free, c возможностью расширенной диагностики и сборки мусора.

Компонент с математикой

Немного линейной алгебры и численных методов. Например, в системах автоматического управления на MCU очень нужна функция для вычисления угла между векторами (с учетом знака, естественно). А это сразу подтягивает реализацию скалярного и векторного умножения. При работе с ЦАП/DAC(ами) пригодится вычисление семпла синуса, семпла PWM, Chirp(а), Saw(а), Fence(а). Еще может понадобится целочисленное возведение в степень и вычисление квадратного корня. Бывает надо 7-битное знаковое число или 13-битное знаковое число преобразовать в стандартный int32_t. Для этого тоже нужны свои математические алгоритмы.

Какую математику приходилось реализовывать в микроконтроллерах вам?

Программная реализация календаря

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

Цифровые фильтры

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

Шифровальщик

Рано или поздно передаваемую прошивку по загрузчику придется шифровать или хранить в памяти в шифрованном виде. Поэтому надо выбрать доверенный и протестированный алгоритм шифрования данных, например AES. В IT чипах AES и вовсе реализован аппаратно и этот компонент можно отнести в BSP.

Парсеры протоколов

Устройство будет взаимодействовать с внешним миров. Ту же прошивку надо передеравать по какому то протоколу (ModBus, yModem). Должна быть программная реализация какого-то протокола или серии протоколов. Больше чем уверен, что в вашей компании есть какой-то собственный компанейский бинарный протокол.

Энергонезависимый журнал

В любой крупной С программе накопится очень много параметров и констант, которые придется варьировать, настраивать, калибровать. Чтобы не пересобирать и перепрошивать каждый раз гаджет надо реализовать энергонезависимый журнал прямо на Target(е). Вот вам War Story. Устройство висит под потолком. Подключился через LoRa к CLI. Прописал новый параметр через CLI, reset(нул) прошивку через CLI и у тебя новая прошивка. Easy.

Код генераторы

В программировании микроконтроллеров часто много повторяющегося по структуре кода. Например синтаксический разбор CAN матриц (8 байт полезных данных в пакете). Поэтому в репозиторий можно добавить утилиты кодогенераторы для генерации C-функций делающих синтаксический разбор пакетов с известной структурой.

Модульные тесты

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

Сборка из Make

Утилита Make это самый гибкий способ управлять циклопическими репозиториями. Можно одной строчкой добавлять/исключать тысячи файлов для тысяч сборок. Поэтому для каждой сборки надо самим писать Makefile для каждого компонента *.mk файл. Сборка из Make стимулирует придерживаться модульности и изоляции компонентов. Make идеален для масштабирования кодовой базы.

Можно добавить кучу проверок зависимостей и assert(ов) на этапе bash скриптов прямо в *.mk файлах еще до компиляции самого кода, так как язык программирования make поддерживает условные операторы и функции. Можно очень много ошибок отловить на этапе отработки утилиты make. При этом make очень прост. Вся спека GNU Make это 224 страницы. Cтю Фельдман (автор make) просто гений.

Скрипты автосборки

Каждая сборка должна собираться из скриптов. Скрипты должны записывать во On-Chip Flash hash последнего коммита и название GIT ветки. Это потом позволит откатиться назад и выявить причину бага в конкретный момент истории.

Буквально открыл папку с проектом, жмакнул *.bat файл и максимум через 2 минуты получил полный комплект артефактов *.hex *.bin *.elf *.map. Easy! Также сборка из скриптов позволит вам добавить сборки на сервер сборки Jenkins и каждое утро следить за тем, что собирается, а что нет.

Скрипты авто прошивки

Должна быть возможность автоматически прошивать Target. Буквально жмакнул скрипт flash.bat из папки с проектом и скормил прошивку микроконтроллеру. И еще и лог обновления сохранился в текстовый файлик.

Этот же скрипт с артефактами можно будет отгрузить клиенту, так как установку IDE для обновления прошивки от user(ов) какого-н фитнес браслета ожидать точно не стоит.

Скрипты отчистки и автоматического форматирования

Это для причесывания кода утилитой clang-format. Чтобы отступы были предсказуемыми по всему проекту. Это позволит писать более просты регулярные выражения для навигации по коду утилитой grep.

Batch cкрипт авто очистки в корне репозитория позволит удалить временные файлы c расширениям *.d *.o *.obj *.bak *.i *.pp и высвободить тонну места на диске. Тем более что у вас на диске будет как минимум 2 экземпляра репозитория: workspcaсe для работы и release для Jenkins(а).

Авто генерация документации

Надо относится как программированию не только к созданию артефактов. Надо относится как к программированию также для создания документации. При разработке Firmware документацией является схема toolchain, блок схемы плат, блок схемы комплексов, инструкции. Все эти *.pdf, *.svg можно синтезировать из кода на языке dot. Поэтому должны быть makefile(ы) для сборки документации. Текстовые документы можно авто генерировать на LaTeX

Вывод

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

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

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

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

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

Давайте строить хорошие репозитории.

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


  1. lolikandr
    24.09.2022 23:37
    +4

    Часть описанного - прямо маст хэв: загрузчик, отдельный код для поддержки контроллеров, плат, чипов, скрипты сборки. Но есть и прям специфичное. Например, CLI держать на борту - это очень спорно, особенно если это cli можно собрать из той же кодовой базы и запускать на pc. Нет кнопок - не нужен код анти-дребезга. Make - прошлый век, лучше пользовать ninja-build, под него есть генераторы, тот же cmake уже давно в него умеет.

    В целом, список - очень годный!


    1. aabzel Автор
      24.09.2022 23:49

      make хороший, надежный. Вся спека GNU Make это всего-навсего 224 страницы.

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

      Вот чем ninja лучше make? Прошивки и так собираются быстро. Jenkins за ночь вообще всё соберет.

      CMake и make может генерировать.

      Просто make позволяет удобнее управлять модульностью. Makefile(лы) лучше вручную писать. Так они хоть будут лучше читаться.

      Когда нет CMake - значит нет и ошибок сборки на этапе CMake.


      1. F0iL
        25.09.2022 00:22
        +4

        Makefile(лы) лучше вручную писать. Так они хоть будут лучше читаться.

        Если у вас сборка основана на CMake, то вам вообще не надо будет читать сгенеренные Makefile'ы (вот честно, мне за десяток лет ни разу не пришлось) - нужно будет читать только сами CMakeLists.txt.

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

        И даже начав делать все на чистом make, со временем вы рано или поздно обрастете костылями типа makedepend или запилите свой высокоуровневый велосипед. Как метко сказали на одном сайте: to be honest, a simple Makefile is very simple to write and efficient once you know the syntax fairly well. Then you add more and more things as the time pass, and it starts to morph into a giant huge monster that eats puppies.

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


    1. aabzel Автор
      24.09.2022 23:59
      -1

      Make - прошлый век, лучше пользовать ninja-build

      написать make all быстрее, чем написать ninja all. Поэтому make удобнее. )


      1. F0iL
        25.09.2022 00:31
        -1

        Эти команды оьычно вызываются из IDE или из CI-пайплайна, так что разницы никакой :)


      1. lolikandr
        25.09.2022 06:30
        +2

        Во-первых, при прочих равных - ninja отработает быстрее, и когда счёт идёт на десятки секунд - не хочется ждать ни одной лишней. Во-вторых, при правильном скрипте - all писать не обязательно. В-третих, сделайте алиас на одну-две буквы и пишите ещё быстрее )


  1. Zuy
    25.09.2022 08:07
    +2

    А почему вдруг загрузчик обязательно надо обновлять по UART?! Например по CAN он тоже отлично обновляется, и даже по, прости господи, USB


    1. aabzel Автор
      25.09.2022 15:54

      Проше UART интерфейсов нет. Всего пару регистров читать писать.
      Драйверы CAN и тем более USB могут просто не поместиться в 32kByte On-Chip NorFlash памяти загрузчика.


      1. Zuy
        25.09.2022 21:22
        +1

        32KB вам мало чтобы USB device сделать? Интересно как же я USB host запихнул в 16. А на счёт CAN так и вообще смешно, там сложность не сильно больше UART.

        Ну а вообще у меня больше претензия к обязательности обновления по UART декларируемой в статье. Возьмите туже Tesla там все блоки обновляются только по CAN, а про UART большинство инженеров даже не знают.


        1. aabzel Автор
          25.09.2022 21:39

          Работали с одной OutSource компанией.
          От них был загрузчик для STM32F413ZGJ6. 32kByte на весь загрузчик.
          Писали они на С++ 17 в IAR. Все что им туда удалось утрамбовать это драйвер-SPI (на SPL), драйвер SPI-NOR FLASH для MX25L6433F и все.
          Больше ничего не влезло.


        1. aabzel Автор
          25.09.2022 21:42
          +1

          CAN это замечательно. Вот только переходники с USB-CAN(12910 RUR) стоят дороже чем переходники с USB-UART (335 RUR) раз в 10...20


      1. alexac
        25.09.2022 22:51
        +4

        Зануда мод: UART требует делителя Частот, настройки скорости передачи, настройки наличия и количества старт/стоп битов, настройки проверки на четность, etc. SPI передает тактовый сигнал от мастера к слейву синхронно с данными, с опциональной адресацией слейвов по chip-select, а потому весь интерфейс SPI на слейве состоит из двух сдвиговых регистров и двух регистров буфферов для синхронизации с тактовой частотой контроллера. Самый простой драйвер UART всегда будет сложнее, чем самый простой драйвер SPI-slave.