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
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.
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:
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.
Eugen Sch.
Leroy105
Eugen Sch.
Leroy105
Eugen Sch.
Das Photon
jonk
Leroy105
jonk
user_1818839