Рис. И. Кийко
Всем доброго здравия!
Помните наверное бородатый анекдот, а может быть и правдивую историю про то, как студента спрашивали о способе измерить высоту здания с помощью барометра. Студент привел, по-моему около 20 или 30 способов, при этом не назвав прямого(через разницу давления), которого ожидал преподаватель.
Примерно в том же ключе я хочу продолжить обсуждение использования С++ для микроконтроллеров и рассмотреть способы как можно работать с регистрами используя С++. И хочу заметить, что для достижения безопасного обращения к регистрам простого пути не будет. Попытаюсь показать все плюсы и минусы способов. Если вы знаете еще способы, кидайте их в комментарии. Итак начнем:
Способ 1. Очевидный и, очевидно, не самый лучший
Самый распространенный способ, который также применяется в С++, является использование описания структур регистров из заголовочного файла от производителя. Для демонстрации я возьму два регистра порта А (ODR — регистр выходных данных и IDR — регистра входных данных) микроконтроллера STM32F411, чтобы можно было выполнить «ембедерский» «Hello world» — моргнуть светодиодом.
int main() {
GPIOA->ODR ^= (1 << 5) ;
GPIOA->IDR ^= (1 << 5) ; //ГЛУПОСТЬ, но я же не знал
}
Давайте посмотрим, что тут происходит, и как эта конструкция работает. В заголовочнике для микропроцессора есть структура GPIO_TypeDef
и определение указателя на эту структуру GPIOA
. Выглядит это следующим образом:
typedef struct
{
__IO uint32_t MODER; //port mode register, Address offset: 0x00
__IO uint32_t OTYPER; //port output type register, Address offset: 0x04
__IO uint32_t OSPEEDR; //port output speed register, Address offset: 0x08
__IO uint32_t PUPDR; //port pull-up/pull-down register, Address offset: 0x0C
__IO uint32_t IDR; //port input data register, Address offset: 0x10
__IO uint32_t ODR; //port output data register, Address offset: 0x14
__IO uint32_t BSRR; //port bit set/reset register, Address offset: 0x18
__IO uint32_t LCKR; //port configuration lock register, Address offset: 0x1C
__IO uint32_t AFR[2]; //alternate function registers, Address offset: 0x20-0x24
} GPIO_TypeDef;
#define PERIPH_BASE 0x40000000U //Peripheral base address in the alias region
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000U)
#define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000U)
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
Если выразиться простыми человеческими словам, то вся структура типа GPIO_TypeDef
«ложится» по адресу GPIOA_BASE
, а при обращении к конкретному полю структуры, вы по сути обращается к адресу этой структуры + смещение до элемента этой структуры. Если убрать #define GPIOA
, то код выглядел бы так:
((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ;
((GPIO_TypeDef *) GPIOA_BASE)->IDR ^= (1 << 5) ; //ГЛУПОСТЬ
Применительно к языку программирования С++ здесь происходит преобразование целочисленного адреса к типу указатель на структуру GPIO_TypeDef
. Но в С++ при использовании Си преобразования компилятор пытается выполнить преобразование в следующей последовательности:
- const_cast
- static_cast
- static_cast следующей за const_cast,
- reinterpret_cast
- reinterpret_cast следующий за const_cast
т.е. если компилятор не смог преобразовать тип используя const_cast, он пытается применить static_cast и так далее. В итоге вызов:
((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ;
есть ни что иное как:
reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE)->ODR ^= (1 << 5) ;
На самом деле для С++ приложений правильно было бы «натянуть» структуру на адрес вот так:
GPIO_TypeDef * GPIOA{reinterpret_cast<GPIO_TypeDef *>(GPIOA_BASE)} ;
В любом случае из-за преобразования типов существует большой минус этого подхода для С++. Заключается он в том, что reinterpret_cast
нельзя использовать ни в constexpr
конструкторах и функциях, ни в параметрах шаблона, а это существенно сужает использование возможностей С++ для микроконтроллеров.
Поясню это на примерах. Вполне возможно сделать так:
struct Test {
const int a;
const int b;
} ;
template<Test* mystruct>
constexpr const int Geta() {
return mystruct->a;
}
Test test{1,2};
int main() {
Geta<&test>() ;
}
Но вот так уже сделать нельзя:
template<GPIO_TypeDef * mystruct>
constexpr volatile uint32_t GetIdr() {
return mystruct->IDR;
}
int main() {
//GPIOA это reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE)
//использует преобразование типов, и в параметры шаблона его передавать нельзя
GetIdr<GPIOA>() ; //Ошибка
}
// И вот так тоже сделать нельзя:
struct Port {
constexpr Port(GPIO_TypeDef * ptr): port(*ptr) {}
GPIO_TypeDef & port ;
}
//Так как GPIOA использует reinterpret_cast, то конструктор
//перестает быть constexpr и невозможно выполнить статическую инициализацию
constexpr Port portA{GPIOA}; // тут будет ошибка
Таким образом прямое использование такого подхода накладывает существенные ограничения на использование С++. Мы не сможем расположить объект который хочет использовать указатель на GPIOA
в ROM используя средства языка, и не сможем использовать преимущества метапрограммирования для такого объекта.
Кроме того, вообще такой способ не safety (как говорят наши западные партнеры). Ведь вполне возможно сделать какую-то ГЛУПОСТЬ
В связи с вышесказанным резюмируем:
Плюсы
- Используется заголовочник от производителя (он проверен, в нем нет ошибок)
- Нет дополнительных телодвижений и затрат, берешь и используешь
- Простота использования
- Все знают и понимают этот способ
- Никаких накладных
Минусы
- Ограниченное использование метапрограммирования
- Невозможность использовать в constexpr конструкторах
- При использовании в классах обертках, дополнительных расход ОЗУ, на указатель на объект этой структуры
- Можно сделать ГЛУПОСТЬ
Теперь посмотрим на способ №2
Способ 2. Брутальный
Очевидно, что каждый ембед программист держит в голове адреса всех регистров для всех микроконтроллеров, поэтому можно просто всегда использовать следующий способ, вытекающий из первого:
*reinterpret_cast<volatile uint32_t *>(GpioaOdrAddr) ^= (1 <<5) ;
*reinterpret_cast<volatile uint32_t *>(GpioaIdrAddr) ^= (1 <<5) ; //ГЛУПОСТЬ
В любом месте программы, всегда можно вызвать преобразование к volatile uint32_t
адресу регистра и установить там хоть что.
Плюсов тут особо нет, а к тем минусам, что есть добавится еще неудобство использования и необходимость самому прописывать адрес каждого регистра во отдельном файле. Поэтому переходим в способу №3.
Способ 3. Очевидный и очевидно правильнее
Если доступ к регистрам происходит через поле структуры, то вместо указателя на объект структуры можно использовать целочисленный адрес структуры. Адрес структур есть в заголовочном файле от производителя (например, GPIOA_BASE для GPIOA), поэтому его не надо помнить, а применять можно и в шаблонах и в constexpr выражениях, а затем уже «накладывать» структуру на этот адрес.
template<uint32_t addr, uint32_t pinNum>
struct Pin {
using Registers = GPIO_TypeDef ;
__forceinline static void Toggle() {
// располагаем структуру по адресу addr
Registers *GpioPort{reinterpret_cast<Registers*>(addr)};
GpioPort->ODR ^= pinNum ;
}
};
int main() {
using Led1 = Pin<GPIOA_BASE, 5> ;
Led1::Toggle() ;
}
Особых минусов, с моей точки зрения нет. В принципе рабочий вариант. Но все равно, давайте разберем другие способы.
Способ 4. Экзотерическая обертка
Для ценителей понятного кода, можно сделать обертку над регистром, чтобы обращаться к ним было удобно и выглядело «красиво», сделать конструктор, переопределить операторы:
class Register {
public:
explicit Register(uint32_t addr) : ptr{ reinterpret_cast<volatile uint32_t *>(addr) } {
}
__forceinline inline Register& operator^=(const uint32_t right) {
*ptr ^= right;
return *this;
}
private:
volatile uint32_t *ptr; //указатель хранящий адрес регистра
};
int main() {
Register Odr{GpioaOdrAddr};
Odr ^= (1 << 5);
Register Idr{GpioaIdrAddr};
Idr ^= (1 << 5); //ГЛУПОСТЬ
}
Как видно, снова придется либо помнить целочисленные адреса всех регистров, либо где-то их задавать, а еще придется хранить указатель, на адрес регистра. Но что опять не очень, снова в конструкторе происходит reinterpret_cast
Одни минусы, а к тем, что в первом и втором варианте добавилась еще необходимость на каждый используемый регистр хранить указатель в 4 байта в ОЗУ. В общем не вариант. Смотрим следующий.
Способ 4,5. Экзотерическая обертка с шаблоном
Добавляем крупинку метапрограммирования, но пользы от этого не сильно много. От предыдущего этот способ отличается только тем, что адрес передается не в конструктор, а в параметре шаблона, экономим немного на регистрах при передаче адреса в конструктор, уже хорошо:
template<uint32_t addr>
class Register {
public:
Register() : ptr{reinterpret_cast<volatile uint32_t *>(addr)} {
}
__forceinline inline Register &operator^=(const uint32_t right) {
*ptr ^= right;
return *this;
}
private:
volatile std::uint32_t *ptr;
};
int main() {
using GpioaOdr = Register<GpioaOdrAddr>;
GpioaOdr Odr;
Odr ^= (1 << 5);
using GpioaIdr = Register<GpioaIdrAddr>;
GpioaIdr Idr;
Idr ^= (1 << 5); //ГЛУПОСТЬ
}
А так, те же грабли, вид сбоку.
Способ 5. Разумный
Очевидно, что от указателя надо избавляться, поэтому сделаем тоже самое, но уберем из класса ненужный указатель.
template<uint32_t addr>
class Register {
public:
__forceinline Register &operator^=(const uint32_t right) {
*reinterpret_cast<volatile uint32_t *>(addr) ^= right;
return *this;
}
};
using GpioaOdr = Register<GpioaOdrAddr>;
GpioaOdr Odr;
Odr ^= (1 << 5);
using GpioaIdr = Register<GpioaIdrAddr>;
GpioaIdr Idr;
Idr ^= (1 << 5); //ГЛУПОСТЬ
Можно остановиться здесь и немного порассуждать. Это способ сразу решает 2 проблемы, которые до этого наследовались от первого метода. Во первых, теперь я могу использовать указатель на объект Register
в шаблоне, а во вторых я его могу передавать в constexrp
конструктор.
template<Register * register>
constexpr uint32_t Get() {
return register::Get();
}
Register<GpioaOdrAddr> GpioAOdr;
int main() {
Geta<&GpioaOdr>() ; //Все Ок
}
//и так могу
struct Port {
constexpr Port(Register& ref): port(ref) {}
Register & register ;
}
constexpr Port portA{GpioaOdr};
Конечно, нужно снова, либо обладать эйдетической памятью на регистры, либо определить руками все адреса регистров где-то…
Плюсы
- Простота использования
- Возможность использования метапрограммирования
- Возможность использовать в constexpr конструкторах
Минусы
- Не используется проверенный заголовочный файл от производителя
- Надо самому задавать все адреса регистров
- Нужно создавать объект класс Register
- Можно сделать ГЛУПОСТЬ
Отлично, но минусов все еще много…
Способ 6. Разумнее разумного
В предыдущем методе, чтобы обратиться к регистру необходимо было создать объект этого регистра, это ненужные траты ОЗУ и ПЗУ, поэтому делаем обертку со статическими методами.
template<uint32_t addr>
class Register {
public:
__forceinline inline static void Xor(const uint32_t mask)
{
*reinterpret_cast<volatile uint32_t *>(addr) ^= mask;
}
};
int main() {
using namespace Case6 ;
using Odr = Register<GpioaOdrAddr>;
Odr::Xor(1 << 5);
using Idr = Register<GpioaIdrAddr>;
Idr::Xor(1 << 5); //ГЛУПОСТЬ
}
Добавляется один плюс
- Никаких накладных. Быстрый компактный код, такой же как и в варианте 1 (При использовании в классах обертках, нет дополнительных расходов ОЗУ, так как объект не создается, а используются статические методы без создания объектов)
Идем дальше…
Способ 7. Убираем ГЛУПОСТЬ
Очевидно, я сделал ГЛУПОСТЬ в коде и записал что-то в регистр, который на самом деле для записи не предназначен. Ничего страшного, конечно, но ГЛУПОСТИ надо запрещать. Давайте запретим делать ГЛУПОСТИ. Для этого введем вспомогательные структуры:
struct WriteReg {};
struct ReadReg {};
struct ReadWriteReg: public WriteReg, public ReadReg {};
Теперь мы сможет задавать регистры для записи, и регистры только для чтения:
template<uint32_t addr, typename T>
class Register
{
public:
//Если в параметр шаблона будет передавать тип WriteReg, то метод будет
// инстанциирован, если нет, то такого метода существовать не будет
__forceinline template <typename T1 = T,
class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>>
Register &operator^=(const uint32_t right)
{
*reinterpret_cast<volatile uint32_t *>(addr) ^= right;
return *this;
}
};
Теперь попробуем откомпилировать наш тест и увидим, что тест не компилируется, потому что оператора ^=
для регистра Idr
не существует:
int main() {
using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ;
GpioaOdr Odr ;
Odr ^= (1 << 5) ;
using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ;
GpioaIdr Idr ;
Idr ^= (1 << 5) ; //ошибка, регистра Idr только для чтения
}
Итак, теперь плюсов становится больше…
Плюсы
- Простота использования
- Возможность использования метапрограммирования
- Возможность использовать в constexpr конструкторах
- Быстрый компактный код, такой же как и в варианте 1
- При использовании в классах обертках, нет дополнительных расходов ОЗУ, так как объект не создается, а используются статические методы без создания объектов
- Нельзя сделать ГЛУПОСТЬ
Минусы
- Не используется проверенный заголовочный файл от производителя
- Надо самому задавать все адреса регистров
- Нужно создавать объект класс Register
Что же давайте уберем возможность создавать класс, чтобы еще сэкономить
Способ 8. Без ГЛУПОСТИ и без объекта класса
Сразу код:
struct WriteReg {};
struct ReadReg {};
struct ReadWriteReg: public WriteReg, public ReadReg {};
template<uint32_t addr, typename T>
class Register {
public:
__forceinline template <typename T1 = T,
class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>>
inline static void Xor(const uint32_t mask) {
*reinterpret_cast<volatile int*>(addr) ^= mask;
}
};
int main {
using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ;
GpioaOdr::Xor(1 << 5) ;
using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ;
GpioaOdr::Idr(1 << 5) ; //ошибка, регистра Idr только для чтения
}
Добавляем еще один плюс, объект не создаем. Но идем дальше, у нас еще остались минусы
Способ 9. Способ 8 с объединением в структуру
В предыдущем способе, был определен только регистр. Но в способе 1, все регистры объединены в структуры, чтобы можно было удобно по модулям обращаться к ним. Давайте так и сделаем…
namespace Case9
{
struct WriteReg {};
struct ReadReg {};
struct ReadWriteReg: public WriteReg, public ReadReg {};
template<uint32_t addr, typename T>
class Register
{
public:
__forceinline template <typename T1 = T,
class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>>
inline static void Xor(const uint32_t mask)
{
*reinterpret_cast<volatile int*>(addr) ^= mask;
}
};
template<uint32_t addr>
struct Gpio
{
using Moder = Register<addr, ReadWriteReg>; //надо знать сдвиг регистра в структуре
using Otyper = Register<addr + OtyperShift, ReadWriteReg> ;
using Ospeedr = Register<addr + OspeedrShift,ReadWriteReg> ;
using Pupdr = Register<addr + PupdrShift,ReadWriteReg> ;
using Idr = Register<addr + IdrShift, ReadReg> ;
using Odr = Register<addr + OdrShift, WriteReg> ;
};
int main() {
using Gpioa = Gpio<GPIOA_BASE> ;
Gpioa::Odr::Xor(1 << 5) ;
Gpioa::Idr::Xor((1 << 5) ); //ошибка, регистр Idr только для чтения
}
Здесь минус заключается в том, что структуры надо будет прописывать заново, а смещения всех регистров помнить и определять где-то. Было бы хорошо, если бы смещения задавались компилятором, а не человеком, но это позже, а пока рассмотрим еще один интересный способ, подсказанный моим коллегой.
Способ 10. Обертка над регистром через указатель на член структуры
Здесь используется такое понятие как указатель на член структуры и доступ к ним.
Чтобы узнать смещение регистра относительно начала структуры, можно использовать указатель на член структуры: volatile uint32_t T::*member
, он нам вернет смещение члена структуры относительно её начала в байтах. например есть у нас структура
GPIO_TypeDef
, то &GPIO_TypeDef::ODR
будет равно 0х14. Обыграем эту возможность.
template<uint32_t addr, typename T>
class RegisterStructWrapper {
public:
__forceinline template<typename P>
inline static void Xor(P T::*member, int mask) {
reinterpret_cast<T*>(addr)->*member ^= mask ;
}
} ;
using GpioaWarpper = RegisterStructWrapper<GPIOA_BASE, GPIO_TypeDef> ;
int main() {
GpioaWarpper::Xor(&GPIO_TypeDef::ODR, (1 << 5)) ;
return 0 ;
}
Плюсы
- Простота использования
- Возможность использования метапрограммирования
- Возможность использовать в constexpr конструкторах
- Быстрый компактный код, такой же как и в варианте 1
- При использовании в классах обертках, нет дополнительных расходов ОЗУ, так как объект не создается, а используются статические методы без создания объектов
- Нельзя сделать ГЛУПОСТЬ
- Используется проверенный заголовочный файл от производителя
- Не нужно самому задавать все адреса регистров
- Не нужно создавать объект класс Register
Минусы
- Да особо нет, но можно порассуждать на тему понятности кода
Способ 10.5. Объединяем метод 9 и 10
А теперь вычислим адреса регистров из способа 9, с помощью компилятора:
struct WriteReg {};
struct ReadReg {};
struct ReadWriteReg: public WriteReg, public ReadReg {};
template<uint32_t addr, typename T, volatile uint32_t T::*member, typename RegType>
class Register {
public:
__forceinline template <typename T1 = RegType,
class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>>
inline static void Xor(const uint32_t mask)
{
reinterpret_cast<T*>(addr)->*member ^= mask ;
}
};
template<uint32_t addr, typename T>
struct Gpio
{
using Moder = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, ReadWriteReg>;
using Otyper = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OTYPER, ReadWriteReg>;
using Ospeedr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OSPEEDR, ReadWriteReg>;
using Pupdr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::PUPDR, ReadWriteReg>;
using Idr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::IDR, ReadReg>;
using Odr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, WriteReg>;
} ;
Работать с регистрами можно более экзотерично:
using namespace Case11 ;
using Gpioa = Gpio<GPIOA_BASE, GPIO_TypeDef> ;
Gpioa::Odr::Xor(1 << 5) ;
//Gpioa::Idr::Xor((1 << 5) ); //ошибка, регистр Idr только для чтения
Очевидно, что тут придется все структуры переписать снова. Это можно сделать автоматически, каким-нибудь скриптом на Phyton, на входе что-то типа stm32f411xe.h на выходе ваш файл со структурами для использования в С++.
В любом случае, есть несколько различных способов, которые могут подойти в конкретном проекте.
Бонус. Вводим расширение языка и парсим код с помощью Phyton
Проблема работы с регистрами на С++ существует уже давненько. Люди решают её по разному. Конечно было бы замечательно, если бы язык поддерживал что-то типа переименования классов во время компиляции. Ну скажем, а что если было бы так:
template<classname = [PortName]>
class Gpio[Portname] {
__forceinline inline static void Xor(const uint32_t mask) {
GPIO[PortName]->ODR ^= mask ;
}
};
int main() {
using GpioA = Gpio<"A"> ;
GpioA::Xor(5) ;
}
Но к сожалению такого язык не поддерживает. Поэтому решение которое используют люди, это парсинг кода с помощью Python. Т.е. вводится некоторое расширение языка. Код, с использованием этого расширения, подается на Python парсер, который переводит его в С++ код. Такой код выглядит приблизительно так: (пример взят из modm библиотеки вот тут полные исходники ):
%% set port = gpio["port"] | upper
%% set reg = "GPIO" ~ port
%% set pin = gpio["pin"]
class Gpio{{ port ~ pin }} : public Gpio
{
__forceinline inline static void Xor() {
GPIO{{port}}->ODR ^= 1 << {{pin}} ;
}
}
//С помощью скрипта он преобразуется в следующий код
class GpioС5 : public Gpio
{
__forceinline inline static void Xor() {
GPIOС->ODR ^= 1 << 5 ;
}
}
//А использовать его можно так
using Led = GpioС5;
Led::Xor();
На этом все… мое воображение исчерпалось. Если у вас еще есть идеи, велком. Пример со всеми способами лежит тут
Ссылки
Typesafe Register Access in C++
Making things do stuff -Accessing hardware from C++
Making things do stuff – Part 3
Making things do stuff- Structure overlay
Автор: lamerok