/* … */. Verwenden Sie für Kommentare innerhalb der Funktionen und hinter Deklarationen //.#define. Beschreiben Sie dabei auch die Einheit, wenn diese nicht bereits eindeutig aus dem Namen hervorgeht.Beispiel für die Dateibeschreibung
Beispiel für zweckorientierte Kommentare
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 ++
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
im Flash abgelegt werden. Verwenden Sie dann die dazu passenden Zugriffsfunktionen; ein gewöhnlicher RAM-Zugriff ist kein Ersatz dafür.
-Wall und -Wextra. Klären und beheben Sie Warnungen, statt diese ohne Begründung auszublenden.F_CPU muss für alle betroffenen Übersetzungseinheiten zur tatsächlich verwendeten Taktfrequenz passen.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:
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.