
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() {
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 ;
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 {
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 compilateurLes 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() {
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)

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éatoireLes 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:
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;
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 Dans ce cas, vous pouvez les utiliser comme ceci:
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.
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 ; }
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() {
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.