Снова про шаблоны C++ в микроконтроллерах
Вступление
Идея использования шаблонов языка C++ для программирования контроллеров не является чем-то новым, в сети доступно большое количество материалов. Кратко напомню основные преимущества: перенос значительной части ошибок из runtime в compile-time за счет строгого контроля типов, а также приближение к объектно-ориентированному подходу, близкий и удобный многим, без необходимости хранения полей в статическом классе (все поля являются шаблонными параметрами). Однако стоит заметить, что практически все авторы по большому счету ограничиваются в своих работах примерами на работу с регистрами и портами ввода-вывода. В своей статье я хочу продолжить эти идеи.
Нельзя быть чуть-чуть беременным
Объектно-ориентированное программирование подразумевает наличие в программе классов, взаимодействующих между собой. Взаимодействие выражается зависимостью одних классов от других. И здесь проявляется суть заголовка: чтобы передать в класс A в качестве параметра шаблонный класс B со статическими методами, то класс B тоже необходимо делать шаблонным и так далее по всей цепочке.
Например, USART как минимум зависит от своих регистров, тогда объявление соответствующего класса будет выглядеть приблизительно следующим образом (код, связанный с объявлением регистров, взят отсюда, чтобы была связность статей, спасибо @lamerok за крутые материалы и примеры. Сам я пока на использование созданных им оберток не перешел, но планирую.):
template
class Usart
{
public:
static void Init()
{
_Regs::CR1Pack<_Regs::CR1::UE, _Regs::CR1::RE, _Regs::CR1::TE>::Set();
// Еще какие-то настройки.
}
}
С регистрами разобрались, чтобы объявить конкретный экземпляр UART, необходимо специализировать шаблон
using Usart1 = Usart;
Вроде бы все здорово, однако на самом деле конкретный интерфейс USART имеет иные зависимости:
Регистр тактирования (RCC_APB2).
Номер прерывания.
Набор возможных выводов (Tx и Rx).
Dma (Tx и Rx).
И один из четырех принципов ООП — инкапсуляция — намекает на то, что всё перечисленное тоже следует внести в параметры класса. Например, добавим регистр тактирования
template
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
template
void Usart<_Regs, _IRQNumber, _ClockCtrl, _TxPins, _RxPins, _DmaTx, _DmaRx>::SelectTxRxPins()
{
const int8_t txPinIndex = TypeIndex::value;
const int8_t rxPinIndex = !std::is_same_v
? TypeIndex::value
: -1; // Хотя тут нужно переделать, чтобы отличать передачу NullPin от недоступного параметра
static_assert(txPinIndex >= 0);
// В полудуплексном режиме Rx необязательна
static_assert(rxPinIndex >= -1);
SelectTxRxPins();
}
Если при настройке USART вызвать метод SelectTxRxPins с аргументом, которого нет в списке, прошивка не скомпилируется из-за условия в static_assert.
Таким образом, я считаю, что применения шаблонов C++ заслуживают не только регистры и GPIO, а вообще вся периферия и даже драйверы устройств, использующих интерфейсы микроконтроллера.
// Драйвер часов реального времени DS1307
template
class Ds1307
{
...
static Time GetDateTime()
{
Time time;
_I2CBus::Read(Ds1307Address, 0x00, &time, sizeof(time));
...
Объявление экземпляров классов, как обычно, осуществляется объявлением нового типа:
using Rtc = Ds1307;
Только теорию нельзя практику
Из всех авторов материалов, которые я видел, только Константин Чижов (он же neiver, автор статьи Работа с портами ввода-вывода микроконтроллеров на Си++ на ресурсе easyelectronics.ru) поставил запятую после слова «нельзя». В его репозитории на github представлена библиотека «Mcucpp», которая реализует идеи метапрограммирования в микроконтроллерах. На мой взгляд, как это нередко бывает, у проекта есть ряд недостатков, главным из которых считаю невозможность использовать ее из коробки, что отталкивает потенциальных пользователей, особенно новичков (типа меня, который начал заниматься контроллерами в середине 2019, в виде хобби). Так как конкретных проектов и задач у меня нет, я решил начать собирать все наработки Константина, пытаться, насколько это возможно, адаптировать код под разные семейства, писать Doxy-документацию, примеры для добавленного кода, проверять его работоспособность. В результате медленно развивается проект Zhele, в котором я на основе библиотеки Чижова создаю полностью шаблонный фреймворк для контроллеров Stm32. Сразу отмечу, что автором файлов проекта, где изменений немного, пишу Константина Чижова.
«Лучше меньше, да лучше» © В.И. Ленин
На момент написания этой статьи большая часть возможностей контроллеров еще не покрыта библиотекой, однако уже есть и проверены тактирование, gpio, таймеры, интерфейсы i2c/spi/uart/one-wire, драйверы устройств, которые у меня есть.
Приветствую все замечания, предложения и пожелания. Сейчас копаю в сторону генерации custom-шаблонов для CubeIDE. Общение с людьми, связанными с разработкой устройств, показало, что при всех недостатках, куб им нравится, еще более актуально это для тех, кто только начинает погружаться в мир программирования микроконтроллеров, поэтому считаю возможность генерации проектов, использующих шаблоны, сразу в кубе, весьма полезной. Надеюсь осилить этот вопрос и это будет темой следующей статьи.
Источники
Работа с портами ввода-вывода микроконтроллеров на Си++
Трактат о Pinе. Мысли о настройке и работе с пинами на С++ для микроконтроллеров (на примере CortexM)
Безопасный доступ к полям регистров на С++ без ущерба эффективности (на примере CortexM)
Использование шаблонного метапрограммирования для микроконтроллеров AVR