Strukturierte Programmierung auf Mikrocontrollern
Grundlegendes
In diesem Artikel sollen die Grundlagen der sauberen Aufteilung eines Programms in Module, Schichten und Strukturen erklärt werden, besonders bezogen auf Mikrocontroller.
Leider fällt im Forum oft auf, dass sehr gerne einfach alles in eine C-Datei "geklatscht" wird, was für kleinere Programme halbwegs funktionieren mag, bei größeren Projekten jedoch versagt, da man schnell die Übersicht verliert und sich auch die Wartbarkeit des Codes sehr verschlechtert.
Für die stukturierte (professionelle) Programmierung sind 3 Faktoren von Bedeutung:
Planung
Zahllose Softwareentwickler starten ihre Programmierkarriere out of the box, d.h. sie haben keine Ausbildung und bringen sich das Programmieren selbst bei. Sie starten daher oft direkt mit der Entwicklungsplattform und Beispiel-Codes, die sie modifizieren. Damit wird sozusagen auf der Ebene der Umsetzung gestartet und die Funktion des Programmes quasi nebenbei mit geplant. Dieses für kleine Projekte funktionable Vorgehen bringt dann bei wachsenden Programmen Probleme, weil der Entwickler niemals ein strukturiertes Entwickeln gelernt hat. Diese Personen haben dann in der Praxis das Problem, dass sie gesteigerten Anforderungen in Sachen Planung und Dokumentation gegenüberstehen und die althergebrachte bottom-up Methode nicht mehr gefragt ist. Unglücklicherweise wird selbst an Hochschulen nur sehr selten auf praktische Belange diesbezüglich Rücksicht genommen.
Am Anfang eines Projektes steht daher sinnvollerweise ein Konzept, in dem wichtige Dinge vorab erfasst sind, damit man sich während des Entwickelns nicht "verläuft". Als nützlich haben sich erwiesen:
- Beschreibung der Funktionen eines Programmes mit Benutzereingriffsmöglichkeiten (use cases), jeweilige Reaktion des Programms (responses, event handling), autonome Aktionen im Hintergrund (interrupts, polling), erlaubte Funktionen, verbotene Zustände
- Erstellung eines Blockdiagramms zur Aufteilung der funktionellen Module und Beschreibung der Untermodule und ihrer Funktion
- Erstellung von Ablaufdiagrammen für kompliziertere Module mit detaillierter Beschreibung der Funktionen
- Festlegung der Interaktion mit Hardware, Boot-Reihenfolgen, Test an Peripherie etc.
- Hinweise zur Umsetzung unter Rücksichtnahme auf Randbedingungen der speziellen Hardware (z.B. Timing, Tempo des Controllers, Loop-Anzahl, Verzögerungen infolge polling, maximale Interrupttiefe, und -dichte.
- Beschreibung der mathematischen Funktionen, Niederlegung der Formeln, Beispielrechnungen in Excel mit Test der Auflösungen und Rundungsuntersuchung samt Bereichsgrenzenprüfung etc.
- Erstellung eines Arbeitsplans mit Zeitabschätzungen f+r alle Module, um Möglichkeiten der Parallelisierung und Zeitoptimierung erkennen zu können und Termine für mile stones festlegen zu können
Diese Vordokumentation ergibt einen Leitfaden für die Entwicklung (= Umsetzung) und hilft, dass mehrere Personen an einem Projekt arbeiten können. Zudem ist das Projekt so leichter erweiterbar, da sofort ersichtlich ist, was die Software können sollte und was sie nicht kann und worauf Rücksicht genommen wurde, bzw was ignoriert wurde, da es bei der ersten Festlegung der Funktionen nicht relevant war.
Diese Dokumentation wird bei Änderungen aktualisiert und um die finale Doku erweitert. Sie bildet die Basis für Änderungen, die Erzeugung von Abkömmlingen und auch die Planung neuer Projekte: Hat man nämlich einen Stamm an Dokumenten beisammen, ist die Planung und Definition oft nur noch ein Copy&Paste und man hat direkt einen Leitfaden der TODOs im neuen Projekt. Die Chance, etwas zu vergessen oder zu übersehen, wird damit drastisch verringert.
Die Vordokumentation ist auch Grundlage für die tägliche Umsetzung und erlaubt eine enge Projektverfolgung, weil jederzeit ersichtlich ist, wie weit man bereits ist und wie weit der Weg zum nächsten mile stone noch ist. Vor allem ist damit erkennbar, wenn die Teamarbeit aus dem Ruder läuft und es kann rasch reagiert und umgeplant werden.
Versionsverwaltung
Oft kommt es vor, dass man an einem Programm arbeitet und irgendwann nach einer Änderung gar nichts mehr funktioniert und man es, warum auch immer, nicht schafft, den alten Zustand wiederherzustellen. Labile Naturen werfen dann meist das Projekt einfach hin, echte Männer fangen von vorn an ;-). Beides ist keine Lösung. Besonders interessant wird es, wenn man mit mehreren Personen an einer Datei arbeiten möchte und mehrere zur gleichen Zeit auf der selben Datei arbeiten. Wenn jeder einfach speichert, bleiben nur die letzten Änderungen erhalten.
Dies löst ein Versionsverwaltungssystem, auch Source Code Management (SCM) oder Version Control System (VCS) genannt. Bekannte Versionsverwaltungen sind RCS, CVS, SVN, GIT. Ich möchte mich hier auf SVN beschränken. Eine gute Grundlagenerklärung zur Funktion von SVN bietet der Wikipedia-Artikel Subversion. Ich bitte den Leser, sich diesen Artikel gründlich zu Gemüte zu führen. Dort sind vor allem wichtige Grundbegriffe erklärt, die den Rahmen dieses Artikels sprengen würden.
Wer unter Linux, Unix oder BSD-Varianten arbeitet, der hat unter allen bekannten Versionsverwaltungen die größte Auswahl. Selbst ein frühes Festlegen auf ein SCM ist nicht unumkehrbar, denn es gibt sogar Konverter, die später den ganzen Code-Baum samt Geschichte umwandeln in ein anderes SCM.
Installation von SVN
Unter Windows empfehle ich den Server VisualSVN und den in die Windows-Oberfläche integrierten Client TortoiseSVN. Unter einem Debian-Derivat (z. B. Kubuntu) installiert man einfach das Paket subversion. Es existieren auch für Linux graphische Clients, auf die ich hier nicht weiter eingehen möchte.
Verwendung von SVN
Zur Verwendung von SVN gibt es eine sehr gute Anleitung unter: BSDwiki. Ein Versionsverwaltungssystem mag zunächst lästig erscheinen. Spätestens, nachdem man das erste Mal seine Software zerschossen hat, mag man es nicht mehr missen.
Dokumentation
Wichtig ist eine gute Dokumentation des Codes. Eine Möglichkeit dabei sind aussagekräftige Kommentare im Programmtext, die es gestatten, einen Zusammenhang zwischen der Implementierung und der Funktion herzustellen. "Aussagekräftig" bedeutet damit, dass man nicht schreibt, 'was' die Codezeile macht, sondern 'warum' und was sie funktionell bedeutet:
schlecht:
maxTests = 5; # Setze maxTests auf 5 <--- redundant und daher nutzlos
gut:
maxTests = 5; # Maximal 5 Abfragedurchläufe <--- Bindung zur gewünschten Funktion.
Weiterhin wird vor allem für größere Sachen empfohlen, ein integriertes Dokumentationssystems zu verwenden. Hier wurden gute Erfahrungen mit Doxygen gemacht. Dieses Programm wurde unter der GPL veröffentlicht und erzeugt u.A. auch sogannte Callgraphs
Doch nicht nur das Vorhandensein inhaltlicher Erläuterungen ist wichtig - auch die Art der Codegestaltung spielt in der Praxis eine wesentliche Rolle:
Formatierung des Quelltextes
Um ein Programm gut und schnell verstehen zu können, muss auch der Quelltext sauber formatiert sein. Denn ein Programm schreibt man einmal, liest es aber viele Male. Dazu müssen ein paar grundlegende Formatierungsregeln beachtet und einheitlich umgesetzt werden.
- Syntax highlighting: Die meisten Editoren und Entwicklungsumgebungen unterstützen das farbige Hervorheben von Schlüsselwörtern. Das erleichtert die Lesbarkeit deutlich. Vor allem Kommentare sind somit leichter lokalisierbar.
- Einrückung: Bei While oder For -Schleife, If-Abfragen oder switch-Anweisungen sollen die Blöcke stets eingerückt werden, um die logische Struktur darzustellen. Dadurch sieht man auch leichter vergessene Klammern oder falsche logische Zuordung in verketteten if-Anweisungen.
- Begrenzung der Zeilenlänge auf 80-100 Zeichen
- Tabulatoren als Leerzeichen einfügen lassen: Das können die Editoren heute allein. Der Vorteil ist, dass der Quelltext danach immer gleich aussieht, und nicht auf einem anderen Editor mit anderer Tabulatoreinstellung verschoben aussieht.
Benennung von Variablen, Makros, Nutzung von Anweisungen
- Selbsterklärende Funktions- und Variablennamen ersparen einem 100000 Kommentare
- Variablennamen sollen in erster Linie den Inhalt einer Variablen beschreiben, nicht ihren Datentyp.
- Defines komplett in GROSSBUCHSTABEN
- Namen von Variablen und Funktionen bzw. Methoden in Kleinbuchstaben; Worttrennung mit Unterstrich oder CamelCase (jede Wortsilbe beginnt mit einem Großbuchstaben, z.B GetInfo, MaxCount etc.)
- Namen wie i, j, k für Indizes für Zählschleifen
- Variablen wie x, y, z für Positionen
- Bei Arrays z.B. einkaufsPreis[i] oder einkaufsPreis[index] verwenden. i bzw. index sind übliche Namen um aus einem Array ein einzelnes Element zu identifizieren.
- Variablen und Makros so lokal wie möglich halten
- Mit globalen Variablen sparsam umgehen und diese auch im Namen kennzeichnen, z. B. den Modulnamen voranstellen 'i8_LOG_Position'
- Vermeidung langer Funktionen / Aufspalten in kleinere Funktionen und Bibliotheken: Verwendest du den gleichen Code an verschiedenen Stellen lohnt es sich diesen in eine Funktion auszulagern.
- Eine Funktion löst genau eine Aufgabenstellung
- Funktionen sollen nur das machen, was der Funktionsname erwarten lässt.
- Wiederverwendbarkeit durch Funktionen, keine doppelten Codeteile, für Geschwindigkeit notfalls #inline verwenden
- Kurze und knackige Berechnungen, keinen Spaghetticode; Werden in der Berechnung Konstanten verwendet, dann am besten einen Kommentar dazu (z. B. b = a*3.6e6 // 3.6e6 ist Millisekunden pro Stunde)
- Statt ?: am besten Verzweigungen verwenden, da dieser oft nicht bekannt ist und auch nur noch selten verwendet wird.
- Leerzeichen und Leerzeilen kosten kein Geld! Aber bitte nicht tonnenweise!
- Kommentare schreibt man für sich selbst, für später
- Kommentare sofort schreiben, hinterher ist man zu faul und nicht mehr zu 100 % im Problem vertieft
- Je genialer die Idee, um so nötiger der Kommentar.
- Zusammenhänge dokumentieren. Die erschliessen sich nicht aus den paar Zeilen Code, auf die man gerade schaut!
- Kommentare sollen die 'Warum'-Frage beantworten und nicht die 'Wie'-Frage! Wie etwas gemacht wird, steht im Code. Aber dort steht nicht warum es gemacht wird.
- Kommentare nach dem Muster "Das ist eine for-Schleife" lösen maximal Schmunzeln aus, es sei denn es handelt sich um ein C-Lehrbuch. Solche Kommentare ("Hier beginnen die Variablen", "Hier beginnen die Funktionen", etc) lässt man besser. Jeder der mehr als 5 Stunden C programmiert erkennt eine for-Schleife auf Anhieb und wenn nicht soll er zuerst ein C-Buch studieren, ehe er sich an Code versucht.
- Die üblichen Regeln der Muttersprache sollten in den Stil einfließen
- Einheitlicher Stil bei Formatierung und Namensgebung
- Vermeidung voreiliger Optimierungen
Beispiele
/* Sicherung gegen doppeltes Einfuegen von Headerfiles */
#ifndef HEADER_FILE_NAME
#define HEADER_FILE_NAME
// Das Headerfile
#endif
#define IN_GROSSBUCHSTABEN // Caps mit underline
int funktionsName(int param); // CamelCase
char varName; // CamelCase
Modularisierung
Nun zum wichtigsten Punkt: Ein Programm richtig in Module und Schichten zu unterteilen. Das ist aus verschiedenen Gründen notwendig.
- Übersichtlichkeit: Vor allem bei größeren Sachen will und muss man den Überblick behalten. Dazu muss ein Programm sauber formatiert und strukturiert sein.
- Wartungsfreundlichkeit: Sowohl in der Entwicklungsphase als auch später bei der Erweiterung/Wartung ist ein gut modularisiertes Programm sehr wichtig
- Speicherverbrauch: Einen Ablauf, welcher mehrfach im Programm verwendet wird, packt man sinnvollerweise in eine Funktion. Dadurch wird nur einmal Speicherplatz benötigt, egal wie oft sie verwendet wird.
- Kapselung: Das Prinzip des Versteckens von Details steigert die Lesbarkeit deutlich, denn eine Funktion, die vielleicht drei Bildschirmseiten füllt, steht einfach als eine Anweisung in einer Zeile. Das ist vor allem deshalb von Vorteil, weil man sich nur einmal mit den Details einer Funktion beschäftigen muss, nämlich dann, wenn man sie erstellt. Für die Nutzung im Programm will man diese Information gar nicht haben, sie stören hier nur (Informationsüberfluß).
- Leistungsfähigkeit: Ein gut modularisiertes Programm erreicht ein bestimmte Funktionalität einfach und kompakt, weil die einzelnen Funktionen so angelegt sind, dass sie einfach und dennoch vielfältig verwendet werden können. Wichtig ist dabei die richtige Portionierung.
- Welche Funktion sollen immer zusammen sein, welche sollten getrennt werden?
- Wie gestaltet man die Parameter für eine Funktion sinnvoll?
- Testbarkeit: Das leidige Thema der Softwareentwicklung ist der Test. Dieser sollte theoretisch alle Fehler finden, praktisch wird das aber oft nicht erreicht. Da Software meist eine recht komplexe Sache ist, kann man sie nur sehr schwer als Gesamtwerk vollständig prüfen. Darum müssen zuerst die Teile einzeln getestet werden. Ein gut modularisiertes Programm kann man leichter testen.
Siehe auch
- Erweiterte LCD-Ansteuerung: Artikel mit einem einfachen Beispiel für strukturierte Programmierung
- Forumsbeitrag: C++ CodeChecking (Style,...)
- Forumsbeitrag: Tutorial für _sauberen_ C-Code
- Forumsbeitrag: goto verpönnt - was dann nehmen?
- Forumsbeitrag: Doku zu AVR Assembler für komplexe Projekte
- Forumsbeitrag: Klingonische Softwareentwickler (Achtung Humor!)
- Forumsbeitrag: Suche Literatur für strukturierte Embedded Programmierung
- Forumsbeitrag: DREI gute Fragen für fast alles
Weblinks
- PC-lint, ein Analyseprogramm für C-Code
- Recommended C Style and Coding Standards, engl.
- Standards and Style for Coding in ANSI C, engl.
- Wikipediaartikel über Programmierstil
- Tool zur statischen Codeanalyse u.a. für C/C++
- Program optimization auf Wikipedia, engl.
- demystifying-the-tlc5940 Umfassende Erklärung zur Ansteuerung eines TLC5940 sowie Hinweisen zum Vorgehen bei der Einarbeitung in neue Hard- und Software, engl.
- The Exceptional Beauty of Doom 3's Source Code (engl.)
- CodeStyleConventions.doc von iD Software für DOOM 3, (lokale Kopie)
- Weniger schlecht programmieren, ISBN: 3897215675
- ThreeStarProgrammer: Warum man keinen hochkomplexen Code schreiben soll (engl.)
- Therac-25 Beschreibung eines Unfalls in der Medizintechnik, hervorgerufen durch Software- und Konzeptfehler
- FATAL DOSE Radiation Deaths linked to AECL Computer Errors (engl.)
- ‘Patterns for Time-Triggered Embedded Systems’ by Michael J. Pont