Vorgaben für die Softwareentwicklung
Codierung und Programmierung
Generelles
Es empfiehlt sich für alle definierten und deklarierten Namen die Englische Sprache zu verwenden. Für Variablen, Funktionen und Kommentare darf die Deutsche Sprache genutzt werden.
-
Neben der Struktur beim Programmieren zählt auch eine interessante und schöne Umsetzung für den Nutzer in die Bewertung.
Kommentare
Stellen Sie Ihrem Programm einen beschreibenden Kommentar voran.
Dieser sollte in der Form sein, wie die Beschreibung in den Übungsprogrammen.
Beschreiben Sie darin, ob
weitere C- oder H-Dateien eingebunden werden müssen,
Jumper gesetzt / geöffnet werden müssen,
spezielle Hardware genutzt werden muss.
Geben Sie auch notwendige Einstellungen an, z.B. Taktfrequenz, Fuses und besondere Compileroptionen.
Beschreibung
/* ============================================================================
mein Programm: Programmbeispiel für mich
============= ===============================================================
Dateiname: MEXLE_MeinProgramm.c
Autoren: Max Integer (Hochschule Heilbronn)
Version: 0.5 vom 28.02.2019
Hardware: z.B. MiniMEXLE Ver. 3.0 (oder angepasste Version 2.0) oder MEXLE2020
AVR-USB-PROGI Ver. 2.0
Software: Microchip Studio Ver. 7.0.1417
Funktion: Dieses Programm soll ein einfaches Beispiel der Ein- und Ausgabe
am MiniMEXLE sein. Ein einfacher Zähler wird hoch- oder
heruntergezählt.
Displayanzeige: Start (für 2 s): Betrieb:
+----------------+ +----------------+
| Mein Programm | |Counter 0 |
| Zaehler | | Up Down |
+----------------+ +----------------+
Tastenfunktion: S2: Up (zählt Counter aufwärts, Überlauf bei 255)
S3: Down (zählt Counter abwärts, Unterlauf bei 0)
Jumperstellung: keine Auswirkung
Fuses im µC: CKDIV8: Aus (keine generelle Vorteilung des Takts)
Header-Files: lcd_lib_de.h (Library zur Ansteuerung des LCD-Displays Ver. 1.2)
Module: 1) getSwitchState: Schalter einlesen
2) setDisplayValues: Werte ausgeben
1) getSwitchState: ...
2) setDisplayValues: ...
============================================================================ */
Es empfiehlt sich die Code-Kommentierung zeilenweise durchzuführen. Schreiben Sie dabei nicht, was im Code bereits steht, sondern was Sie mit dem Code bezwecken.
Die Kommentare sollten zu dem aktuell gehalten werden
Beispiel für Code-Kommentierung
| SCHLECHT |
if(i==0) output=0; // wenn i = 1, output=0
|
| GUT |
if(i==0) output=0; // nur für erstes Element wird der Output zurückgesetzt
|
Während der Entwicklungsphase kann es sich anbieten, Code testweise auszukommentieren. Für die finale Version sollten die Kommentare aber „sauber“ sein.
Falls es alternative Werte gibt, welche optional sinnvoll sind, können diese und deren Konsequenzen in einen Kommentar gepackt werden.
Ergänzen Sie hinter jedem #define und jeder Variablendeklaration eine Kurzbeschreibung. Richten Sie Kommentare an zusammengehörigen Codezeilen an derselben Spalte aus.
Verwenden Sie /* … */ für Datei- und Funktionsbeschreibungen und // für Kommentare an den Codezeilen. Vermeiden Sie doppelte und veraltete Kommentare.
Beispiel für auskommentierten Code
| SCHLECHT |
...
if (i==1) output("eins"); // ToBeChanged: noch an Zähler anpassen
// if (i==3) output("null");
// if (i==4) montagA();
// if (i==5) ??;
...
|
| GUT |
...
if (i==STARTWERT) LCDoutput(startAusgabe); // nur bei i=1 erfolgt eine Ausgabe
// bei den anderen Werten erübrigt sich die
// Ausgabe, weil i <=1
...
|
Makros
Nutzen Sie für die Manipulation von Bits die vorgegebenen Makros.
Beim Erstellen eigener Makros sollte auf Querwirkungen geachtet werden, da ein Makro eine Codeersetzung vor dem Compiler durchführt.
Beispiel für die vordefinierten Makros
| SCHLECHT |
#define TWICE(x) 2*x // Port-Bit Setzen
void beispiel(void)
{
...
PORTD = PORTD & 64;
alterWert = 4;
neuerWert = TWICE(alterWert+2); // durch das Makro wird der Code nur ersetzt
// es ergibt sich also neuerWert = 2*alterWert+2
// der Compiler wertet dieses über Punkt vor Strich aus
// es ergibt sich also 2*4+2=10 und nicht 2*(4+2)=12
...
}
|
| GUT |
// Makros
#define SET_BIT(PORT, BIT) ((PORT) |= (1 <<(BIT))) // Port-Bit Setzen
#define CLR_BIT(PORT, BIT) ((PORT) &= ~(1 <<(BIT))) // Port-Bit Loeschen
#define TGL_BIT(PORT, BIT) ((PORT) ^= (1 <<(BIT))) // Port-Bit Toggeln
void beispiel(void);
/* Den Ausgang deaktivieren und den neuen Wert berechnen. */
void beispiel(void)
{
...
CLR_BIT(PORTD, ENABLE);
alterWert = 4;
neuerWert = 2*(alterWert+2);
...
}
|
Konstanten
Konstanten per #define sollten z.B. für die feste Größe von Arrays verwendet werden. Sie können auch für hardwarenahe Werte wie Portnummern genutzt werden.
Auch Werte in enum sind Konstanten. Für Konstanten mit ähnlichem Hintergrund, z.B. Tage oder Zustände, sollten enum genutzt werden. Damit können Variablen auch als enum-Typ definiert werden, was den Code leserlicher macht.
Bei Defines wird keine Typisierung überwacht. Ist dies notwendig, empfiehlt es sich, const-Variablen zu nutzen.
Konstanten per #define oder enum sind komplett in Großbuchstaben zu schreiben. Bestehen sie aus mehreren Wörtern, werden diese mit Unterstrichen getrennt.
Vermeiden Sie Zahlenwerte im Code, deren Bedeutung erst erraten werden muss. Nutzen Sie dafür aussagekräftige Konstanten.
enum tage {MONTAG, DIENSTAG, MITTWOCH, DONNERSTAG, FREITAG, SAMSTAG, SONNTAG};
for (enum tage aktuellerTag = MONTAG; aktuellerTag <= SONNTAG; aktuellerTag++)
{...};
Beispiel für Konstanten
| SCHLECHT |
// Konstanten
#define CONST1 1.414 // Korrekturwert.
#define PORT1 4 // Erster Port.
#define gameIntro 0 /* ist null */
#define gameStarted 1
#define gamePaused 2
#define gameEnded 3
...
int ZeichenAufLCD[2][16]=0;
...
|
| GUT |
// Konstanten
#define SQRT_OF_2 1.414 // Wurzel aus 2
#define FIRST_PORT 4 // Erster Port für die Eingabe
#define XMAX_LCD 2 // Anzahl der Zeilen
#define YMAX_LCD 16 // Anzahl der Spalten
enum gameStates
{
GAMESTATE_INTRO, // Startanzeige.
GAMESTATE_STARTED, // Laufendes Spiel.
GAMESTATE_PAUSED, // Unterbrochenes Spiel.
GAMESTATE_ENDED // Ergebnisanzeige.
};
...
int ZeichenAufLCD[XMAX_LCD][YMAX_LCD]=0;
...
|
Variablen - leserlich, initialisiert und separat
Wenn sich Werte im Programm zur Laufzeit ändern, so sollten diese als Variable angelegt werden.
Nutzen Sie soweit möglich const für Werte, welche zur Laufzeit nicht mehr geändert werden. Gemeinsam benötigte Konstanten können in einem geeigneten Header gepflegt werden; Definitionen von Objekten gehören dabei grundsätzlich in die zugehörige C-Datei.
Wichtig: Das gilt z.B. für Grenzen von Schleifen ( for(int i=0; i<iMax;i=i+1)) oder für Sonderzeichen. Bei größeren Programmen biete es sich an die const Variablen mit in einem separaten header zu pflegen.
Variablen beginnen mit Kleinbuchstaben.
Bestehen Namen aus mehreren Wörtern, werden die folgenden Wörter ohne Unterstrich direkt angefügt, aber groß geschrieben. Dies wird auch als „Binnenmajuskel“ oder „camelCase“ bezeichnet.
Vermeiden Sie zu allgemeine Namen wie anzahl, uebergabewert oder string. Sinnvoller sind Namen wie anzahlBuchstaben, stunden oder ausgabeString. Durch die Autovervollständigung sind auch längere Namen schnell einzugeben.
Nutzen Sie auch bei Zählvariablen aussagekräftige Namen.
Auch eine zu allgemeine Deklaration kann zu Problemen führen. Schlecht ist z.B. int a;. Wählen Sie Datentyp, Vorzeichen und Wertebereich bewusst.
Es bietet sich an, bei der Definition bereits zu initialisieren. Deklarieren Sie jede Variable separat und lesen Sie keine Variable vor ihrer Initialisierung.
Verwenden Sie für Wahrheitswerte bool mit true und false. In den hier verwendeten C-Versionen wird dafür <stdbool.h> eingebunden.
Nutzen Sie eine Variable nicht nacheinander für mehrere unabhängige Aufgaben. Halten Sie ihren Gültigkeitsbereich möglichst klein.
Beispiel für Variablen
| SCHLECHT |
// Variablen
int spieler = 2; // unklar, ob Konstante
int gem_Lae_1; // unklar, ob es ein Vorzeichen besitzt;
// unklar, ob es nur 8bit sein sollten
// unklar, welche Länge gemeint ist
char gem_Lae_2; // unklar, ob es ein Vorzeichen besitzt;
// unklar, welche Länge gemeint ist
char wasKopie2; // unklar,Was was ist
|
| GUT |
// Variablen
const uint8_t maxAnzSpieler = 2; // Maximale Anzahl der Spieler
uint16_t gemesseneLaenge = 0; // gemessene Länge in Meter
uint8_t gemesseneBreite = 0; // gemessene Breite in Meter
bool istZeichenAusgabeErlaubt = true; // Freigabe für die Ausgabe eines Zeichens.
|
Anweisungsblöcke und Funktionen
Bei größeren Programmen ist auch die Aufteilung in mehrere Dateien sinnvoll, also z.B. main.c, LED.c und motorDriver.c. Dabei sollte darauf geachtet werden, dass Variablen und Konstanten möglichst nur im Kontext der jeweiligen Datei genutzt werden und zugehörige Header-Dateien angelegt werden. Das ermöglicht ein separates Testen einzelner Module, z.B. mit einer Datei testLED.c, welche LED.h einbindet.
Beschränken Sie dateiinterne Funktionen und Variablen mit static auf die jeweilige C-Datei. Öffentliche Schnittstellen werden in der zugehörigen Header-Datei deklariert.
Führen Sie die Funktionsdeklarationen vor der ersten Funktionsdefinition auf. Vermeiden Sie längere Erklärblöcke zwischen diesen Deklarationen.
Nutzen Sie den Zeileneinschub, den Microchip Studio automatisch anbietet.
Für die Benennung von Funktionen bietet sich - wie bei Variablen - camelCase an. Zum leichteren Verständnis sollten Funktionsnamen ein Verb enthalten. Bei englischen Namen steht dieses am Anfang, z.B. sendI2cMessage() oder isI2cMessagePending().
Damit wird der Code besser lesbar, z.B. if (isI2cMessagePending()) { sendI2cMessage(); }.
Vermeiden Sie zu viele Leerzeilen.
Stellen Sie jeder Funktion eine kurze Beschreibung voran. Aus dieser sollte hervorgehen, was Sinn und Zweck der Funktion ist.
Beispiel für Anweisungsblöcke
| SCHLECHT |
uint8_t unter2_neu(uint8_t Was)
{
int a;
for(a=10;a<20;a++)
{
if(arr[a]==Was)
{
return a;
};
};
return 0;
}
|
| GUT |
#define KEIN_SCHLUESSEL UINT8_MAX // Rückgabewert außerhalb der gültigen Suchpositionen.
static uint8_t schluesselPositionFinden(uint8_t schluessel);
/* Das Array schluesselArray nach dem übergebenen Schlüssel durchsuchen.
* Bei Erfolg wird die erste Fundstelle zurückgegeben, sonst KEIN_SCHLUESSEL.
*/
static uint8_t schluesselPositionFinden(uint8_t schluessel)
{
for (uint8_t aktuelleSchluesselPosition = ERSTE_SCHLUESSEL_POSITION;
aktuelleSchluesselPosition <= LETZTE_SCHLUESSEL_POSITION;
aktuelleSchluesselPosition++)
{
if (schluesselArray[aktuelleSchluesselPosition] == schluessel)
{
return aktuelleSchluesselPosition; // Bei mehreren Treffern die erste Fundstelle verwenden.
}
}
return KEIN_SCHLUESSEL; // Eine erfolglose Suche von einer gültigen Position unterscheiden.
}
In diesem Beispiel müssen die Suchgrenzen zum vorhandenen Array passen. Die letzte Suchposition muss kleiner als UINT8_MAX sein.
Weiterhin bietet es sich an, einen Zeiger auf das Array und die Suchgrenzen als Parameter zu übergeben, um die Funktion flexibler anwenden zu können. |
Arrays, Schleifen und Abfragen
Es ist sinnvoll, die Schleifenvariable direkt im for-Kopf zu deklarieren und zu initialisieren, also z.B. for (uint8_t element = 0; element < MAX; element++).
Vermeiden Sie Zugriffe auf Bereiche von Arrays, welche nicht definiert wurden. So erhält man bei einem Array uint8_t array[10]; mit Zugriff auf array[-1], array[10] oder array[21] keine Werte des eigentlichen Array, sondern Werte von anderen Variablen. Ein Beschreiben dieser Bereich kann zu unerwarteten Werten in anderen Variablen führen.
Beispiel für Arrays, Schleifen und Abfragen
| SCHLECHT |
#define MAX 10
uint8_t array[MAX];
for (uint8_t a = 0; a <= MAX; a++)
{
array[a * 2] = array[a * 2 - 1];
}
|
| GUT |
#define ARRAY_GROESSE 10 // Anzahl der vorhandenen Arrayelemente.
#define PAAR_GROESSE 2 // Anzahl der Elemente pro zu bearbeitendem Paar.
uint8_t array[ARRAY_GROESSE] = {0}; // Werte der zu bearbeitenden Paare.
for (uint8_t paar = 1; paar * PAAR_GROESSE < ARRAY_GROESSE; paar++)
{
array[paar * PAAR_GROESSE] = array[paar * PAAR_GROESSE - 1];
}
|
Programmoptimierung - kurz und übersichtlich
Ziel ist ein leicht lesbarer und wartbarer Code. Halten Sie deswegen alle Funktionen schlank - auch main(). Als Richtwert sollten Funktionen höchstens 50 Codezeilen enthalten.
Versuchen Sie sinnvolle Unterfunktionen zu programmieren. Trennen Sie Eingabe, Verarbeitung und Ausgabe.
Versuchen Sie, sinnvolle Unterfunktionen zu programmieren. Trennen Sie Eingabe, Verarbeitung und Ausgabe. Vermeiden Sie aber unnötige Kleinstfunktionen, welche nur einen einfachen Aufruf weiterreichen.
Überlegen Sie sich beim Einsatz von Copy-Paste, warum dies nicht als Unterfunktion, Schleife oder Array lösbar ist.
Beispiel für ähnliche Zeilen
| SCHLECHT |
temp = hunderter;
lcd_goto(1, 0);
lcd_putc(0x30 + temp % 10);
temp = zehner;
lcd_goto(1, 1);
lcd_putc(0x30 + temp % 10);
temp = einser;
lcd_goto(1, 2);
lcd_putc(0x30 + temp % 10);
|
| GUT |
#define ASCII_ZERO 0x30 // Zeichencode der Ziffer Null.
#define MOD_TEN 10 // Basis der dezimalen Ziffernausgabe.
#define DISP_LINE1 1 // Displayzeile für den Zahlenwert.
#define DISP_POS0 0 // Position der Hunderterziffer.
#define DISP_POS1 1 // Position der Zehnerziffer.
#define DISP_POS2 2 // Position der Einerziffer.
static void printDecimalDigit(uint8_t x, uint8_t y, uint8_t digitToBePrinted);
/* Eine einzelne Dezimalziffer an der gewünschten Displayposition ausgeben. */
static void printDecimalDigit(uint8_t x, uint8_t y, uint8_t digitToBePrinted)
{
lcd_goto(x, y);
lcd_putc(ASCII_ZERO + digitToBePrinted % MOD_TEN);
}
/* Aufrufe innerhalb der Anzeigefunktion: */
printDecimalDigit(DISP_LINE1, DISP_POS0, hunderter);
printDecimalDigit(DISP_LINE1, DISP_POS1, zehner);
printDecimalDigit(DISP_LINE1, DISP_POS2, einser);
|
Prüfen Sie, ob aufeinanderfolgende ähnliche if-Anweisungen sich direkt über Arrays lösen lassen (Beispiel Verzweigungen 1). Wählen Sie bei unterschiedlichen Tätigkeiten je nach Wert eine switch-Anweisung (Beispiel Verzweigungen 2).
Auch switch kann durch verschiedene Vereinfachungen verbessert werden.
Beispiel für Verzweigungen 1 - Umwandlung in Array
| SCHLECHT |
...
if (i==0) output("null"); // wenn 0 dann null
if (i==1) output("eins"); // wenn 1 dann eins
if (i==2) output("zwei"); // wenn 2 dann zwei
if (i==3) output("drei"); // wenn 3 dann drei
if (i==4) output("vier"); // wenn 4 dann vier
if (i==5) output("fünf"); // wenn 5 dann fünf
...
|
| GUT |
#define MAX_ANZ_AUSGABE 6 // Anzahl der verfügbaren Zahlwörter.
#define MAX_ZEICHEN_AUSGABE 6 // Platz für das längste Zahlwort einschließlich Nullzeichen bei UTF-8.
const char ausgabeZahl[MAX_ANZ_AUSGABE][MAX_ZEICHEN_AUSGABE] =
{"null", "eins", "zwei", "drei", "vier", "fünf"}; // Zuordnung zwischen Zahl und Text.
if (i < MAX_ANZ_AUSGABE) { outputToLCD(ausgabeZahl[i]); }
Hier ist i eine vorzeichenlose Variable. |
Beispiel für Verzweigungen 2 - Umwandlung in Switch-Case
| SCHLECHT |
...
if (i==0) doZero(); // wenn 0 dann null
if (i==1) doOne(); // wenn 1 dann eins
if (i==2) doTwo(); // wenn 2 dann zwei
if (i==3) doThree(); // wenn 3 dann drei
if (i==4) doFour(); // wenn 4 dann vier
if (i==5) doFive(); // wenn 5 dann fünf
...
|
| GUT |
...
switch(i) {
case 1: doOne(); break; // könnte alternativ auch
case 2: doTwo(); break; // über Pointer auf Funktionen
case 3: doThree(); break; // wie Beispiel 3 gelöst
case 4: doFour(); break; // werden
case 5: doFive(); break;
default: break;
};
...
|
Beispiel für Verzweigungen 3 - Optimierung von Switch-Case
| OPTIMIERBAR |
...
switch (i)
{
case 1: doOne(); break;
case 2: doOne(); break;
case 3: doThree(); doFour(); doFive(); break;
case 4: doFour(); doFive(); break;
case 5: doFive(); break;
case 6: doOne(); break;
case 7: doOne(); break;
case 8: doOne(); break;
case 9: doOne(); break;
default: break;
}
...
|
| BESSER |
...
switch(i) {
case 1:
case 2: // Einzelne Zahlen können mit case's untereinander ...
case 6 ... 9: doOne(); break; // ... und Gruppen mit '...' kombiniert werden
case 3: doThree(); // Ohne Break werden alle folgenden Befehle bis zum nächsten Break ausgeführt
case 4: doFour();
case 5: doFive();
default: break;
};
...
Die Schreibweise case 6 … 9 ist eine Compiler-Erweiterung. Einzelne case-Marken sind auch ohne diese Erweiterung möglich. |
Beispiel Verzweigungen 4 - Umwandlung in for-Schleife und Arrays
| SCHLECHT |
...
if (( 0<i) && (i<= 7)) j=j+2;
if (( 7<i) && (i<=12)) j=j+5;
if ((12<i) && (i<=20)) j=j+3;
if ((20<i) && (i<=22)) j=j+10;
if ((22<i) && (i<=60)) j=j+7;
if ((60<i) && (i<=85)) j=j+1;
...
|
| GUT |
...
#define MAX_STEPS 6 // Anzahl der getrennten Wertebereiche.
const uint8_t grenze[MAX_STEPS + 1] = {0, 7, 12, 20, 22, 60, 85}; // Grenzen der Wertebereiche.
const uint8_t jSummand[MAX_STEPS] = {2, 5, 3, 10, 7, 1}; // Zugehörige Änderungen von j.
for (uint8_t steps = 0; steps < MAX_STEPS; steps++)
{
if ((grenze[steps] < i) && (i <= grenze[steps + 1]))
{
j = j + jSummand[steps];
break; // Die Wertebereiche schließen sich gegenseitig aus.
}
}
...
|
Falls sich Bedingungen gegenseitig ausschließen, nutzen Sie else if (Beispiel Verzweigungen 5).
Falls unabhängig von der Bedingung dieselben Anfangs- oder Endanweisungen ausgeführt werden, sollten diese nicht mehrfach in den Zweigen stehen (Beispiel Verzweigungen 6).
Berücksichtigen Sie bei switch immer den default-Fall und prüfen Sie die Verwendung von break. Ein Durchfallen zum nächsten Fall muss beabsichtigt und kommentiert sein.
Nutzen Sie bereits geprüfte Bedingungen auch für zusammengehörige Zuweisungen, statt dieselbe Beziehung erneut abzufragen.
Beispiel für Verzweigungen 5 - Verwenden von Else if
| SCHLECHT |
...
if (( 0<i) && (i<= 7)) j=j+2;
if (( 7<i) && (i<=12)) {
j=j+5;
DoOne();
}
if ((12<i) && (i<=20)) j=j+3;
...
|
| GUT |
...
if (( 0<i) && (i<= 7)) { j = j + 2;}
else if (( 7<i) && (i<=12)) {
j=j+5;
DoOne();
}
else if ((12<i) && (i<=20)) { j = j + 3;};
...
|
Beispiel Verzweigungen 6 - Reduzieren der Anweisungen
| SCHLECHT |
if (i <= 7)
{
j = j + 2;
doOne();
}
else if ((7 < i) && (i <= 12))
{
j = j + 5;
doZero();
doOne();
}
else if (12 < i)
{
j = j + 3;
doZero();
doOne();
}
|
| GUT |
if (i <= 7)
{
j = j + 2;
}
else if (i <= 12)
{
j = j + 5;
doZero();
}
else
{
j = j + 3;
doZero();
}
doOne(); // Alle Zweige enden mit derselben Tätigkeit.
|
Nutzen Sie in main() immer eine Endlosschleife, um an den Anfang zurückzukehren. Verwenden Sie dazu nicht den Aufruf von main() in main(). Setzen Sie stattdessen die für einen Neustart benötigten Zustände gezielt zurück.
Verwenden Sie in den Kursprojekten kein goto, damit der Programmablauf übersichtlich bleibt.
Wenn Sie verschachtelte Schleifen verlassen wollen, können Sie break und ein Flag nutzen.
Beispiel für Schleifen 1 - main()
| SCHLECHT |
int main(void)
{
initAll();
while (1)
{
eingabe();
verarbeitung();
if (cancelButton) { main(); }
ausgabe();
}
}
|
| GUT |
int main(void);
/* Die Hardware einmalig und jeden neuen Ablauf gezielt initialisieren. */
int main(void)
{
initOneTimeFunctions();
while (1)
{
initOtherFunctions();
cancelButton = false;
while (!cancelButton)
{
eingabe();
verarbeitung();
if (!cancelButton) { ausgabe(); }
}
}
}
|
Beispiel für Schleifen 2 - Abbrechen verschachtelter Schleifen
| SCHLECHT |
for (uint8_t xpos = 0; xpos < 10; xpos++)
{
initYPos();
for (int ypos = 0; ypos < 20; ypos++)
{
eingabe();
verarbeitung();
if (cancelButton) { goto Abbruch; }
}
}
Abbruch:
;
|
| GUT |
const uint8_t xposMax = 10; // Anzahl der zu bearbeitenden Spalten.
const uint8_t yposMax = 20; // Anzahl der zu bearbeitenden Zeilen.
for (uint8_t xpos = 0; xpos < xposMax; xpos++)
{
initYPos();
for (uint8_t ypos = 0; ypos < yposMax; ypos++)
{
eingabe();
verarbeitung();
if (cancelButton) { break; } // Zunächst die innere Schleife verlassen.
}
if (cancelButton) { break; } // Anschließend auch die äußere Schleife verlassen.
}
Beachten Sie, dass cancelButton für diesen Ablauf konsistent ausgewertet werden muss. Eine gleichzeitige Änderung durch eine ISR ist gesondert zu berücksichtigen. |
_delay_ms() und _delay_us() sind zu 99% nicht notwendig. Verwenden Sie stattdessen Interrupts, bzw. Timer. Verwenden Sie stattdessen Interrupts bzw. Timer. Es sollten Takte angelegt werden, z.B. takt10ms, takt100ms und takt1s, welche im Hauptprogramm abgefragt werden.
Häufig bietet es sich an, innerhalb dieser Takte mit einer Zustandsmaschine zu arbeiten.
Halten Sie ISR kurz. Umfangreiche Berechnungen und Ausgaben gehören ins Hauptprogramm. Teilen Sie größere Arbeiten so auf, dass die Zeitraster weiter eingehalten werden.
Zwischen ISR und Hauptprogramm gemeinsam genutzte Daten benötigen gegebenenfalls volatile und atomare Zugriffe. volatile allein schützt nicht vor unterbrochenen Zugriffen.
Ein Flag merkt sich, dass mindestens ein Ereignis eingetroffen ist. Wenn jedes Ereignis gezählt werden muss, reicht ein Flag nicht aus.
Dokumentieren Sie die Belegung von Pins, Timern und Schnittstellen. Prüfen Sie auch die von Libraries verwendeten Ressourcen.
Beachten Sie Flash-, SRAM- und Stackbedarf. const allein legt Daten beim klassischen ATmega328P nicht automatisch ausschließlich im Flash ab.
Wenn Sie Zahlen speichern und mathematisch weiterverwenden, wandeln Sie diese erst bei der Ausgabe in das
ASCII-Format um.
Beispiel für Variablen mit Ausgabe
| SCHLECHT |
zahlenwert = (adcWert / 10) % 10 + 0x30;
zahlenwerte[i] = zahlenwert;
flaeche = (zahlenwert - 0x30) * breite;
LCD_putc(zahlenwerte[i]);
|
| GUT |
#define ASCII_ZERO 0x30 // Zeichencode der Ziffer Null.
#define MOD_TEN 10 // Basis der verwendeten Dezimalstelle.
laenge = (adcWert / MOD_TEN) % MOD_TEN;
laengen[i] = laenge;
flaeche = laenge * breite;
LCD_putc(laengen[i] + ASCII_ZERO);
|
Testen und Abgabe
Testen Sie möglichst nach jeder kleinen, zusammenhängenden Änderung. Prüfen Sie auch Grenzfälle wie den ersten und letzten Arrayeintrag, Zählergrenzen und einen Neustart.
Übersetzen Sie mit aktivierten Compilerwarnungen und klären Sie die Ursachen der Warnungen.
Prüfen Sie, ob das Projekt aus den abgegebenen Dateien vollständig neu übersetzt und ausgeführt werden kann. Geben Sie dafür notwendige Einstellungen und Library-Versionen an.
Entfernen Sie alte Versuche und unbenutzte Dateien. Halten Sie die Bedienungsbeschreibung und die Kommentare aktuell.
Bewertung
Zur Bewertung lege ich diese Checkliste (xls-File) als Maßstab an.