Я себе делал библиотеку классов для работы с GPIO. Хардкорненько
получается. 1. Каждый пин это шаблонный класс, который знает идентификатор и тип своего GPIO-порта, свою позицию в порту, свою альтернативную функцию и начальное состояние. И умеет базовые действмия типа set, clear, write, read, toggle, mode выполнять через свой GPIO-порт.
2. GPIO-порт знает как на контроллере выполнять основные действия, знает начальное состояние порта при включении, содержит маску задействованных в нём пинов. Умеет делать множество архитектурнозависимых оптимизаций для операций с портом.
3. Класс для задания режимов работы порта. Он собирает в единое список пинов, портов, других списков. Группирует их по портам. Проверяет список на дублирование. Вычисляет все незадействованные пины и задаёт им значение по умоланию. Вычисляет для каждого порта какие из регистров для задания режимов надо менять, а какие нет. Опционально, исключает из модифицируемых регистров те, значения которых при включении находятся в нужном значении.
Процесс конфигурации начинается с "обзывания" пинов согласно схеме. Так как пины это типы, то в ход пускаем using.
Затем прописываем режимы и начальные состояния с помощью списков типов. Собираем списки в один и вызываем метод задания конфигурации.
Листинг установки режимов и состояний 32 ног контроллера STM32G431 из кода с картинки. Можно обратить внимание, что нет ни одной операция "чтение-модификация-запись", далеко не во все регистры произведена запись, использованы 32, 16 и 8-битные записи в регистры. Все вычисления и оптимизации сделаны на этапе компиляции.
MOV R1,#+1207959552
LDR.N R2,??__low_level_init_0+0x30
STR R2,[R1, #+0]
MOVS R3,#+36
STRB R3,[R1, #+15]
MOVS R2,#+16
LDR.W R12,??__low_level_init_0+0x34
STR R12,[R1, #+8]
STRB R2,[R1, #+34]
MOV R3,#+50529027
STR R3,[R1, #+24]
MOVW R2,#+39613
STRH R2,[R1, #+1024]
MOVS R3,#+0
STRB R3,[R1, #+1037]
MOVW R2,#+53187
STRH R2,[R1, #+1032]
LDR.N R3,??__low_level_init_0+0x38
STR R3,[R1, #+1056]
Не реализована привязка возможных альтернативных функций к пинам. При моём зоопарке используемых контроллеров это неподъёмная база данных. Но одно то, что я руками не раcпихиваю биты по регистрам, а работаю в понятиях режимов и имею множество проверок списков на этапе компиляции уже на порядок снижает трудоёмкость и количество ошибок.
То что пины и списки пинов это типы, позволяет строить из них библиотеку шаблонных классов. Например, примитивный класс светодиода. Описываем что он умеет: вкл, выкл, поменять состояние, прочитать состояние. Описываем это один раз, задавая пин шаблонным параметром. При этом как в железе будет этот пин меняться нас не волнует совсем, хоть по радиоканалу, компилятор сам всё сделает при инстанцировании.
template <typename TPin, bool invert=false>
struct TLed final
{
static inline void On() { invert ? TPin::clear() : TPin::set(); }
static inline void Off() { invert ? TPin::set() : TPin::clear(); }
static inline void Toggle() { TPin::toggle(); }
static inline bool Read() { return invert ? !TPin::read() : TPin::read(); }
inline TLed& operator=(bool state)
{
if(state) On(); else Off();
return *this;
}
inline operator bool() const { return Read(); }
};
Построение библиотеки через шаблонные классы позволяет весь код иметь в заголовочных файлах, а значит компилятор может оптимизировать и инлайнить намного больше кода, чем при традиционном модульном программировании на Си.
Для примера, простой метод шаблонного класса легко и непринуждённо инлайнится в месте вызова
// Статический метод шаблонного класса
static inline void Init(SPI_BR PRESC, CPOL CPOL_=CPOL::_0, CPHA CPHA_=CPHA::_0)
{
base()->CR2 = _VAL2FLD(SPI_CR2_DS,8-1) | SPI_CR2_FRXTH;
base()->CR1 = _VAL2FLD(SPI_CR1_BR,PRESC) | SPI_CR1_MSTR | SPI_CR1_SPE | SPI_CR1_SSI | SPI_CR1_SSM | uint32_t(CPOL_) | uint32_t(CPHA_);
}
//Вызов в любом месте программы заинлайнится
SPI::spi3::Init(SPI::SPI_BR::PCLK_DIV2, SPI::CPOL::_0, SPI::CPHA::_1);
MOV R2,#+5888
LDR.N R1,??__low_level_init_0+0x3C
STR R2,[R1, #+4]
MOVW R3,#+837
STR R3,[R1, #+0]