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

15bcc1a104d825d167a13cc79e0f4278

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

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

  1. Использования контекстных подсказок и автодополнения при работе с GPIO.

  2. Получение максимально оптимального кода. 1–2 ассемблерных инструкции.

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

Изначально я посмотрел как доступ к портам организован на платформе Arduino, и конечно данный способ далеко не оптимален с точки зрения производительности. Сначала происходит поиск порта по индексу, и только потом обращение. Тут мне в голову пришла мысль о использовании constexpr выражений и классов для реализации обоих пунктов сразу. Итак приступим. В моем случае код не будет кросплатформенным, т.к. можно считать что это часть HAL (Hardware Abstraction Layer). Код был написан для микроконтроллера STM32F103xxx.

Для начала определим адреса портов.

static constexpr const uint32_t GPIOA_BASE = 0×40010800;

static constexpr const uint32_t GPIOB_BASE = 0×40010C00;

static constexpr const uint32_t GPIOC_BASE = 0×40011000;

Теперь определим настройки порты, которые будут нам доступны.

enum class GpioMode: uint8_t {

InputAnalog = 0×00,

InputFloating,

InputWithPullup,

OutputPushPull = 0×04,

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 
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(GpioAddress)->CRL &= ~(mask << maskOffset);
        reinterpret_cast(GpioAddress)->CRL |= ((static_cast(mode) & 0x03) << maskOffset);
        reinterpret_cast(GpioAddress)->CRL |= ((static_cast(oSpeed) & 0x03) << (maskOffset + 2));
    } else {
        // Error
        // TO DO: add error compile time error message. 
    }
}

static inline bool get() {
    return (reinterpret_cast(GpioAddress)->IDR & GpioPinMask);
}

static inline void set() {
    reinterpret_cast(GpioAddress)->BSRR = GpioPinMask;
    asm volatile("dsb;");
}

static inline void reset(){
    reinterpret_cast(GpioAddress)->BRR = GpioPinMask;
    asm volatile("dsb;");
}

static inline void invert(){
    reinterpret_cast(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 PA0;

constexpr const GpioPin PA1;

constexpr const GpioPin PA2;

constexpr const GpioPin PB0;

constexpr const GpioPin PB1;

constexpr const GpioPin PB2;

constexpr const GpioPin PB3;

constexpr const GpioPin PC0;

constexpr const GpioPin PC1;

constexpr const GpioPin PC2;

Что можно было усовершенстворовать? Внутри класса можно добавить различные проверки, которые будут выполнятся на этапе компиляции. Например проверки адресов.

И наконец пример использования. Выглядит как обычный класс, но компилпируетсся в 2–3 иассемблерных инструкции. При этом работает автодополнение, покрайне мере в eclipse.

PA0.set ();

PA0.invert ();

PA0.set ();

PA0.reset ();

PA0.invert ();

© Habrahabr.ru