Оптимизированный доступ к GPIO. Или GPIO как constexpr класс. С++

Kate

Administrator
Команда форума
Данный пост будет посвящен программированию на C++, и использованию constexpr объектов с целью повышения уровня удобства и одновременно оптимизации кода с точки зрения размера и производительности.
В процессе работы над одним из проектов, задумался: "нельзя ли сделать удобный доступ к GPIO портам на STM32, и при этом сделать его оптимальным по размеру кода и производительности". Что я хотел получить:
  1. Использования контекстных подсказок и автодополнения при работе с GPIO.
  2. Получение максимально оптимального кода. 1-2 ассемблерных инструкции.
  3. Потенциальная возможность добавить проверки на уровне компиляции, которые не будут влиять на производительность.
Изначально я посмотрел как доступ к портам организован на платформе Arduino, и конечно данный способ далеко не оптимален с точки зрения производительности. Сначала происходит поиск порта по индексу, и только потом обращение. Тут мне в голову пришла мысль о использовании constexpr выражений и классов для реализации обоих пунктов сразу. Итак приступим. В моем случае код не будет кросплатформенным, т.к. можно считать что это часть HAL (Hardware Abstraction Layer). Код был написан для микроконтроллера STM32F103xxx.
Для начала определим адреса портов.
static constexpr const uint32_t GPIOA_BASE = 0x40010800;
static constexpr const uint32_t GPIOB_BASE = 0x40010C00;
static constexpr const uint32_t GPIOC_BASE = 0x40011000;
Теперь определим настройки порты, которые будут нам доступны.
enum class GpioMode : uint8_t {
InputAnalog = 0x00,
InputFloating,
InputWithPullup,
OutputPushPull = 0x04,
OutputOpenDrain,
AlternatePushPull,
AlternateOpenDrain
};
enum class GpioOutputSpeed : uint8_t {
Input,
Max10Mhz,
Max2Mhz,
Max50Mhz
};
Напишем класс порта. Класс оформим в виде шаблона. Все функции класса определим как static inline. Это делается для оптимизации кода. Шаблонный класс в данном случае используется для группировки функций, хранения параметорв в виде constexpr значений. Т.е. данные параметры будут доступны только на этапе компиляции, а после компиляции, код будет оптимизирован до минимального количества инструкции. В идеале до одной-двух ассемблерных инструкций при доступе к порту, даже при компиляции с опцией "-O0". В вункции доступа к портам добавим барьерные инструкции dsb.
Небольшое отступление. На собеседованиях часто задают вопрос про volatile, которым мягко говоря задолбали уже. У меня большая просьба к тем кто проводит собеседования в сфере embedded: "не могли бы вы с ходу, не подглядывая, своими словами рассказать для чего нужны инструкции dmb, dsb и isb в системе команд arm"? Полагаю, вопрос про volatile отпадет сам собой.
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<volatile GPIO* const>(GPIOA_BASE))
#define GPIOB (reinterpret_cast<volatile GPIO* const>(GPIOB_BASE))
#define GPIOC (reinterpret_cast<volatile GPIO* const>(GPIOC_BASE))
Осталось только объявить constexpr классы:
constexpr const GpioPin<GPIOA_BASE, 0> PA0;
constexpr const GpioPin<GPIOA_BASE, 1> PA1;
constexpr const GpioPin<GPIOA_BASE, 2> PA2;
constexpr const GpioPin<GPIOB_BASE, 0> PB0;
constexpr const GpioPin<GPIOB_BASE, 1> PB1;
constexpr const GpioPin<GPIOB_BASE, 2> PB2;
constexpr const GpioPin<GPIOB_BASE, 3> PB3;
.....
constexpr const GpioPin<GPIOC_BASE, 0> PC0;
constexpr const GpioPin<GPIOC_BASE, 1> PC1;
constexpr const GpioPin<GPIOC_BASE, 2> PC2;
Что можно было усовершенстворовать? Внутри класса можно добавить различные проверки, которые будут выполнятся на этапе компиляции. Например проверки адресов.
И наконец пример использования. Выглядит как обычный класс, но компилпируетсся в 2-3 иассемблерных инструкции. При этом работает автодополнение, покрайне мере в eclipse.
PA0.set();
PA0.invert();
PA0.set();
PA0.reset();
PA0.invert();


 
Сверху