С++ (18+) для микроконтроллеров - разведка боем
Программы для микроконтроллеров де-факто пишутся либо на Си либо на ассемблере. Си это процедурный язык программирования со статической слабой типизацией, обладающий простым и понятным синтаксисом, высокой переносимостью (в сравнении с ассемблером), он хорошо подходит для системного программирования и в конце-концов успешно прошел испытание временем. И если у Си всё так хорошо, зачем все ведущие производители микроконтроллеров (PIC32, MSP430, AVR) дружно подтянули в свои инструментарии для разработчиков C++ компиляторы ? В отличие от своего предшественника Си, C++ декларирует поддержку различных парадигм программирования — процедурное, объектно-ориентированное (ООП), обобщённое (шаблоны). Вместе с тем C++ не является серебряной пулей для решения проблем своего предшественника — ручное управление памятью по-прежнему на плечах программиста. C++ добавляет к Си объектно-ориентированные возможности — вводятся классы, которые обеспечивают три самых важных свойства ООП: инкапсуляцию, наследование и полиморфизм. ООП при грамотном использовании призвано повысить производительность труда программистов, чему посвящено немало различной литературы на любой вкус и цвет.
Если отбросить религиозные пристрастия, то стоит признать, что на данный момент C++ это единственный нативный язык без GC (garbage collector), который позволяет легко переключаться от самого низкого уровня, близкого к аппаратуре, до очень высокого, вроде ООП и обобщенного программирования. При этом данный язык снабжен широким набором инструментария, книг и документации, различных интернет-ресурсов. Плюс огромное количество разработчиков во всем мире.
Запускаться будем на микроконтроллерах MSP430, компилятор как обычно на сайте производителя. MSP430 — семейство 16-разрядных микроконтроллеров фирмы «Texas Instruments», которые имеют фон-Неймановскую архитектуру с единым адресным пространством для команд и данных (для сравнения у Microchip PIC и Atmel AVR Гарвардская архитектура).
C++ в большинстве случаев обратно совместим со своим предшественником, так что поначалу всё выглядит как в старом добром Си и совсем нестрашно. Частота микроконтроллеров MSP430 задаётся программно и может меняться в процессе выполнения для снижения энергопотребления. Сетка частот встроенного тактового генератора описана в документации - регистры RSEL служат для грубой настройки, DCO для более точной. Микроконтроллер msp430f1611 имеет шесть восьмибитных портов ввода-вывода, а шесть делится на три и получаем 16 RGB светодиодов.
Тут в коде три интересных момента, касающихся С++:
constexpr - константа на этапе компиляции, в отличие от const не занимает физический адрес в памяти
auto - автоматическое выведение типа на этапе компиляции исходя из присваиваемого значения
app::run() - вызов метода из пространства имён app
функция run находится в пространстве имён namespace app - в пределах одного пространства имён идентификаторы (имена) должны быть уникальны, тогда как один и тот же идентификатор может быть определён в нескольких пространствах имён
для переменной rgb_strip отсутствует выражение инициализации и поэтому происходит инициализация по умолчанию - для классов, структур и объединений это инициализация с помощью конструктора по умолчанию (до классов мы ещё дойдём)
цикл по контейнеру for (const auto& c : colors) позволяет пройтись по всем элементам без явного указания индекса и стало быть без ошибок, связанных с неправильным индексом за пределами контейнера
С++ даёт возможность программисту избегать лишних звёздочек - доступ к переменным по ссылке auto& c обычно безопаснее и более строго проверяется компилятором чем через разыменовывание указателя *с в стиле Си
using msp430_port_t = decltype(P1OUT); - псевдоним типа, в данном случае можно было бы спокойно заменить на Cи typedef decltype(P1OUT) msp430_port_t - это дело вкуса и стиля
decltype(P1OUT) - в большинстве случаев нам не нужно знать какой там именно у микроконтроллера порт - 8, 16 или 32 бит, но его тип нам понадобится т.к. проверку типов на этапе компиляции никто не отменял
Pin<msp430_port_t, std::uint8_t> - псевдоним шаблона, кто это такой и зачем он нужен станет понятно чуть дальше
Конструктор копирования. В вежливой форме предлагаю его сразу отключать и забыть - для этого достаточно просто наследовать классы от класса NonCopyable . В таком случае кривой код, пытающийся создать побитовую копию объекта просто сломает компиляцию и это правильно.
все поля и методы классов class (а также структур struct ) имеют права доступа - по умолчанию все содержимое класса является доступным для чтения и записи только для него самого т.е. закрыто (private), а для структуры struct наоборот по умолчанию всё открыто для всех (public)
поля red, green, blue в классе RgbStripLayer (слой анимации) статические - все экземпляры этого класса используют одну и ту же копию этих полей и тем самым экономится память RAM
std::array - это контейнер для массива фиксированного размера, имеет ту же семантику, что и Cи массивы плюс знает собственный размер, поддерживает присваивание, итераторы и т.д.
методы, совпадающие с именем класса это конструкторы, RgbStripLayer(): mem() это конструктор по умолчанию т.к. не содержит аргументов, в нашем случае в конструкторе контейнер mem инициализируется нулями
слои анимаций RgbStripLayer можно смешивать между собой - для этого перегружен оператор +=
объявление класса и реализация в отдельных файлах в С++ не является обязательной, однако позволяет при необходимости ускорить сборку проекта за счёт т.н. раздельной компиляции - также как и в Си собственно
шаблоны C++ позволяют задавать обобщённые алгоритмы без привязки к некоторым параметрам - в случае c MonoStrip это конкретные порты ввода вывода p1, p2 различные у каждого цвета, код шаблона сгенерируется на этапе компиляции и в этом они схожи с макросами Си
каждый класс в C++ использует свое пространство имен, если внутри класса записано только объявление, реализация должна быть определена в другом месте с помощью операции доступа к области видимости ::
Ну и последний класс он совсем простой - каждая нога микроконтроллера это объект. За счёт использования шаблонов класс Pin ничего не знает об архитектуре микроконтроллера - тип данных порта ввода-вывода является параметром шаблона:
если тело метода определено при объявлении класса, метод автоматически являются встроенным (inline) - обычно встроенными делают короткие методы
аргументы функций в С++ могут иметь значения по умолчания как например const bool v = true , это же можно было бы сделать и с помощью перегрузки функций
Одной из типичных ошибок при программировании на C++ является использование неинициализированных переменных. Чтение из неинициализированной переменной по Стандарту является неопределённым поведением. На практике обычно оказывается прочитанным какое-то полуслучайное мусорное значение, причём оно может быть разным от запуска к запуску и разным на разных платформах. Отсюда и получается, что программа ни с того ни с сего работает по-разному.
Как и в Си, глобальные переменные простых типов в С++ автоматически инициализируются нулями, локальные — нет. Если же мы работаем с объектами, для которых определён конструктор по умолчанию, он будет вызван в любом случае и в нём рекомендуется по умолчанию инициализировать все поля, чтобы не сталкиваться с тем, что программа работает нестабильно. Рассмотрим следующий пример:
Достаточно отключить оптимизацию компилятора и убрать из rgb.h конструктор по умолчанию RgbStripLayer(): mem() как программа начнёт вести себя по-разному ok | bug.
Вообще по-хорошему нормальный язык просто не должен позволять использовать неинициализированные переменные. Необязательно нужно указывать конкретное значение, значением по умолчанию может стать все тот же ноль. Для выявления неинициализированных переменных у gcc подобных компиляторов есть опция командной строки -Weffc++ , также можно посмотреть в сторону Valgrind или статических анализаторов кода.