Verwendung globaler Variablen in eingebetteten Systemen

17

Ich habe angefangen, Firmware für mein Produkt zu schreiben und bin hier ein Anfänger. Ich habe viele Artikel gelesen, in denen es darum ging, keine globalen Variablen oder Funktionen zu verwenden. Gibt es ein Limit für die Verwendung globaler Variablen in einem 8-Bit-System oder ist es ein vollständiges Nein-Nein? Wie soll ich globale Variablen in meinem System verwenden oder vollständig vermeiden?

Ich möchte wertvolle Ratschläge von euch zu diesem Thema entgegennehmen, um meine Firmware kompakter zu machen.

Rookie91
quelle
Diese Frage betrifft nicht nur eingebettete Systeme. Ein Duplikat finden Sie hier .
Lundin
@Lundin Aus Ihrem Link: "Heutzutage ist das nur in eingebetteten Umgebungen von Bedeutung, in denen der Arbeitsspeicher sehr begrenzt ist. Bevor Sie davon ausgehen, dass eingebettet mit anderen Umgebungen identisch ist, sollten Sie davon ausgehen, dass die Programmierregeln in allen Bereichen gleich sind."
Endolith
Der @ endolith- staticDateibereich ist nicht dasselbe wie "global", siehe meine Antwort unten.
Lundin

Antworten:

31

Sie können globale Variablen erfolgreich verwenden, sofern Sie die @ Phil-Richtlinien beachten. Es gibt jedoch einige gute Möglichkeiten, um Probleme zu vermeiden, ohne den kompilierten Code weniger kompakt zu machen.

  1. Verwenden Sie lokale statische Variablen für den dauerhaften Status, auf den Sie nur innerhalb einer Funktion zugreifen möchten.

    #include <stdint.h>
    void skipper()
    {
        static uint8_t skip_initial_cycles = 5;
        if (skip_initial_cycles > 0) {
            skip_initial_cycles -= 1;
            return;
        }
        /* ... */
    }
    
  2. Verwenden Sie eine Struktur, um zusammengehörige Variablen zusammenzuhalten und klarer zu machen, wo sie verwendet werden sollen und wo nicht.

    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. Verwenden Sie globale statische Variablen, um die Variablen nur in der aktuellen C-Datei sichtbar zu machen. Dies verhindert den versehentlichen Zugriff von Code in anderen Dateien aufgrund von Namenskonflikten.

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

Wenn Sie eine globale Variable in einer Interruptroutine ändern und an einer anderen Stelle lesen, gehen Sie wie folgt vor:

  • Markieren Sie die Variable volatile.
  • Stellen Sie sicher, dass es für die CPU atomar ist (dh 8-Bit für eine 8-Bit-CPU).

ODER

  • Verwenden Sie einen Sperrmechanismus, um den Zugriff auf die Variable zu schützen.
richarddonkin
quelle
flüchtige und / oder atomare Variablen helfen Ihnen nicht, Fehler zu vermeiden, Sie benötigen eine Art Sperre / Semaphor oder maskieren Interrupts kurz, wenn Sie in die Variable schreiben.
John U
3
Das ist eine ziemlich enge Definition von "gut funktionieren". Mein Punkt war, dass das Deklarieren von Volatilität keine Konflikte verhindert. Auch Ihr drittes Beispiel ist keine gute Idee - zwei separate Globale mit demselben Namen zu haben, erschwert zumindest das Verständnis / die Pflege des Codes.
John U
1
@JohnU Du solltest nicht volatile verwenden, um Wettkampfbedingungen zu verhindern, das hilft auch nicht. Sie sollten volatile verwenden, um gefährliche Fehler bei der Compileroptimierung zu vermeiden, die bei Compilern für eingebettete Systeme häufig auftreten.
Lundin
2
@JohnU: Die normale Verwendung von volatileVariablen besteht darin, Code, der in einem Ausführungskontext ausgeführt wird, zuzulassen, dass Code in einem anderen Ausführungskontext weiß, dass etwas passiert ist. Auf einem 8-Bit-System kann ein Puffer, der eine Zweierpotenz von maximal 128 Bytes enthalten soll, mit einem flüchtigen Byte verwaltet werden, das die Gesamtlebensdauer der in den Puffer eingegebenen Bytes angibt (Mod. 256) und eine andere gibt die Lebensdauer der herausgenommenen Bytes an, vorausgesetzt, nur ein Ausführungskontext legt Daten im Puffer ab und nur einer entnimmt Daten daraus.
Superkatze
2
@JohnU: Es ist zwar möglich, Sperren zu verwenden oder Interrupts vorübergehend zu deaktivieren, um den Puffer zu verwalten, dies ist jedoch nicht unbedingt erforderlich oder hilfreich. Wenn der Puffer 128-255 Bytes enthalten müsste, müsste sich die Codierung geringfügig ändern, und wenn er mehr als das enthalten müsste, wäre wahrscheinlich das Deaktivieren von Interrupts erforderlich, aber auf einem 8-Bit-System sind die Puffer wahrscheinlich klein; Systeme mit größeren Puffern können im Allgemeinen atomare Schreibvorgänge ausführen, die größer als 8 Bit sind.
Superkatze
24

Die Gründe, warum Sie in einem 8-Bit-System keine globalen Variablen verwenden möchten, sind die gleichen, die Sie auch in keinem anderen System verwenden möchten: Sie erschweren die Überlegung, wie sich das Programm verhält.

Nur schlechte Programmierer haben Probleme mit Regeln wie "Keine globalen Variablen verwenden". Gute Programmierer verstehen den Grund hinter den Regeln und behandeln sie dann eher als Richtlinien.

Ist Ihr Programm leicht zu verstehen? Ist sein Verhalten vorhersehbar? Ist es einfach, Teile davon zu modifizieren, ohne andere Teile zu beschädigen? Wenn die Antwort auf jede dieser Fragen Ja lautet , sind Sie auf dem Weg zu einem guten Programm.

Phil Frost
quelle
1
Was @MichaelKaras gesagt hat - zu verstehen, was diese Dinge bedeuten und wie sie Dinge beeinflussen (und wie sie dich beißen können), ist das Wichtigste.
John U
5

Sie sollten die Verwendung globaler Variablen (kurz "Globals") nicht ganz vermeiden. Aber Sie sollten sie vernünftig verwenden. Die praktischen Probleme bei übermäßigem Gebrauch von Globalen:

  • Globale Werte sind in der gesamten Zusammenstellungseinheit sichtbar. Jeder Code in der Kompilierungseinheit kann ein Global ändern. Die Konsequenzen einer Änderung können überall dort auftauchen, wo dieses Global ausgewertet wird.
  • Globale Zeichen erschweren daher das Lesen und Verstehen des Codes. Der Programmierer muss immer alle Stellen im Auge behalten, an denen das Globale ausgewertet und zugeordnet wird.
  • Übermäßige Verwendung von Globals erhöht die Fehleranfälligkeit des Codes.

Es wird empfohlen g_, dem Namen globaler Variablen ein Präfix hinzuzufügen . Zum Beispiel g_iFlags. Wenn Sie die Variable mit dem Präfix im Code sehen, erkennen Sie sofort, dass es sich um eine globale Variable handelt.

Nick Alexeev
quelle
2
Die Flagge muss nicht global sein. Der ISR könnte beispielsweise eine Funktion aufrufen, die eine statische Variable hat.
Phil Frost
+1 Ich habe noch nie von so einem Trick gehört. Ich habe diesen Absatz aus der Antwort entfernt. Wie würde die staticFlagge für die sichtbar werden main()? Bedeuten Sie, dass die gleiche Funktion, die die hat, staticsie main()später zurückgeben kann?
Nick Alexeev
Das ist eine Möglichkeit, es zu tun. Möglicherweise übernimmt die Funktion das Setzen des neuen Zustands und gibt den alten Zustand zurück. Es gibt viele andere Möglichkeiten. Möglicherweise haben Sie eine Quelldatei mit einer Funktion zum Setzen des Flags und eine andere zum Abrufen, wobei eine statische globale Variable den Flag-Status enthält. Obwohl es sich technisch gesehen um eine "globale" C-Terminologie handelt, ist ihr Geltungsbereich auf die Datei beschränkt, die nur die Funktionen enthält, die bekannt sein müssen. Das heißt, der Geltungsbereich ist nicht "global", was wirklich das Problem ist. C ++ bietet zusätzliche Kapselungsmechanismen.
Phil Frost
4
Einige Methoden zur Vermeidung von globalen Fehlern (z. B. der Zugriff auf Hardware nur über Gerätetreiber) sind möglicherweise für eine 8-Bit-Umgebung mit hohem Ressourcenmangel zu ineffizient.
Spehro Pefhany
1
@SpehroPefhany Gute Programmierer verstehen den Grund hinter den Regeln und behandeln die Regeln eher als Richtlinien. Wenn die Richtlinien in Konflikt stehen, wägt der gute Programmierer die Waage sorgfältig ab.
Phil Frost
4

Der Vorteil globaler Datenstrukturen in eingebetteter Arbeit besteht darin, dass sie statisch sind. Wenn jede Variable, die Sie benötigen, global ist, wird Ihnen nie versehentlich der Speicher ausgehen, wenn Funktionen eingegeben werden und Speicherplatz für sie auf dem Stapel erstellt wird. Aber wozu dann Funktionen? Warum nicht eine große Funktion, die die gesamte Logik und Prozesse abwickelt - wie ein BASIC-Programm ohne GOSUB. Wenn Sie diese Idee weit genug bringen, haben Sie ein typisches Assembler-Programm aus den 1970er Jahren. Effizient und unmöglich zu warten und Fehler zu beheben.

Verwenden Sie also Globals mit Bedacht, wie Statusvariablen (zum Beispiel, wenn jede Funktion wissen muss, ob sich das System im Interpretations- oder Ausführungsstatus befindet) und andere Datenstrukturen, die von vielen Funktionen gesehen werden müssen und wie @PhilFrost sagt, das Verhalten von deine funktionen vorhersehbar? Gibt es eine Möglichkeit, den Stapel mit einer Eingabezeichenfolge zu füllen, die niemals endet? Dies sind Fragen für das Algorithmus-Design.

Beachten Sie, dass Statik innerhalb und außerhalb einer Funktion eine unterschiedliche Bedeutung hat. /programming/5868947/differenz zwischen-static-variable-inside-and-out-of-function

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

C. Towne Springer
quelle
1
Viele Compiler für eingebettete Systeme weisen automatische Variablen statisch zu, überlagern jedoch Variablen, die von Funktionen verwendet werden, die nicht gleichzeitig im Gültigkeitsbereich liegen können. Dies ergibt im Allgemeinen eine Speichernutzung, die der schlechtesten möglichen Nutzung für ein stapelbasiertes System entspricht, in dem tatsächlich alle statisch möglichen Aufrufsequenzen auftreten können.
Supercat
4

Globale Variablen sollten nur für einen wirklich globalen Zustand verwendet werden. Die Verwendung einer globalen Variablen zur Darstellung von beispielsweise dem Breitengrad der Nordgrenze der Karte funktioniert nur, wenn es immer nur eine "Nordgrenze der Karte" geben kann. Wenn der Code in Zukunft möglicherweise mit mehreren Karten mit unterschiedlichen Nordgrenzen arbeiten muss, muss der Code, der eine globale Variable für die Nordgrenze verwendet, wahrscheinlich überarbeitet werden.

In typischen Computeranwendungen gibt es oft keinen besonderen Grund anzunehmen, dass es nie mehr als eine von etwas geben wird. In eingebetteten Systemen sind solche Annahmen jedoch oft weitaus vernünftiger. Während es möglich ist, dass ein typisches Computerprogramm aufgerufen wird, um mehrere gleichzeitige Benutzer zu unterstützen, ist die Benutzeroberfläche eines typischen eingebetteten Systems für die Bedienung durch einen einzelnen Benutzer ausgelegt, der mit seinen Tasten und seinem Display interagiert. Als solches wird es zu jedem Zeitpunkt einen einzelnen Benutzeroberflächenstatus haben. Das System so zu gestalten, dass mehrere Benutzer mit mehreren Tastaturen und Anzeigen interagieren können, würde viel mehr Komplexität und Implementierungszeit erfordern als das Entwerfen für einen einzelnen Benutzer. Wenn das System nie zur Unterstützung mehrerer Benutzer aufgerufen wird, Jeder zusätzliche Aufwand, der zur Erleichterung dieser Nutzung aufgewendet wird, wird vergeudet. Sofern nicht mit einer Mehrbenutzerunterstützung zu rechnen ist, ist es wahrscheinlich sinnvoller, den für eine Einzelbenutzeroberfläche verwendeten Code zu verwerfen, wenn eine Mehrbenutzerunterstützung erforderlich ist, als zusätzliche Zeit für das Hinzufügen von Mehrbenutzerunterstützung aufzuwenden. Benutzerunterstützung, die wahrscheinlich nie benötigt wird.

Ein verwandter Faktor bei eingebetteten Systemen ist, dass in vielen Fällen (insbesondere bei Benutzeroberflächen) die Verwendung mehrerer Threads die einzig praktikable Möglichkeit ist, mehr als eine Funktion zu unterstützen. In Ermangelung eines anderen Multi-Threading-Bedarfs ist es wahrscheinlich besser, ein einfaches Single-Threading-Design zu verwenden, als die Systemkomplexität durch Multi-Threading zu erhöhen, das wahrscheinlich nie wirklich notwendig ist. Wenn das Hinzufügen von mehr als einem Element ohnehin ein umfangreiches Redesign des Systems erfordern würde, spielt es keine Rolle, ob auch die Verwendung einiger globaler Variablen überarbeitet werden muss.

Superkatze
quelle
Es ist also kein Problem, globale Variablen beizubehalten, die nicht miteinander in Konflikt geraten. ZB: Day_cntr, week_cntr usw. zum Zählen der Tage bzw. der Woche. Und ich vertraue darauf, dass man nicht absichtlich viele globale Variablen verwenden sollte, die dem gleichen Zweck ähneln und diese klar definieren müssen. Vielen Dank für die überwältigende Reaktion. :)
Rookie91
-1

Viele Leute sind über dieses Thema verwirrt. Die Definition einer globalen Variablen lautet:

Etwas, auf das von überall in Ihrem Programm zugegriffen werden kann.

Dies ist nicht dasselbe wie Dateibereichsvariablen , die durch das Schlüsselwort deklariert werden static. Dies sind keine globalen Variablen, sondern lokale private Variablen.

 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.

Sollten Sie globale Variablen verwenden? Es gibt einige Fälle, in denen es in Ordnung ist:

In allen anderen Fällen dürfen Sie niemals globale Variablen verwenden. Es gibt nie einen Grund dafür. Verwenden Sie stattdessen Dateibereichsvariablen , was völlig in Ordnung ist.

Sie sollten sich bemühen, unabhängige, autonome Codemodule zu schreiben, die für eine bestimmte Aufgabe entwickelt wurden. Innerhalb dieser Module sollten sich interne Dateibereichsvariablen als private Datenelemente befinden. Diese Entwurfsmethode ist als Objektorientierung bekannt und allgemein als gutes Design anerkannt.

Lundin
quelle
Warum die Gegenstimme?
m.Alin
2
Ich denke, "globale Variable" kann auch verwendet werden, um Zuordnungen zu einem globalen Segment (nicht Text, Stapel oder Heap) zu beschreiben. In diesem Sinne sind / können dateistatische und funktionsstatische Variablen "global" sein. Im Zusammenhang mit dieser Frage ist es ziemlich klar, dass sich global auf das Segment "Umfang nicht Zuteilung" bezieht (obwohl es möglich ist, dass das OP dies nicht wusste).
Paul A. Clayton
1
@ PaulA.Clayton Ich habe noch nie von einem offiziellen Begriff namens "Global Memory Segment" gehört. Ihre Variablen werden an einer der folgenden Stellen abgelegt : Register oder Stack (Laufzeitzuordnung), Heap (Laufzeitzuordnung), .data- Segment (explizit initialisierte statische Speichervariablen), .bss- Segment (statische Speichervariablen mit Nullen ), .rodata (gelesen) -nur Konstanten) oder .text (Teil des Codes). Wenn sie irgendwo anders landen, ist das eine projektspezifische Einstellung.
Lundin
1
@ PaulA.Clayton Ich vermute, dass das, was Sie als "globales Segment" bezeichnen, das .dataSegment ist.
Lundin