Présentation

Dans ces environnements, le modèle Singleton, principe de conception qui limite une classe à une seule instance et fournit un point d'accès global, peut être un outil puissant pour gérer les ressources matérielles partagées, les canaux de communication et l'état du système. Cependant, l'application incorrecte de ce modèle peut introduire des bogues subtils, dégrader les performances et augmenter la consommation d'énergie. Cet article élargit les lignes directrices standard Singleton en un ensemble de pratiques optimales axées sur la production et adaptées aux systèmes embarqués, couvrant l'initialisation paresseuse, la sécurité des fils, l'efficacité de la mémoire et les pièges communs.

Comprendre le modèle Singleton dans les systèmes embarqués

Le modèle Singleton garantit qu'il existe à tout moment un objet d'une classe. Dans les systèmes intégrés, cela est particulièrement utile pour représenter les périphériques, les pilotes de périphériques et les gestionnaires de ressources qui doivent maintenir une vision globale cohérente.

  • Les contrôleurs UART/USART – une seule instance devrait gérer la transmission et recevoir les tampons.
  • Les pilotes ADC (Analog-to-Digital Converter) – plusieurs clients doivent lire les mêmes résultats de conversion sans duplication.
  • Modules de gestion de puissance – un seul point de décision pour entrer dans les modes de sommeil ou d'activité.
  • Accès à une horloge en temps réel (RTC)[ – une seule source d'horloge doit être synchronisée par toutes les tâches.
  • Les planificateurs de tâches[ dans les systèmes à métaux nus – une seule boucle ou un seul gestionnaire d'interruption répartit toutes les tâches de coopération.

Sans Singleton, les développeurs ont souvent recours à des variables globales ou à des structures statiques, ce qui peut conduire à un état incohérent entre les modules. Le modèle Singleton impose une méthode d'accès disciplinée, mais son implémentation doit être adaptée aux contraintes du matériel embarqué : espace limité de pile et d'accumulation, absence d'allocation de mémoire dynamique dans certains contextes, présence d'interruptions et d'opérations en temps réel.

Une idée fausse courante est que le modèle Singleton est simplement une «variable globale dans une robe fantaisie». Dans les systèmes embarqués, le modèle doit être mis en œuvre avec un contrôle soigneux sur le timing de l'instantialisation, la sécurité des fils (y compris les contextes d'interruption) et le comportement de la puissance-aware.

Meilleures pratiques de mise en œuvre

1. Utiliser l'initialisation paresseuse avec la sensibilisation au pouvoir

L'initialisation par lassitude signifie que l'instance Singleton n'est créée qu'à la première fois. Cette approche conserve les cycles de mémoire et de processeur pendant le démarrage, ce qui est critique dans les appareils alimentés par batterie ou les ressources. Considérez un pilote UART rarement utilisé dans un nœud de capteur de faible puissance : retarder sa création jusqu'à l'arrivée d'une commande série peut sauver plusieurs centaines d'octets de RAM et éviter d'initialiser inutilement l'horloge périphérique.

Exemple en C (en utilisant une variable statique):[

// uart_driver.h
typedef struct {
 volatile uint32_t *base_addr;
 // ... other fields
} UART_HandleTypeDef;

UART_HandleTypeDef* UART_GetInstance(void);

// uart_driver.c
#include "uart_driver.h"
#include "chip_peripherals.h"

UART_HandleTypeDef* UART_GetInstance(void) {
 static UART_HandleTypeDef instance;
 static int initialized = 0;
 if (!initialized) {
 instance.base_addr = (uint32_t*)UART1_BASE;
 // Perform peripheral-specific configuration
 UART_Configure(instance.base_addr);
 initialized = 1;
 }
 return &instance;
}

Notez que le drapeau d'initialisation est un entier. Dans les environnements à simple filetage, sans interruption, cela est sûr, mais des mesures supplémentaires sont nécessaires pour un accès simultané (voir la section suivante).

L'initialisation par lassitude permet également au système intégré de reporter l'alimentation en puissance de la périphérie à une vitesse absolument nécessaire. Si le Singleton gère un dispositif à courant élevé (par exemple un modem GSM ou un module Wi-Fi), la création de l'instance réduit la consommation moyenne d'énergie. Cependant, soyez prudent : si le Singleton créé par lassitude est accessible à l'intérieur d'une routine de service d'interruption qui nécessite un timing déterministe, le premier accès peut entraîner une latence importante.

Pour une discussion plus approfondie sur l'initialisation paresseuse ou avide dans les systèmes à ressources limitées, voir [FLT:][FLT:][FLT

2. Assurer la sécurité des fils de filetage pour les multiples attaques et les interruptions

Les systèmes embarqués mélangent souvent une boucle principale, des routines de service d'interruption (ISR), et parfois un RTOS (Real-Time Operating System). Lorsqu'un Singleton est accessible à partir de contextes multiples, les conditions de course peuvent corrompre son état, surtout lors de l'initialisation paresseuse. L'exemple classique : deux tâches appellent simultanément , les deux voient , et les deux essaient de configurer le matériel, causant une double initialisation ou la corruption des données.

La sécurité des fils dans les environnements intégrés diffère des systèmes de bureau :

  • Les interruptions ne peuvent pas utiliser de mutexes de blocage – un mutex qui pourrait donner le CPU causera une impasse s'il est appelé d'un IST.
  • RTOS tasks – utilisez un mutex ou un sémaphore pour protéger l'accès Singleton. Si le RTOS supporte l'héritage prioritaire, utilisez-le pour éviter l'inversion prioritaire.
  • Arrière-métal avec programmation coopérative – si le Singleton n'est accessible que depuis la boucle principale, aucune protection supplémentaire n'est nécessaire, mais vérifiez que les RSI n'appellent jamais directement le Singleton.

Exemple: Singleton sans fil avec mutex CMSIS-RTOS

#include "cmsis_os2.h"
#include "singleton.h"

static GPIO_TypeDef* instance = NULL;
static osMutexId_t mutex_id;

void Singleton_Init(void) {
 mutex_id = osMutexNew(NULL);
}

GPIO_TypeDef* Singleton_GetInstance(void) {
 osMutexAcquire(mutex_id, osWaitForever);
 if (instance == NULL) {
 instance = (GPIO_TypeDef*)GPIOA_BASE;
 GPIO_ConfigureInstance(instance);
 }
 osMutexRelease(mutex_id);
 return instance;
}

Dans un RSI, une approche plus sûre consiste à utiliser des opérations atomiques (p. ex. dans GCC) ou une machine sans serrure. Pour simplifier, de nombreux systèmes de production effectuent une initialisation avide avant de permettre des interruptions, évitant ainsi toute adéquation avec les temps d'exécution.

] La documentation de l'ARM CMSIS fournit des lignes directrices pour les périphériques sans fil : [CMSIS-Core (ARM) thread security[.

3. Gardez le poids léger de Singleton – Pas de lourd, pas de constructeur complexe

Les systèmes embarqués ont souvent une mémoire de tas limitée, et de nombreux projets critiques pour la sécurité interdisent complètement l'attribution dynamique (MISRA-C:2012 Règle 21.3). Par conséquent, Singletons devrait être alloué statiquement ou placé dans des régions de mémoire dédiée. Évitez d'utiliser ou , car les erreurs de fragmentation et de mémoire peuvent entraîner des défaillances difficiles à trouver.

L'initialisation de Singleton , devrait être minimale :

  • Conservez une adresse de base ou une poignée du périphérique.
  • Définir les paramètres de configuration par défaut (p. ex. taux de baud, diviseur d'horloge).
  • Ne démarrez pas les périphériques qui consomment de l'énergie avant qu'un client n'appelle explicitement une opération (p. ex. .

En C++, vous pouvez implémenter un simpleton Meyer-Singleton en utilisant une variable locale statique, qui est garantie être créée exactement une fois (C++11 et plus tard garantir l'initialisation statique sans fil). Cependant, sachez que le destructeur ne peut jamais être appelé si le système utilise une boucle , et l'ordre d'initialisation statique entre les unités de traduction peut être difficile.

Exemple d'un Singleton léger en C++ (Meyer) :

class UART {
public:
 static UART& getInstance() {
 static UART instance; // C++11 thread-safe by default
 return instance;
 }
private:
 UART() {
 // lightweight – only store address, no peripheral init
 base_ = (uint32_t*)UART1_BASE;
 }
 uint32_t* base_;
};

Ce modèle utilise une mémoire de tas zéro et n'incombe que le coût d'un pointeur statique et d'un seul contrôle. Cependant, si le constructeur effectue des opérations longues, il devrait être déplacé vers une méthode d'initialisation séparée que l'utilisateur appelle explicitement après la stabilité de l'alimentation.

4. Évitez les dépendances circulaires et le couplage serré

Comme Singletons fournit un accès global, ils peuvent encourager un design "objet dieu" où de nombreux modules récupèrent directement la même instance. Cela rend le code difficile à tester et à maintenir. La meilleure pratique est d'injecter l'interface Singletons (ou pointeur) dans les modules qui en ont besoin, plutôt que de les faire appeler en interne.

Par exemple, au lieu de:

void Sensor_Task(void) {
 UART_Transmit(UART_GetInstance(), "Hello");
}

Préférez :

void Sensor_Task(UART_HandleTypeDef* uart) {
 UART_Transmit(uart, "Hello");
}

Cela découple la tâche du point d'accès de Singleton, ce qui facilite l'injection d'un UART simulé lors des tests.

Pièges fréquents à éviter

Surutilisation de l ' État dans le monde

Dans les systèmes intégrés, cela peut être particulièrement nocif lorsque le Singleton gère des états de puissance ou des interruptions qui affectent d'autres modules. Mitigation: Limiter le nombre de Singletons à un ou deux par sous-système (par exemple, un gestionnaire d'horloge et un enregistreur d'erreur).

Ignorer les contraintes de puissance

Créer un Singleton qui initialise un périphérique de haute puissance pendant le démarrage du système peut gaspiller l'énergie dans des applications à faible cycle. Par exemple, un récepteur GPS Singleton ne devrait être créé que lorsqu'une tâche de navigation est active. Solution: Mettre en œuvre un Singleton « shallow » qui ne tient qu'une poignée et fournit des méthodes d'alimentation/d'alimentation explicites que l'utilisateur appelle au besoin.

Neglecting le nettoyage approprié

Dans de nombreux systèmes embarqués, le Singleton survit à l'application – il n'y a pas de phase de « chute ». Cependant, si le système supporte le chargement dynamique (par exemple, le chargeur de démarrage à l'application) ou la reconfiguration de l'exécution, le Singleton peut avoir besoin de libérer des ressources. Les fuites de mémoire de Singletons sont rares dans l'allocation statique, mais les registres périphériques laissés en état actif peuvent drainer la puissance ou causer des conflits lors de la réinitialisation. Practice: Fournissez une méthode qui réinitialise le matériel et réinitialise en option le drapeau d'instance.

Défis en matière de testabilité

Les appels d'accès à un simpleton (p. ex. ) rendent impossible le remplacement de l'instance par un double test. Cela viole le principe ouvert/fermé et décourage les tests de firmware d'écriture. Approche : Exposer une variable globale ou utiliser un setter pour l'injection pendant les tests (gardé par un .

Le développement par essai pour le C incorporé est couvert par Le développement par essai pour le C embarqué par James W. Grenning (fournit des modèles pour le découplage des monotons).

Modèles de mise en œuvre en C et C++

C – Local statique avec verrouillage explicite

Le modèle C commun utilise une variable statique à l'intérieur d'une fonction, gardée par un mutex ou désactivée. Ceci est simple mais nécessite une manipulation attentive du garde:

// Recommended for single-core with interrupts disabled during init
MyPeripheral* MyPeripheral_GetInstance(void) {
 static MyPeripheral inst;
 static bool initialized = false;
 if (!initialized) {
 // Disable interrupts
 __disable_irq();
 if (!initialized) { // double-check after lock
 MyPeripheral_InitHardware(&inst);
 initialized = true;
 }
 __enable_irq();
 }
 return &inst;
}

Ce modèle de verrouillage double-coché ne fonctionne que si le compilateur ne réordre pas les magasins et si l'architecture garantit des lectures/écritures atomiques pour le drapeau (généralement un mot aligné 32 bits sur ARM Cortex-M). Pour plus de sécurité, utilisez et éventuellement une barrière mémoire.

C++ – Static Local avec constexpr et RAII

Le C++11 Meyer , Singleton, est un thread-safe par la norme, mais soyez conscient que les variables locales statiques peuvent avoir un mécanisme de verrouillage caché qui consomme la pile. Pour les systèmes extrêmement limités, considérez une simple variable de membre statique initialisée avec un constructeur qui est appelé au début dans (initialisation de l'aigueur).

Conclusion

Le modèle Singleton reste un outil utile dans les systèmes embarqués lorsqu'il est appliqué avec discipline. En adhérant à l'initialisation paresseuse avec la sensibilisation à la puissance, en mettant en œuvre la sécurité du filet pour le modèle de proximité cible, et en maintenant une empreinte légère, les développeurs peuvent éviter les pièges classiques de l'état global, des déchets de puissance, et des difficultés de test. Toujours se demander si un Singleton est vraiment nécessaire – souvent une structure globale simple avec des fonctions d'accès explicites peut servir le même but sans la cérémonie supplémentaire.

Traitements clés:

  • Utilisez l'initialisation paresseuse pour conserver les ressources, mais précédez avec empressement pour les chemins critiques dans le temps.
  • Verrouillez le Singleton avec le mécanisme approprié pour votre RTOS ou interrompre l'architecture; ne bloquez jamais à l'intérieur d'un RSI.
  • Évitez l'allocation de tas – préférez la mémoire statique.
  • Conception pour la testabilité en injectant l'interface Singleton , plutôt que d'appeler des accédeurs globaux.
  • Fournir des routines explicites de désinitialisation pour la gestion de l'énergie et la reconfiguration.

Pour en savoir plus: