10 ++ ways to work with hardware registers in C ++ (for example, IAR and Cortex M)

    Choosing the safest path
    Fig. I. Kiyko

    Good health to all!

    You probably remember a bearded anecdote, and maybe a true story about how a student was asked about a way to measure the height of a building using a barometer. The student cited, in my opinion, about 20 or 30 ways, without mentioning the direct (through the difference in pressure) that the teacher expected.

    In approximately the same vein, I want to continue discussing the use of C ++ for microcontrollers and consider ways of working with registers using C ++. And I want to note that in order to achieve safe access to the registers there will be no easy way. I will try to show all the pros and cons of the methods. If you know more ways, throw them in the comments. So, let's begin:

    Method 1. Obvious and obviously not the best


    The most common method, which is also used in C ++, is to use the description of register structures from the header file from the manufacturer. For demonstration, I will take two port A registers (ODR - output data register and IDR - input data register) of the STM32F411 microcontroller so that I can do the “embroidery” “Hello world” - blink the LED.

    int main() {
      GPIOA->ODR ^= (1 << 5) ;
      GPIOA->IDR ^= (1 << 5) ; //ГЛУПОСТЬ, но я же не знал
    }

    Let's see what happens here and how this design works. The header for the microprocessor has a structure GPIO_TypeDefand definition of a pointer to this structure GPIOA. It looks like this:

    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)
    

    To put it in simple human words, then the whole structure of the type GPIO_TypeDef"lies" at the address GPIOA_BASE, and when referring to a specific field of the structure, you are essentially referring to the address of this structure + offset to an element of this structure. If removed #define GPIOA, the code would look like this:

    ((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ;
    ((GPIO_TypeDef *) GPIOA_BASE)->IDR ^= (1 << 5) ; //ГЛУПОСТЬ
    

    In relation to the C ++ programming language, an integer address is converted to a pointer to a structure type GPIO_TypeDef. But in C ++, when using C conversion, the compiler tries to perform the conversion in the following sequence:

    • const_cast
    • static_cast
    • static_cast next to const_cast,
    • reinterpret_cast
    • reinterpret_cast next to const_cast

    those. if the compiler could not convert the type using const_cast, it tries to apply static_cast and so on. As a result, the call:

    ((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ;

    there is nothing like:

    reinterpret_cast (GPIOA_BASE)->ODR ^= (1 << 5) ;

    In fact, for C ++ applications, it would be correct to “pull” the structure onto the address like this:

    GPIO_TypeDef * GPIOA{reinterpret_cast(GPIOA_BASE)} ;

    In any case, due to type conversion, there is a big minus to this approach for C ++. It consists in the fact that it reinterpret_castcan not be used neither in constexprdesigners and functions, nor in the template parameters, and this significantly narrows the use of C ++ features for microcontrollers.
    I will explain this with examples. It is possible to do so:

     struct Test {
      const int a;
      const int b;
    } ;
    template
    constexpr const int Geta() {
      return mystruct->a;
    }
    Test test{1,2};
    int main() {
      Geta<&test>() ;
    }
    

    But you can’t do this already:

     
    template
    constexpr volatile uint32_t GetIdr() {
      return mystruct->IDR;
    }
    int main() {
    //GPIOA это  reinterpret_cast (GPIOA_BASE) 
    //использует преобразование типов, и в параметры шаблона его передавать нельзя
      GetIdr() ; //Ошибка
    }
    // И вот так тоже сделать нельзя:
    struct Port {
      constexpr Port(GPIO_TypeDef * ptr): port(*ptr) {} 
      GPIO_TypeDef & port ;
    }
    //Так как GPIOA использует reinterpret_cast, то конструктор 
    //перестает быть constexpr и невозможно выполнить статическую инициализацию
    constexpr Port portA{GPIOA}; // тут будет ошибка
    

    Thus, the direct use of this approach imposes significant restrictions on the use of C ++. We will not be able to locate the object that wants to use the pointer to GPIOAin ROM, using the means of the language, and we will not be able to take advantage of metaprogramming for such an object.
    In addition, in general, this method is not safety (as our Western partners say). After all, it is quite possible to make some NON-FUNNESS.
    In connection with the above, we summarize:

    pros


    • The heading from the manufacturer is used (it is checked, it has no errors)
    • There are no additional gestures and costs, you take and use
    • Ease of use
    • Everyone knows and understands this method.
    • No overhead

    Minuses


    • Limited use of metaprogramming
    • Inability to use in constexpr constructors
    • When using wrappers in classes, the additional consumption of RAM is a pointer to an object of this structure
    • You can make stupid
    Now let's look at method number 2

    Method 2. Brutal


    It is obvious that every embed programmer keeps in mind the addresses of all registers for all microcontrollers, so you can simply always use the following method, which follows from the first:

    *reinterpret_cast(GpioaOdrAddr) ^= (1 <<5) ;
    *reinterpret_cast(GpioaIdrAddr) ^= (1 <<5) ; //ГЛУПОСТЬ

    Anywhere in the program, you can always call the conversion to the volatile uint32_tregister address and install at least something there.
    There are especially no pluses here, but to those minuses that there is added inconvenience to use and the need to write the address of each register in a separate file yourself. Therefore, we turn to the method number 3.

    Method 3. Obvious and obviously more correct


    If access to the registers occurs through the structure field, then instead of a pointer to the structure object, you can use the integer structure address. The address of the structures is in the header file from the manufacturer (for example, GPIOA_BASE for GPIOA), so you do not need to remember it, but you can use it in templates and in constexpr expressions, and then "overlay" the structure to this address.

    template
      struct Pin {   
          using Registers = GPIO_TypeDef ;
          __forceinline static void Toggle() {
            // располагаем структуру по адресу addr
            Registers *GpioPort{reinterpret_cast(addr)}; 
            GpioPort->ODR ^= (1 << pinNum) ;
          }
      };
    int main() {
      using Led1 =  Pin ;
      Led1::Toggle() ;
    }
    

    There are no special minuses, from my point of view. In principle, a working option. But still, let's take a look at other ways.

    Method 4. Exoteric Wrap


    For connoisseurs of understandable code, you can make a wrapper over the register so that it is convenient to access them and looks “beautiful”, make a constructor, redefine operators:

    class Register  {
        public:
          explicit Register(uint32_t addr) : ptr{ reinterpret_cast(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); //ГЛУПОСТЬ
    }
    

    As you can see, again you will either have to remember the integer addresses of all the registers, or set them somewhere, and you will also have to store a pointer to the register address. But again, what’s not very good, again in the constructor, there are reinterpret_cast
    some minuses, and to the fact that in the first and second variant there is still a need for each register used to store a pointer of 4 bytes in RAM. In general, not an option. We look at the following.

    Method 4,5. Exoteric Wrap with Pattern


    We add a grain of metaprogramming, but there is not much benefit from this. This method differs from the previous one only in that the address is transferred not to the constructor, but in the template parameter, we save a little on registers when passing the address to the constructor, it’s already good:

    template
      class Register  {
        public:
          Register() : ptr{reinterpret_cast(addr)}  {
          }
          __forceinline inline Register &operator^=(const uint32_t right)  {
            *ptr ^= right;
            return *this;
          }
        private:
          volatile std::uint32_t *ptr;
      };
    int main() {
        using GpioaOdr = Register;
        GpioaOdr Odr;
        Odr ^= (1 << 5);
        using GpioaIdr = Register;
        GpioaIdr Idr;
        Idr ^= (1 << 5); //ГЛУПОСТЬ
    }
    

    And so, the same rake, side view.

    Method 5. Reasonable


    Obviously, you need to get rid of the pointer, so let's do the same, but remove the unnecessary pointer from the class.

    template
      class Register  {
        public:
          __forceinline  Register &operator^=(const uint32_t right)   {
            *reinterpret_cast(addr) ^= right;
            return *this;
          }
      };
       using GpioaOdr = Register;
        GpioaOdr Odr;
        Odr ^= (1 << 5);
        using GpioaIdr = Register;
        GpioaIdr Idr;
        Idr ^= (1 << 5); //ГЛУПОСТЬ
    

    You can stay here and think a little. This method immediately solves 2 problems that were previously inherited from the first method. Firstly, now I can use a pointer to an object Registerin the template, and secondly, I can pass it to the constexrpconstructor.

    template
    void Xor(uint32_t mask) {
      *register ^= mask ;
    }
    Register  GpioaOdr;
    int main() {
      Xor<&GpioaOdr>(1 << 5) ; //Все Ок
    }
    //и так могу
    struct Port {
      constexpr Port(Register& ref): register(ref) {} 
      Register & register ;
    }
    constexpr Port portA{GpioaOdr}; 
    

    Of course, it is necessary again, either to have eidetic memory for the addresses of the registers, or to manually determine all the addresses of the registers somewhere in a separate file ...

    pros


    • Ease of use
    • Ability to use metaprogramming
    • Ability to use in constexpr constructors

    Minuses


    • The verified header file from the manufacturer is not used
    • You must set all the addresses of the registers yourself
    • You need to create an object of class Register
    • You can make stupid

    Great, but there are still a lot of minuses ...

    Method 6. Smarter than reasonable


    In the previous method, in order to access the register it was necessary to create an object of this register, this is an unnecessary waste of RAM and ROM, so we do a wrapper with static methods.

    template
      class Register  {
        public:
          __forceinline  inline static void Xor(const uint32_t mask)
          {
            *reinterpret_cast(addr) ^= mask;
          }
      };
    int main() {
        using namespace Case6 ;
        using Odr = Register;
        Odr::Xor(1 << 5);
        using Idr = Register;
        Idr::Xor(1 << 5); //ГЛУПОСТЬ
    }
    

    One plus added
    • No overhead. Fast compact code, the same as in option 1 (When using wrappers in classes, there is no additional RAM cost, since the object is not created, but static methods are used without creating objects)
    Move on…

    Method 7. Remove stupidity


    Obviously, I am constantly doing NON-FUNNY in the code and writing something into the register, which is actually not intended for writing. It's okay, of course, but STUPIDness must be prohibited. Let’s forbid to do nonsense. To do this, we introduce auxiliary structures:

      struct WriteReg {};
      struct ReadReg {};
      struct ReadWriteReg: public WriteReg, public ReadReg {};

    Now we can set the registers for writing, and the registers are read-only:

    template
      class Register 
      {
        public:
         //Если в параметр шаблона будет передавать тип WriteReg, то метод будет
        // инстанциирован, если нет, то такого метода существовать не будет 
          __forceinline template ::value>>
          Register &operator^=(const uint32_t right)
          {
            *reinterpret_cast(addr) ^= right;
            return *this;
          }
      };
    

    Now let's try to compile our test and see that the test does not compile, because the ^=register operator Idrdoes not exist:

       int main()  {
        using GpioaOdr  = Register ;
        GpioaOdr Odr ;
        Odr ^= (1 << 5) ;
        using GpioaIdr  = Register ;
        GpioaIdr Idr ;
        Idr ^= (1 << 5) ; //ошибка, регистр Idr только для чтения
      }

    So, now there are more pluses ...

    pros


    • Ease of use
    • Ability to use metaprogramming
    • Ability to use in constexpr constructors
    • Fast compact code, the same as in option 1
    • When using wrappers in classes, there is no additional RAM cost, since the object is not created, but static methods are used without creating objects
    • You can not do stupidity

    Minuses


    • The verified header file from the manufacturer is not used
    • You must set all the addresses of the registers yourself
    • You need to create an object of class Register

    So let's remove the opportunity to create a class to save more

    Method 8. Without NONSENSE and without a class object


    Immediately code:

      struct WriteReg {};
      struct ReadReg {};
      struct ReadWriteReg: public WriteReg, public ReadReg {};
      template
      class Register  {
          public:
          __forceinline template ::value>>
            inline static void Xor(const uint32_t mask)  {
              *reinterpret_cast(addr) ^=  mask;
            }
        };
      int main {
        using GpioaOdr  = Register ;
        GpioaOdr::Xor(1 << 5) ;
        using GpioaIdr  = Register ;
        GpioaIdr::Xor(1 << 5) ; //ошибка, регистр Idr только для чтения
      }
    

    We add one more plus, we do not create an object. But move on, we still have cons

    Method 9. Method 8 with structure integration


    In the previous method, only case was defined. But in method 1, all registers are combined into structures so that you can conveniently access them by modules. Let's do it ...

    namespace Case9
    {
      struct WriteReg {};
      struct ReadReg {};
      struct ReadWriteReg: public WriteReg, public ReadReg {};
      template
      class Register
        {
          public:
          __forceinline template ::value>>
            inline static void Xor(const uint32_t mask)
            {
              *reinterpret_cast(addr) ^=  mask;
            }
        };
      template
      struct Gpio  
      {
        using Moder = Register; //надо знать сдвиг регистра в структуре
        using Otyper = Register ;
        using Ospeedr = Register ;
        using Pupdr = Register ;
        using Idr = Register ;
        using Odr = Register ;
      };
    int main() {
        using Gpioa = Gpio ;
        Gpioa::Odr::Xor(1 << 5) ;
        Gpioa::Idr::Xor((1 << 5) ); //ошибка,  регистр Idr только для чтения
      }
    

    Here the minus is that the structures will need to be registered anew, and the offsets of all the registers should be remembered and determined somewhere. It would be nice if the offsets were set by the compiler, and not by the person, but this is later, but for now we will consider another interesting method suggested by my colleague.

    Method 10. Wrap over the register through a pointer to a member of the structure


    Here we use such a concept as a pointer to a member of the structure and access to them .

    template
    class RegisterStructWrapper {
    public:
      __forceinline  template
       inline static void Xor(P T::*member, int mask) {
        reinterpret_cast(addr)->*member ^= mask ; //Обращаемся к члену структуры, который передали в параметре шаблона. 
      }  
    } ;
    using GpioaWrapper = RegisterStructWrapper ;
    int main() {
      GpioaWrapper::Xor(&GPIO_TypeDef::ODR, (1 << 5)) ;
      GpioaWrapper::Xor(&GPIO_TypeDef::IDR, (1 << 5)) ;  //ГЛУПОСТЬ
      return 0 ;
    }
    

    pros


    • Ease of use
    • Ability to use metaprogramming
    • Возможность использовать в constexpr конструкторах
    • Быстрый компактный код, такой же как и в варианте 1
    • При использовании в классах обертках, нет дополнительных расходов ОЗУ, так как объект не создается, а используются статические методы без создания объектов
    • Используется проверенный заголовочный файл от производителя
    • Не нужно самому задавать все адреса регистров
    • Не нужно создавать объект класс Register

    Минусы


    • Можно сделать ГЛУПОСТЬ и еще порассуждать на тему понятности кода

    Способ 10.5. Объединяем метод 9 и 10


    To find out the shift of the register relative to the beginning of the structure, you can use the pointer to the member of the structure: volatile uint32_t T::*memberit will return the offset of the member of the structure relative to its beginning in bytes. For example, we have a structure GPIO_TypeDef, then the address &GPIO_TypeDef::ODRwill be 0x14.
    We beat this opportunity and calculate the addresses of the registers from method 9, using the compiler:

    struct WriteReg {};
      struct ReadReg {};
      struct ReadWriteReg: public WriteReg, public ReadReg {};
      template
      class Register {
        public:
          __forceinline template ::value>>
          inline static void Xor(const uint32_t mask)
          {
            reinterpret_cast(addr)->*member ^= mask ;
          }
      };
      template
      struct Gpio
      {
        using Moder = Register;
        using Otyper = Register;
        using Ospeedr = Register;
        using Pupdr = Register;
        using Idr = Register;
        using Odr = Register;
      } ;

    You can work with registers more exoterically:

    using namespace Case11 ;
        using Gpioa = Gpio ;
        Gpioa::Odr::Xor(1 << 5) ;
        //Gpioa::Idr::Xor((1 << 5) ); //ошибка,  регистр Idr только для чтения

    Obviously, here all the structures will have to be rewritten again. This can be done automatically, by some script in Phyton, at the input something like stm32f411xe.h at the output of your file with structures for use in C ++.
    In any case, there are several different ways that may work in a particular project.

    Bonus We introduce the language extension and parsim code using Phyton


    The problem of working with registers in C ++ has been around for quite some time. People solve it in different ways. Of course it would be great if the language supported something like renaming classes at compile time. Well, let's say, what if it were like this:

    template
    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) ;
    }
    

    But unfortunately this language does not support. Therefore, the solution people use is parsing code using Python. Those. some language extension is introduced. The code, using this extension, is fed to the Python parser, which translates it into C ++ code. Such code looks something like this: (an example is taken from the modm library; here are the full sources ):

    %% 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();
    


    Update: Bonus. SVD files and parser on Phyton


    Forgot to add another option. ARM releases a register description file for each SVD manufacturer. From which you can then generate a C ++ file with a description of the registers. Paul Osborne has compiled all of these files on GitHub . He also wrote a Python script to parse them.

    That's all ... my imagination is exhausted. If you still have ideas, feel free to. An example with all the methods lies here.

    References


    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

    Also popular now: