====== 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|
/* Run a button-controlled counter on the MEXLE board.
*
* File: main.c
* Authors: Names of the project members
* Version: 1.0, 2026-11-07
* Hardware: ATmega328P, MEXLE board and the specified display module
* Clock: 18.432 MHz; F_CPU must match the hardware or simulator clock
* Toolchain: Microchip Studio; record the compiler version used for submission
* Operation: S2 increases the counter; S3 decreases it without wrapping
* Display: Show the current counter value on the first display line
* Wiring: Describe the actual pins and any required jumpers here
* Resources: Timer0 provides the time base; the display uses hardware SPI
* Files: main.c, main.h and the documented display library
* Build: Record any required compiler symbols and linker options here
*/
Ersetzen Sie die Angaben durch die tatsächlichen Daten Ihres Projekts. Ein Dateikopf mit unveränderten Platzhaltern ist keine vollständige Beschreibung.
++++
++++Beispiel für zweckorientierte Kommentare|
**SCHLECHT**
if (sampleIndex == 0) { total = 0; } // Set total to zero when sampleIndex is zero.
**GUT**
if (sampleIndex == 0) { total = 0; } // Start a new measurement series without values from the previous series.
Der zweite Kommentar erklärt den Zusammenhang mit der Messaufgabe.
++++
==== 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|
**SCHLECHT**
#define TWICE(value) 2 * value // Double the supplied expression.
uint8_t oldValue = 4; // Example input for the calculation.
uint8_t newValue = TWICE(oldValue + 2);
Hier entsteht ''2 * oldValue + 2'' statt ''2 * (oldValue + 2)''.
**GUT**
#define SET_BIT(port, bit) ((port) |= (1U << (bit))) // Set one selected bit without replacing the other port bits.
#define CLR_BIT(port, bit) ((port) &= ~(1U << (bit))) // Clear one selected bit without replacing the other port bits.
#define TGL_BIT(port, bit) ((port) ^= (1U << (bit))) // Toggle one selected bit without replacing the other port bits.
Die Bitnummer muss zum Register und zum verwendeten Datentyp passen. Die Makros sichern einen gemeinsamen Registerzugriff aus Hauptprogramm und ISR nicht automatisch gegen Unterbrechungen ab.
++++
==== 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|
**SCHLECHT**
#define gameIntro 0 // Initial screen.
#define gameStarted 1 // Active game.
#define gamePaused 2 // Paused game.
**GUT**
typedef enum
{
GAME_INTRO, // Wait for the player to start.
GAME_RUNNING, // Accept input and advance the game.
GAME_PAUSED, // Keep the current scene without advancing the game.
GAME_ENDED // Show the result and offer a restart.
} GameState;
static GameState gameState = GAME_INTRO; // Current state of this game module.
Die Typbezeichnung ''GameState'' und die Variable ''gameState'' erfüllen unterschiedliche Aufgaben. Die Werte des ''enum'' sind 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 ''''.
* Wählen Sie Datentyp und Vorzeichen passend zum Wertebereich. Für definierte Ganzzahlbreiten bieten sich die Typen aus '''' 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|
**SCHLECHT**
int a, b;
char flag;
**GUT**
#include
#include
#define INITIAL_LIFE_COUNT 3U // Lives available when a new game starts.
uint16_t measuredLengthMm = 0; // Most recent measured length in millimetres.
uint8_t remainingLifeCount = INITIAL_LIFE_COUNT; // Lives available before the game ends.
bool hasNewMeasurement = false; // Requests processing of a newly captured measurement.
Die Einheit ist im Namen des Messwerts erkennbar. Die anfängliche Lebensanzahl ist als benannte Konstante festgelegt.
++++
==== 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|
**SCHLECHT**
uint8_t unter2_neu(uint8_t *arr, uint8_t Was)
{
int a;
for(a=10;a<20;a++)
{
if(arr[a]==Was)
{
return a;
};
};
return 0;
}
Die Namen erklären die Aufgabe nicht ausreichend. Die Suchgrenzen sind fest eingetragen, obwohl die Arraylänge unbekannt ist. Außerdem ist die Einrückung uneinheitlich.
**GUT**
#include
#define KEY_NOT_FOUND UINT8_MAX // Reserve a return value outside the valid index range.
static uint8_t findKeyPosition(const uint8_t *keys, uint8_t keyCount, uint8_t key);
/* Find the first occurrence of a key in the supplied array.
* keys must provide at least keyCount readable elements.
* Return the matching index, or KEY_NOT_FOUND when no element matches.
* A count of zero performs an empty search without accessing the array.
*/
static uint8_t findKeyPosition(const uint8_t *keys, uint8_t keyCount, uint8_t key)
{
for (uint8_t keyPosition = 0; keyPosition < keyCount; ++keyPosition) // Keep the search inside the supplied array bounds.
{
if (keys[keyPosition] == key) { return keyPosition; } // Select the first occurrence when a key appears more than once.
}
return KEY_NOT_FOUND; // Do not confuse an unsuccessful search with a match at position zero.
}
Die Funktion erhält das Array, seine Elementanzahl und den gesuchten Wert. Bei maximal 255 Elementen liegen gültige Positionen zwischen 0 und 254. Der Wert 255 bleibt für „nicht gefunden“ reserviert. ''const'' verhindert Änderungen der Elemente über den übergebenen Zeiger. Der Aufrufer ist dafür verantwortlich, dass die angegebene Elementanzahl zum Array passt.
++++
==== 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|
**SCHLECHT**
for (uint8_t index = 0; index <= elementCount; ++index)
{
values[index * 2] = values[index * 2 - 1];
}
Bereits im ersten Durchlauf entsteht ein ungültiger Index. Auch die obere Grenze passt nicht zu den verwendeten Ausdrücken.
**GUT**
#include
#define PAIR_LENGTH 2U // Number of adjacent elements belonging to one pair.
static void copyFirstToSecondOfPairs(uint8_t *values, uint8_t elementCount);
/* Copy the first value of each complete pair into its second element.
* values must provide elementCount writable elements.
* Leave an unpaired final element unchanged.
*/
static void copyFirstToSecondOfPairs(uint8_t *values, uint8_t elementCount)
{
for (uint8_t firstIndex = 0; firstIndex + 1U < elementCount; firstIndex += PAIR_LENGTH)
{
values[firstIndex + 1U] = values[firstIndex]; // Change only pairs whose two elements both belong to the array.
}
}
Die Schleife prüft direkt, ob auch das zweite Element des nächsten Paares vorhanden ist. Bei einer ungeraden Elementanzahl bleibt das letzte Element unverändert.
++++
==== 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|
**SCHLECHT**
if (digit == 0) { outputText("zero"); }
if (digit == 1) { outputText("one"); }
if (digit == 2) { outputText("two"); }
**GUT**
#include
#define DIGIT_NAME_COUNT 3U // Number of supported names in this small example.
#define DIGIT_NAME_SIZE 5U // Capacity for the longest name and its terminating null character.
static const char DIGIT_NAMES[DIGIT_NAME_COUNT][DIGIT_NAME_SIZE] =
{"zero", "one", "two"}; // Keep the index-to-name mapping together.
void outputText(const char *text);
static void outputDigitName(uint8_t digit);
/* Output a supported digit name, or an explicit fallback for an invalid index. */
static void outputDigitName(uint8_t digit)
{
if (digit < DIGIT_NAME_COUNT) { outputText(DIGIT_NAMES[digit]); }
else { outputText("unknown"); } // Reject unsupported input before using it as an array index.
}
Die Zuordnung ist an einer Stelle erkennbar. Die Indexprüfung bleibt auch bei einer Tabellenlösung erforderlich.
++++
++++Beispiel für switch und einen definierten Fehlerfall|
typedef enum
{
MOTOR_STOPPED, // The output must remain inactive.
MOTOR_RUNNING // The output may drive the motor.
} MotorState;
void stopMotor(void);
void startMotor(void);
static void applyMotorState(MotorState state);
/* Apply the selected output state and stop safely for an unsupported state value. */
static void applyMotorState(MotorState state)
{
switch (state)
{
case MOTOR_STOPPED:
stopMotor();
break;
case MOTOR_RUNNING:
startMotor();
break;
default:
stopMotor(); // An invalid state must not leave the actuator running.
break;
}
}
Ein leerer ''default''-Zweig ist nicht automatisch die passende Fehlerbehandlung. Das sinnvolle Verhalten hängt von der Aufgabe ab. Gleiche Tätigkeiten mehrerer Fälle können durch direkt aufeinanderfolgende ''case''-Marken zusammengefasst werden. Compiler-Erweiterungen wie ''case 1 ... 5'' werden in den Grundlagenbeispielen vermieden.
++++
++++Beispiel für gemeinsame Anweisungen|
**SCHLECHT**
if (hasNewMeasurement)
{
calculateMeasurement();
updateDisplay();
}
else
{
readButtons();
updateDisplay();
}
**GUT**
if (hasNewMeasurement) { calculateMeasurement(); }
else { readButtons(); }
updateDisplay(); // Both paths finish with the same display update.
Die gemeinsame Endanweisung wird nur einmal geschrieben. Prüfen Sie beim Zusammenfassen immer, ob die Ausführungsreihenfolge unverändert bleibt.
++++
++++Beispiel für eine übersichtliche Hauptschleife|
Die Funktionen für Initialisierung, Eingabe, Verarbeitung und Ausgabe werden in ''main.h'' deklariert. Der dateiinterne Helfer ''take10msTask()'' übernimmt eine anstehende Rasteranforderung; seine Implementierung aus dem folgenden Interrupt-Beispiel steht ebenfalls in ''main.c''.
#include
#include "main.h"
static bool take10msTask(void);
int main(void);
/* Initialize the hardware once and keep the cyclic application work in the main context. */
int main(void)
{
initializeHardware(); // Establish defined I/O states and the timer-based scheduler before normal operation.
while (true)
{
if (take10msTask())
{
readInputs(); // Capture one consistent input set for this update.
updateModel(); // Process the captured values without mixing in display operations.
writeOutputs(); // Publish the resulting actuator and display state.
}
}
}
++++
==== 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 ''''. 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|
Der Timer muss zuvor so konfiguriert werden, dass sein Compare-Interrupt das gewünschte Raster erzeugt. Das Beispiel zeigt den Datenaustausch, nicht die vollständige Timerinitialisierung.
#include
#include
#include
static volatile bool is10msTaskPending = false; // Combine timer events until the main loop accepts the next update.
static bool take10msTask(void);
/* Publish a time-base event without performing application work in the interrupt. */
ISR(TIMER1_COMPA_vect)
{
is10msTaskPending = true; // Request application work at the next opportunity in the main loop.
}
/* Accept and clear the pending request without losing an interrupt between the two operations. */
static bool take10msTask(void)
{
bool hasPendingTask = false; // Local snapshot used after interrupts have been restored.
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
hasPendingTask = is10msTaskPending; // Preserve the accepted request for processing after the protected block.
is10msTaskPending = false; // Allow a later interrupt to publish a new request independently.
}
return hasPendingTask; // Keep all input, calculation and drawing work outside the protected block.
}
Das gemeinsame Lesen und Löschen verhindert einen verlorenen Flag-Eintrag zwischen diesen beiden Operationen. Mehrere Timerereignisse vor der Übernahme werden trotzdem absichtlich zusammengefasst. Für eine genaue Ereignisanzahl reicht dieses Flag nicht aus.
++++
==== 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.
* 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. ''-Wall'' und ''-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_CPU'' muss 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 [[https://wiki.mexle.org/_media/microcontrollertechnik/bewertung_ss24_prj99.xlsx|Bewertungscheckliste]] sowie die hier beschriebenen Vorgaben zugrunde. Beachten Sie auch die [[https://wiki.mexle.org/microcontrollertechnik/presentation_and_submission|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:
* [[https://avrdudes.github.io/avr-libc/avr-libc-user-manual/group__util__atomic.html|AVR-LibC: atomare Zugriffe und Interruptschutz]]
* [[https://avrdudes.github.io/avr-libc/avr-libc-user-manual/pgmspace.html|AVR-LibC: Daten im Programmspeicher]]
Diese überarbeitete Fassung basiert auf den [[https://wiki.mexle.org/microcontrollertechnik/vorgaben_fuer_die_softwareentwicklung|bisherigen MEXLE-Vorgaben für die Softwareentwicklung]] und den ergänzenden Vorgaben von Prof. Tim Fischer. Die bisherige Wikiseite steht unter [[https://creativecommons.org/licenses/by-sa/4.0/|CC BY-SA 4.0]]; diese bearbeitete Fassung wird unter derselben Lizenz bereitgestellt.