10 ++ formas de trabajar con registros de hardware en C ++ (por ejemplo, IAR y Cortex M)

Elegir el camino más seguro
Fig. I. Kiyko

Buena salud a todos!

Probablemente recuerde una anécdota barbuda, y tal vez una historia real sobre cómo se le preguntó a un estudiante sobre una forma de medir la altura de un edificio usando un barómetro. El estudiante citó, en mi opinión, alrededor de 20 o 30 formas, sin mencionar el directo (a través de la diferencia de presión) que el maestro esperaba.

Aproximadamente en el mismo sentido, quiero seguir discutiendo el uso de C ++ para microcontroladores y considerar formas de trabajar con registros que usan C ++. Y quiero señalar que para lograr un acceso seguro a los registros no habrá una manera fácil. Trataré de mostrar todos los pros y los contras de los métodos. Si conoce más formas, tírelas en los comentarios. Entonces comencemos:

Método 1. Obvio y obviamente no es el mejor.


El método más común, que también se usa en C ++, es usar la descripción de las estructuras de registro del archivo de encabezado del fabricante. Para la demostración, tomaré dos registros de puerto A (ODR - registro de datos de salida e IDR - registro de datos de entrada) del microcontrolador STM32F411 para que pueda hacer el "bordado" "Hola mundo" - parpadee el LED.

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

Veamos qué sucede aquí y cómo funciona este diseño. El encabezado del microprocesador contiene la estructura GPIO_TypeDef y una definición de puntero a esta estructura GPIOA . Se ve así:

 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) 

Para ponerlo en palabras humanas simples, entonces toda la estructura del tipo GPIO_TypeDef "se establece" en la dirección GPIOA_BASE , y cuando se refiere a un campo específico de la estructura, se refiere esencialmente a la dirección de esta estructura + desplazamiento a un elemento de esta estructura. Si elimina #define GPIOA , el código se vería así:

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

En relación con el lenguaje de programación C ++, una dirección entera se convierte en un tipo de puntero a la estructura GPIO_TypeDef . Pero en C ++, cuando se usa la conversión C, el compilador intenta realizar la conversión en la siguiente secuencia:

  • const_cast
  • static_cast
  • static_cast junto a const_cast,
  • reinterpret_cast
  • reinterpret_cast junto a const_cast

es decir si el compilador no pudo convertir el tipo usando const_cast, intenta aplicar static_cast y así sucesivamente. Como resultado, la llamada:

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

no hay nada como:

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

De hecho, para aplicaciones C ++, sería correcto "tirar" la estructura a la dirección de esta manera:

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

En cualquier caso, debido a la conversión de tipo, hay un gran inconveniente en este enfoque para C ++. Consiste en el hecho de que reinterpret_cast no puede usarse ni en constructores y funciones constexpr , ni en parámetros de plantilla, y esto reduce significativamente el uso de características C ++ para microcontroladores.
Explicaré esto con ejemplos. Es posible hacerlo:

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

Pero no puedes hacer esto ya:

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

Por lo tanto, el uso directo de este enfoque impone restricciones significativas en el uso de C ++. No podremos ubicar el objeto que quiere usar el GPIOA en ROM usando las herramientas de lenguaje, y no podremos aprovechar la metaprogramación para dicho objeto.
Además, en general, este método no es seguro (como dicen nuestros socios occidentales). Después de todo, es bastante posible hacer algo NO DIVERTIDO
En relación con lo anterior, resumimos:

Pros


  • Se utiliza el encabezado del fabricante (está marcado, no tiene errores)
  • No hay gestos y costos adicionales, usted toma y usa
  • Facilidad de uso
  • Todos conocen y entienden este método.
  • Sin gastos generales

Contras


  • Uso limitado de metaprogramación
  • Incapacidad de uso en constructores constexpr
  • Cuando se usan envoltorios en clases, el consumo adicional de RAM es un puntero a un objeto de esta estructura
  • Puedes hacer estupido
Ahora veamos el método número 2

Método 2. Brutal


Es obvio que cada programador de inserción tiene en cuenta las direcciones de todos los registros para todos los microcontroladores, por lo que simplemente puede usar siempre el siguiente método, que se deduce del primero:

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

En cualquier parte del programa, siempre puede llamar la conversión a la dirección de registro volatile uint32_t e instalar al menos algo allí.
Aquí especialmente no hay ventajas, pero a esos inconvenientes se les agrega un inconveniente y la necesidad de escribir la dirección de cada registro en un archivo separado. Por lo tanto, pasamos al método número 3.

Método 3. Obvio y obviamente más correcto.


Si el acceso a los registros se produce a través del campo de estructura, en lugar de un puntero al objeto de estructura, puede usar la dirección de estructura entera. La dirección de las estructuras está en el archivo de encabezado del fabricante (por ejemplo, GPIOA_BASE para GPIOA), por lo que no necesita recordarlo, pero puede usarlo en plantillas y en expresiones constexpr, y luego "superponer" la estructura a esta dirección.

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

No hay inconvenientes especiales, desde mi punto de vista. En principio, una opción de trabajo. Pero aún así, echemos un vistazo a otras formas.

Método 4. Envoltura exotérica


Para los conocedores del código comprensible, puede hacer una envoltura sobre el registro para que sea conveniente acceder a ellos y se vea "hermoso", hacer un constructor, redefinir operadores:

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

Como puede ver, nuevamente tendrá que recordar las direcciones enteras de todos los registros, o establecerlas en algún lugar, y también tendrá que almacenar un puntero en la dirección del registro. Pero lo que no es muy bueno nuevamente, reinterpret_cast ocurre nuevamente en el constructor
Algunas desventajas, y al hecho de que en la primera y segunda versión se agregó la necesidad de que cada registro utilizado almacene un puntero a 4 bytes en RAM. En general, no es una opción. Nos fijamos en lo siguiente.

Método 4,5. Envoltura exotérica con patrón


Agregamos un grano de metaprogramación, pero no hay mucho beneficio de esto. Este método difiere del anterior solo en que la dirección se transfiere no al constructor, sino que en el parámetro de plantilla, ahorramos un poco en los registros al pasar la dirección al constructor, ya es bueno:

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

Y así, el mismo rastrillo, vista lateral.

Método 5. Razonable


Obviamente, debe deshacerse del puntero, así que hagamos lo mismo, pero eliminemos el puntero innecesario de la clase.

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

Puedes quedarte aquí y pensar un poco. Este método resuelve inmediatamente 2 problemas que se heredaron previamente del primer método. En primer lugar, ahora puedo usar el puntero al objeto Register en la plantilla y, en segundo lugar, puedo pasarlo al constructor constexrp .

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

Por supuesto, necesita nuevamente, ya sea tener memoria eidética para las direcciones de los registros, o determinar manualmente todas las direcciones de los registros en algún lugar en un archivo separado ...

Pros


  • Facilidad de uso
  • Posibilidad de usar metaprogramación
  • Capacidad de uso en constructores constexpr

Contras


  • El archivo de encabezado verificado del fabricante no se utiliza
  • Debe configurar todas las direcciones de los registros usted mismo
  • Necesita crear un objeto de clase Registrarse
  • Puedes hacer estupido

Genial, pero todavía hay muchos inconvenientes ...

Método 6. Más inteligente que razonable


En el método anterior, para acceder al registro era necesario crear un objeto de este registro, esto es un desperdicio innecesario de RAM y ROM, por lo que hacemos un contenedor con métodos estáticos.

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

Uno más agregado
  • Sin gastos generales. Código compacto rápido, igual que en la opción 1 (cuando se usan contenedores en clases, no hay costo adicional de RAM, ya que el objeto no se crea, pero los métodos estáticos se usan sin crear objetos)
Adelante ...

Método 7. Eliminar la estupidez


Obviamente, estoy constantemente haciendo NO DIVERTIDO en el código y escribiendo algo en el registro, que en realidad no está destinado a escribir. Está bien, por supuesto, pero la ESTUPIDEZ debe estar prohibida. Vamos a prohibir hacer tonterías. Para hacer esto, presentamos estructuras auxiliares:

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

Ahora podemos establecer los registros para escribir, y los registros son de solo lectura:

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

Ahora intentemos compilar nuestra prueba y ver que la prueba no se compila, porque el operador ^= para el registro Idr no existe:

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

Entonces, ahora hay más ventajas ...

Pros


  • Facilidad de uso
  • Posibilidad de usar metaprogramación
  • Capacidad de uso en constructores constexpr
  • Código compacto rápido, igual que en la opción 1
  • Cuando se usan envoltorios en clases, no hay costo de RAM adicional, ya que el objeto no se crea, pero los métodos estáticos se usan sin crear objetos
  • No puedes hacer estupidez

Contras


  • El archivo de encabezado verificado del fabricante no se utiliza
  • Debe configurar todas las direcciones de los registros usted mismo
  • Necesita crear un objeto de clase Registrarse

Entonces, eliminemos la oportunidad de crear una clase para ahorrar más

Método 8. Sin NENSENSE y sin un objeto de clase


Codificar de inmediato:

  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    } 

Agregamos una ventaja más, no creamos un objeto. Pero sigue adelante, todavía tenemos contras

Método 9. Método 8 con integración de estructuras


En el método anterior, solo se definió el caso. Pero en el método 1, todos los registros se combinan en estructuras para que pueda acceder cómodamente a ellos por módulos. Hagámoslo ...

 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    } 

Aquí el punto negativo es que las estructuras deberán registrarse nuevamente, y los desplazamientos de todos los registros deben recordarse y determinarse en alguna parte. Sería bueno que el compilador estableciera las compensaciones, y no la persona, pero esto es más tarde, pero por ahora consideraremos otro método interesante sugerido por mi colega.

Método 10. Envuelva el registro a través de un puntero a un miembro de la estructura.


Utiliza dicho concepto como puntero a un miembro de la estructura y acceso a ellos .

 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


  • Facilidad de uso
  • Posibilidad de usar metaprogramación
  • Capacidad de uso en constructores constexpr
  • Código compacto rápido, igual que en la opción 1
  • Cuando se usan envoltorios en clases, no hay costo de RAM adicional, ya que el objeto no se crea, pero los métodos estáticos se usan sin crear objetos
  • Se utiliza el archivo de encabezado verificado del fabricante.
  • No es necesario configurar todas las direcciones de registro usted mismo
  • No es necesario crear un objeto de clase Registrarse

Contras


  • Puede hacer tonterías e incluso especular sobre la comprensión del código.

Método 10.5. Combina los métodos 9 y 10


Para averiguar el desplazamiento del registro en relación con el comienzo de la estructura, puede usar el puntero al miembro de la estructura: volatile uint32_t T::*member , devolverá el desplazamiento del miembro de la estructura en relación con su comienzo en bytes. Por ejemplo, tenemos la estructura GPIO_TypeDef , entonces la dirección &GPIO_TypeDef::ODR será 0x14.
Aprovechamos esta oportunidad y calculamos las direcciones de los registros a partir del método 9, utilizando el compilador:

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

Puede trabajar con registros más exotéricamente:

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

Obviamente, aquí todas las estructuras tendrán que ser reescritas nuevamente. Esto puede hacerse automáticamente, mediante algún script en Phyton, en la entrada algo así como stm32f411xe.h en la salida de su archivo con estructuras para usar en C ++.
En cualquier caso, hay varias formas diferentes que pueden funcionar en un proyecto en particular.

Bono Introducimos la extensión de idioma y el código de parsim usando Phyton


El problema de trabajar con registros en C ++ ha existido desde hace bastante tiempo. La gente lo resuelve de diferentes maneras. Por supuesto, sería genial si el lenguaje admitiera algo como renombrar clases en tiempo de compilación. Bueno, digamos, ¿y si fuera así?

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

Pero desafortunadamente este lenguaje no es compatible. Por lo tanto, la solución que usa la gente es analizar el código usando Python. Es decir Se introduce alguna extensión de idioma. El código, usando esta extensión, se alimenta al analizador Python, que lo traduce a código C ++. Dicho código se parece a esto: (se toma un ejemplo de la biblioteca de modm; aquí están las fuentes completas ):

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


Actualización: Bono. Archivos SVD y analizador en Phyton


Olvidé agregar otra opción. ARM lanza un archivo de descripción de registro para cada fabricante de SVD. Desde el cual puede generar un archivo C ++ con una descripción de los registros. Paul Osborne ha compilado todos estos archivos en GitHub . También escribió un script de Python para analizarlos.

Eso es todo ... mi imaginación está agotada. Si todavía tiene ideas, siéntase libre de hacerlo. Un ejemplo con todos los métodos se encuentra aquí.

Referencias


Typesafe Register Access en C ++
Hacer que las cosas hagan cosas: acceder al hardware desde C ++
Hacer que las cosas hagan cosas - Parte 3
Hacer que las cosas hagan cosas- Superposición de estructura

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


All Articles