📜 ⬆️ ⬇️

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

Choosing the safest path
Fig. I. Kiyko

All good health!

Remember, perhaps, a bearded anecdote, and perhaps a true story about how a student was asked about how to measure the height of a building using a barometer. The student led, in my opinion, in about 20 or 30 ways, without mentioning the direct (through the pressure difference) that the teacher expected.
')
Approximately in the same vein, I want to continue the discussion of using C ++ for microcontrollers and consider ways to work with registers using C ++. And I want to note that there will be no easy way to achieve safe access to registers. I will try to show all the pros and cons of ways. 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 the two registers of port A (ODR - output data register and IDR - input data register) of the STM32F411 microcontroller, so that you can execute the Hello world - blink the LED.

int main() { GPIOA->ODR ^= (1 << 5) ; GPIOA->IDR ^= (1 << 5) ; //,      } 

Let's see what is happening here and how this design works. In the header for the microprocessor, there is a GPIO_TypeDef structure and the definition of a pointer to this GPIOA structure. 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 entire structure of the type GPIO_TypeDef “falls” 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 you remove #define GPIOA , the code would look like this:

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

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


those. if the compiler was unable to convert the type using const_cast, it tries to use static_cast and so on. As a result, the challenge:

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

there is nothing like:

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

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

 GPIO_TypeDef * GPIOA{reinterpret_cast<GPIO_TypeDef *>(GPIOA_BASE)} ; 

In any case, due to type conversion, there is a big disadvantage of this approach for C ++. It constexpr in the fact that reinterpret_cast can not be used in constexpr constructors and functions, nor in the template parameters, and this significantly reduces the use of C ++ features for microcontrollers.
Let me explain this with examples. It is possible to do this:

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

But this is already impossible to do:

 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}; //    

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 GPIOA in ROM using the language tools, 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 possible to do some stupidity.
In connection with the foregoing, we summarize:

pros


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

Minuses


  • Limited use of metaprogramming
  • Impossibility to use in constexpr constructors
  • When used in class wrappers, additional RAM consumption, on a pointer to an object of this structure
  • Can do stupidity
Now look at the method number 2

Method 2. Brutal


Obviously, each programmer keeps in mind the addresses of all registers for all microcontrollers, so you can always just use the following method, which follows from the first:

 *reinterpret_cast<volatile uint32_t *>(GpioaOdrAddr) ^= (1 <<5) ; *reinterpret_cast<volatile uint32_t *>(GpioaIdrAddr) ^= (1 <<5) ; // 

In any place of the program, you can always call the conversion to the volatile uint32_t address of the register and install anything there.
There are no advantages here, but to those minuses that there will be added the inconvenience of use and the need to prescribe the address of each register in a separate file. Therefore, we turn to 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 address of the structure. The address of the structures is in the header file from the manufacturer (for example, GPIOA_BASE for GPIOA), so it is not necessary to remember it, but you can apply it both in templates and in constexpr expressions, and then “overlay” the structure to this address.

 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 ^= (1 << pinNum) ; } }; int main() { using Led1 = Pin<GPIOA_BASE, 5> ; Led1::Toggle() ; } 

There are no particular disadvantages, from my point of view. In principle, the working version. But still, let's look at other ways.

Method 4. Exoteric wrapper


For lovers of understandable code, you can make a wrapper over the register, so that you can access them conveniently and look “beautiful”, make a constructor, redefine operators:

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

As you can see, again you will have to either remember the integer addresses of all registers, or set them somewhere, and still have to store a pointer to the address of the register. But again, not so much, reinterpret_cast happens again in the constructor
There are some drawbacks, and the fact that in the first and second versions there was added the need for each register used to store a pointer of 4 bytes in RAM. In general, not an option. Look next.

Method 4.5. Exoteric Template Wrapper


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 not transferred to the constructor, but in the template parameter, we save a little on the registers when passing the address to the constructor, this is already good:

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

And so, the same rake, side view.

Method 5. Reasonable


Obviously, we need to get rid of the pointer, so we will do the same, but remove the unnecessary pointer from the class.

 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); // 

You can stop here and talk a little. This method immediately solves 2 problems that were inherited from the first method. First, now I can use a pointer to a Register object in the template, and secondly, I can pass it on to the constexrp constructor.

 template<Register * register> void Xor(uint32_t mask) { *register ^= mask ; } Register<GpioaOdrAddr> 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 to the addresses of registers, or to determine by hands all the addresses of registers somewhere in a separate file ...

pros


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

Minuses


  • Not using a valid header file from the manufacturer
  • It is necessary to set all the addresses of registers
  • You need to create an object class Register
  • Can do stupidity

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, these are unnecessary waste of RAM and ROM, so we make a wrapper with static methods.

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

One plus is 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)
Go ahead…

Method 7. Remove stupidity


Obviously, I constantly do STUPID in the code and write something into a register that is not really intended for writing. It's okay, of course, but stupidity should be prohibited. Let's ban doing stupid. 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<uint32_t addr, typename RegisterType> class Register { public: //       WriteReg,    // ,  ,       __forceinline template <typename T = RegisterType, class = typename std::enable_if_t<std::is_base_of<WriteReg, T>::value>> Register &operator^=(const uint32_t right) { *reinterpret_cast<volatile uint32_t *>(addr) ^= right; return *this; } }; 

Now we will try to compile our test and see that the test is not compiled, because the ^= operator for the Idr register Idr not exist:

  int main() { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr Odr ; Odr ^= (1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr Idr ; Idr ^= (1 << 5) ; //,  Idr    } 

So, now there are more advantages ...

pros


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

Minuses


  • Not using a valid header file from the manufacturer
  • It is necessary to set all the addresses of registers
  • You need to create an object class Register

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

Method 8. Without stupidity and without class object


Immediately code:

  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> ; GpioaIdr::Xor(1 << 5) ; //,  Idr    } 

We add one more plus, we do not create the object. But we go further, we still have cons

Method 9. Method 8 with integration into the structure


In the previous method, only the register was defined. But in method 1, all the registers are combined into structures so that it is convenient for the modules to access them. Let's do it ...

 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    } 

Here, the minus is that the structure will need to be re-written, and the offset of all the registers to remember and define somewhere. It would be nice if the bias were asked by the compiler, not by the person, but this is later, but for now let's consider another interesting way suggested by my colleague.

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


It uses such a thing as a pointer to a member of the structure and access to them .

 template<uint32_t addr, typename T> class RegisterStructWrapper { public: __forceinline template<typename P> inline static void Xor(PT::*member, int mask) { reinterpret_cast<T*>(addr)->*member ^= mask ; //   ,     . } } ; using GpioaWrapper = RegisterStructWrapper<GPIOA_BASE, GPIO_TypeDef> ; 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
  • Ability to use in constexpr constructors
  • Fast compact code, the same as in option 1
  • When used in class wrappers, there is no additional RAM cost, since the object is not created, but static methods are used without creating objects.
  • Used verified header file from manufacturer
  • No need to set all the addresses of registers
  • No need to create an object class Register

Minuses


  • You can make stupidity and even speculate on the clarity of the code

Method 10.5. Combining method 9 and 10


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

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

You can work with registers more exoterically:

 using namespace Case11 ; using Gpioa = Gpio<GPIOA_BASE, GPIO_TypeDef> ; Gpioa::Odr::Xor(1 << 5) ; //Gpioa::Idr::Xor((1 << 5) ); //,  Idr    

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

Bonus Enter language extension and parsim code using Phyton


The problem of working with registers in C ++ has been around for quite a while. People decide it differently. 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<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) ; } 

But unfortunately this language does not support. Therefore, the solution that people use is parsing the 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 like this: (example is taken from the modm library here is the complete source code ):

 %% 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 Gpio5 : public Gpio { __forceinline inline static void Xor() { GPIO->ODR ^= 1 << 5 ; } } //     using Led = Gpio5; 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 collected 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, well. An example with all the ways is here.

Links


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

Source: https://habr.com/ru/post/459204/


All Articles