Singleton colocando objetos en ROM y variables estáticas (C ++ usando el microcontrolador Cortex M4 como ejemplo)

imagen

En un artículo anterior, ¿Dónde se almacenan sus constantes en un microcontrolador CortexM (usando el compilador C ++ IAR como ejemplo) , se discutió la cuestión de cómo colocar objetos constantes en la ROM. Ahora, quiero decirte cómo puedes usar el patrón generador solitario para crear objetos en ROM.


Introduccion


Ya se ha escrito mucho sobre Singleton (en lo sucesivo, Singleton), sus lados positivo y negativo. Pero a pesar de sus deficiencias, tiene muchas propiedades útiles, especialmente en el contexto del firmware para microcontroladores.

Para empezar, para un software de microcontrolador confiable, no se recomienda crear objetos dinámicamente y, por lo tanto, no es necesario eliminarlos. A menudo, los objetos se crean una vez y viven desde el momento en que se inicia el dispositivo, hasta que se apaga. Tal objeto incluso puede ser una pata de puerto, a la que se conecta un LED, se crea una vez y, ciertamente, no irá a ningún lado mientras se ejecuta la aplicación, y obviamente puede ser Singleton. Alguien debería crear tales objetos y podría ser Singleton.

Singleton también le dará una garantía de que el mismo objeto que describe el tramo de babor no se creará dos veces si se usa repentinamente en varios lugares.

Otra, en mi opinión, una propiedad notable de Singleton es su facilidad de uso. Por ejemplo, como en el caso del controlador de interrupciones, un ejemplo con el que se encuentra al final del artículo. Pero por ahora, trataremos con el propio Singleton.

Singleton creando objetos en RAM


En general, ¿se han escrito muchos artículos sobre ellos, Singleton (Loner) o una clase estática? , o Tres años del patrón Singleton . Por lo tanto, no me enfocaré en lo que es Singleton y describiré todas las opciones para su implementación. En cambio, me centraré en dos opciones que se pueden usar en el firmware.
Para empezar, aclararé cuál es la diferencia entre el firmware para el microcontrolador y el por qué algunas implementaciones de singleton para este software son "mejores" que otras. Algunos criterios provienen de los requisitos para el firmware, y algunos solo de mi experiencia:

  • En el firmware no se recomienda crear objetos dinámicamente.
  • A menudo, en el firmware, un objeto se crea estáticamente y nunca se destruye.
  • Bueno, si la ubicación del objeto se conoce en la etapa de compilación

En base a estos supuestos, consideramos dos variantes de Singleton con objetos creados estáticamente, y probablemente el más famoso y común es Meyers Singleton, por cierto, aunque debería ser seguro para subprocesos según el estándar C ++, los compiladores para firmware lo hacen así (por ejemplo, IAR), solo cuando la opción especial está habilitada:

template <typename T> class Singleton { public: static T & GetInstance() { static T instance ; return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; } ; 

Utiliza inicialización retrasada, es decir La inicialización de un objeto ocurre solo la primera vez que GetInstance() llama a GetInstance() ; considérelo como una inicialización dinámica.

 int main() { //   Timer1      auto& objRef = Singleton<Timer1>::GetInstance(); //  ,      auto& objRef1 = Singleton<Timer1>::GetInstance(); return 0; } 

Y Singleton sin inicialización retrasada:

 template <typename T> class Singleton { public: static constexpr T & GetInstance() { return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; private: inline static T instance ; //      } ; 

Ambos Singleton crean objetos en RAM, la diferencia es que para el segundo, la inicialización ocurre inmediatamente después de que se inicia el programa, y ​​el primero se inicializa en la primera llamada.

¿Cómo se pueden usar en la vida real? De acuerdo con la vieja tradición, intentaré mostrar esto usando el ejemplo de un LED. Entonces, supongamos que necesitamos crear un objeto de la clase Led1 , que en realidad es solo un alias de la clase Pin<PortA, 5> :

 using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; Led1 myLed ; //        RAM constexpr GreenLed greenLed ; //        ROM int main() { static GreenLed myGreenLed ; //     RAM Led1 led1; //     myGreenLed.Toggle(); led1.Toggle() ; } 

Por si acaso, las clases de puerto y pin se ven así
 constexpr std::uint32_t OdrAddrShift = 20U; template <std::uint32_t addr> struct Port { __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 Pin { // Singleton   ,     friend class Singleton<Pin> ; public: __forceinline inline void Toggle() const { T::Toggle(pinNum) ; } //  = const Pin & operator=(const Pin &) = delete ; private: // ,      constexpr Pin() {} ; //  ,      //   ,      constexpr Pin(const Pin &) = default ; } ; 


En el ejemplo, creé hasta 4 objetos diferentes del mismo tipo en RAM y ROM, que realmente funcionan con la misma salida del puerto A. Lo cual no es muy bueno aquí:
Bueno, lo primero es que aparentemente olvidé que GreenLed y Led1 son del mismo tipo y creé varios objetos idénticos que ocupan espacio en diferentes direcciones. De hecho, incluso olvidé que ya había creado globalmente objetos de las GreenLed Led1 y GreenLed , y también los creé localmente.

Segundo, declarar objetos globales en general no es bienvenido,

Pautas de programación para una mejor optimización del compilador
Las variables locales del módulo (variables declaradas estáticas) son preferibles a
variables globales (no estáticas). También evite tomar la dirección de variables estáticas a las que se accede con frecuencia.

y los objetos locales solo están disponibles en el ámbito de la función main ().

Por lo tanto, reescribimos este ejemplo usando Singleton:

 using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; int main() { //        GreenLed //   GreenLed& myGreenLed = Singleton<GreenLed>::GetInstance(); //            Led1& led1 = Singleton<Led1>::GetInstance(); myGreenLed.Toggle() ; led1.Toggle() ; //  , Singleton<Led1>::GetInstance().Toggle() } 

En este caso, no importa lo que olvide, mis enlaces siempre apuntarán al mismo objeto. Y puedo obtener este enlace en cualquier parte del programa, en cualquier método, incluido, por ejemplo, en el método estático del controlador de interrupciones, pero más sobre eso más adelante. Para ser justos, debo decir que el código no hace nada, y el error en la lógica del programa no ha desaparecido. Bueno, de acuerdo, averigüemos dónde y cómo, en general, se ubicó este objeto estático creado por Singleton y cómo se inicializó.

Objeto estático


Antes de descubrirlo, sería bueno entender qué es un objeto estático.

Si declara miembros de clase con la palabra clave estática, esto significa que los miembros de clase simplemente no están vinculados a instancias de clase, son variables independientes y puede acceder a dichos campos sin crear un objeto de clase. Nada amenaza sus vidas desde el momento en que nacen hasta que se lanza el programa.

Cuando se usa en una declaración de objeto, el especificador estático determina solo la vida útil del objeto. En términos generales, la memoria para dicho objeto se asigna cuando se inicia el programa y se libera cuando finaliza el programa; cuando se inicia, también se lleva a cabo su inicialización. Las excepciones son solo objetos estáticos locales que, aunque "mueren" solo al final del programa, esencialmente "nacen", o más bien, se inicializan la primera vez que pasan por su declaración.

La inicialización dinámica de una variable local con almacenamiento estático se realiza por primera vez en el momento del primer paso a través de su declaración; dicha variable se considera inicializada al finalizar su inicialización. Si un subproceso pasa por una declaración variable en el momento de su inicialización por otro subproceso, entonces debe esperar a que se complete la inicialización.

En las siguientes llamadas, la inicialización no ocurre. Todo lo anterior se puede reducir a una frase, solo puede existir una instancia de un objeto estático.

Dichas dificultades conducen al hecho de que el uso de variables y objetos estáticos locales en el firmware generará una sobrecarga adicional. Puede verificar esto con un simple ejemplo:

 struct Test1{ Test1(int value): j(value) {} int j; } ; Test1 &foo() { static Test1 test(10) ; return test; } int main() { for (int i = 0; i < 10; ++i) { foo().j ++; } return 0; } 

Aquí, la primera vez que se llama a la función foo() , el compilador debe verificar que el objeto estático local test1 aún no se haya inicializado y llamar al constructor del objeto Test1(10) , y en el segundo y posteriores pases, debe asegurarse de que el objeto ya esté inicializado y omitir este paso. yendo directamente a la return test .

Para hacer esto, el compilador simplemente agrega un indicador de protección adicional foo()::static guard for test 0x00100004 0x1 Data Lc main.o e inserta el código de verificación. En la primera declaración de una variable estática, este indicador de protección no se establece y, por lo tanto, el objeto debe inicializarse llamando al constructor; durante la próxima pasada, este indicador ya está establecido, por lo que ya no hay necesidad de inicialización y se omite la llamada del constructor. Además, esta verificación se realizará continuamente en el bucle for.



Y si habilita la opción que le garantizará la inicialización en aplicaciones de subprocesos múltiples, habrá aún más código ... (consulte la llamada para capturar y liberar un recurso durante la inicialización está subrayada en naranja)

imagen

Por lo tanto, el precio de usar una variable u objeto estático en el firmware aumenta tanto en tamaño de RAM como en tamaño de código. Y este hecho sería bueno tener en cuenta y considerar al desarrollar.

Otra desventaja es el hecho de que el indicador de protección nace junto con la variable estática, su vida útil es igual a la vida útil del objeto estático, es creado por el compilador y no tiene acceso a él durante el desarrollo. Es decir si de repente por alguna razón

ver accidente aleatorio
Las causas de los errores aleatorios son: (1) partículas alfa resultantes del proceso de descomposición, (2) neutrones, (3) una fuente externa de radiación electromagnética y (4) diafonía interna.

Si el indicador de 1 va a 0, se llama nuevamente a la inicialización con el valor inicial. Esto no es bueno, y también hay que tenerlo en cuenta. Para resumir las variables estáticas:
Para cualquier objeto estático (ya sea una variable local o un atributo de clase), la memoria se asigna una vez y no cambiará en toda la aplicación.

Las variables estáticas locales se inicializan durante el primer paso a través de una declaración de variable.

Los atributos de clase estáticos, así como las variables globales estáticas, se inicializan inmediatamente después de que se inicia la aplicación. Además, este orden no está definido
Ahora de vuelta a Singleton.

Singleton colocando objeto en ROM


De todo lo anterior, podemos concluir que para nosotros, Singleton Mayers puede tener las siguientes desventajas: costos adicionales de RAM y ROM, un indicador de seguridad no controlado y la imposibilidad de colocar un objeto en ROM debido a la inicialización dinámica.

Pero tiene una ventaja maravillosa: controlas el tiempo de inicialización del objeto. Solo el desarrollador mismo llama a GetInstance() primera vez en el momento en que lo necesita.

Para deshacerse de las tres primeras deficiencias, es suficiente usar

Singleton sin inicialización retrasada
 template<typename T, class Enable = void> class Singleton { public: Singleton(const Singleton&) = delete ; Singleton& operator = (const Singleton&) = delete ; Singleton() = delete ; static T& GetInstance() { return instance; } private: static T instance ; } ; template<typename T, class Enable> T Singleton<T,Enable>::instance ; 


Aquí, por supuesto, hay otro problema, no podemos controlar el tiempo de inicialización del objeto de instance , y de alguna manera debemos proporcionar una inicialización muy transparente. Pero este es un problema separado, no nos detendremos en él ahora.

Este Singleton se puede volver a hacer para que la inicialización del objeto sea completamente estática en el momento de la compilación y se cree una instancia del objeto T en la ROM utilizando static constexpr T instance lugar de static T instance :

 template <typename T> class Singleton { public: static constexpr T & GetInstance() { return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; private: // constexpr  constexpr   //           T static constexpr T instance{T()}; } ; template<typename T> constexpr T Singleton<T>::instance ; 

Aquí, la creación e inicialización del objeto será realizada por el compilador en la etapa de compilación y el objeto caerá en el segmento .readonly. Es cierto que la clase en sí misma debe cumplir las siguientes reglas:
  • La inicialización de un objeto de esta clase debe ser estática. (El constructor debe ser constexpr)
  • La clase debe tener un constructor de copia constexpr
  • Los métodos de clase de un objeto de clase no deberían cambiar los datos de un objeto de clase (todos los métodos const)

Por ejemplo, esta opción es bastante posible:

 class A { friend class Singleton<A>; public: const A & operator=(const A &) = delete ; int Get() const { return test2.Get(); } void Set(int v) const { test.SetB(v); } private: B& test; //    RAM const C& test2; //    ROM //      constexpr A(const A &) = default ; //     RAM  ROM,  Singleton constexpr A() : test(Singleton<B>::GetInstance()), test2(Singleton<C>::GetInstance()) { } }; int main() { //      ROM auto& myObject = Singleton<A>::GetInstance() ; //           myObject.Set(myObject.Get()) ; cout<<"Singleton<A> - address: "<< &myObject <<std::endl; } 

Genial, puedes usar Singleton para crear objetos en ROM, pero ¿qué pasa si algunos objetos deberían estar en la RAM? Obviamente, debe mantener de alguna manera dos especializaciones para Singleton, una para objetos RAM y la otra para objetos en ROM. Puede hacerlo ingresando, por ejemplo, para todos los objetos que se deben colocar en la clase base ROM:

Especialización para Singleton creando objetos en ROM y RAM
 //    ,     ROM class RomObject{}; //  ROM  template<typename T> class Singleton<T, typename std::enable_if_t<std::is_base_of<RomObject, T>::value>> { public: Singleton(const Singleton&) = delete; Singleton& operator = (const Singleton&) = delete; Singleton() = delete; static constexpr const T& GetInstance() { return instance; } private: static constexpr T instance{T()}; }; template<typename T> constexpr T Singleton<T, typename std::enable_if_t<std::is_base_of<RomObject, T>::value>>::instance ; //  RAM  template<typename T, class Enable = void> class Singleton { public: Singleton(const Singleton&) = delete; Singleton& operator = (const Singleton&) = delete; Singleton() = delete; constexpr static T& GetInstance() { return instance; } private: static T instance ; }; template<typename T, class Enable> T Singleton<T,Enable>::instance ; 


En este caso, puede usarlos así:

 //      RAM,   SetB()    (j) class B { friend class Singleton<B>; public: const B & operator=(const B &) = delete ; void SetB(int value) { j = value ; } private: // ,        B(const B &) = default ; B() = default; int j = 0; } //      ROM class A: public RomObject{ friend class Singleton<A>; public: const A & operator=(const A &) = delete ; int Get() const { return test2.Get(); } //     B,    void Set(int v) const { test.SetB(v); } private: B& test; //    RAM const C& test2; //    ROM //        A(const A &) = default ; //     RAM  ROM,  Singleton constexpr A() : test(Singleton<B>::GetInstance()), test2(Singleton<C>::GetInstance()) { } }; int main() { //      ROM auto& romObject = Singleton<A>::GetInstance() ; //    B  RAM auto& ramObject = Singleton<B>::GetInstance() ; //           ramObject.SetB(romObject.Get()) ; cout<<"Singleton<A> - address: "<< &romObject <<std::endl; cout<<"Singleton<B> - address: "<< &ramObject <<std::endl; } 

¿Cómo puedes usar un Singleton así en la vida real?

Ejemplo Singleton


Intentaré mostrar esto en el ejemplo del funcionamiento del temporizador y el LED. La tarea es simple, parpadea el LED en el temporizador. El temporizador se puede configurar.

El principio de funcionamiento será el siguiente: cuando se llama a la interrupción, se llama al método OnInterrupt() del temporizador, que a su vez llamará al método de conmutación de LED a través de la interfaz del suscriptor.

Obviamente, el objeto LED debe estar en la ROM, ya que no tiene sentido crearlo en la RAM, ni siquiera hay datos en él. En principio, ya lo he descrito anteriormente, así que solo agregue la herencia de RomObject , cree un constructor constexpr y también herede la interfaz para procesar eventos desde el temporizador.

Objeto LED
 //      class ITimerSubscriber { public: virtual void OnTimeOut() const = 0; } ; template <typename T, std::uint8_t pinNum> class Pin: public RomOject, public ITimerSubscriber { // Singleton   ,     friend class Singleton<Pin> ; public: __forceinline inline void Toggle() const { T::Toggle(pinNum) ; } //       __forceinline inline void OnTimeOut() const override { Toggle() ; } //  = const Pin & operator=(const Pin &) = delete ; private: // ,      constexpr Pin() = default ; Pin(const Pin &) = default ; } ; 

Pero haré el temporizador específicamente en RAM con una pequeña hoja de ruta, almacenaré un enlace a la estructura TIM_TypeDef , un punto y un enlace de suscriptor, y configuraré el temporizador en el constructor (aunque sería posible hacer que el temporizador también vaya a ROM):

Temporizador de clase
 class Timer { public: const Timer & operator=(const Timer &) = delete ; void SetPeriod(const std::uint16_t value) { period = value ; timer.PSC = TimerClockSpeed / 1000U - 1U ; timer.ARR = value ; } //      __forceinline inline void OnInterrupt() { if ((timer.SR & TIM_SR_UIF) && (timer.DIER & TIM_DIER_UIE)) { //   ,     OnTimeOut //       Toggle() subscriber->OnTimeOut() ; timer.SR &=~ TIM_SR_UIF ; } } //    TimeOut  ,   ITimerSubscriber,   __forceinline inline void Subscribe(const ITimerSubscriber& obj) { subscriber = &obj ; } inline void Start() { timer.CR1 |= TIM_CR1_URS ; timer.DIER |= TIM_DIER_UIE ; SetPeriod(period) ; timer.CR1 &=~TIM_CR1_OPM ; timer.EGR |= TIM_EGR_UG ; timer.CR1 |= TIM_CR1_CEN ; } protected: // ,         explicit Timer(TIM_TypeDef& tim): timer{tim} {}; const ITimerSubscriber * subscriber = nullptr ; TIM_TypeDef& timer ; std::uint16_t period = 1000; } ; 


 //       class BlinkTimer: public Timer { friend class Singleton<BlinkTimer> ; public: const BlinkTimer & operator=(const BlinkTimer &) = delete ; private: BlinkTimer(const BlinkTimer &) = default ; inline BlinkTimer(): Timer{*TIM2} { } } ; int main() { BlinkTimer & blinker = Singleton<BlinkTimer>::GetInstance() ; using Led1 = Pin<PortA, 5> ; // Led1,   ROM,      blinker.Subscribe(Singleton<Led1>::GetInstance()) ; blinker.Start() ; } 

En este ejemplo, un objeto de la clase BlinkTimer se encuentra en la RAM y un objeto de la clase Led1 se encuentra en la ROM. No hay objetos globales adicionales en el código. En el lugar donde se necesita la instancia de clase, simplemente llamamos a GetInstance() para esta clase

Queda por agregar un controlador de interrupción a la tabla de vectores de interrupción. Y aquí, es muy conveniente usar Singleton. En el método estático de la clase responsable de manejar las interrupciones, puede llamar al método del objeto envuelto en Singleton.

 extern "C" void __iar_program_start(void) ; class InterruptHandler { public: static void DummyHandler() { for(;;) {} } static void Timer2Handler() { //   BlinkTimer Singleton<BlinkTimer>::GetInstance().OnInterrupt(); } }; using tIntFunct = void(*)(); using tIntVectItem = union {tIntFunct __fun; void * __ptr;}; #pragma segment = "CSTACK" #pragma location = ".intvec" const tIntVectItem __vector_table[] = { { .__ptr = __sfe( "CSTACK" ) }, //    __iar_program_start, //      InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, 0, 0, 0, 0, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, 0, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, //External Interrupts InterruptHandler::DummyHandler, //Window Watchdog InterruptHandler::DummyHandler, //PVD through EXTI Line detect/EXTI16 .... InterruptHandler::Timer2Handler, //      BlinkTimer InterruptHandler::DummyHandler, //TIM3 ... InterruptHandler::DummyHandler, //SPI 5 global interrupt }; extern "C" void __cmain(void) ; extern "C" __weak void __iar_init_core(void) ; extern "C" __weak void __iar_init_vfp(void) ; #pragma required = __vector_table void __iar_program_start(void) { __iar_init_core() ; __iar_init_vfp() ; __cmain() ; } 

Un poco sobre la mesa en sí, cómo funciona todo:
Inmediatamente después del encendido o después de un reinicio, un reinicio se interrumpe con el número -8 , en la tabla es un elemento cero, de acuerdo con la señal de reinicio, el programa cambia al vector del elemento cero, donde el puntero a la parte superior de la pila se inicializa primero. Esta dirección se toma de la ubicación del segmento STACK que configuró en la configuración del vinculador. Inmediatamente después de inicializar el puntero, vaya al punto de entrada del programa, en este caso, a la dirección de la función __iar_program_start . A continuación, el código se inicializa inicializando sus variables globales y estáticas, inicializando el coprocesador con un punto flotante, si se incluyó en la configuración, etc. Si ocurre una interrupción, el controlador de interrupción por el número de interrupción en la tabla va a la dirección del manejador de interrupciones. En nuestro caso, este es InterruptHandler::Timer2Handler , que, a través de Singleton, llama al método OnInterrupt() de nuestro temporizador de parpadeo, que, a su vez, OnTimeOut() método OnTimeOut() la pata de puerto.

En realidad eso es todo, puedes ejecutar el programa. Un ejemplo de trabajo para IAR 8.40 se encuentra aquí .
Aquí puede encontrar un ejemplo más detallado del uso de Singleton para objetos en ROM y RAM.

Enlaces de documentación:


PD: En la imagen al comienzo del artículo, de todos modos, Singleton no es ROM, sino WHISKY.

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


All Articles