Оптимизированный Доступ К Gpio. Или Gpio Как Класс Constexpr. С++

Добрый день жители Хабра.

Этот пост будет посвящен программированию на C++ и использованию объектов constexpr с целью повышения уровня удобства и одновременной оптимизации кода с точки зрения размера и производительности.

Работая над одним из проектов, я задумался: «можно ли сделать удобный доступ к портам GPIO на STM32, и в то же время сделать его оптимальным с точки зрения размера кода и производительности».

Что я хотел получить:

  1. Использование контекстных подсказок и автодополнения при работе с GPIO.
  2. Получение наиболее оптимального кода.

    1-2 инструкции по сборке.

  3. Возможность добавления проверок на уровне компиляции, которые не повлияют на производительность.

Изначально я смотрел, как организован доступ к портам на платформе Arduino, и конечно этот метод далеко не оптимален с точки зрения производительности.

Сначала порт ищется по индексу, и только потом осуществляется вызов.

Тогда мне в голову пришла идея использовать выражения и классы constexpr для реализации обоих пунктов сразу.

Итак, давайте начнем.

В моем случае код не будет кроссплатформенным, потому что.

мы можем считать это частью HAL (Hardware Abstraction Layer).

Код был написан для микроконтроллера STM32F103xxx. Для начала определимся с адресами портов.

статический constexpr const uint32_t GPIOA_BASE = 0x40010800; статический constexpr const uint32_t GPIOB_BASE = 0x40010C00; статический constexpr const uint32_t GPIOC_BASE = 0x40011000;
Теперь определим настройки порта, которые будут нам доступны.

класс перечисления GpioMode: uint8_t { Входной аналоговый = 0x00, ВводПлавающий Входсподтягивание ВыходПушПулл = 0x04, ВыходOpenDrain, АльтернативныйPushPull, АльтернативныйОткрытыйСлив }; класс перечисления GpioOutputSpeed: uint8_t { Вход Макс10 МГц, Макс2МГц, Макс50 МГц };
Давайте напишем класс порта.

Мы создадим класс в виде шаблона.

Мы определяем все функции класса как статические встроенные.

Это сделано для оптимизации кода.

Класс шаблона в данном случае используется для группировки функций и хранения параметров в виде значений constexpr. Те.

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

В идеале — до одной или двух ассемблерных инструкций на доступ к порту, даже при компиляции с опцией «-O0».

Добавим инструкции барьера dsb в функции доступа к порту.

Небольшое отступление.

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

У меня большая просьба к тем, кто проводит собеседования во встроенных полях: «не могли бы вы сказать мне сразу, не подглядывая, своими словами, какие инструкции dmb, dsb и isb нужны в системе управления вооружением»? Думаю вопрос про волатильные отпадёт сам собой.

   

template<uint32_t GpioAddr, uint8_t pinNo> struct GpioPin { static constexpr const uint32_t GpioAddress = GpioAddr; static constexpr const uint8_t GpioPinNo = pinNo; static constexpr const uint32_t GpioPinMask = (1 << pinNo); static constexpr const uint32_t GpioConfPerReg = 8; static inline void mode(const GpioMode mode, const GpioOutputSpeed oSpeed = GpioOutputSpeed::Input){ if constexpr (GpioPinNo < GpioConfPerReg){ static constexpr const uint32_t maskBitCount = 4; static constexpr const uint32_t maskOffset = (pinNo * maskBitCount) & 0x1F; static constexpr const uint32_t mask = (1 << (maskBitCount + 1)) - 1; reinterpret_cast<volatile GPIO* const>(GpioAddress)->CRL &= ~(mask << maskOffset); reinterpret_cast<volatile GPIO* const>(GpioAddress)->CRL |= ((static_cast<uint32_t>(mode) & 0x03) << maskOffset); reinterpret_cast<volatile GPIO* const>(GpioAddress)->CRL |= ((static_cast<uint32_t>(oSpeed) & 0x03) << (maskOffset + 2)); } else { // Error // TO DO: add error compile time error message. } } static inline bool get() { return (reinterpret_cast<volatile GPIO* const>(GpioAddress)->IDR & GpioPinMask); } static inline void set() { reinterpret_cast<volatile GPIO* const>(GpioAddress)->BSRR = GpioPinMask; asm volatile("dsb;"); } static inline void reset(){ reinterpret_cast<volatile GPIO* const>(GpioAddress)->BRR = GpioPinMask; asm volatile("dsb;"); } static inline void invert(){ reinterpret_cast<volatile GPIO* const>(GpioAddress)->ODR ^= GpioPinMask; asm volatile("dsb;"); } }

Теперь давайте объявим определения портов как адреса типов.

#define GPIOA (reinterpret_cast (GPIOA_BASE)) #define GPIOB (reinterpret_cast (GPIOB_BASE)) #define GPIOC (reinterpret_cast (GPIOC_BASE))
Остается только объявить классы constexpr: constexpr const GpioPin ПА0; constexpr const GpioPin ПА1; constexpr const GpioPin ПА2; constexpr const GpioPin ПБ0; constexpr const GpioPin ПБ1; constexpr const GpioPin ПБ2; constexpr const GpioPin ПБ3; .

constexpr const GpioPin ПК0; constexpr const GpioPin ПК1; constexpr const GpioPin ПК2; Что можно улучшить? Внутри класса можно добавлять различные проверки, которые будут выполняться на этапе компиляции.

Например, проверка адресов.

И наконец пример использования.

Выглядит как обычный класс, но компилируется в 2-3 ассемблерные инструкции.

В этом случае автодополнение работает, по крайней мере, в eclipse. PA0.set(); PA0.инвертировать(); PA0.set(); PA0.reset(); PA0.инвертировать();

Код доступен по ссылке:
https://github.com/hwswdevelop/BluePillUsbDebugPrototyping/blob/main/Template/System/Core/gpio.h
Теги: #stm32 #arm #C++ #GPIO #HAL
Вместе с данным постом часто просматривают: