Verwendung globaler Variablen in eingebetteten Systemen

Ich habe angefangen, Firmware für mein Produkt zu schreiben, und ich bin hier ein Neuling. Ich habe viele Artikel darüber gelesen, wie man keine globalen Variablen oder Funktionen verwendet. Gibt es eine Begrenzung für die Verwendung globaler Variablen in einem 8-Bit-System oder ist es ein komplettes "Nein-Nein". Wie sollte ich globale Variablen in meinem System verwenden oder sollte ich sie vollständig vermeiden?

Ich würde gerne wertvolle Ratschläge von euch zu diesem Thema annehmen, um meine Firmware kompakter zu machen.

Diese Frage betrifft nicht nur eingebettete Systeme. Ein Duplikat finden Sie hier .
@Lundin Von Ihrem Link: "Heutzutage ist das nur in eingebetteten Umgebungen von Bedeutung, in denen der Speicher ziemlich begrenzt ist. Etwas, das Sie wissen sollten, bevor Sie davon ausgehen, dass eingebettete Umgebungen mit anderen Umgebungen identisch sind, und davon ausgehen, dass die Programmierregeln überall gleich sind."
@endolith staticfile scope ist nicht dasselbe wie "global", siehe meine Antwort unten.

Antworten (6)

Sie können globale Variablen erfolgreich verwenden, solange Sie die Richtlinien von @Phil beachten. Hier sind jedoch einige nette Möglichkeiten, ihre Probleme zu vermeiden, ohne den kompilierten Code weniger kompakt zu machen.

  1. Verwenden Sie lokale statische Variablen für den dauerhaften Zustand, 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 verwandte Variablen zusammenzuhalten, um klarer zu machen, wo sie verwendet werden sollten 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 innerhalb der aktuellen C-Datei sichtbar zu machen. Dadurch wird ein versehentlicher Zugriff durch Code in anderen Dateien aufgrund von Namenskonflikten verhindert.

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

Als letzte Anmerkung, wenn Sie eine globale Variable innerhalb einer Interrupt-Routine ändern und an anderer Stelle lesen:

  • 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.
flüchtige und/oder atomare Variablen helfen Ihnen nicht, Fehler zu vermeiden, Sie brauchen eine Art Sperre/Semaphor oder um Interrupts beim Schreiben in die Variable kurz zu maskieren.
Flüchtige atomare Variablen funktionieren perfekt in 8-Bit-CPUs, wenn sie nur in einem Kontext geschrieben und nur in anderen Kontexten gelesen werden. Nur wenn Sie mehrere Schreiber haben, müssen Sie einen Sperrmechanismus verwenden oder Interrupts deaktivieren. Das Sperren ist jedoch normalerweise erforderlich, da die relevanten Daten mehr sind, als in einen atomaren Variablenzugriff passen.
Das ist eine ziemlich enge Definition von "gut funktionieren". Mein Punkt war, dass das Deklarieren von etwas Flüchtigem keine Konflikte verhindert. Außerdem ist Ihr drittes Beispiel keine gute Idee - zwei separate Globals mit demselben Namen zu haben, macht den Code zumindest schwieriger zu verstehen / zu warten.
@richarddonkin Vielen Dank. Aber ich habe einen Zweifel bezüglich Punkt 3. Werden die beiden globalen statischen Variablen als getrennt behandelt, wenn wir sie in zwei separaten Quelldateien im selben Projekt initialisieren. Ohk, dann muss der Compiler Quelldateien als Module behandeln und solche Dinge separat behandeln, oder? Korrigieren Sie mich, wenn ich falsch liege.
@Rookie91 Ja, der Compiler behandelt jede C-Quelldatei als Kompilierungs-"Modul", und globale statische Variablen sind in ihren jeweiligen "Modulen" enthalten. Dies gilt jedoch nur für statische globale Variablen. Außerdem versuchen Sie, wie @John kommentierte, normalerweise nicht, verschiedene globale Variablen mit demselben Namen zu benennen. es passiert versehentlich. Das Markieren staticverhindert, dass der Compiler sie verwirrt, wenn Sie diesen Fehler machen.
@richarddonkin Vielen Dank, dass Sie mich dazu gebracht haben, es gut zu verstehen.
Gern geschehen. (Bitte markieren Sie die Antwort als Antwort, wenn Sie der Meinung sind, dass dies die Antwort auf Ihre Frage ist.)
@JohnU Sie sollten volatile nicht verwenden, um Rennbedingungen zu verhindern, das hilft in der Tat nicht. Sie sollten volatile verwenden, um gefährliche Compiler-Optimierungsfehler zu vermeiden, die häufig in Compilern für eingebettete Systeme auftreten.
@JohnU: Die normale Verwendung von volatileVariablen besteht darin, Code, der in einem Ausführungskontext ausgeführt wird, zu ermöglichen, Code in einem anderen Ausführungskontext wissen zu lassen, dass etwas passiert ist. Auf einem 8-Bit-System kann ein Puffer, der eine Zweierpotenz von Bytes von nicht mehr als 128 halten wird, mit einem flüchtigen Byte verwaltet werden, das die Gesamtlebensdauer der Bytes angibt, die in den Puffer gelegt werden (mod 256) und ein anderer zeigt die Lebenszeitzahl der herausgenommenen Bytes an, vorausgesetzt, dass nur ein Ausführungskontext Daten in den Puffer legt und nur einer Daten daraus entnimmt.
@JohnU: Es ist zwar möglich, eine Art Sperre zu verwenden oder Interrupts vorübergehend zu deaktivieren, um den Puffer zu verwalten, aber es ist wirklich nicht notwendig oder hilfreich. Wenn der Puffer 128-255 Bytes enthalten müsste, müsste die Codierung geringfügig geändert werden, und wenn er mehr als das enthalten müsste, wäre wahrscheinlich das Deaktivieren von Interrupts erforderlich, aber auf einem 8-Bit-System sind Puffer eher klein; Systeme mit größeren Puffern können im Allgemeinen atomare Schreibvorgänge mit mehr als 8 Bit ausführen.

Die Gründe, warum Sie in einem 8-Bit-System keine globalen Variablen verwenden möchten, sind die gleichen, aus denen Sie sie in keinem anderen System verwenden möchten: Sie erschweren das Nachdenken über das Verhalten des Programms.

Nur schlechte Programmierer hängen sich an Regeln wie "keine globalen Variablen verwenden" auf. Gute Programmierer verstehen den Grund hinter den Regeln und behandeln die Regeln dann eher wie Richtlinien.

Ist Ihr Programm leicht verständlich? 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 ist , dann sind Sie auf dem Weg zu einem guten Programm.

Was @MichaelKaras gesagt hat – es ist wichtig zu verstehen, was diese Dinge bedeuten und wie sie Dinge beeinflussen (und wie sie dich beißen können).
Könnte kein schlechter Programmierer sein, könnte ein Programmierer sein, der Einschränkungen von externen Stellen (z. B. Regierungen und anderen Regulierungsbehörden) hat, die erfordern, dass keine globalen Variablen für das Projekt verwendet werden, an dem sie arbeiten ... und sicherlich möchten Sie globale Variablen immer MINIMIEREN und verwenden Sie sie nur bei Bedarf

Auf die Verwendung globaler Variablen (kurz "Globals") sollten Sie nicht ganz verzichten. Aber Sie sollten sie mit Bedacht einsetzen. Die praktischen Probleme bei exzessivem Einsatz von Globals:

  • Globals sind in der gesamten Kompilationseinheit sichtbar. Jeder Code in der Kompilierungseinheit kann eine globale ändern. Die Folgen einer Änderung können überall dort auftauchen, wo dieses Global ausgewertet wird.
  • Infolgedessen erschweren Globals das Lesen und Verstehen des Codes. Der Programmierer muss immer alle Stellen im Auge behalten, an denen das Global ausgewertet und zugewiesen wird.
  • Die übermäßige Verwendung von Globals macht den Code fehleranfälliger.

g_Es empfiehlt sich, 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.

Die Flagge muss nicht global sein. Die ISR könnte zum Beispiel eine Funktion aufrufen, die eine statische Variable hat.
+1 Ich habe noch nie von einem solchen 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 dieselbe Funktion, die das hat static, es main()später zurückgeben kann?
Das ist eine Möglichkeit, es zu tun. Vielleicht nimmt die Funktion den neuen Zustand zum Setzen und gibt den alten Zustand zurück. Es gibt viele andere Möglichkeiten. Vielleicht haben Sie eine Quelldatei mit einer Funktion zum Setzen des Flags und eine andere zum Abrufen mit einer statischen globalen Variablen, die den Flag-Status enthält. Obwohl dies technisch gesehen eine "globale" C-Terminologie ist, ist ihr Geltungsbereich auf genau diese Datei beschränkt, die nur die Funktionen enthält, die sie kennen müssen. Das heißt, sein Geltungsbereich ist nicht "global", was wirklich das Problem ist. C++ bietet zusätzliche Kapselungsmechanismen.
Einige Methoden zur Vermeidung von Globals (z. B. Zugriff auf Hardware nur über Gerätetreiber) sind möglicherweise zu ineffizient für eine 8-Bit-Umgebung mit starker Ressourcenknappheit.
@SpehroPefhany Gute Programmierer verstehen den Grund hinter den Regeln und behandeln die Regeln dann eher wie Richtlinien. Wenn die Richtlinien widersprüchlich sind, wägt der gute Programmierer sorgfältig ab.

Der Vorteil globaler Datenstrukturen in eingebetteten Arbeiten besteht darin, dass sie statisch sind. Wenn jede Variable, die Sie brauchen, global ist, dann wird Ihnen nie versehentlich der Speicher ausgehen, wenn Funktionen eingegeben werden und Platz für sie auf dem Stack geschaffen wird. Aber warum haben sie dann an diesem Punkt Funktionen? Warum nicht eine große Funktion, die die gesamte Logik und alle Prozesse handhabt - wie ein BASIC-Programm ohne erlaubtes GOSUB. Wenn Sie diese Idee weit genug treiben, haben Sie ein typisches Assemblerprogramm aus den 1970er Jahren. Effizient und unmöglich zu warten und Fehler zu beheben.

Verwenden Sie also Globals mit Bedacht, wie Zustandsvariablen (z. B. wenn jede Funktion wissen muss, ob sich das System im Interpretations- oder Ausführungszustand befindet) und andere Datenstrukturen, die von vielen Funktionen gesehen werden müssen und wie @PhilFrost sagt, das Verhalten von ist Ihre Funktionen vorhersehbar? Gibt es eine Möglichkeit, den Stapel mit einem Eingabestring zu füllen, der niemals endet? Dies sind Angelegenheiten für den Entwurf von Algorithmen.

Beachten Sie, dass Statik innerhalb und außerhalb einer Funktion unterschiedliche Bedeutung hat. https://stackoverflow.com/questions/5868947/difference-between-static-variable-inside-and-outside-of-a-function

https://stackoverflow.com/questions/5033627/static-variable-inside-of-a-function-in-c

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 sein können. dies ergibt im Allgemeinen eine Speicherauslastung, die der ungünstigstmöglichen Auslastung für ein Stack-basiertes System entspricht, in dem tatsächlich alle statisch möglichen Aufrufsequenzen auftreten können.

Globale Variablen sollten nur für wirklich globale Zustände verwendet werden. Die Verwendung einer globalen Variablen, um so etwas wie zB den Breitengrad der nördlichen Kartengrenze darzustellen, funktioniert nur, wenn es immer nur eine "nördliche Kartengrenze" geben kann. Wenn der Code in Zukunft mit mehreren Karten arbeiten muss, die unterschiedliche nördliche Grenzen haben, muss Code, der eine globale Variable für die nördliche Grenze verwendet, wahrscheinlich überarbeitet werden.

Bei 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 viel vernünftiger. Während es möglich ist, dass ein typisches Computerprogramm aufgerufen wird, um mehrere gleichzeitige Benutzer zu unterstützen, ist die Benutzerschnittstelle eines typischen eingebetteten Systems für die Bedienung durch einen einzelnen Benutzer ausgelegt, der mit seinen Tasten und seiner Anzeige interagiert. Als solches wird es zu jedem Zeitpunkt einen einzigen Benutzerschnittstellenzustand haben. Das System so zu entwerfen, dass mehrere Benutzer mit mehreren Tastaturen und Displays interagieren könnten, würde viel mehr Komplexität erfordern und viel länger in der Implementierung dauern, als es für einen einzelnen Benutzer zu entwerfen. Wenn das System nie aufgefordert wird, mehrere Benutzer zu unterstützen, jeder zusätzliche Aufwand, der investiert wird, um eine solche Verwendung zu erleichtern, wird verschwendet. Sofern es nicht wahrscheinlich ist, dass Mehrbenutzerunterstützung erforderlich ist, wäre es wahrscheinlich klüger, das Risiko einzugehen, den für eine Einzelbenutzeroberfläche verwendeten Code verwerfen zu müssen, falls Mehrbenutzerunterstützung erforderlich ist, als zusätzliche Zeit damit zu verbringen, Mehrbenutzer hinzuzufügen. Benutzerunterstützung, die wahrscheinlich nie benötigt wird.

Ein verwandter Faktor bei eingebetteten Systemen ist, dass in vielen Fällen (insbesondere bei Benutzerschnittstellen) die einzige praktische Möglichkeit, mehr als einen von etwas zu unterstützen, darin besteht, mehrere Threads zu verwenden. In Ermangelung einer anderen Notwendigkeit für Multi-Threading ist es wahrscheinlich besser, ein einfaches Single-Threading-Design zu verwenden, als die Systemkomplexität mit Multi-Threading zu erhöhen, das wahrscheinlich nie wirklich notwendig ist. Wenn das Hinzufügen von mehr als einem von etwas sowieso eine riesige Neugestaltung des Systems erfordern würde, spielt es keine Rolle, ob es auch eine Überarbeitung der Verwendung einiger globaler Variablen erfordert.

Es ist also kein Problem, globale Variablen zu behalten, die nicht miteinander kollidieren. zB: Day_cntr , week_cntr usw. zum Zählen von Tagen bzw. Wochen. Und ich vertraue darauf, dass man nicht absichtlich viele globale Variablen verwenden sollte, die demselben Zweck ähneln, und diese klar definieren muss. Vielen Dank für die überwältigende Antwort.:)

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

Etwas, das von überall in Ihrem Programm zugänglich ist.

Dies ist nicht dasselbe wie Dateibereichsvariablen , die durch das Schlüsselwort deklariert werden static. Das 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 ein paar 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 vollkommen 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 Datenmitglieder befinden. Diese Entwurfsmethode ist als Objektorientierung bekannt und weithin als gutes Design anerkannt.

Warum die Ablehnung?
Ich denke, "globale Variable" kann auch verwendet werden, um Zuordnungen zu einem globalen Segment (nicht Text, Stack oder Heap) zu beschreiben. In diesem Sinne sind/können statische Datei- und Funktionsvariablen "global" sein. Im Zusammenhang mit dieser Frage ist etwas klar, dass sich global auf den Bereich und nicht auf das Zuordnungssegment bezieht (obwohl es möglich ist, dass das OP dies nicht wusste).
@PaulA.Clayton Ich habe noch nie von einem formalen Begriff namens "globales Speichersegment" gehört. Ihre Variablen landen an einem der folgenden Orte: Register oder Stack (Laufzeitzuweisung), Heap (Laufzeitzuweisung), .data- Segment (explizit initialisierte statische Speichervariablen), .bss- Segment (genullte statische Speichervariablen), .rodata (read -nur Konstanten) oder .text (Teil des Codes). Wenn sie irgendwo anders landen, ist das eine projektspezifische Einstellung.
@PaulA.Clayton Ich vermute, dass das, was Sie als "globales Segment" bezeichnen, das .dataSegment ist.