Hallo zusammen,
bis jetzt hab ich sehr einfach Software einfach so vor mich hin
programmiert (ohne vorher gross zu überlegen oder pläne zu zeichnen).
Aber wie geht man vor wenn die Software etwas komplizierter wird? Gibt
ja schnell sehr viele Fehlerquellen.
Habt ihr ein paar Tipps?
Gibt es irgendwelche Grundlegende sachen dir man beachten sollte? Viel
if else benutzen oder wenig. Viel ineinander verschachteln oder
möglichst wenig. Viele verschachtelungen sollte man maximal benützen?
Wann benutzt man State Machines?
Wie dokumentiert ihr Software? Immer, garnie oder im nachhinein?
Zeichnet ihr Struktorgramme oder einfach Kommentare?
Wann verwendet man C++ bei uCs?
Und zum Schluss, habt ihr noch eine Buchempfehlung für C speziell für
uCs?
Danke und schönen Abend
C-Master
Die wichtigsten Dinge:
- Main Loop immer schön klein halten
Hier so wenig Funktionalität wie möglich unterbringen und diese hinter
klingenden Funktionsaufrufen verbergen.
- Selbsterklärende Namen für Variablen und Funktionen wählen, ganz egal
wie lang diese werden.
- So wenig globale Variablen wie möglich, dies sollte die absolute
Ausnahme sein!
- Selbsterklärenden Code schreiben
Viele hier versuchen immer noch das kleinste Quäntchen an Performance
herauszukitzeln. Dies geht oft zu Lasten der Lesbarkeit, Wartbarkeit und
Portierbarkeit und ist (fast) nie nötig.
- Zustandsautomaten sind ein sehr gutes Stichwort
Diese Dinger sind ein Garant für guten Code, weil man von Anfang an
gezwungen wird alles nach Lehrbuch zu machen.
Hier erstellt man zuerst das Konzept, und dann erst den Code. Das
Konzept ist gleichzeitig die beste Doku.
Bei der Implementierung gibt es keine Fehler, da das vorher erstellte
Zustandsdiagramm eindeutig ist. War das Konzept richtig, dann
funktioniert das Programm auch.
Grundsätzlich sollte man sich bei jeder Aufgabe fragen, ob diese sich
sinnvoll mit einem Automaten lösen lässt.
Thomas wrote:
> - So wenig globale Variablen wie möglich, dies sollte die absolute> Ausnahme sein!>
Wie macht Ihr es dann, wenn Ihr Zustände o.ä. in mehreren Funktionenen
benötigt?
Ein Beispiel: Meine Rolladensteuerung - Ich benötige Uhrzeit,
Schaltzeiten etc. in vielen Funktionen. Daher habe ich diese als globale
Variablen implementiert. Die Alternative wäre, alle diese Werte als
Variablen in Funktionsaufrufen zu übergeben. Dies würde m.E. aber zu
großen Overhead bei Funktionsaufrufen führen ...
Was macht man hier? Ist es Zielführend alle mehrfach benötigten
Variablen in ein globes Struct zu packen? Damit hätte man dann nur eine
globale Variable ?
Also wie macht Ihrs ?
Gruß
Andreas
Alle nötigen Zustandsvariablen in einer Struktur zusammenfassen und dann
einen Pointer auf diese Struktur den einzelnen Funktionen übergeben
(=Handle auf den Kontext).
Der Globale Namensbereich ist begrenzt. Häufig hat man ja auch "status"
Variablen, die dann mehrfach vorkommen, dann gibt es Kollisionen, also
einfach in ne struct und die struct nach dem Modul benennen.
Auch sehr hilfreich, die sache mit dem Kontextpointer, dann kann man den
selben Code auch mehrfach benutzen (z.B. Filteranwndungen, ...).
Meines Erachtens geht Leistung nicht gegen Lesbarkeit. Ich habe erst
letzten Monat ein in C geschriebenes Modul (sehr gut lesbar, 100%
portierbar), in ASM umgesetzt und nur etwa 40% eingespart. Das meisste
natürlich beim Push/Pop (Interrupts).
Interrupts sind eh ein gutes Stichwort: ich habe gute Erfahrung gemacht,
dieses moeglichst klein zu halten, also wenig Funktionalität dort
reinzubringen. Wenn man dann noch ausschließlich Inline Funktionen dort
verwendet, kommen auch brauchbar schnelle und kleine Binärdaten aus dem
Compiler raus.
> Wieso eigentlich keine globalen Variablen? Gibts da einen vernünftigen> Grund?
Der Hauptgrund ist der, dass bei globalen Variablen schwer
nachvollziehbar ist, woher sie ihre Inhalte stammen. Um das
herauszufinden, muss man den Quellcode des gesamten Programms nach
Funktionen durchscannen, die die eine bestimmte Variable beschreiben,
anschließend nachschauen, welche anderen Funktionen diese Funktionen
aufrufen usw. Problematisch wird es vor allem dann, wenn globale
Variablen an sehr vielen Stellen im Programm beschrieben werden.
Bei lokalen Variablen muss hingegen nur eine Funktion durchgelesen
werden. Selbst wenn Werte als Argumente an eine Funktion übergeben
werden, ist die Suche nach ihrem Ursprung immer noch mit begrenztem
Aufwand möglich.
Es ist aber überhaupt kein Problem, Uboot-Stockis Uhrzeitvariable
global zu machen, wenn aus Namensgebung und Dokumentation klar wird,
dass sie nur an einer Stelle, nämlich vielleicht in einem zyklisch
aufgerufenen Interrupthandler beschrieben wird. Würde man diese als
Argument über mehrere Aufrufebenen durchreichen, würde die Lesbarkeit
sogar schlechter, weil dabei nicht sofort klar wird, dass es sich bei
den Argumenten in Wirklichkeit immer um Kopien der gleichen Variable
handelt.
Es ist ebenfalls überhaupt kein Problem, eine Satz Parameter global zu
machen, wenn klar ist, dass diese einmal beim Programmstart
initialisiert (bspw. aus einem EEPROM gelesen) werden, und danach kaum
noch verändert werden. Idealerweise packt man all diese Variablen in
eine Struktur, um damit auszudrücken, dass ihre Inhalte alle aus der
gleichen Quelle stammen.
Bei der Übergabe von Werten zwischen Interrupthandlern und anderen
Programmteilen führt sowieso kein sinnvoller Weg an globalen Variablen
vorbei. Aber auch hier kann man die Lesbarkeit verbessern, wenn man
jeweils alle globalen Variablen eines Interrupthandlers zu einer
Struktur (bspw. timer_irqvars, uart_irqvars usw.) zusammenfasst.
Globale Variablen sind also nicht böse, genauso wenig wie Gotos. Man
muss sie nur mit Bedacht einsetzen.
yalu wrote:
> Globale Variablen sind also nicht böse, genauso wenig wie Gotos. Man> muss sie nur mit Bedacht einsetzen.
Hi yalu,
bei dem meisten was du von dir lässt nicke ich oft und gerne, wie auch
hier bei den globalen Variablen, aber bei gotos muss ich dir
widersprechen.
Gotos führen zu Sphagetti Code und es gibt immer eine Möglichkeit diese
zu vermeiden.
Sehe ich ähnlich, gotos habe ich in den letzten 15 Jahren Programmierung
nicht mehr gebraucht. Ich glaub auch nicht, dass mein Programmcode
dadurch vorteilhafter geworden wäre.
Bin aber auch ein Gegner vom blinden Befolgen irgendwelcher Regeln. Wenn
jemand den sinnvollen Einsatz von Gotos findet, darf er die gerne
einsetzen.
Die Verteufelung von goto in C ist wahrscheinlich so alt wie die
Sprachdefintion. Sicher, man kann gotos immer irgendwie vermeiden und es
gibt einige übergreifende oder in-house Richtlinien, die die Verwendung
von goto auch verbieten. Aber vor allem in Funktionen mit vielen
Verschachtelungen, bei denen z.B. sehr viele mögliche Fehlerzustände
abgefragt werden, vermeiden machmal "mit Bedacht" eingesetzte label und
ein paar gotos ausufernde und unübersichtliche if-else Konstrukte und
manchmal auch die meiner Meinung nach viel fehleranfälligeren innerhalb
einer Funktion verstreuten return fehlercodeX (mehr als ein return in
einer Funktion wird in zumindest in zwei mir bekannten Richtlinien
ebenfalls untersagt). Das ist dann im Prinzip ein realtiv
übersichtliches, im Vergleich zu C++ allerdings recht primitives,
exception-handling.
Nur meine 0,02EUR dazu; Diskussionen zu goto in C füllen wahrscheinlich
inzwischen zehntausende von Foren-, Mailinglist-Threads und
Internetseiten.
goto ist so eine Sache. Ich kannte dies anfangs garnicht, (was
vielleicht auch gut so war), aber mittlerweile nutze ich es ab und zu.
Es gibt ein paar Fälle in denen es durchaus brauchbar ist:
z.B. wenn man mehrere Schleifen auf einmal verlassen möchte. OK, man
kann es auch anderst machen indem man eine Variable setzt, die dann in
allen Schleifen nacheinander ein break auslöst, oder gleich ein return
um die komplette Funktion zu verlassen. Manchmal hat goto aber ein paar
kleine Vorteile.
Mir ging es ähnlich, ich kannte goto noch aus basic, war mir aber lange
nicht bewusst das ich damit auch in c arbeiten kann, heute bin ich froh
das ich es nicht von anfang an kannte. Was ich häufiger sehe ist das
gotos meistens schlechten Code unterstützen, weil man sich vorher keine
Gedanken gemacht hat.
Ein Beispiel was mir spontan einfällt ist das Rausspringen aus
irgendwelchen Such-Schleifen, bei einfachen Konstruktionen tut es ein
break, bei verschachtelten Angelegenheiten packt man das Ganze einfach
in eine Unterfunktion. Wenn man von vornerein gotos gedanklich
ausblendet wird der Code meistens strukturierter. Im Idealfall hat man
also dann eine Suchfunktion die als Rückgabewert einen Pointer auf die
Gesuchte Stelle oder NULL enthält. Oder wenn nur das Enthalten eines
Wertes in einem Feld interessiert, ist ein if (beinhaltet(Feld, x)) {};
auch besser lesbar als irgendwelche Sprünge.
Das schlimmste Negativ Beispiel was ich bisher gesehen hab war ein Code
mit etwa 800 Zeilen und alles in der main, dafür aber massig gotos...
(Funktionen aus Standardheadern mal ausgenommen.)
Erinnert mich an den alten Spruch: “Real Programmers can write
FORTRAN programs in any language.”
Sprich: man kann alles, was es gibt, immer gehörig missbrauchen (und
C setzt dem Missbrauch wenig Schranken). Sinnvoll benutzt, ist aber
oft nichts dagegen einzuwenden.
Gerade bei sehr kleinen Controllern können globale Variablen u. U.
zu einfacherem (und kleinerem) Code führen, und eine fehlende
Übersichtlichkeit ist dort nicht das Problem, was es bei einem
Projekt auf einem Controller mit 64 und mehr KiB an Code sein kann.
Gleichermaßen bringt ein "goto", das fünf Klammerungsebenen einspart,
ggf. mehr Übersichtlichkeit in das Projekt, als man bei strikter
Meidung dieses Worts erreichen kann.
Was die Globalen Variablen angeht, bastel ich auch bei Kleinstprojekten
jeden Funktionsblock (Hauptprogramm, Timer, UART, etc.) in jeweils
eigene C Files und binde sie bei Bedarf in andere Programme wieder ein,
damit erledigen sich die meisten globalen Variablen von selbst.
panam wrote:
> Für was brauchts eigentlich Pointer?? Das hab ich nie gecheckt. Man kann> doch gleich die Variable übergeben!
Das ist dann aber eine Einbahnstrasse in C.
Der Wert geht zwar in die Funktion hinein, aber eine Veränderung
des Wertes in einer Funktion führt nicht zu einer Veränderung
der aufrufenden Variablen des Aufrufers.
Wenn du das so willst: super, genau das was du brauchst. Du kannst
nach Herzenslust innerhalb der Funktion mit dem Argument
fuhrwerken, ohne das du dem Aufrufer etwas zerstörst.
Wenn du aber haben willst, dass eine Änderung an einem Argument
auch für den Aufrufer sichtbar ist, dann geht das in c nur,
in dem der Aufrufer die Adresse übergibt (aka. einen Pointer)
an der diese Änderung gemacht werden soll.
Die andere Spielwiese für Pointer sind: dynamische Datenstrukturen.
Also Arrays, Listen, Maps, Strings die zur Laufzeit bei Bedarf
anwachsen und kleiner werden können.
Pointer sind ganz einfach: Das sind Variablen, der Inhalt eine
Speicheradresse ist. So wie ein int eine ganze Zahl aufnimmt,
ein double eine Fliesskommazahl, so nimmt eine Pointer-Variable
eine Speicheradresse auf. Mehr steckt da nicht dahinter :-)
> Bei Java wurde das doch auch> abgeschafft.
Auch in Java gibt es noch Pointer. Nur heissen sie dort AFAIK
Referenzen.
panam wrote:
> Für was brauchts eigentlich Pointer?? Das hab ich nie gecheckt. Man kann> doch gleich die Variable übergeben! Bei Java wurde das doch auch> abgeschafft.
Jein, in Java und C++ gibts Referenzen, das ist praktisch ein stark
vereinfachter Pointer.
Wenn man einen Pointer übergibt, bleiben Änderungen an der Variablen
erhalten da direkt der Inhalt der Speicheradresse der Variablen geändert
wird.
Bei Variablenübergabe wird mit einer Kopie innerhalb der Funktion
gearbeitet und nach dem rausspringen aus dieser Funktion ist diese weg.
Ausserdem kostet das Anlegen dieser Kopie Speicher und Zeit.
EDIT: Immer wieder erstaunlich wie schnell Mr. Buchegger solche
Erklärungen aus dem Ärmel zaubert.
Das war der Original-Beitrag (nur noch mal zur Erinnerung):
------------>>
Hallo zusammen,
bis jetzt hab ich sehr einfach Software einfach so vor mich hin
programmiert (ohne vorher gross zu überlegen oder pläne zu zeichnen).
Aber wie geht man vor wenn die Software etwas komplizierter wird? Gibt
ja schnell sehr viele Fehlerquellen.
Habt ihr ein paar Tipps?
Gibt es irgendwelche Grundlegende sachen dir man beachten sollte? Viel
if else benutzen oder wenig. Viel ineinander verschachteln oder
möglichst wenig. Viele verschachtelungen sollte man maximal benützen?
Wann benutzt man State Machines?
Wie dokumentiert ihr Software? Immer, garnie oder im nachhinein?
Zeichnet ihr Struktorgramme oder einfach Kommentare?
Wann verwendet man C++ bei uCs?
Und zum Schluss, habt ihr noch eine Buchempfehlung für C speziell für
uCs?
Danke und schönen Abend
C-Master
<<------------ Ende des Beitrag
Eines sollte man beachten:
Das Programmieren ist kein Selbstzweck!
Ein Programm setzt eine definierte Aufgabe in Software um(, weil es so
am einfachsten geht).
Das Ganze sieht dann sinngemäß folgendermaßen aus:
1. Es zeichnet sich eine Aufgabe ab, die zu lösen ist.
2. Die Aufgabe wird ausreichend durchdacht und dabei dokumentiert.
3. Der Benutzer des Ganzen versteht das Ergebnis von 2. und segnet es
ab.
4. Die Lösung der Aufgabe wird aus Benutzersicht beschrieben und vom
Benutzer abgesegnet.
5. Die Hardware für die Lösung wird konzipiert.
6. Das Design der Software erfolgt:
6.1 Grobdesign,
6.2 Feindesign bis herunter zum Pseudocode.
7. Die Hardware wird hergestellt.
8. Erst jetzt wird die Software programmiert, und zwar in den Pseudocode
hinein.
9. Der Software-Produzent testet (White Box Test).
10. Der Benutzer nimmt das System ab (einschließlich Black Box Test).
Die Dauer der einzelnen Schritte kann hierbei von Minuten bis hin zu
Jahren betragen. Das Ergebnis des Ganzen ist jedenfalls ein
durchdachtes, sauber dokumentiertes, wartbares und (meist)
funktionsfähiges System.
Der Anteil der eigentlichen Codierung am gesamten Zeitaufwand liegt
erfahrungsgemäß bei weniger als 20%.
Zum Schluß noch eins: Wenn ein Programm kompliziert ist, dann stimmt
etwas mit dem Programm nicht!
Grüße
Bernhard
Genau, nochmal zu ein paar Ausgangsfragen zurück:
>Wie dokumentiert ihr Software? Immer, garnie oder im nachhinein?
Eigentlich nur durch selbsterklärende Funktions/Variablennamen und an
komplizierteren Stellen vereinzelte Kommentare im Quelltext.
>Zeichnet ihr Struktorgramme oder einfach Kommentare?
Nur wenns nach 3 Versuchen im Kopf nicht hinhaut, dann aber lieber
Flussdiagramme, Struktogramme mag ich irgendwie nicht.
Bitte entschuldigt, wenn ich das Topic wieder ein wenig verlasse. Habe
den folgenden Text vor etwa zwei Stunden geschriebeni, als der Inhalt
noch aktueller war, bin aber noch nicht dazu gekommen, ihn
abzuschicken:
Tim T. schrieb:
> aber bei gotos muss ich dir widersprechen.
Das sollte natürlich auch überhaupt kein Aufruf zur Verwendung von
Gotos sein, sondern eher einer gegen pauschale Verteufelungen im
Allgemeinen.
Martin Thomas schrieb:
> Aber vor allem in Funktionen mit vielen Verschachtelungen, bei denen> z.B. sehr viele mögliche Fehlerzustände abgefragt werden, vermeiden> machmal "mit Bedacht" eingesetzte label und ein paar gotos> ausufernde und unübersichtliche if-else Konstrukte
Genau das ist der Fall, und wahrscheinlich der einzige, wo Gotos
sinnvoll sein können. Beispiel:
1
voidfunction(void){
2
// Aktion 1
3
if(Fehler1)gotoerror;
4
5
// Aktion 2
6
if(Fehler2)gotoerror;
7
8
// Aktion 3
9
if(Fehler3)gotoerror;
10
11
// Aktion 4
12
if(Fehler4)gotoerror;
13
14
// Aktion 5
15
if(Fehler5)gotoerror;
16
17
gotoend;
18
19
error:
20
// Fehlerbehandlung
21
22
end:
23
// abschließende Aktionen, die in jedem Fall ausgeführt werden
24
// müssen
25
}
Hier wird sofort klar, dass nacheinander die Aktionen 1 bis 5
ausgeführt werden und im Fehlerfall in die gemeinsame
Fehlerbehhandlung verzweigt wird.
Dieselbe Funktion ohne Gotos:
1
voidfunction(void){
2
intok=0;
3
4
// Aktion 1
5
if(!Fehler1){
6
// Aktion 2
7
if(!Fehler2){
8
// Aktion 3
9
if(!Fehler3){
10
// Aktion 4
11
if(!Fehler4){
12
// Aktion 5
13
if(!Fehler5){
14
ok=1;
15
}
16
}
17
}
18
}
19
}
20
21
if(!ok){
22
// Fehlerbehandlung
23
}
24
25
// abschließende Aktionen, die in jedem Fall ausgeführt werden
26
// müssen
27
}
Hier ist es nicht sofort offensichtlich, was beim Auftreten eines
Fehlers passiert. Um das zu erkennen, muss man sich mühsam die
If-Treppe am anderen Ende wieder hochhangeln (man stelle sich zudem
vor, die einzelnen Aktionen bestehen aus jeweils mehreren
Anweisungen, so dass der Code insgesamt länger wird).
Außerdem benötigt man eine izusätzliche Variable, um die
Erfolgsinformation aus der innersten If-Anweisung nach außen zu
tragen.
Ganz abgesehen davon wird der Code unnötig "breit", vor allem, wenn
man nicht nur 5 sondern vielleicht 20 Fehlerfälle zu untersuchen hat.
Ein Switch-Case in C ist auch nicht viel anderes als ein Mehrfach-
Goto, bei dem an Stelle der Labels abzuprüfende Werte stehen. Würde
man den Switch deswegen durch eine verschachtelte und mehrfach
eingerückte If-Anweisung ersetzen?
> Das ist dann im Prinzip ein realtiv übersichtliches, im Vergleich zu> C++ allerdings recht primitives, exception-handling.
Genau richtig. Die C++-Exceptions sind für die Fehlerbehandlung wie im
obigen Beispiel gemacht. Damit kann man in C++ nicht nur Gotos,
sondern auch Longjumps auf saubere Art und Weise ersetzen.
Jetzt aber wieder zurück zur Ausgangsfrage ;-)
Was aber, wenn du für die Fehlerbehandlung ganz viele Variablen aus
den Aktionen 1 bis 5 brauchst? Die darin enthaltenen Werte müsstest du
irgendwie aus FehlerInFunktion() heraus- und in function()
hineinbekommen. Vielleicht ein Fall für globale Variablen? ;-)
Man müsste jetzt ein konkretes Anwendungsbeispiel parat haben. Ich
habe jetzt gerade keins hier, da der Fall zugegebenermaßen nicht allzu
oft auftaucht.
Aber ich denke, wenn jemand tatsächlich auf so einen Fall stößt, der
ohne Gotos nur ungeschickt gelöst werden kann, wird er es nach dem
bisher Geschriebenen wahrscheinlich selbst merken. Wie gesagt, es geht
mir ja nicht darum, dass jetzt jeder seinen bereits funktionsfähigen
Code mit Gotos spickt. Von Gotos ist nach wie vor abzuraten, nur
generell vertaufeln sollte man es nicht.
Ich breche alle Konzepte auf saubere State-Maschines runter.
DIese sind (zuerst) unabhängig voneinander und könnn sich nur durch
funktion(saufrufe) beeinflussen...