Снова про шаблоны C++ в микроконтроллерах

от автора

Вступление

Идея использования шаблонов языка C++ для программирования контроллеров не является чем-то новым, в сети доступно большое количество материалов. Кратко напомню основные преимущества: перенос значительной части ошибок из runtime в compile-time за счет строгого контроля типов, а также приближение к объектно-ориентированному подходу, близкий и удобный многим, без необходимости хранения полей в статическом классе (все поля являются шаблонными параметрами). Однако стоит заметить, что практически все авторы по большому счету ограничиваются в своих работах примерами на работу с регистрами и портами ввода-вывода. В своей статье я хочу продолжить эти идеи.

Нельзя быть чуть-чуть беременным

Объектно-ориентированное программирование подразумевает наличие в программе классов, взаимодействующих между собой. Взаимодействие выражается зависимостью одних классов от других. И здесь проявляется суть заголовка: чтобы передать в класс A в качестве параметра шаблонный класс B со статическими методами, то класс B тоже необходимо делать шаблонным и так далее по всей цепочке.

Например, USART как минимум зависит от своих регистров, тогда объявление соответствующего класса будет выглядеть приблизительно следующим образом (код, связанный с объявлением регистров, взят отсюда, чтобы была связность статей, спасибо @lamerok за крутые материалы и примеры. Сам я пока на использование созданных им оберток не перешел, но планирую.):

template <typename _Regs> class Usart { public:   static void Init()   {     _Regs::CR1Pack<_Regs::CR1::UE, _Regs::CR1::RE, _Regs::CR1::TE>::Set();     // Еще какие-то настройки.   } }

С регистрами разобрались, чтобы объявить конкретный экземпляр UART, необходимо специализировать шаблон

using Usart1 = Usart<USART1>;

Вроде бы все здорово, однако на самом деле конкретный интерфейс USART имеет иные зависимости:

  1. Регистр тактования (RCC_APB2).

  2. Номер прерывания.

  3. Набор возможных выводов (Tx и Rx).

  4. Dma (Tx и Rx).

И один из четырех принципов ООП — инкапсуляция — намекает на то, что всё перечисленное тоже следует внести в параметры класса. Например, добавим регистр тактования:

template <typename _Regs, typename _ClockCtrl> class Usart { public:   static void Init()   {     // Инициализация скорее всего подразумевает включение тактования периферии     _ClockCtrl::Enable()     _Regs::CR1Pack<_Regs::CR1::UE, _Regs::CR1::RE, _Regs::CR1::TE>::Set();     ...   } }

Теперь включение тактования модуля инкапсулировано в его инициализацию. Можно заметить, что в случае многократного вызова метода Init каждый раз будет производиться лишняя операция чтения/записи в регистра RCC_ARB2, но часто ли такое встречается в реальности?

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

 // Пример настройки TX UART на SPL. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_40MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); //инициализируем 

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

// Метод выбора Tx и Rx выводов. template<typename _Regs, IRQn_Type _IRQNumber, typename _ClockCtrl, typename _TxPins, typename _RxPins, typename _DmaTx, typename _DmaRx> template<typename TxPin, typename RxPin> void Usart<_Regs, _IRQNumber, _ClockCtrl, _TxPins, _RxPins, _DmaTx, _DmaRx>::SelectTxRxPins() {   const int8_t txPinIndex = TypeIndex<TxPin, _TxPins>::value;   const int8_t rxPinIndex = !std::is_same_v<RxPin, IO::NullPin>         ? TypeIndex<RxPin, typename _RxPins>::value         : -1; // Хотя тут нужно переделать, чтобы отличать передачу NullPin от недоступного параметра   static_assert(txPinIndex >= 0);   // В полудуплексном режиме Rx необязательна   static_assert(rxPinIndex >= -1);   SelectTxRxPins<txPinIndex, rxPinIndex>(); }

Если при настройке USART вызвать метод SelectTxRxPins с аргументом, которого нет в списке, прошивка не скомпилируется из-за условия в static_assert.

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

// Драйвер часов реального времени DS1307 template <typename _I2CBus> class Ds1307 {   ...   static Time GetDateTime()   {     Time time;     _I2CBus::Read(Ds1307Address, 0x00, &time, sizeof(time));     ...

Объявление экземпляров классов, как обычно, осуществляется объявлением нового типа:

using Rtc = Ds1307<I2c1>;

Только теорию нельзя практику

Из всех авторов материалов, которые я видел, только Константин Чижов (он же neiver, автор статьи Работа с портами ввода-вывода микроконтроллеров на Си++ на ресурсе easyelectronics.ru) поставил запятую после слова «нельзя». В его репозитории на github представлена библиотека «Mcucpp», которая реализует идеи метапрограммирования в микроконтроллерах. На мой взгляд, как это нередко бывает, у проекта есть ряд недостатков, главным из которых считаю невозможность использовать ее из коробки, что отталкивает потенциальных пользователей, особенно новичков (типа меня, который начал заниматься контроллерами в середине 2019, в виде хобби). Так как конкретных проектов и задач у меня нет, я решил начать собирать все наработки Константина, пытаться, насколько это возможно, адаптировать код под разные семейства, писать Doxy-документацию, примеры для добавленного кода, проверять его работоспособность. В результате медленно развивается проект Zhele, в котором я на основе библиотеки Чижова создаю полностью шаблонный фреймворк для контроллеров Stm32. Сразу отмечу, что автором файлов проекта, где изменений немного, пишу Константина Чижова.

«Лучше меньше, да лучше» © В.И. Ленин

На момент написания этой статьи большая часть возможностей контроллеров еще не покрыта библиотекой, однако уже есть и проверены тактование, gpio, таймеры, интерфейсы i2c/spi/uart/one-wire, драйверы устройств, которые у меня есть.

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

Источники

  1. Работа с портами ввода-вывода микроконтроллеров на Си++

  2. Трактат о Pinе. Мысли о настройке и работе с пинами на С++ для микроконтроллеров (на примере CortexM)

  3. Безопасный доступ к полям регистров на С++ без ущерба эффективности (на примере CortexM)

  4. Использование шаблонного метапрограммирования для микроконтроллеров AVR

ссылка на оригинал статьи https://habr.com/ru/post/540064/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *