Utilisation de variables globales dans les systèmes embarqués

17

J'ai commencé à écrire le firmware de mon produit et je suis une recrue ici. J'ai parcouru de nombreux articles sur la non-utilisation des variables ou fonctions globales. Existe-t-il une limite pour l'utilisation de variables globales dans un système 8 bits ou s'agit-il d'un «non-non» complet? Comment dois-je utiliser des variables globales dans mon système ou dois-je les éviter complètement?

Je voudrais prendre de précieux conseils de votre part à ce sujet pour rendre mon firmware plus compact.

Rookie91
la source
Cette question n'est pas propre aux systèmes embarqués. Un double peut être trouvé ici .
Lundin
@Lundin À partir de votre lien: "Ces jours-ci, cela n'a d'importance que dans les environnements embarqués où la mémoire est assez limitée.
endolith
La staticportée du fichier @endolith n'est pas la même chose que "globale", voir ma réponse ci-dessous.
Lundin

Réponses:

31

Vous pouvez utiliser des variables globales avec succès, tant que vous gardez à l'esprit les directives de @ Phil. Cependant, voici quelques bonnes façons d'éviter leurs problèmes sans rendre le code compilé moins compact.

  1. Utilisez des variables statiques locales pour un état persistant auquel vous ne souhaitez accéder qu'à l'intérieur d'une fonction.

    #include <stdint.h>
    void skipper()
    {
        static uint8_t skip_initial_cycles = 5;
        if (skip_initial_cycles > 0) {
            skip_initial_cycles -= 1;
            return;
        }
        /* ... */
    }
  2. Utilisez une structure pour garder les variables liées ensemble, pour préciser où elles doivent être utilisées et où elles ne le sont pas.

    struct machine_state {
         uint8_t level;
         uint8_t error_code;
    } machine_state;
    
    struct led_state {
        uint8_t red;
        uint8_t green;
        uint8_t blue;
    } led_state;
    
    void machine_change_state()
    {
        machine_state.level += 1;
        /* ... */
        /* We can easily remember not to use led_state in this function. */
    }
    
    void machine_set_io()
    {
        switch (machine_state.level) {
        case 1:
            PIN_MACHINE_IO_A = 1;
            /* ... */
        }
    }
  3. Utilisez des variables statiques globales pour rendre les variables visibles uniquement dans le fichier C actuel. Cela empêche un accès accidentel par le code dans d'autres fichiers en raison de conflits de noms.

    /* time_machine.c */
    static uint8_t current_time;
    /* ... */
    
    /* delay.c */
    static uint8_t current_time; /* A completely separate variable for this C file only. */
    /* ... */

Enfin, si vous modifiez une variable globale dans une routine d'interruption et la lisez ailleurs:

  • Marquez la variable volatile.
  • Assurez-vous qu'il est atomique pour le CPU (c'est-à-dire 8 bits pour un CPU 8 bits).

OU

  • Utilisez un mécanisme de verrouillage pour protéger l'accès à la variable.
richarddonkin
la source
les variables volatiles et / ou atomiques ne vont pas vous aider à éviter les erreurs, vous avez besoin d'une sorte de verrou / sémaphore, ou pour masquer brièvement les interruptions lors de l'écriture dans la variable.
John U
3
C'est une définition assez étroite de «bien fonctionner». Mon point était que déclarer quelque chose de volatile n'empêche pas les conflits. De plus, votre 3e exemple n'est pas une bonne idée - avoir deux globales distinctes avec le même nom rend à tout le moins le code plus difficile à comprendre / à maintenir.
John U
1
@JohnU Vous ne devez pas utiliser de volatile pour éviter les conditions de concurrence, en effet cela n'aidera pas. Vous devez utiliser volatile pour éviter les bogues dangereux d'optimisation du compilateur courants dans les compilateurs de systèmes intégrés.
Lundin
2
@JohnU: L'utilisation normale des volatilevariables est de permettre au code s'exécutant dans un contexte d'exécution de laisser le code dans un autre contexte d'exécution savoir que quelque chose s'est produit. Sur un système 8 bits, un tampon qui va contenir une puissance de deux octets ne dépassant pas 128 peut être géré avec un octet volatile indiquant le nombre total d'octets à vie mis dans le tampon (mod 256) et un autre indiquant le nombre de vie d'octets extraits, à condition qu'un seul contexte d'exécution place les données dans le tampon et qu'un seul en retire les données.
supercat
2
@JohnU: Bien qu'il soit possible d'utiliser une forme de verrouillage ou de désactiver temporairement les interruptions pour gérer le tampon, ce n'est vraiment pas nécessaire ni utile. Si le tampon devait contenir 128 à 255 octets, le codage devrait changer légèrement, et s'il devait en contenir plus, la désactivation des interruptions serait probablement nécessaire, mais sur un système 8 bits, les tampons sont susceptibles d'être petits; les systèmes avec des tampons plus gros peuvent généralement effectuer des écritures atomiques de plus de 8 bits.
supercat
24

Les raisons pour lesquelles vous ne voudriez pas utiliser des variables globales dans un système 8 bits sont les mêmes que vous ne voudriez pas les utiliser dans un autre système: elles rendent difficile le raisonnement sur le comportement du programme.

Seuls les mauvais programmeurs se bloquent sur des règles comme "n'utilisez pas de variables globales". Les bons programmeurs comprennent la raison derrière les règles, puis traitent les règles plus comme des directives.

Votre programme est-il facile à comprendre? Son comportement est-il prévisible? Est-il facile d'en modifier des parties sans casser d'autres parties? Si la réponse à chacune de ces questions est oui , alors vous êtes sur la bonne voie pour un bon programme.

Phil Frost
la source
1
Ce que @MichaelKaras a dit - comprendre ce que ces choses signifient et comment elles affectent les choses (et comment elles peuvent vous mordre) est la chose importante.
John U
5

Vous ne devez pas complètement éviter d'utiliser des variables globales ("globals" pour faire court). Mais, vous devez les utiliser judicieusement. Les problèmes pratiques d'une utilisation excessive des globaux:

  • Les globaux sont visibles dans toute l'unité de compilation. Tout code dans l'unité de compilation peut modifier un global. Les conséquences d'une modification peuvent apparaître n'importe où où ce global est évalué.
  • En conséquence, les globaux rendent le code plus difficile à lire et à comprendre. Le programmeur doit toujours garder à l'esprit tous les endroits où le global est évalué et attribué.
  • Une utilisation excessive des globaux rend le code plus sujet aux défauts.

Il est recommandé d'ajouter un préfixe g_au nom des variables globales. Par exemple,g_iFlags ,. Lorsque vous voyez la variable avec le préfixe dans le code, vous reconnaissez immédiatement qu'il s'agit d'un global.

Nick Alexeev
la source
2
Le drapeau n'a pas besoin d'être un global. L'ISR pourrait appeler une fonction qui a une variable statique, par exemple.
Phil Frost
+1 Je n'ai jamais entendu parler d'un tel truc auparavant. J'ai supprimé ce paragraphe de la réponse. Comment le staticdrapeau deviendrait-il visible pour le main()? Voulez-vous dire que la même fonction qui a le staticpeut le renvoyer à la main()dernière?
Nick Alexeev
C'est une façon de procéder. Peut-être que la fonction prend le nouvel état à définir et renvoie l'ancien état. Il existe de nombreuses autres façons. Peut-être avez-vous un fichier source avec une fonction pour définir l'indicateur, et un autre pour l'obtenir, avec une variable globale statique contenant l'état de l'indicateur. Bien qu'il s'agisse techniquement d'une terminologie «globale» selon C, sa portée est limitée à ce seul fichier, qui ne contient que les fonctions à connaître. Autrement dit, sa portée n'est pas «globale», ce qui est vraiment le problème. C ++ fournit des mécanismes d'encapsulation supplémentaires.
Phil Frost
4
Certaines méthodes pour éviter les globaux (telles que l'accès au matériel uniquement via des pilotes de périphérique) peuvent être trop inefficaces pour un environnement 8 bits gravement privé de ressources.
Spehro Pefhany
1
@SpehroPefhany Les bons programmeurs comprennent la raison des règles, puis les traitent plus comme des directives. Lorsque les directives sont en conflit, le bon programmeur pèse soigneusement la balance.
Phil Frost
4

L'avantage des structures de données globales dans le travail intégré est qu'elles sont statiques. Si chaque variable dont vous avez besoin est globale, vous ne manquerez jamais accidentellement de mémoire lorsque des fonctions sont entrées et que de l'espace est prévu pour elles sur la pile. Mais alors, à ce moment-là, pourquoi avoir des fonctions? Pourquoi pas une grande fonction qui gère toute la logique et les processus - comme un programme BASIC sans GOSUB autorisé. Si vous prenez cette idée assez loin, vous aurez un programme de langage d'assemblage typique des années 1970. Efficace et impossible à entretenir et à dépanner.

Utilisez donc les globaux judicieusement, comme les variables d'état (par exemple, si chaque fonction a besoin de savoir si le système est dans un état d'interprétation ou d'exécution) et d'autres structures de données qui doivent être vues par de nombreuses fonctions et, comme le dit @PhilFrost, c'est le comportement de vos fonctions prévisibles? Est-il possible de remplir la pile avec une chaîne d'entrée qui ne se termine jamais? Ce sont des questions pour la conception d'algorithmes.

Notez que statique a une signification différente à l'intérieur et à l'extérieur d'une fonction. /programming/5868947/difference-between-static-variable-inside-and-outside-of-a-function

/programming/5033627/static-variable-inside-of-a-function-in-c

C. Towne Springer
la source
1
De nombreux compilateurs de systèmes intégrés allouent des variables automatiques de manière statique, mais superposent des variables utilisées par des fonctions qui ne peuvent pas être dans le champ d'application simultanément; cela donne généralement une utilisation de la mémoire qui est égale à l'utilisation la plus défavorable possible pour un système basé sur la pile dans lequel toutes les séquences d'appels statiquement possibles peuvent en fait se produire.
supercat
4

Les variables globales ne doivent être utilisées que pour un état véritablement global. L'utilisation d'une variable globale pour représenter quelque chose comme par exemple la latitude de la limite nord de la carte ne fonctionnera que s'il ne peut jamais y avoir qu'une seule "limite nord de la carte". Si à l'avenir le code pourrait devoir fonctionner avec plusieurs cartes ayant des limites nord différentes, le code qui utilise une variable globale pour la limite nord devra probablement être retravaillé.

Dans les applications informatiques typiques, il n'y a souvent aucune raison particulière de supposer qu'il n'y aura jamais plus d'un élément. Dans les systèmes embarqués, cependant, ces hypothèses sont souvent beaucoup plus raisonnables. Bien qu'il soit possible qu'un programme informatique typique soit appelé à prendre en charge plusieurs utilisateurs simultanés, l'interface utilisateur d'un système embarqué typique sera conçue pour être utilisée par un seul utilisateur interagissant avec ses boutons et son écran. En tant que tel, il aura à tout moment un état d'interface utilisateur unique. Concevoir le système de manière à ce que plusieurs utilisateurs puissent interagir avec plusieurs claviers et écrans nécessiterait beaucoup plus de complexité et prendrait beaucoup plus de temps à mettre en œuvre que de le concevoir pour un seul utilisateur. Si le système n'est jamais appelé à prendre en charge plusieurs utilisateurs, tout effort supplémentaire investi pour faciliter une telle utilisation sera gaspillé. Sauf s'il est probable qu'une assistance multi-utilisateurs sera nécessaire, il serait probablement plus sage de risquer d'avoir à supprimer le code utilisé pour une interface mono-utilisateur dans le cas où une assistance multi-utilisateurs est nécessaire, que de passer plus de temps à ajouter plusieurs support utilisateur qui ne sera probablement jamais nécessaire.

Un facteur connexe avec les systèmes embarqués est que dans de nombreux cas (impliquant notamment des interfaces utilisateur), le seul moyen pratique de prendre en charge plusieurs éléments serait d'utiliser plusieurs threads. En l'absence d'un autre besoin de multi-threading, il est probablement préférable d'utiliser une conception simple à un seul thread que d'augmenter la complexité du système avec un multi-threading qui n'est probablement jamais vraiment nécessaire. Si ajouter plus d'un élément nécessiterait de toute façon une énorme refonte du système, cela n'aura pas d'importance si cela nécessite également de retravailler l'utilisation de certaines variables globales.

supercat
la source
Donc, garder des variables globales qui ne se heurteront pas ne sera pas un problème. par exemple: Day_cntr, week_cntr etc. pour compter les jours et la semaine respectivement.Et j'espère que l'on ne devrait pas délibérément utiliser beaucoup de variables globales qui ressemblent au même objectif et doivent clairement les définir.Merci beaucoup pour la réponse écrasante. :)
Rookie91
-1

Beaucoup de gens sont confus à ce sujet. La définition d'une variable globale est:

Quelque chose qui est accessible de partout dans votre programme.

Ce n'est pas la même chose que les variables de portée de fichier , qui sont déclarées par le mot-clé static. Ce ne sont pas des variables globales, ce sont des variables privées locales.

 int x; // global variable
 static int y; // file scope variable

 void some_func (void) {...} // added to demonstrate that the variables above are at file scope.

Devriez-vous utiliser des variables globales? Il y a quelques cas où ça va:

Dans tous les autres cas, vous n'utiliserez jamais de variables globales. Il n'y a jamais de raison de le faire. Utilisez plutôt des variables d'étendue de fichier , ce qui est parfaitement correct.

Vous devez vous efforcer d'écrire des modules de code indépendants et autonomes conçus pour effectuer une tâche spécifique. À l'intérieur de ces modules, les variables de portée de fichier interne doivent résider en tant que membres de données privées. Cette méthode de conception est connue sous le nom d’orientation objet et est largement reconnue comme une bonne conception.

Lundin
la source
Pourquoi le downvote?
m.Alin
2
Je pense que la «variable globale» peut également être utilisée pour décrire les allocations à un segment global (pas le texte, la pile ou le tas). En ce sens, les variables statiques de fichier et de fonction statique sont / peuvent être "globales". Dans le contexte de cette question, il est quelque peu clair que global fait référence au périmètre et non au segment d'allocation (bien qu'il soit possible que le PO l'ignore).
Paul A. Clayton
1
@ PaulA.Clayton Je n'ai jamais entendu parler d'un terme officiel appelé "segment de mémoire globale". Vos variables se retrouveront à l'un des emplacements suivants: registres ou pile (allocation d'exécution), tas (allocation d'exécution), segment .data (variables de stockage statique explicitement initialisées), segment .bss (variables de stockage statique mises à zéro), .rodata (lecture -seules constantes) ou .text (partie du code). S'ils se retrouvent ailleurs, c'est un paramètre spécifique au projet.
Lundin
1
@ PaulA.Clayton Je soupçonne que ce que vous appelez "segment global" est le .datasegment.
Lundin