Globale Variablen vs. Zeiger im eingebetteten Design [geschlossen]

Ich habe ein paar eingebettete 8-Bit-Systeme geschrieben, und die Codebasis, die ich geerbt und erweitert habe, besteht im Grunde zu 80 % aus globalen Variablen (extern flüchtig) und dann nach Bedarf aus nicht globalen Steuerflags und logischen Variablen.

Das Endergebnis ist, dass Sie am Ende viele void() -Funktionen haben, um die globalen Variablen zu ändern.

Die Systeme laufen gut und die Software ist gut lesbar und leicht zu bearbeiten, aber ich habe immer diesen Designnörgel im Hinterkopf, dass ich alles umgestalten und auf das nächste Design verweisen sollte.

Ich habe keine Religiosität in Bezug auf den Aspekt der Speichernutzung, der statische Speicher ist dazu da, genauso viel zu verwenden wie der Heap. Es ist mehr eine Frage, wenn die Systeme größer werden, vermute ich, dass die Globals vielleicht anfangen, ein größeres Hindernis zu sein, als Sie denken.

Verwenden viele Ihrer erfahrenen Embedded-Software-Programmierer in Ihren 8-Bit-Systemen ausgiebig Zeiger?

Wir verwenden keine verknüpften Listen oder irgendetwas Ausgefeiltes, wo Sie wirklich Zeiger benötigen und um Speicher dynamisch zuzuweisen. Ich könnte das nächste System sehen, das ich schreibe, ich verwende Strukturen anstelle von Variablen, um Informationen etwas logischer zu gruppieren, und Sie würden Zeiger verwenden, anstatt die Strukturen zu ändern.

Außerdem können die Funktionen wiederverwendet werden, wenn Sie Ihren Code verweisen, aber in gewisser Weise sind die Funktionen ziemlich trivial und in den eingebetteten Systemen, die ich gebaut habe, einmalig. Sehr anwendungsspezifisches Zeug.

FÜR DIE NACHFOLGERSCHAFT: Auch im ARM KEIL C51 Forum gepostet

DA DER POST EIN BISSCHEN GING 8051 BETONUNG: BESTER C51-COMPILER-LEITFADEN, DER JEMALS GESCHRIEBEN WURDE

Gegebenenfalls werden Zeiger verwendet. Die Frage ist sowohl unklar, zu weit gefasst als auch meinungsbasiert.
Wo ist angemessen?
Siehe den zweiten Teil des Kommentars. Dies ist etwas, was Sie normalerweise lernen, wenn Sie Programmieren lernen und etwas Erfahrung sammeln.
Klingt nach einer ziemlich religiösen Antwort. "Mein Sohn, die Sonne wird scheinen und der Herr wird sich zeigen - und du wirst wissen, wann die 'Angemessenheit' angekommen ist". Super herablassend. Ich könnte das Ganze mallocieren und auf das Ganze verweisen, ich glaube nicht, dass es einen Gewinn bringt.
Dies wird durch die Sache „zu weit gefasst“ und „meinungsbasiert“ abgedeckt. Man kann es „religiös“ und „herablassend“ nennen.
Sie sagen, dass Probleme auftreten könnten, wenn das System größer wird. Die meisten eingebetteten Systeme werden jedoch aufgrund der begrenzten verfügbaren Ressourcen nicht zu groß. Ich würde globale Variablen vermeiden, wenn ich ein eingebettetes Linux-System programmiere, sehe aber kein ernsthaftes Problem damit in einem Bare-Metal-System mit wenigen kB.
Ich kann nicht einmal ansatzweise zählen, wie oft ich das monatelang debattiert gesehen habe. Eine der wichtigeren Fragen, die ich stellen muss, wenn ich sehe, dass jemand so etwas anfängt, lautet: „Was betrachten Sie als ‚eingebettete Programmierung‘ und wie grenzen Sie es von anderer Programmierung ab?“ Die Antwort darauf ist oft recht aufschlussreich.
Ich denke, Photon hat eine gute Abgrenzung, wenige kB Speicher sind ein guter Ausgangspunkt für die Einbettung. Ein fehlendes Betriebssystem ist ein weiterer guter Indikator. Persönlich habe ich an kleinen 8-Bit-PICs und 8051s gearbeitet. Wenn Sie jetzt ein wirklich einfaches RTOS auf einem 32-Bit-System ausführen, wissen Sie, ob das Teil des heiligen Kanons eingebetteter Geräte ist.
@ Leroy105 Einige scheinen eingebettet zu definieren als "nur eine andere Art zu sagen, dass jede Programmierung auf ein Produkt abzielt, das ein Benutzer nicht als Allzweckgerät zum Ausführen von Programmen verwendet." Das macht die Definition zu einer Frage dessen, was ein Benutzer denkt/macht, und eliminiert alle sehr wichtigen Unterscheidungen, die für Programmierer selbst wichtig sind. Für mich sind es die Vielfalt der erforderlichen Tools und die Vielfalt der persönlichen und unterschiedlichen Fähigkeiten und Kenntnisse, die vom Programmierer (nicht nur über das Programmieren) verlangt werden, die den Unterschied ausmachen. (Ich erwarte zum Beispiel gute Linker-Kenntnisse.)
Es gibt auch in der eingebetteten Programmierung Argumente dafür, die Verwendung von Zeigern zu minimieren oder sogar ganz zu eliminieren – insbesondere dort, wo es auf Zuverlässigkeit ankommt. MISRA-C zum Beispiel schränkt die Verwendung von Zeigern ein.

Antworten (2)

Ich denke, ich kann sehen, woher Sie kommen. (Nachdem ich Ihre Kommentare gelesen habe.) Dieser ist für mich aussagekräftiger:

... sehen Sie Menschen, die Software entwickeln und in diesen winzigen eingebetteten Systemen einfach Globals um jeden Preis vermeiden? Du kannst die Katze so oder so häuten, die Globals scheinen mir schneller zu sein.

(Ich stelle mir vorerst auch die Kerne der Serien 8051/8031/8052/8032 vor.)

Nehmen wir eine sehr einfache Methode zum Erstellen von Betriebssystemwarteschlangen für ein sehr einfaches Betriebssystem. Wir brauchen mindestens eine Bereitschaftswarteschlange und eine Schlafwarteschlange. (Wir könnten Semaphor-Warteschlangen hinzufügen, aber lassen Sie uns das nicht, weil ich dies auf ein absolutes Minimum beschränken möchte.) Wir möchten auch eine begrenzte Anzahl von Prozessen unterstützen. (Schließlich ist dies eine kleine MCU ohne viel verfügbaren Speicher.)


Lassen Sie uns zunächst ein paar Konstanten definieren:

#define NPROC 10
#define TAILPRIORITY 10000  /* you will see the need later */

Lassen Sie uns für die Warteschlangen einen Linked-List-Ansatz verfolgen und die Tatsache erkennen, dass wir das Einfügen und Löschen benötigen, und möchten diese Operationen ziemlich einfach zu erreichen machen. Daraus schließen wir, dass wir sowohl next- als auch prev- Zeiger für jeden Warteschlangeneintrag haben wollen. Außerdem wollen wir einen Schwerpunktbereich unterstützen:

typedef struct proc_s proc_t;
typedef struct proc_s {
    int priority;
    proc_t *next;
    proc_t *prev;
} proc_t;

Jetzt können wir dies einfach statisch definieren (der Geltungsbereich könnte auf Dateiebene gehalten werden):

proc_t readyhead, readytail, sleephead, sleeptail, freehead, freetail, proc[NPROC];

Schauen wir uns nun an, was zum Entfernen und Einfügen eines Prozesses in eine Warteschlange erforderlich ist (Beachten Sie, dass die Warteschlangenvariablen ready und sleep vollständige proc_t-Typen sind, nicht nur Zeiger auf sie. Dies vereinfacht den folgenden Code.) getfirst", weil wir das für einige Zwecke brauchen.

/* Assumes that 'item' actually resides within a queue. */
proc_t * remove( proc_t * item ) {
    item->prev->next= item->next;
    item->next->prev= item->prev;
    return item;
}
/* Not to be used if 'item' is already in another queue. */
/* Priority insertion assumes that all queues end in a tail */
proc_t * insert( proc_t * queue, proc_t * item, int priority ) {
    proc_t *n, *p;
    for ( n= queue->next; n->priority < priority; n= n->next ) ;
    item->next= n;
    item->prev= p= n->prev;
    item->priority= priority;
    p->next= item;
    n->prev= item;
    return item;
}
proc_t * getfirst( proc_t * queue ) {
    if ( queue->next->priority == TAILPRIORITY )
        return NULL;
    return remove( queue->next );
}

All das obige setzt natürlich voraus, dass die Anfangs- und Endzeiger der Warteschlange richtig initialisiert sind und dass alle Einträge in proc[] zuerst alle in die freie Warteschlange eingefügt und sequentiell entfernt werden. (Es wird auch davon ausgegangen, dass der Schwanz immer eine "Priorität" hat, die den größtmöglichen Wert hat und größer ist als jeder gültige Prozess besitzen kann: TAILPRIORITY.)


Was könnten wir noch tun? Ohne dies wie oben in Stücke zu zerlegen, hier ist ein weiterer Versuch:

#define NPROC (10)
#define TAILPRIORITY (10000)
#define READYQUEUE (NPROC)
#define SLEEPQUEUE (NPROC+2)
int next[PROC+4];
int prev[PROC+4];
int prio[PROC+4];
int remove( int item ) {
    int n= next[item], p= prev[item];
    next[p]= n;
    prev[n]= p;
    return item;
}
int insert( int queue, int item, int priority ) {
    int n, p;
    for ( n= next[queue]; prio[n] < priority; n= next[n] ) ;
    next[item]= n;
    prev[item]= p= prev[n];
    prio[item]= priority;
    next[p]= item;
    prev[n]= item;
    return item;
}
int getfirst( int queue ) {
    if ( next[queue] > NPROC )
        return -1;
    return remove( next[queue] );
}

Der C-Compiler kennt nun die Adresse der Arrays next[] und prev[] und prio[] im Voraus. getfirst() muss nicht mehr von einem speziellen Prioritätswert abhängen (obwohl insert() so etwas immer noch benötigt, aber auch das könnte jetzt geändert werden.)


Ist dies aus Gründen der Codegröße und/oder des Leistungsarguments von Bedeutung? Womöglich. Es kommt auf den Compiler an. Probieren Sie zum Grinsen diese beiden unterschiedlichen Ansätze mit dem SDCC-Compiler aus und sehen Sie sich jeweils den generierten Assembler-Code an. Was denken Sie?

Was ist mit dem Fall für die Lesbarkeit? Was ist für Sie lesbarer? (Ich habe nicht auf „besonders lesbar“ oder „besonders unlesbar“ geschossen, sondern auf „konsistent zueinander“.)

Was ist mit wartbar? Was wäre, wenn Sie den Knotentyp der verknüpften Liste erweitern müssten? Wäre es wartungsfreundlicher, ein weiteres Array hinzuzufügen (2. Quellcodebeispiel)? Oder wartbarer, um ein weiteres Element zu einer Struktur hinzuzufügen (1. Quellcodebeispiel)? Würde es überhaupt so viel Unterschied machen?

Angenommen, Sie würden einen verknüpften Listenknoten herumreichen? Ist es besser, einen Zeiger oder einen Index zu übergeben? Beachten Sie, dass das Übergeben eines Zeigers es einer Funktion ermöglicht, auf jedes Element innerhalb der Struktur zuzugreifen, selbst wenn dies nicht vorgesehen ist. Aber das Übergeben eines Index könnte es ermöglichen, die Sichtbarkeit bestimmter Teile „anderswo“ zu platzieren, sodass der Compiler bei einem Versuch einen Fehler ausgeben könnte. Aber es gibt natürlich noch andere Überlegungen. Welche Argumente sehen Sie, pro und contra?

Und so geht es.


Persönlich? Stilkonsistenz finde ich vielleicht am wichtigsten. Ich kann mich an fast jeden Programmierstil gewöhnen – sogar an solche, die ich überhaupt nicht mag. Solange die Programmierung konsistent ist , ist es meistens nur eine Frage der Eingewöhnung und dann des Nachmachens. Wenn die Programmierung jedoch von einer Denkweise zu einer anderen und dann zu einer anderen wechselt und der Code wenig oder keine Konsistenz aufweist, finde ich es ziemlich schwierig, ihn gut zu lesen und/oder zu pflegen. Also das ist wohl das Wichtigste für mich. Legen Sie einen Stil fest und bleiben Sie dann im Einklang mit dem Stil.

Es gibt einige Bereiche, die mit Problemen behaftet sind. Verwenden Sie beispielsweise den Heap in einem eingebetteten System anstelle von statischem Speicher. (Ich nehme an, das gilt besonders für die 8051-Familie.) Für einen Compiler und Linker ist es sehr, sehr einfach, den für statische Arrays erforderlichen Gesamtspeicherplatz zu berechnen und Ihnen mitzuteilen, ob er in den Prozessor passt, den Sie gerade verwenden . Aber es ist sehr schwierig, ähnliche Speicherfehler zu finden, wenn Sie es nur herausfinden können, indem Sie das Programm ausführen und sicherstellen, dass Sie alle erforderlichen verschiedenen Bedingungscodes ausführen, um genau die richtige Kombination von Ereignissen zu erzwingen, um den Speicher zu überschreiten , bei der Verwendung von Haufen.

An Haufen ist an sich nichts auszusetzen. Aber in der Praxis mit eingebetteten Systemen bedarf es meiner Meinung nach einer guten Begründung. Also würde ich nach expliziten Kriterien suchen, die seine Verwendung rechtfertigen, falls verwendet.

Also suche ich auch nach handwerklichem Denken im Code. Warum wurden bestimmte Entscheidungen getroffen? Zeigen sie ein gutes Urteilsvermögen? In Fällen, in denen für einige Verwendungen außergewöhnliche Nachweise erforderlich sind, wurden diese Nachweise erbracht und sind sie sinnvoll? Usw.


Im Fall der 8051-Familie sind die Anweisungen, die zum direkten Verweisen auf bestimmte zugewiesene Adressen erforderlich sind, jedoch so viel kleiner (Coderaum) und so viel schneller, dass alle guten C-Compiler dafür (eine sehr stark schrumpfende Anzahl von Unternehmen) übrigens) wird einen Mechanismus zur statischen Aufrufpfadanalyse bereitstellen, sodass lokalen Funktionsvariablen (mit einigen Erfolgsaussichten) feste Adressen zugewiesen werden können. In diesem speziellen Fall würde ich also erwarten, dass Statics viel häufiger verwendet werden, unabhängig davon, ob sie dateilokal oder global im Geltungsbereich verfügbar sind. Beim 8051 macht es einfach zu viel Sinn.

Die Übergänge, an denen ich beteiligt war, sind von 4-8 Bit ALU und Speicherbreiten auf 32/64/128 gegangen, wobei 32-Bit jetzt ziemlich verbreitet ist. Die Definition, welche Art von Controller Sie in einem kleinen Gerät finden können, hat sich ebenfalls geändert, und ich bin überhaupt nicht überrascht, dass Linux für kaum mehr als das Blinken einiger LEDs auf einem 16/32 ARM7TDMI-Kern verwendet wird. (Oder sogar ein Cortex-A53-Superscale.) Was auf der C-Codierungsebene angemessen ist, wird also ein wenig variieren.

Ich erwarte Quellcodekonsistenz und wohlüberlegte Argumentation für die getroffenen Designentscheidungen. Das ist für mich das Wichtigste. Darüber hinaus bin ich in meiner Meinung etwas flexibler.

Das ist ein toller Einblick auf den Haufen. An dieses Risiko hatte ich nicht gedacht. Außerdem hatte ich nicht an Maschinenbefehle in Bezug auf den Zeiger-Overhead gedacht. Persönlich finde ich die Zeigersyntax schwieriger zu lesen. Wenn Sie sich neuere Cortex-M-MCUs ansehen, können sie Zeigeroperationen besser handhaben? Ich vermute ja, denn die meisten ARM-SDKs, die ich gesehen habe, sind im Wesentlichen alle Strukturen und Zeiger und Funktionen, die Zeiger annehmen.
Ich kichere, wenn ich diese Analyse zum 8051 lese. Ich verstehe, was Sie Compiler-weise gesagt haben, wenn Sie wirklich über Zeiger vs. Statik sprechen möchten .... barrgroup.com/Embedded-Systems/How-To/Optimal-C-Code- 8051
@Leroy105 Freut mich, dass es dir ein wenig gefallen hat. Ich wollte sofort an etwas denken, das halbwegs interessant, aber auch so einfach ist, dass ich die Dinge nicht zu tief in den Schlamm stecke.

Was Sie "wollen", ist Refactoring. Wenn es für ein Geschäft ist (also Geld im Spiel ist), tun Sie es nur, wenn die Zeit (also das Geld) Ihnen mehr gibt:

  • Wartbarkeit
  • Lesbarkeit
  • Möglichkeit zu erweitern

Wenn es sich um ein Hobbyprojekt handelt und Sie das Gefühl haben, dass es besser ist, Zeiger zu verwenden, machen Sie weiter. Andernfalls (und die oben genannten Punkte sind nicht anwendbar oder es ist nicht so viel Zeit dafür aufzuwenden), lassen Sie den Code so, wie er ist.

Es ist für ein Unternehmen, also nein – wir wollen nicht umgestalten. Da haben wir ein starkes Gefühl. ;) Ich weiß nicht, ob das Ihr Steuerhaus ist: Sehen Sie bei Ihrer Arbeit Leute, die Software entwickeln und in diesen winzigen eingebetteten Systemen einfach Globals um jeden Preis vermeiden? Du kannst die Katze so oder so häuten, die Globals scheinen mir schneller zu sein.
Ich bin ein professioneller Software-Ingenieur, aber nicht in AVRs, sondern in eingebetteter Software (Bit-Software, wie > 50 Millionen Zeilen). Da dort Modularisierung (wegen Wartbarkeit, Erweiterbarkeit) wichtiger ist, werden viele Punkte verwendet, teilweise aber auch Globals (aber statisch innerhalb einer Datei).
Das ist in größeren Codebasen sinnvoll, um die Modularisierung zu priorisieren ... das ist wirklich hilfreich, von einem Fachmann zu hören, was er in seiner Organisation sieht!