Данный пост будет посвящен программированию на C++, и использованию constexpr объектов с целью повышения уровня удобства и одновременно оптимизации кода с точки зрения размера и производительности.
В процессе работы над одним из проектов, задумался: "нельзя ли сделать удобный доступ к GPIO портам на STM32, и при этом сделать его оптимальным по размеру кода и производительности". Что я хотел получить:
Для начала определим адреса портов.
Небольшое отступление. На собеседованиях часто задают вопрос про 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;");
}
}
Теперь объявим определения портов в виде прведенных к типу адресов.
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();
В процессе работы над одним из проектов, задумался: "нельзя ли сделать удобный доступ к GPIO портам на STM32, и при этом сделать его оптимальным по размеру кода и производительности". Что я хотел получить:
- Использования контекстных подсказок и автодополнения при работе с GPIO.
- Получение максимально оптимального кода. 1-2 ассемблерных инструкции.
- Потенциальная возможность добавить проверки на уровне компиляции, которые не будут влиять на производительность.
Для начала определим адреса портов.
Теперь определим настройки порты, которые будут нам доступны.static constexpr const uint32_t GPIOA_BASE = 0x40010800;
static constexpr const uint32_t GPIOB_BASE = 0x40010C00;
static constexpr const uint32_t GPIOC_BASE = 0x40011000;
Напишем класс порта. Класс оформим в виде шаблона. Все функции класса определим как static inline. Это делается для оптимизации кода. Шаблонный класс в данном случае используется для группировки функций, хранения параметорв в виде constexpr значений. Т.е. данные параметры будут доступны только на этапе компиляции, а после компиляции, код будет оптимизирован до минимального количества инструкции. В идеале до одной-двух ассемблерных инструкций при доступе к порту, даже при компиляции с опцией "-O0". В вункции доступа к портам добавим барьерные инструкции dsb.enum class GpioMode : uint8_t {
InputAnalog = 0x00,
InputFloating,
InputWithPullup,
OutputPushPull = 0x04,
OutputOpenDrain,
AlternatePushPull,
AlternateOpenDrain
};
enum class GpioOutputSpeed : uint8_t {
Input,
Max10Mhz,
Max2Mhz,
Max50Mhz
};
Небольшое отступление. На собеседованиях часто задают вопрос про 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;");
}
}
Теперь объявим определения портов в виде прведенных к типу адресов.
Осталось только объявить constexpr классы:#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 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();
Код доступен по ссылке: |
https://github.com/hwswdevelop/BluePillUsbDebugPrototyping/blob/main/Template/System/Core/gpio.h |
Оптимизированный доступ к GPIO. Или GPIO как constexpr класс. С++
Добрый день, жители Хабра. Данный пост будет посвящен программированию на C++, и использованию constexpr объектов с целью повышения уровня удобства и одновременно оптимизации кода с точки зрения...
habr.com