Cómo parpadear 4 LED en CortexM usando C ++ 17, tupla y un poco de fantasía

Buena salud a todos!

Cuando enseño a los estudiantes a desarrollar software embebido para microcontroladores en la universidad, uso C ++ y, a veces, doy a los estudiantes que están especialmente interesados ​​en todo tipo de tareas para identificar a los estudiantes dotados que están especialmente enfermos .

Una vez más, a estos estudiantes se les dio la tarea de parpadear 4 LED usando el lenguaje C ++ 17 y la biblioteca estándar de C ++, sin conectar bibliotecas adicionales, como CMSIS y sus archivos de encabezado con una descripción de las estructuras de registro, etc. El ganador con el código gana en ROM será el tamaño más pequeño y la RAM menos gastada. La optimización del compilador no debe ser superior a Media. Compilador IAR 8.40.1.
El ganador va a Canarias y obtiene 5 para el examen.

Yo tampoco he resuelto este problema antes, así que les diré cómo lo resolvieron los estudiantes y qué me pasó. Le advierto de inmediato que es poco probable que dicho código pueda usarse en aplicaciones reales, por eso publiqué la publicación en la sección "Programación anormal", aunque quién sabe.

Condiciones de la tarea


Hay 4 LED en los puertos GPIOA.5, GPIOC.5, GPIOC.8, GPIOC.9. Necesitan pestañear. Para tener algo para comparar, tomamos el código escrito en C:

void delay() { for (int i = 0; i < 1000000; ++i){ } } int main() { for(;;) { GPIOA->ODR ^= (1 << 5); GPIOC->ODR ^= (1 << 5); GPIOC->ODR ^= (1 << 8); GPIOC->ODR ^= (1 << 9); delay(); } return 0 ; } 

La función delay() aquí es puramente formal, un ciclo regular, no se puede optimizar.
Se supone que los puertos ya están configurados para la salida y se les aplica el reloj.
También diré de inmediato que el bitbanging no se usó para hacer que el código sea portátil.

Este código toma 8 bytes en la pila y 256 bytes en ROM en la optimización media
255 bytes de memoria de código de solo lectura
1 byte de memoria de datos de solo lectura
8 bytes de memoria de datos readwrite

255 bytes debido al hecho de que parte de la memoria pasó por debajo de la tabla de vectores de interrupción, llamadas a funciones IAR para inicializar un bloque de punto flotante, todo tipo de funciones de depuración y la función __low_level_init, donde se configuraron los puertos.

Entonces, los requisitos completos son:

  • La función main () debe contener el menor código posible
  • No puedes usar macros
  • Compilador IAR 8.40.1 compatible con C ++ 17
  • Los archivos de encabezado CMSIS como "#include" stm32f411xe.h "no se pueden usar
  • Puede usar la directiva __forceinline para funciones en línea
  • Optimización de compilador medio

Decisión de los estudiantes


En general, hubo varias soluciones, mostraré solo una ... no es óptima, pero me gustó.

Como los encabezados no se pueden usar, lo primero que hicieron los estudiantes fue la clase Gpio , que debería almacenar un enlace a los registros de puertos en sus direcciones. Para hacer esto, usan una superposición de estructura, muy probablemente tomaron la idea de aquí: Superposición de estructura :

 class Gpio { public: __forceinline inline void Toggle(const std::uint8_t bitNum) volatile { Odr ^= bitNum ; } private: volatile std::uint32_t Moder; volatile std::uint32_t Otyper; volatile std::uint32_t Ospeedr; volatile std::uint32_t Pupdr; volatile std::uint32_t Idr; volatile std::uint32_t Odr; //    static_assert(sizeof(Gpio) == sizeof(std::uint32_t) * 6); } ; 

Como puede ver, identificaron inmediatamente la clase Gpio con atributos que deberían ubicarse en las direcciones de los registros correspondientes y un método para cambiar el estado por el número de las patas:
Luego determinamos la estructura para GpioPin contiene el puntero a Gpio y el número de la pata:

 struct GpioPin { volatile Gpio* port ; std::uint32_t pinNum ; } ; 

Luego hicieron una serie de LED que se ubican en las patas específicas del puerto y lo Toggle() llamando al método Toggle() de cada LED:

 const GpioPin leds[] = {{reinterpret_cast<volatile Gpio*>(GpioaBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9} } ; struct LedsDriver { __forceinline static inline void ToggelAll() { for (auto& it: leds) { it.port->Toggle(it.pinNum); } } } ; 

Bueno, en realidad todo el código:
 constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; class Gpio { public: __forceinline inline void Toggle(const std::uint8_t bitNum) volatile { Odr ^= bitNum ; } private: volatile std::uint32_t Moder; volatile std::uint32_t Otyper; volatile std::uint32_t Ospeedr; volatile std::uint32_t Pupdr; volatile std::uint32_t Idr; volatile std::uint32_t Odr; } ; //    static_assert(sizeof(Gpio) == sizeof(std::uint32_t) * 6); struct GpioPin { volatile Gpio* port ; std::uint32_t pinNum ; } ; const GpioPin leds[] = {{reinterpret_cast<volatile Gpio*>(GpioaBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9} } ; struct LedsDriver { __forceinline static inline void ToggelAll() { for (auto& it: leds) { it.port->Toggle(it.pinNum); } } } ; int main() { for(;;) { LedsContainer::ToggleAll() ; delay(); } return 0 ; } 


Estadísticas de su código sobre optimización media:
275 bytes de memoria de código de solo lectura
1 byte de memoria de datos de solo lectura
8 bytes de memoria de datos readwrite

Una buena solución, pero ocupa mucha memoria :)

Mi decision


Por supuesto, decidí no buscar formas simples y decidí actuar de manera seria :).
Los LED están en diferentes puertos y diferentes patas. Lo primero que necesita es crear la clase Port , pero para deshacerse de los punteros y las variables que ocupan RAM, debe utilizar métodos estáticos. La clase de puerto podría verse así:

 template <std::uint32_t addr> struct Port { //  -  }; 

Como parámetro de plantilla, tendrá una dirección de puerto. En el "#include "stm32f411xe.h" , por ejemplo, para el puerto A, se define como GPIOA_BASE. Pero no se nos permite usar los encabezados, por lo que solo necesitamos hacer nuestra propia constante. Como resultado, la clase se puede usar así:

 constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ; 

Para parpadear, necesita el método Toggle (const std :: uint8_t bit), que cambiará el bit requerido mediante una operación OR exclusiva. El método debe ser estático, agréguelo a la clase:

 template <std::uint32_t addr> struct Port { //   __forceinline,        __forceinline inline static void Toggle(const std::uint8_t bitNum) { *reinterpret_cast<std::uint32_t*>(addr+20) ^= (1 << bitNum) ; //addr + 20  ODR  } }; 

Excelente Port<> es, puede cambiar el estado de las piernas. El LED se encuentra en un tramo específico, por lo que es lógico hacer un Pin clase, que tendrá el Port<> y el número del tramo como parámetros de plantilla. Dado que el tipo Port<> es plantilla, es decir diferentes para diferentes puertos, solo podemos transmitir el tipo universal T.

 template <typename T, std::uint8_t pinNum> struct Pin { __forceinline inline static void Toggle() { T::Toggle(pinNum) ; } } ; 

Es malo que podamos pasar cualquier tontería de tipo T que tenga un método Toggle() y esto funcionará, aunque se supone que solo deberíamos pasar el tipo Port<> . Para protegernos de esto, haremos que Port<> herede de la clase base de PortBase , y en la plantilla verificaremos que nuestro tipo aprobado se base en PortBase . Obtenemos lo siguiente:

 constexpr std::uint32_t OdrAddrShift = 20U; struct PortBase { }; template <std::uint32_t addr> struct Port: PortBase { __forceinline inline static void Toggle(const std::uint8_t bit) { *reinterpret_cast<std::uint32_t*>(addr ) ^= (1 << bit) ; } }; template <typename T, std::uint8_t pinNum, class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>> //   struct Pin { __forceinline inline static void Toggle() { T::Toggle(pinNum) ; } } ; 

Ahora la plantilla se instancia solo si nuestra clase tiene la clase base PortBase .
En teoría, ya puedes usar estas clases, veamos qué sucede sin optimización:

 using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ; using Led1 = Pin<PortA, 5> ; using Led2 = Pin<PortC, 5> ; using Led3 = Pin<PortC, 8> ; using Led4 = Pin<PortC, 9> ; int main() { for(;;) { Led1::Toggle(); Led2::Toggle(); Led3::Toggle(); Led4::Toggle(); delay(); } return 0 ; } 

271 bytes de memoria de código de solo lectura
1 byte de memoria de datos de solo lectura
24 bytes de memoria de datos readwrite

¿De dónde provienen estos 16 bytes adicionales en RAM y 16 bytes en ROM? Provienen del hecho de que pasamos el parámetro de bit a la función Toggle (const std :: uint8_t bit) de la clase Port, y el compilador, al ingresar a la función principal, guarda 4 registros adicionales en la pila a través de los cuales pasa este parámetro, luego usa estos registros en los que se almacenan los valores del número de tramo para cada Pin y al salir de main restaura estos registros de la pila. Y aunque en esencia este es un tipo de trabajo completamente inútil, ya que las funciones están integradas, pero el compilador actúa de acuerdo con el estándar.
Puede deshacerse de esto eliminando la clase de puerto en general, pasando la dirección del puerto como un parámetro de plantilla para la clase Pin , y dentro del método Toggle() , calcule la dirección del registro ODR:

 constexpr std::uint32_t OdrAddrShift = 20U; template <std::uint32_t addr, std::uint8_t pinNum, struct Pin { __forceinline inline static void Toggle() { *reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift ) ^= (1 << bit) ; } } ; using Led1 = Pin<GpioaBaseAddr, 5> ; 

Pero esto no se ve muy bien y fácil de usar. Por lo tanto, esperamos que el compilador elimine esta preservación innecesaria del registro con una pequeña optimización.

Ponemos la optimización en Medium y vemos el resultado:
251 bytes de memoria de código de solo lectura
1 byte de memoria de datos de solo lectura
8 bytes de memoria de datos readwrite

Wow wow wow ... tenemos 4 bytes menos
codigo
255 bytes de memoria de código de solo lectura
1 byte de memoria de datos de solo lectura
8 bytes de memoria de datos readwrite


¿Cómo puede ser esto? Echemos un vistazo al ensamblador en el depurador para el código C ++ (izquierda) y el código C (derecha):

imagen

Se puede ver que, en primer lugar, el compilador incorporó todas las funciones, ahora no hay llamadas y, en segundo lugar, optimizó el uso de registros. Se puede ver, en el caso del código C, el compilador utiliza el registro R1 o R2 para almacenar las direcciones de puerto y realiza operaciones adicionales cada vez que se conmuta el bit (guarde la dirección en el registro en R1 o en R2). En el segundo caso, usa solo el registro R1, y dado que las últimas 3 llamadas para la conmutación son siempre del puerto C, ya no es necesario guardar la misma dirección del puerto C en el registro. Como resultado, se guardan 2 equipos y 4 bytes.

Aquí es un milagro de los compiladores modernos :) Bueno, está bien. En principio, uno podría detenerse allí, pero sigamos adelante. No creo que sea posible optimizar nada más, aunque probablemente no sea correcto, si tiene ideas, escriba los comentarios. Pero con la cantidad de código en main () puedes trabajar.

Ahora quiero que todos los LED estén en algún lugar del contenedor, y podría llamar al método, cambiar todo ... Algo así:

 int main() { for(;;) { LedsContainer::ToggleAll() ; delay(); } return 0 ; } 

No insertaremos estúpidamente el cambio de 4 LED en la función LedsContainer :: ToggleAll, porque no es interesante :). Queremos colocar los LED en un contenedor y luego revisarlos y llamar al método Toggle () en cada uno.

Los estudiantes usaron una matriz para almacenar punteros en los LED. Pero tengo diferentes tipos, por ejemplo: Pin<PortA, 5> , Pin<PortC, 5> , y no puedo almacenar punteros a diferentes tipos en una matriz. Puede hacer una clase base virtual para todos los Pin, pero luego aparecerá una tabla de funciones virtuales y no tendré éxito en ganar estudiantes.

Por lo tanto, usaremos la tupla. Le permite almacenar objetos de diferentes tipos. Este caso se verá así:

 class LedsContainer { private: constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } 

Hay un gran contenedor, almacena todos los LED. Ahora agregue el método ToggleAll() :

 class LedsContainer { public: __forceinline static inline void ToggleAll() { //        } private: constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } 

No puede simplemente recorrer los elementos de una tupla, ya que el elemento de la tupla solo debe recibirse en la etapa de compilación. Para acceder a los elementos de la tupla hay un método de obtención de plantillas. Bueno, es decir si escribimos std::get<0>(records).Toggle() , entonces se llama al método Toggle() para el objeto de la clase Pin<PortA, 5> , si std::get<1>(records).Toggle() , entonces se llama al método Toggle() para el objeto de la clase Pin<Port, 5> y así sucesivamente ...

Podrías limpiar la nariz de tus alumnos y simplemente escribir:

  __forceinline static inline void ToggleAll() { std::get<0>(records).Toggle(); std::get<1>(records).Toggle(); std::get<2>(records).Toggle(); std::get<3>(records).Toggle(); } 

Pero no queremos forzar al programador que apoyará este código y le permitirá hacer un trabajo adicional, gastando los recursos de su empresa, por ejemplo, en caso de que aparezca otro LED. Tendrá que agregar el código en dos lugares, en la tupla y en este método, y esto no es bueno y el propietario de la empresa no estará muy satisfecho. Por lo tanto, evitamos la tupla usando métodos auxiliares:

 class class LedsContainer { friend int main() ; public: __forceinline static inline void ToggleAll() { //    3,2,1,0    ,     visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>()); } private: __forceinline template<std::size_t... index> static inline void visit(std::index_sequence<index...>) { Pass((std::get<index>(records).Toggle(), true)...); //    get<3>(records).Toggle(), get<2>(records).Toggle(), get<1>(records).Toggle(), get<0>(records).Toggle() } __forceinline template<typename... Args> static void inline Pass(Args... ) {//      } constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } 

Parece aterrador, pero advertí al principio del artículo que el método shizany no es muy común ...

Toda esta magia de arriba en la etapa de compilación hace literalmente lo siguiente:

 //  LedsContainer::ToggleAll() ; //   4 : Pin<Port, 9>().Toggle() ; Pin<Port, 8>().Toggle() ; Pin<PortC, 5>().Toggle() ; Pin<PortA, 5>().Toggle() ; //     Toggle() inline,   : *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 9) ; *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 8) ; *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 5) ; *reinterpret_cast<std::uint32_t*>(0x40020014 ) ^= (1 << 5) ; 

Siga compilando y verifique el tamaño del código sin optimización:

El código que compila
 #include <cstddef> #include <tuple> #include <utility> #include <cstdint> #include <type_traits> //#include "stm32f411xe.h" #define __forceinline _Pragma("inline=forced") constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; constexpr std::uint32_t OdrAddrShift = 20U; struct PortBase { }; template <std::uint32_t addr> struct Port: PortBase { __forceinline inline static void Toggle(const std::uint8_t bit) { *reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift) ^= (1 << bit) ; } }; template <typename T, std::uint8_t pinNum, class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>> struct Pin { __forceinline inline static void Toggle() { T::Toggle(pinNum) ; } } ; using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ; //using Led1 = Pin<PortA, 5> ; //using Led2 = Pin<PortC, 5> ; //using Led3 = Pin<PortC, 8> ; //using Led4 = Pin<PortC, 9> ; class LedsContainer { friend int main() ; public: __forceinline static inline void ToggleAll() { //    3,2,1,0    ,     visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>()); } private: __forceinline template<std::size_t... index> static inline void visit(std::index_sequence<index...>) { Pass((std::get<index>(records).Toggle(), true)...); } __forceinline template<typename... Args> static void inline Pass(Args... ) { } constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } ; void delay() { for (int i = 0; i < 1000000; ++i){ } } int main() { for(;;) { LedsContainer::ToggleAll() ; //GPIOA->ODR ^= 1 << 5; //GPIOC->ODR ^= 1 << 5; //GPIOC->ODR ^= 1 << 8; //GPIOC->ODR ^= 1 << 9; delay(); } return 0 ; } 


Prueba de ensamblador, desempaquetado según lo planeado:
imagen

Vemos que la memoria es excesiva, 18 bytes más. Los problemas son los mismos, más otros 12 bytes. No entendí de dónde venían ... tal vez alguien me lo explique.
283 bytes de memoria de código de solo lectura
1 byte de memoria de datos de solo lectura
24 bytes de memoria de datos readwrite

Ahora lo mismo en la optimización Media y he aquí ... tenemos un código idéntico a la implementación de C ++ en la frente y un código C más óptimo.
251 bytes de memoria de código de solo lectura
1 byte de memoria de datos de solo lectura
8 bytes de memoria de datos readwrite

Ensamblador
imagen

Como puede ver, gané, fui a las Islas Canarias y estoy contento de descansar en Chelyabinsk :), pero los estudiantes también fueron geniales, ¡aprobaron el examen con éxito!

A quién le importa, el código está aquí

¿Dónde puedo usar esto? Bueno, se me ocurrió, por ejemplo, que tenemos parámetros en la memoria EEPROM y una clase que describe estos parámetros (leer, escribir, inicializar al valor inicial). La clase es una plantilla, como Param<float<>> , Param<int<>> y necesita, por ejemplo, restablecer todos los parámetros a los valores predeterminados. Aquí es donde puede ponerlos a todos en una tupla, ya que el tipo es diferente y llama al método SetToDefault() en cada parámetro. Es cierto que si hay 100 de esos parámetros, la ROM se comerá mucho, pero la RAM no sufrirá.

PD: Debo admitir que en la optimización máxima este código tiene el mismo tamaño que en C y en mi solución. Y todos los esfuerzos del programador para mejorar el código se reducen al mismo código ensamblador.

P.S1 Gracias 0xd34df00d por buenos consejos. Puede simplificar el desempaquetado de una tupla con std::apply() . El código de la función ToggleAll() luego se simplifica a esto:

  __forceinline static inline void ToggleAll() { std::apply([](auto... args) { (args.Toggle(), ...); }, records); } 

Desafortunadamente, en el IAR, std :: apply todavía no está implementado en la versión actual, pero funcionará también, vea la implementación con std :: apply

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


All Articles