Вступление

Идея использования шаблонов языка 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