
Рис. И. Кийко
Всем доброго здравия!
Помните наверное бородатый анекдот, а может быть и правдивую историю про то, как студента спрашивали о способе измерить высоту здания с помощью барометра. Студент привел, по-моему около 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
ссылка на оригинал статьи https://habr.com/ru/post/459204/
Добавить комментарий