Singleton plaçant des objets dans la ROM et des variables statiques (C ++ en utilisant le microcontrôleur Cortex M4 comme exemple)

image

Dans l'article précédent Où vos constantes sont stockées sur le microcontrôleur CortexM (en utilisant le compilateur C ++ IAR comme exemple) , la question de savoir comment placer des objets constants dans la ROM a été discutée. Maintenant, je veux vous dire comment vous pouvez utiliser le modèle de générateur isolé pour créer des objets dans la ROM.


Présentation


Beaucoup a déjà été écrit sur Singleton (ci-après dénommé Singleton) ses côtés positifs et négatifs. Mais malgré ses lacunes, il possède de nombreuses propriétés utiles, notamment dans le cadre du firmware des microcontrôleurs.

Pour commencer, pour un logiciel de microcontrôleur fiable, il n'est pas recommandé de créer des objets de manière dynamique, et il n'est donc pas nécessaire de les supprimer. Souvent, les objets sont créés une fois et vivent à partir du moment où l'appareil est démarré, jusqu'à ce qu'il soit éteint. Un tel objet peut même être la jambe de port à laquelle la LED est connectée, il est créé une fois, et il n'ira certainement nulle part pendant que l'application est en cours d'exécution, et il peut évidemment s'agir de Singleton. Quelqu'un devrait créer de tels objets et ce pourrait être Singleton.

Singleton vous donnera également la garantie que le même objet décrivant la jambe de port ne sera pas créé deux fois s'il est soudainement utilisé à plusieurs endroits.

Une autre, à mon avis, une propriété remarquable de Singleton est sa facilité d'utilisation. Par exemple, comme dans le cas du gestionnaire d'interruption, dont un exemple se trouve à la fin de l'article. Mais pour l'instant, nous allons traiter avec Singleton lui-même.

Singleton créant des objets en RAM


En général, beaucoup d'articles ont déjà été écrits à leur sujet, Singleton (Loner) ou une classe statique? ou le modèle de l'âge de trois singleton . Par conséquent, je ne vais pas me concentrer sur ce qu'est Singleton et décrire toutes les nombreuses options pour sa mise en œuvre. Au lieu de cela, je me concentrerai sur deux options qui peuvent être utilisées dans le firmware.
Pour commencer, je vais clarifier quelle est la différence entre le firmware du microcontrôleur par rapport à l'habituel et pourquoi certaines implémentations singleton pour ce logiciel sont "meilleures" que d'autres. Certains critères proviennent des exigences pour le firmware, et certains simplement de mon expérience:

  • Dans le firmware, il n'est pas recommandé de créer des objets dynamiquement
  • Souvent dans le firmware, un objet est créé statiquement et n'est jamais détruit.
  • Eh bien, si l'emplacement de l'objet est connu au stade de la compilation

Sur la base de ces hypothèses, nous considérons deux variantes de Singleton avec des objets créés statiquement, et probablement la plus célèbre et la plus commune est Meyers Singleton, soit dit en passant, bien qu'elle devrait être sécurisée pour les threads par la norme C ++, les compilateurs pour le firmware le font comme ceci (par exemple, IAR), uniquement lorsque l'option spéciale est activée:

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

Il utilise une initialisation retardée, c'est-à-dire L'initialisation d'un objet ne se produit que la première fois que GetInstance() appelée; considérez cette initiation dynamique.

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

Et Singleton sans initialisation retardée:

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

Les deux singleton créent des objets dans la RAM, la différence est que pour le second, l'initialisation se produit immédiatement après le démarrage du programme et le premier est initialisé lors du premier appel.

Comment peuvent-ils être utilisés dans la vraie vie. Selon la vieille tradition, je vais essayer de le montrer en utilisant l'exemple d'une LED. Supposons donc que nous ayons besoin de créer un objet de classe Led1 , qui n'est en fait qu'un alias de la classe 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() ; } 

Au cas où, les classes Port et Pin ressemblent à ceci
 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 ; } ; 


Dans l'exemple, j'ai créé jusqu'à 4 objets différents du même type en RAM et ROM, qui fonctionnent en fait avec la même sortie du port A. Ce qui n'est pas très bon ici:
Eh bien, la première chose est que j'ai apparemment oublié que GreenLed et Led1 sont du même type et ont créé plusieurs objets identiques prenant de la place à différentes adresses. En fait, j'ai même oublié que j'avais déjà créé globalement des objets des GreenLed Led1 et GreenLed , et les GreenLed également créés localement.

Deuxièmement, déclarer généralement des objets globaux n'est pas le bienvenu,

Lignes directrices de programmation pour une meilleure optimisation du compilateur
Les variables locales du module - les variables déclarées statiques - sont préférées aux
variables globales (non statiques). Évitez également de prendre l'adresse des variables statiques fréquemment utilisées.

et les objets locaux ne sont disponibles que dans le cadre de la fonction main ().

Par conséquent, nous réécrivons cet exemple en utilisant 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() } 

Dans ce cas, quoi que j'oublie, mes liens pointeront toujours vers le même objet. Et je peux obtenir ce lien n'importe où dans le programme, dans n'importe quelle méthode, y compris, par exemple, dans la méthode statique du gestionnaire d'interruption, mais plus à ce sujet plus tard. En toute honnêteté, je dois dire que le code ne fait rien et que l'erreur dans la logique du programme n'a pas disparu. Eh bien, voyons où et comment en général cet objet statique créé par Singleton a été localisé et comment il a été initialisé?

Objet statique


Avant de le découvrir, il serait bon de comprendre ce qu'est un objet statique.

Si vous déclarez des membres de classe avec le mot clé statique, cela signifie que les membres de classe ne sont tout simplement pas liés aux instances de classe, ce sont des variables indépendantes et vous pouvez accéder à ces champs sans créer d'objet de classe. Rien ne menace leur vie depuis leur naissance jusqu'à la sortie du programme.

Lorsqu'il est utilisé dans une déclaration d'objet, le spécificateur statique détermine uniquement la durée de vie de l'objet. En gros, la mémoire d'un tel objet est allouée au démarrage du programme et libérée à la fin du programme; au démarrage, leur initialisation a également lieu. Les seules exceptions sont les objets statiques locaux qui, bien qu'ils ne «meurent» qu'à la fin du programme, sont essentiellement «nés», ou plutôt, sont initialisés la première fois qu'ils passent par leur déclaration.

L'initialisation dynamique d'une variable locale avec stockage statique est effectuée pour la première fois lors du premier passage à travers sa déclaration; une telle variable est considérée comme initialisée à l'issue de son initialisation. Si un thread passe par une déclaration de variable au moment de son initialisation par un autre thread, il doit attendre la fin de l'initialisation.

Dans les appels suivants, l'initialisation ne se produit pas. Tout ce qui précède peut être réduit à une phrase, une seule instance d'un objet statique peut exister.

De telles difficultés conduisent au fait que l'utilisation de variables et d'objets statiques locaux dans le firmware entraînera une surcharge supplémentaire. Vous pouvez le vérifier avec un exemple simple:

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

Ici, la première fois que la fonction foo() est appelée, le compilateur doit vérifier que l'objet statique local test1 n'a pas encore été initialisé et appeler le constructeur de l'objet test1 Test1(10) , et dans la deuxième passe et les suivantes, il doit s'assurer que l'objet est déjà initialisé et ignorer cette étape. aller directement pour return test .

Pour ce faire, le compilateur ajoute simplement un indicateur de protection supplémentaire foo()::static guard for test 0x00100004 0x1 Data Lc main.o et insère le code de vérification. Lors de la première déclaration d'une variable statique, cet indicateur de protection n'est pas défini et donc l'objet doit être initialisé en appelant le constructeur; lors de la prochaine passe, cet indicateur est déjà défini, il n'y a donc plus besoin d'initialisation et l'appel du constructeur est ignoré. De plus, cette vérification sera effectuée en continu dans la boucle for.



Et si vous activez l'option qui vous garantira l'initialisation dans les applications multi-thread, alors il y aura encore plus de code ... (voir l'appel à capturer et libérer la ressource lors de l'initialisation est souligné en orange)

image

Ainsi, le prix d'utilisation d'une variable ou d'un objet statique dans le micrologiciel augmente à la fois en taille de RAM et en taille de code. Et ce fait serait bien à garder à l'esprit et à considérer lors du développement.

Un autre inconvénient est le fait que le drapeau de protection est né avec la variable statique, sa durée de vie est égale à la durée de vie de l'objet statique, il est créé par le compilateur lui-même et vous n'y avez pas accès pendant le développement. C'est-à-dire si tout à coup pour une raison

voir crash aléatoire
Les causes des erreurs aléatoires sont: (1) les particules alpha résultant du processus de désintégration, (2) les neutrons, (3) une source externe de rayonnement électromagnétique et (4) la diaphonie interne.

Si l'indicateur de 1 passe à 0, l'initialisation avec la valeur initiale est à nouveau appelée. Ce n'est pas bon, et il faut aussi garder à l'esprit. Pour résumer les variables statiques:
Pour tout objet statique (que ce soit une variable locale ou un attribut de classe), la mémoire est allouée une fois et ne changera pas tout au long de l'application.

Les variables statiques locales sont initialisées lors du premier passage à travers une déclaration de variable.

Les attributs de classe statiques, ainsi que les variables globales statiques, sont initialisés immédiatement après le démarrage de l'application. De plus, cet ordre n'est pas défini
Revenons maintenant à Singleton.

Singleton plaçant un objet dans la ROM


De tout ce qui précède, nous pouvons conclure que pour nous, Singleton Mayers peut avoir les inconvénients suivants: des coûts supplémentaires de RAM et de ROM, un indicateur de sécurité non contrôlé et l'incapacité de placer un objet dans la ROM en raison de l'initialisation dynamique.

Mais il a un merveilleux plus: vous contrôlez le temps d'initialisation de l'objet. Seul le développeur lui-même appelle GetInstance() première fois au moment où il en a besoin.

Pour se débarrasser des trois premières lacunes, il suffit d'utiliser

Singleton sans initialisation retardée
 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 ; 


Ici, bien sûr, il y a un autre problème, nous ne pouvons pas contrôler le temps d'initialisation de l'objet instance , et nous devons en quelque sorte fournir une initialisation très transparente. Mais c'est un problème distinct, nous ne nous attarderons pas là-dessus maintenant.

Ce singleton peut être repensé de sorte que l'initialisation de l'objet soit complètement statique au moment de la compilation et qu'une instance de T créée dans la ROM en utilisant static constexpr T instance au lieu 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 ; 

Ici, la création et l'initialisation de l'objet seront effectuées par le compilateur au stade de la compilation et l'objet tombera dans le segment .readonly. Certes, la classe elle-même doit satisfaire aux règles suivantes:
  • L'initialisation d'un objet de cette classe doit être statique. (Le constructeur doit être constexpr)
  • La classe doit avoir un constructeur de copie constexpr
  • Les méthodes de classe d'un objet de classe ne doivent pas modifier les données d'un objet de classe (toutes les méthodes const)

Par exemple, cette option est tout à fait possible:

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

Génial, vous pouvez utiliser Singleton pour créer des objets en ROM, mais que faire si certains objets doivent être en RAM? Évidemment, vous devez en quelque sorte conserver deux spécialisations pour Singleton, une pour les objets RAM, l'autre pour les objets en ROM. Vous pouvez le faire en entrant, par exemple, pour tous les objets qui doivent être placés dans la classe de base ROM:

Spécialisation pour Singleton créant des objets en ROM et 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 ; 


Dans ce cas, vous pouvez les utiliser comme ceci:

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

Comment pouvez-vous utiliser un tel Singleton dans la vraie vie.

Exemple de singleton


Je vais essayer de le montrer sur l'exemple de la minuterie et de la LED. La tâche est simple, faire clignoter la LED sur la minuterie. La minuterie peut être réglée.

Le principe de fonctionnement sera le suivant, lorsque l'interruption est appelée, la méthode OnInterrupt() du temporisateur sera appelée, qui à son tour appellera la méthode de commutation LED via l'interface d'abonné.

Évidemment, l'objet LED doit être en ROM, car il ne sert à rien de le créer en RAM, il n'y a même pas de données dedans. En principe, je l'ai déjà décrit ci-dessus, il suffit donc d'y ajouter l'héritage de RomObject , de créer un constructeur constexpr et d'hériter également de l'interface pour le traitement des événements du temporisateur.

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

Mais je créerai le temporisateur spécifiquement dans la RAM avec une petite lettre de voiture, je stockerai un lien vers la structure TIM_TypeDef , un point et le lien d'un abonné, et je configurerai le temporisateur dans le constructeur (bien qu'il soit possible de faire passer également le temporisateur vers la ROM):

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

Dans cet exemple, un objet de classe BlinkTimer était situé dans la RAM et un objet de classe Led1 était situé dans la ROM. Aucun objet global supplémentaire dans le code. À l'endroit où l'instance de classe est nécessaire, nous appelons simplement GetInstance() pour cette classe

Il reste à ajouter un gestionnaire d'interruption à la table des vecteurs d'interruption. Et ici, il est très pratique d'utiliser Singleton. Dans la méthode statique de la classe responsable de la gestion des interruptions, vous pouvez appeler la méthode de l'objet encapsulé dans 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 peu sur la table elle-même, comment tout cela fonctionne:
Immédiatement après la mise sous tension ou après une réinitialisation, une réinitialisation est interrompue avec le nombre -8 , dans le tableau, il s'agit d'un élément zéro, selon le signal de réinitialisation, le programme passe au vecteur d'élément zéro, où le pointeur vers le haut de la pile est initialisé en premier. Cette adresse provient de l'emplacement du segment STACK que vous avez configuré dans les paramètres de l'éditeur de liens. Immédiatement après l'initialisation du pointeur, accédez au point d'entrée du programme, dans ce cas, à l'adresse de la fonction __iar_program_start . Ensuite, le code est initialisé en initialisant vos variables globales et statiques, en initialisant le coprocesseur avec une virgule flottante, s'il était inclus dans les paramètres, etc. Si une interruption se produit, le contrôleur d'interruption par le numéro d'interruption dans le tableau va à l'adresse du gestionnaire d'interruption. Dans notre cas, il s'agit d' InterruptHandler::Timer2Handler , qui, via Singleton, appelle la méthode OnInterrupt() de notre temporisateur de clignotement, qui, à son tour, OnTimeOut() méthode OnTimeOut() la jambe de port.

En fait, c'est tout, vous pouvez exécuter le programme. Un exemple de travail pour IAR 8.40 se trouve ici .
Un exemple plus détaillé de l'utilisation de Singleton pour des objets en ROM et RAM peut être trouvé ici .

Liens de documentation:


PS Dans l'image du début de l'article, tout de même, Singleton n'est pas ROM, mais WHISKY.

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


All Articles