Vorgaben für die Softwareentwicklung
Codierung und Programmierung
Generelles
- Ziel ist ein Programm, welches seine Aufgabe zuverlässig erfüllt und dessen Aufbau auch von anderen Personen verstanden werden kann. Neben der Funktion zählen deshalb auch die Lesbarkeit, die Dokumentation und eine interessante, gut bedienbare Umsetzung.
- Verwenden Sie für Bezeichner und Code-Kommentare einheitlich Englisch. Die Projektbeschreibung im Wiki und die Bedienungsanleitung können auf Deutsch geschrieben werden.
- Verwenden Sie für C-Beispiele C99 oder einen neueren Sprachstandard. Dokumentieren Sie die im Projekt gewählte Compiler-Einstellung.
- Planen Sie zunächst, welche Aufgaben das Programm erfüllen soll. Überlegen Sie anschließend, welche Funktionen, Daten und Schnittstellen dafür benötigt werden.
- Unterscheiden Sie Eingabe, Verarbeitung und Ausgabe. Eine Änderung an der Anzeige sollte z.B. nicht gleichzeitig eine Änderung an der Berechnung eines Messwertes erfordern.
- Nutzen Sie vorhandene Libraries, wo dies sinnvoll ist. Geben Sie deren Herkunft und Version an. Kennzeichnen Sie, welche Teile selbst entwickelt oder verändert wurden, und beachten Sie die jeweiligen Lizenzbedingungen.
- Die folgenden Beispiele sind teilweise Programmausschnitte. Deklarationen und Funktionen, welche zum jeweiligen Thema nichts beitragen, können darin fehlen. Mit SCHLECHT gekennzeichnete Beispiele sollen typische Fehler zeigen und nicht übernommen werden.
- Formulierungen wie „Verwenden Sie“ oder „Vermeiden Sie“ beschreiben die Vorgaben für die Abgabe. Empfehlungen wie „Es bietet sich an“ lassen eine begründete andere Lösung zu.
Kommentare
- Stellen Sie jeder C-Datei einen beschreibenden Kommentar voran. Orientieren Sie sich dabei an den Übungsprogrammen.
- Beschreiben Sie die Funktion des Programms, die Bedienung und die benötigte Hardware. Geben Sie insbesondere an, ob weitere Dateien eingebunden, Jumper gesetzt oder zusätzliche Bauteile angeschlossen werden müssen.
- Dokumentieren Sie auch Einstellungen, welche für das Übersetzen oder Ausführen notwendig sind, z.B. Mikrocontrollertyp, Taktfrequenz, Compileroptionen und Simulator-Einstellungen.
- Stellen Sie jeder Funktionsdefinition eine kurze Beschreibung voran. Beschreiben Sie ihren Zweck und bei Bedarf Parameter, Rückgabewerte, Voraussetzungen und wichtige Nebenwirkungen.
- Nutzen Sie für Datei- und Funktionsbeschreibungen
/* … */. Verwenden Sie für Kommentare innerhalb der Funktionen und hinter Deklarationen//. - Dokumentieren Sie den Code ausführlich und möglichst zeilennah. Erklären Sie, was Sie mit einer Anweisung bezwecken. Eine bloße Übersetzung der C-Anweisung hilft meist wenig. Besonders wichtig sind Sonderfälle, Grenzen und Annahmen.
- Eine Kurzbeschreibung gehört hinter jede Variablendeklaration und jedes
#define. Beschreiben Sie dabei auch die Einheit, wenn diese nicht bereits eindeutig aus dem Namen hervorgeht. - Richten Sie Kommentare an zusammengehörigen Codezeilen an derselben Spalte aus. Vermeiden Sie doppelte Kommentare und Kommentare, welche nur die Formatierungsregeln wiedergeben.
- Passen Sie Kommentare bei Änderungen des Codes mit an. Entfernen Sie für die Abgabe auskommentierte Versuche und überholte Hinweise. Frühere Versionen gehören in die Versionsverwaltung.
- Falls alternative Einstellungen sinnvoll sind, können Sie deren Bedeutung und Konsequenzen in einem Kommentar erklären.
Beispiel für die Dateibeschreibung
Beispiel für zweckorientierte Kommentare
Makros
- Nutzen Sie für die Manipulation von Bits die vorgegebenen Makros oder gleichwertige, verständliche Bitoperationen.
- Beachten Sie, dass ein Makro vor dem Übersetzen durch Text ersetzt wird. Klammern Sie deshalb seine Parameter und den gesamten Ausdruck geeignet ein.
- Vermeiden Sie Makros, welche einen Parameter mehrfach auswerten. Ein Aufruf mit z.B.
counter'' kann sonst unerwartete Nebenwirkungen haben. Nutzen Sie für Berechnungen mit solchen Parametern besser eine Funktion. * Verändern Sie an einem Register nur die Bits, welche tatsächlich zu Ihrer Aufgabe gehören. Eine vollständige Zuweisung kann andere Einstellungen überschreiben. * Prüfen Sie im Datenblatt, wie das betreffende Register beschrieben werden muss. Die üblichen Bitmakros eignen sich nicht unverändert für jedes Statusregister, z.B. bei Flags, welche durch das Schreiben einer Eins gelöscht werden. ++++Beispiel für Makros ==== Konstanten ==== * Vermeiden Sie Zahlenwerte im Code, deren Bedeutung erst erraten werden muss. Legen Sie dafür benannte Konstanten an, z.B. für Arraygrößen, Zeitintervalle, Grenzwerte und Pinbelegungen. * Offensichtliche Werte wie ''0'' und ''1'' können direkt verwendet werden. Auch bei anderen Werten ist entscheidend, ob ihre Bedeutung im Zusammenhang verständlich ist. Ein Name wie ''TEN'' erklärt weniger als ''DECIMAL_BASE''. * Schreiben Sie symbolische Konstanten in Großbuchstaben. Trennen Sie mehrere Wörter durch Unterstriche, z.B. ''MAX_PLAYER_COUNT'' oder ''DISPLAY_REFRESH_TICKS''. * Nutzen Sie ''#define'', wenn ein Wert auch für den Präprozessor oder als feste Arraygröße benötigt wird. Für typisierte, unveränderliche Daten bieten sich ''const''-Objekte an. * Nutzen Sie für zusammengehörige Zustände oder Auswahlwerte ein ''enum''. Dadurch können auch Parameter und Variablen passend typisiert werden. * Legen Sie gemeinsam benötigte Konfigurationen und Typen in einem geeigneten Header ab. Konstanten, welche nur zu einer C-Datei gehören, können dort verbleiben. * ''const'' beschreibt die Unveränderlichkeit eines Objekts. Es ist beim klassischen ATmega328P keine allgemeine Zusicherung, dass das Objekt keinen SRAM benötigt. Beachten Sie dazu auch den Abschnitt zum Speicherbedarf. ++++Beispiel für zusammengehörige Konstanten ==== Variablen - leserlich, initialisiert und separat ==== * Wählen Sie Namen, welche die Bedeutung der gespeicherten Werte beschreiben. Vermeiden Sie zu allgemeine Namen wie ''value'', ''number'' oder ''data'', wenn z.B. ''measuredVoltageMv'' genauer wäre. * Variablennamen beginnen mit einem Kleinbuchstaben. Weitere Wörter werden ohne Unterstrich angefügt und beginnen mit einem Großbuchstaben, z.B. ''remainingLifeCount''. * Nutzen Sie auch für Schleifenzähler aussagekräftige Namen. Je größer der Gültigkeitsbereich, desto wichtiger ist ein eindeutiger Name. * Verwenden Sie für Wahrheitswerte ''bool'' sowie ''true'' und ''false''. Namen wie ''isMotorRunning'' oder ''hasNewMeasurement'' machen ihre Bedeutung deutlich. Bei C-Versionen vor C23 benötigen Sie dafür ''<stdbool.h>''. * Wählen Sie Datentyp und Vorzeichen passend zum Wertebereich. Für definierte Ganzzahlbreiten bieten sich die Typen aus ''<stdint.h>'' an, z.B. ''uint8_t'', ''int16_t'' und ''uint32_t''. * Beachten Sie auch Zwischenwerte einer Berechnung. Ein ausreichend großer Zieltyp verhindert nicht automatisch einen Überlauf im vorher ausgewerteten Ausdruck. * Initialisieren Sie Variablen, bevor deren Wert gelesen wird. Eine Initialisierung bei der Deklaration ist meist übersichtlich. Ein Wert muss aber nicht zuerst mit Null beschrieben werden, wenn er vor seiner ersten Nutzung ohnehin eindeutig zugewiesen wird. * Nutzen Sie pro Deklaration eine Variable. Ergänzen Sie eine kurze Beschreibung und gegebenenfalls die Einheit. * Verwenden Sie eine Variable nicht nacheinander für mehrere unabhängige Bedeutungen. Ein Messwert sollte z.B. nicht später als Schleifenzähler dienen. * Halten Sie Variablen möglichst lokal. Verwenden Sie globale Variablen nur, wenn deren gemeinsamer Zugriff tatsächlich erforderlich ist. ++++Beispiel für Variablen ==== Anweisungsblöcke und Funktionen ==== * Teilen Sie Ihr Projekt in sinnvolle Unterstrukturen. Jede Funktion sollte eine klar erkennbare Aufgabe erfüllen, z.B. Tasten einlesen, einen Messwert verarbeiten oder die Anzeige aktualisieren. * Halten Sie Funktionen übersichtlich. Eine Funktion sollte höchstens 50 Codezeilen enthalten. Wird sie länger, prüfen Sie, welche Teilaufgaben sinnvoll ausgelagert werden können. Komprimieren Sie den Code nicht nur, um die Zeilenzahl einzuhalten. * Vermeiden Sie auch unnötige Kleinstfunktionen, welche nur einen einfachen Aufruf weiterreichen, ohne dessen Bedeutung verständlicher zu machen. * Bei größeren Programmen ist die Aufteilung in mehrere Dateien sinnvoll, z.B. ''main.c'', ''led.c'' und ''motorDriver.c''. Das ermöglicht ein separates Testen einzelner Module, z.B. mit einer Datei ''testLed.c'', welche ''led.h'' einbindet. * Öffentliche Funktionsdeklarationen gehören in die zugehörige Header-Datei. Definitionen von Funktionen und veränderlichen globalen Objekten gehören in eine C-Datei. Nutzen Sie für gemeinsam benötigte globale Objekte gegebenenfalls eine ''extern''-Deklaration im Header. * Funktionen und Daten, welche nur innerhalb einer C-Datei benötigt werden, werden mit ''static'' auf diese Datei beschränkt. Schützen Sie Header-Dateien durch Include-Guards gegen mehrfaches Einbinden. * Deklarieren Sie alle Funktionen vor ihrer ersten Verwendung. Die Prototypen dateiinterner Funktionen stehen gesammelt vor der ersten Funktionsdefinition. Vermeiden Sie längere Erklärblöcke zwischen diesen Prototypen. * Funktionsnamen beginnen mit einem Verb und beschreiben die Tätigkeit, z.B. ''readButtons()'', ''updateDisplay()'' oder ''sendI2cMessage()''. Abfragen können z.B. ''isMotorRunning()'' heißen. Verwenden Sie camelCase. * Nutzen Sie die automatische Einrückung von Microchip Studio. Zusammengehörige Anweisungen stehen auf derselben Einrückungsebene. Vermeiden Sie unnötige Leerzeilen; eine Leerzeile reicht meist zur Trennung von Arbeitsschritten. * Verwenden Sie geschweifte Klammern auch bei einzelnen Anweisungen. Sehr kurze Bedingungen können einzeilig stehen, z.B. ''if (position > MAX_POSITION) { position = MAX_POSITION; }''. Umfangreichere Blöcke werden mehrzeilig geschrieben. * Verwenden Sie für bedingte Zuweisungen ''if'' und ''else'' statt des Operators ''?:''. Dadurch bleiben auch mehrstufige Entscheidungen für die gemeinsame Durchsicht gut nachvollziehbar. * Legen Sie für Funktionen klare Schnittstellen fest: Welche Parameter sind zulässig? Was wird zurückgegeben? Werden Daten oder Hardwarezustände verändert? Wie wird ein Fehler angezeigt? * Ein Rückgabewert für „nicht gefunden“ darf nicht gleichzeitig eine gültige Fundstelle bezeichnen. ++++Beispiel für Anweisungsblöcke ==== Arrays, Schleifen und Abfragen ==== * Deklarieren Sie Schleifenzähler möglichst direkt im ''for''-Kopf und initialisieren Sie diese dort. Dadurch bleibt ihr Gültigkeitsbereich auf die Schleife begrenzt. * Ein Array mit ''ELEMENT_COUNT'' Elementen besitzt die Indizes 0 bis ''ELEMENT_COUNT - 1''. Prüfen Sie insbesondere Schleifenbedingungen und berechnete Indizes. * Ein Zugriff außerhalb des Arrays ist ein Programmfehler. Er kann andere Daten verändern oder zu undefiniertem Verhalten führen. Dass ein Programm zunächst weiterläuft, zeigt deshalb nicht, dass der Zugriff zulässig war. * Übergeben Sie bei Funktionen mit Arrayparametern auch die benötigte Elementanzahl oder verwenden Sie eine eindeutig dokumentierte feste Größe. * Unterscheiden Sie ''sizeof'' und ''strlen'': ''sizeof'' liefert eine Größe in Bytes, ''strlen'' die Zeichenanzahl eines korrekt nullterminierten Strings ohne Abschlusszeichen. Ein Arrayparameter einer Funktion ist ein Zeiger; aus dessen ''sizeof'' lässt sich die ursprüngliche Arraylänge nicht bestimmen. * Berücksichtigen Sie bei Textpuffern Platz für das abschließende Nullzeichen. * Schleifen, welche im laufenden Betrieb ausgeführt werden, müssen rechtzeitig enden. Prüfen Sie auch Warteschleifen auf Hardwarezustände und sehen Sie gegebenenfalls einen Timeout vor. ++++Beispiel für Arraygrenzen ==== Programmoptimierung - kurz und übersichtlich ==== * Ziel ist ein gut lesbarer und wartbarer Code. Eine geringere Zeilenzahl allein ist noch keine Verbesserung. * Halten Sie insbesondere ''main()'' schlank. Dort sollten Initialisierung und übergeordneter Ablauf erkennbar sein. Die eigentliche Arbeit wird in passende Funktionen aufgeteilt. * Prüfen Sie beim Kopieren von Code, ob sich die gemeinsame Aufgabe durch eine Funktion, eine Schleife oder eine Tabelle ausdrücken lässt. * Nutzen Sie Arrays für Zuordnungen zwischen einem Index und Daten. Nutzen Sie ''switch'', wenn abhängig von einem Zustand unterschiedliche Tätigkeiten ausgeführt werden sollen. Nicht jede kurze Verzweigung muss durch eine Tabelle ersetzt werden. * Berücksichtigen Sie in jedem ''switch'' einen ''default''-Zweig. Legen Sie darin bewusst fest, was bei einem unerwarteten Wert geschehen soll. Prüfen Sie die ''break''-Anweisungen. Ein Durchfallen in den nächsten Zweig darf nicht versehentlich entstehen. * Gegenseitig ausschließende Bedingungen werden als ''if'', ''else if'' und gegebenenfalls ''else'' geschrieben. Nutzen Sie unabhängige ''if''-Anweisungen nur, wenn mehrere Bedingungen gleichzeitig zutreffen und ausgeführt werden dürfen. * Ziehen Sie gemeinsame Anfangs- oder Endanweisungen aus Verzweigungen heraus, sofern Reihenfolge und Verhalten dabei erhalten bleiben. * Nutzen Sie bereits ausgewertete Bedingungen. Zusammengehörige Zuweisungen können oft im selben Zweig erfolgen, ohne dieselbe Beziehung erneut zu prüfen. * Verwenden Sie in ''main()'' eine Endlosschleife. Rufen Sie ''main()'' nicht erneut auf, um einen Neustart auszulösen. Ein gewünschter Neustart wird durch das gezielte Zurücksetzen der zugehörigen Zustände umgesetzt. * Verwenden Sie in den Kursprojekten kein ''goto''. Halten Sie den Kontrollfluss durch Funktionen, Schleifen, ''break'', ''return'' und gegebenenfalls Zustände nachvollziehbar. Eine Abbruchbedingung für verschachtelte Schleifen muss konsistent ausgewertet werden. * Halten Sie Zahlen während ihrer Verarbeitung als Zahlen vor. Wandeln Sie diese erst an der Ausgabeschnittstelle in Text oder ASCII-Zeichen um. * Optimieren Sie zunächst den Ablauf: Vermeiden Sie z.B. unnötige Neuberechnungen und vollständige Displayausgaben. Prüfen Sie anschließend durch Messung, ob eine Änderung tatsächlich schneller oder speichersparender ist. * ''inline'', Makros und kürzere Ausdrücke sind keine allgemeine Zusicherung für schnelleren Code. Dokumentieren Sie die für die Abgabe verwendete Optimierungsstufe. Das korrekte Verhalten darf nicht von zufälligen Compilerentscheidungen abhängen. ++++Beispiel für eine Zuordnung über ein Array ++++Beispiel für switch und einen definierten Fehlerfall ++++Beispiel für gemeinsame Anweisungen ++++Beispiel für eine übersichtliche Hauptschleife ==== Zeitraster, Interrupts und gemeinsame Daten ==== * Nutzen Sie Timer und Interrupts als Zeitbasis. Die Verarbeitung der regelmäßigen Aufgaben erfolgt im Hauptprogramm, z.B. in einem 10-ms-, 100-ms- und 1-s-Raster. * Verwenden Sie ''_delay_ms()'' und ''_delay_us()'' nicht zur zeitlichen Organisation des laufenden Programms. Kurze Wartezeiten können z.B. bei der Initialisierung einer Hardware notwendig sein; begründen Sie diese anhand der Hardwareanforderungen. * Halten Sie ISR kurz. Berücksichtigen Sie dabei auch aufgerufene Unterfunktionen. Umfangreiche Berechnungen, Displayausgaben und Warteschleifen gehören in das Hauptprogramm. * Eine Rasteraufgabe muss so rechtzeitig fertig werden, dass die nachfolgenden Aufgaben bedient werden können. Teilen Sie große Arbeiten, z.B. das Füllen eines Displays, in begrenzte Teilschritte auf. * Nutzen Sie bei mehrstufigen Abläufen Zustandsautomaten statt langer Warteabläufe. Ein Zustand kann z.B. „Initialisierung“, „bereit“, „aktiv“ oder „Fehler“ beschreiben. * Kennzeichnen Sie Daten, welche zwischen ISR und Hauptprogramm ausgetauscht werden. Bei entsprechenden Zugriffen ist ''volatile'' erforderlich, damit der Compiler die notwendige Beobachtung von Änderungen berücksichtigt. * ''volatile'' verhindert keine Unterbrechung zwischen mehreren Zugriffsschritten. Gemeinsam genutzte mehrbyteige Werte und zusammengehörige Lese-/Änderungsfolgen müssen bei Bedarf atomar bearbeitet werden. * Halten Sie atomare Bereiche möglichst kurz. Nutzen Sie dafür bei AVR-GCC z.B. ''ATOMIC_BLOCK(ATOMIC_RESTORESTATE)'' aus ''<util/atomic.h>''. Führen Sie die eigentliche Verarbeitung anschließend außerhalb des geschützten Bereichs aus. * Legen Sie fest, ob Ereignisse zusammengefasst werden dürfen. Ein boolesches Flag bedeutet „mindestens ein Ereignis liegt vor“. Wenn jedes Ereignis gezählt werden muss, benötigen Sie einen Zähler oder eine geeignete Warteschlange und eine definierte Behandlung ihres Überlaufs. * Dokumentieren Sie, welche Pins, Timer und Schnittstellen von welchem Modul genutzt werden. Zwei Libraries dürfen nicht unbemerkt denselben Timer unterschiedlich konfigurieren. ++++Beispiel für das Übernehmen eines Ereignisflags ++im Flash abgelegt werden. Verwenden Sie dann die dazu passenden Zugriffsfunktionen; ein gewöhnlicher RAM-Zugriff ist kein Ersatz dafür.Speicherbedarf und Hardwarezugriffe
* Unterscheiden Sie Programmspeicher (Flash), Datenspeicher (SRAM) und gegebenenfalls EEPROM. Prüfen Sie den Speicherbedarf im Build-Ergebnis. * Der dort angegebene statische SRAM-Verbrauch enthält nicht den gesamten zur Laufzeit benötigten Stack. Lassen Sie Platz für lokale Variablen, Funktionsaufrufe und Interrupts. * Vermeiden Sie in den Grundlagenprojekten Rekursion und dynamische Speicherverwaltung. Verwenden Sie begrenzte Datenstrukturen mit nachvollziehbarer Größe. * Legen Sie große lokale Arrays nicht ohne Abschätzung ihres Speicherbedarfs an. Ein vollständiges Farbbild kann den SRAM eines kleinen Mikrocontrollers bereits deutlich überschreiten. * Beim klassischen ATmega328P können größere unveränderliche Tabellen und Texte mit ''PROGMEM
- Kapseln Sie Hardwarezugriffe in passenden Modulen. Anwendungscode sollte z.B. nicht an mehreren Stellen unabhängig voneinander dieselben SPI- oder Timerregister umkonfigurieren.
- Initialisieren Sie Ausgänge und Peripherie in einer definierten Reihenfolge. Beachten Sie dabei auch den Zustand angeschlossener Aktoren während des Starts und bei Fehlern.
Testen und Abgabe
- Testen Sie nach kleinen, zusammenhängenden Änderungen. Prüfen Sie zunächst die betroffene Funktion und anschließend ihr Zusammenspiel mit den übrigen Modulen.
- Prüfen Sie neben dem Normalfall auch Grenzen und Sonderfälle: erster und letzter Arrayeintrag, leere Datenmenge, ungültige Eingabe, Zählergrenzen und maximale Belegung.
- Prüfen Sie bei zeitgesteuerten Programmen auch Start, Pause, Neustart und einen längeren Betrieb. Ein Programm kann nach dem Einschalten korrekt aussehen und erst später durch einen Überlauf oder einen blockierenden Ablauf ausfallen.
- Halten Sie wesentliche Tests und deren Ergebnis kurz fest. Bei Änderungen an Geschwindigkeit oder Speicherbedarf gehören auch die verwendeten Einstellungen und Messbedingungen dazu.
- Übersetzen Sie das Programm mit aktivierten Compilerwarnungen, z.B.
-Wallund-Wextra. Klären und beheben Sie Warnungen, statt diese ohne Begründung auszublenden. - Geben Sie die tatsächlich geprüfte Konfiguration an: Mikrocontroller, Taktfrequenz, Compiler-/Linkeroptionen, Library-Versionen und notwendige Simulator-Einstellungen.
F_CPUmuss für alle betroffenen Übersetzungseinheiten zur tatsächlich verwendeten Taktfrequenz passen. - Prüfen Sie, ob das Projekt nach einem vollständigen Neuübersetzen aus den abgegebenen Dateien lauffähig ist. Entfernen Sie alte Versuche, unbenutzte Dateien und irreführende Versionsstände.
- Legen Sie eine kurze Bedienungsbeschreibung bei. Eine andere Person muss das Projekt ohne mündliche Ergänzungen starten und die vorgesehenen Funktionen ausprobieren können.
Bewertung
Zur Bewertung lege ich die für die Veranstaltung bereitgestellte Bewertungscheckliste sowie die hier beschriebenen Vorgaben zugrunde. Beachten Sie auch die Hinweise zur Präsentation und Abgabe.
Prüfen Sie vor der Abgabe insbesondere:
- Erfüllt das Programm die vereinbarte Aufgabe und lässt es sich nachvollziehbar bedienen?
- Sind Namen, Funktionen und Module verständlich aufgebaut?
- Stimmen Kommentare, Bedienungsbeschreibung und tatsächliches Verhalten überein?
- Sind Arraygrenzen, Wertebereiche und Rückgabewerte korrekt behandelt?
- Bleiben ISR kurz und Rasteraufgaben zeitlich begrenzt?
- Sind gemeinsame Daten und Hardware-Ressourcen konsistent behandelt?
- Ist das Projekt vollständig und ohne ungeklärte Compilerwarnungen übersetzbar?
- Sind wichtige Normal-, Grenz- und Fehlerfälle geprüft?
Weiterführende Hinweise:
Diese überarbeitete Fassung basiert auf den bisherigen MEXLE-Vorgaben für die Softwareentwicklung und den ergänzenden Vorgaben von Prof. Tim Fischer. Die bisherige Wikiseite steht unter CC BY-SA 4.0; diese bearbeitete Fassung wird unter derselben Lizenz bereitgestellt.