10++ способов работать с аппаратными регистрами на С++ (на примере IAR и Cortex M)

от автора

Choosing the safest path
Рис. И. Кийко

Всем доброго здравия!

Помните наверное бородатый анекдот, а может быть и правдивую историю про то, как студента спрашивали о способе измерить высоту здания с помощью барометра. Студент привел, по-моему около 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *