- -
- 100%
- +
Die kleinste logische Speichereinheit ist das Bit, das eben die zwei Zustände 1 oder 0 speichern kann. Stell es dir als eine Art elektronischen Schalter vor, der ein- und ausgeschaltet werden kann. Da du mit einem Bit lediglich zwei Zustände abbilden kannst, sind mehrere Bits zur Speicherung der Daten sinnvoll und notwendig. Der Verbund aus 8 Bits wird 1 Byte genannt und ermöglicht es, 28 = 256 unterschiedliche Zustände zu speichern. Die Basis 2 wird verwendet, weil es sich um ein binäres System handelt, das lediglich zwei Zustände kennt. Wir können mit 8 Bits also einen Wertebereich von 0 bis 255 abdecken.
Ich liste hier für den Anfang einmal die wichtigsten Datentypen auf, mit denen du in Zukunft konfrontiert wirst:
Tabelle 1: Datentypen mit entsprechenden Wertebereichen Datentyp Wertebereich Datenbreite Beispiel void keiner null void setup() {} byte 0 bis 255 1 Byte byte wert = 42; unsigned int 0 bis 65.535 2 Bytes unsigned int sekunden = 46547; int –32.768 bis 32.767 2 Bytes int ticks = -325; long –231 bis 231–1 4 Bytes long wert = -3457819; float –3.4 * 1038 bis 3.4 * 1038 4 Bytes float messwert = 27.5679; double siehe float 4 Bytes double messwert = 27.5679; boolean true oder false 1 Byte boolean flag = true; char –128 bis 127 1 Byte char mw = 'm'; String variabel variabel String name = "Erik Bartmann"; Array variabel variabel int pinArray[] = {2, 3, 4, 5};Die meisten der hier gezeigten Datentypen werden wir auch in diesem Buch verwenden.
Was sind Funktionen?
Eine Funktion ist in den meisten höheren Programmiersprachen die Bezeichnung eines Programmkonstrukts, mit dem der Quellcode strukturiert wird, so dass diese Programmteile – die quasi als Unterprogramm bezeichnet werden können – im eigentlichen Hauptprogramm wiederverwendbar sind und somit an unterschiedlichen Stellen mehrfach aufgerufen werden können. In der Objektorientierten Programmierung, auf die ich noch eingehen werde, gibt es vergleichbare Konstrukte, die dort die Bezeichnung Methoden besitzen. Dort kommt auch der Begriff Kapselung erstmalig zur Sprache, wobei diese Bezeichnung ebenfalls auf die Funktionen zutrifft. Eine Kapselung ist eine Zusammenfassung oder das Verbergen von Daten beziehungsweise Informationen, wobei der Zugriff über eine definierte Schnittstelle erfolgt. Diese Schnittstelle wird durch den Funktionsaufruf abgebildet. Möchtest du in deinem Sketch also zum Beispiel mehrfach den Mittelwert zweier Zahlen bilden, dann sind normalerweise immer die folgenden Codezeilen erforderlich, wobei ich das extra etwas umständlich formuliert habe, damit der Sinn der Funktion etwas deutlicher wird. Es gibt Funktionen, die einen Rückgabewert an den Aufrufer zurückliefern, wie das im Moment hier der Fall ist und es gibt Funktionen, die führen etwas aus, ohne dass ein Wert an den Aufrufer zurückgeliefert wird:
float a = 5.4, b = 7.36; float summe = a + b; float mittelwert = summe / 2;
Beim Mittelwert zweier Zahlen werden diese addiert und das Ergebnis durch 2 geteilt. Willst du nun an mehreren Stellen im Sketch diesen Mittelwert bilden, wären immer wieder die beiden unteren Zeilen einzufügen. Das wird jedoch durch die Definition einer Funktion erleichtert. Diese könnte wie folgt aussehen:
float mittelwert(float a, float b) { return (a + b)/2; }
Das sollten wir uns genauer ansehen, denn da kommen viele Dinge zusammen:

Die erste Zeile einer Funktion wird als Signatur bezeichnet und ist die Deklarationszeile einer Funktion. Innerhalb der geschweiften Klammern befindet sich die Definition einer Funktion. Soll eine Funktion etwas an den Aufrufer zurückliefern, steht zu Beginn der sogenannte Rückgabedatentyp, der hier float ist. Das macht jedoch zwingend eine return-Anweisung innerhalb der Definition erforderlich, denn diese ist im Endeffekt für die Rückgabe eines Wertes verantwortlich, womit die Funktion nach ihrem Aufruf auch letztendlich verlassen wird. Man kann einer Funktion keinen, einen oder mehrere Werte beim Aufruf übergeben. In unserem Fall werden zwei Werte vom Datentyp float erwartet, die beim Aufruf an die dort aufgeführten Parameter a beziehungsweise b übergeben werden. Diese Parameter arbeiten wie lokale Variablen, die nach dem Verlassen der Funktion wieder aus dem Speicher entfernt werden, da sie nicht weiter benötigt werden. Die beiden prominentesten Funktionen in der Arduino-Entwicklungsumgebung sind natürlich die setup- und loop-Funktion, die immer vorhanden sein müssen:
void setup() {/* ... */} void loop() {/* ... */}
Es ist zu sehen, dass beide den Rückgabedatentyp void besitzen, was übersetzt leer bedeutet, weil sie keinen Wert zurückliefern. Zudem ist keine Parameterliste zu sehen, was an den leeren runden Klammerpaaren zu erkennen ist. Es können demnach auch beim Aufruf keine Werte mit übergeben werden. Natürlich gäbe es noch viel mehr über Funktionen zu berichten, doch das ist dann Thema von weiteren Bastelkapiteln oder Teil von C++-Tutorials und würde den Umfang dieses Buches etwas sprengen.
Was sind Kontrollstrukturen?
In Kapitel 2 – ich hatte es erwähnt – hast du schon etwas über Befehle erfahren. Sie teilen dem Mikrocontroller mit, was er zu tun hat. Ein Sketch besteht aber in der Regel aus einer ganzen Reihe von Befehlen, die sequentiell abgearbeitet werden. Das Arduino-Board ist mit einer bestimmten Anzahl von Ein- und Ausgängen versehen, an die du diverse elektrische und elektronische Komponenten anschließen kannst. Wenn der Mikrocontroller auf bestimmte Einflüsse von außen reagieren soll, schließt du beispielsweise einen Sensor an einen Eingang an. Die einfachste Form eines Sensors ist ein Schalter oder Taster. Wenn der Kontakt geschlossen wird, soll zum Beispiel eine LED leuchten. Der Sketch muss also eine Möglichkeit haben, eine Entscheidung zu treffen: Ist der Schalter geschlossen, dann versorge die LED mit Spannung (LED leuchtet); ist der Schalter offen, dann trenne die LED von der Spannungsversorgung (LED wird dunkel).

Was ist eine Kontrollstruktur?

Kontrollstrukturen sind Anweisungen in imperativen Programmiersprachen. Sie kommen zum Einsatz, um den Ablauf eines Programms zu steuern und es in eine bestimmte Richtung zu lenken. Dabei kann eine Kontrollstruktur entweder eine Verzweigung oder eine Schleife sein. Zumeist wird ihre Ausführung über logische (boolesche) Ausdrücke gesteuert.
Wir werfen zu Beginn einen Blick auf ein Flussdiagramm, das uns zeigt, wie der Ablauf der Sketch-Ausführung in bestimmte Bahnen gelenkt wird, so dass es sich nicht mehr um einen linearen Verlauf handelt. Der Sketch steht beim Erreichen einer Kontrollstruktur an einem Scheideweg und muss sehen, wie es weitergehen soll. Als Entscheidungsgrundlage dient ihm eine Bedingung, die es zu bewerten gilt.
Die if-Anweisung
Programmtechnisch nutzen wir die if-Anweisung. Es handelt sich um eine Wenn-dann-Entscheidung.

Abb. 3: Das Flussdiagramm einer if-Kontrollstruktur
Wurde die Bedingung als wahr erkannt, folgt die Ausführung einer oder auch mehrerer Anweisungen. Hier wieder ein kurzes Beispiel:
if(tasterStatus == HIGH) digitalWrite(ledPin, HIGH);
Wenn mehrere Befehle in einer if-Anweisung ausgeführt werden sollen, musst du einen Befehlsblock mit den geschweiften Klammerpaaren bilden. Er wird dann als komplette Befehlseinheit ausgeführt:
if(tasterStatus == HIGH) { digitalWrite(ledPin, HIGH); Serial.println("HIGH-Level erreicht."); }
Die if-else-Anweisung
Es gibt noch eine erweiterte Form der if-Kontrollstruktur. Es handelt sich dabei um eine Wenn-dann-sonst-Entscheidung, die sich aus einer if-else-Anweisung ergibt. Das entsprechende Flussdiagramm sieht wie folgt aus:

Abb. 4: Das Flussdiagramm einer if-else-Kontrollstruktur
Das folgende Codebeispiel zeigt dir die Syntax der if-else-Anweisung:
if(tasterStatus == HIGH) digitalWrite(ledPin, HIGH); else digitalWrite(ledPin, LOW);
Die switch-case-Anweisung
Sollen mehrere Abfragen hintereinander erfolgen, kann das natürlich über ein Konstrukt mehrfacher if-then-else-Anweisungen erfolgen. Es gibt jedoch noch eine einfachere Variante, die vereinfacht zu schreiben und damit auch besser lesbar ist, die sogenannte switch-case-Anweisung (Abbildung 5).
Die Syntax dazu sieht wie folgt aus:
switch(var) { case label1: // Anweisung(en) break; case label2: // Anweisung(en) break; default: // Anweisung(en) break; }

Abb. 5: Das Flussdiagramm einer switch-case-Kontrollstruktur
Folgende Parameter sind hierbei erlaubt:
var: eine Variable mit den erlaubten Datentypen int und char.
label1, label2: Konstanten mit den erlaubten Datentypen int und char.
Du solltest unbedingt darauf achten, dass nach der Ausführung einer Anweisung der Schleifendurchlauf mit break unterbrochen wird, da sonst die folgenden Sprungmarken ebenfalls geprüft und die dort aufgeführten Anweisungen vielleicht ausgeführt werden. Die letzte break-Anweisung Quelltext nach der default-Anweisung ist nicht zwingend erforderlich.
Operatoren
Natürlich gibt es bei den Kontrollstrukturen und den zu testenden Bedingungen nicht nur die Prüfung auf Gleichheit. Die folgende Tabelle zeigt alle C++-Vergleichsoperatoren:
Tabelle 2: Vergleichsoperatoren Vergleichsoperator Bedeutung Beispiel == ist gleich if(a==b) {...} <= ist kleiner gleich if(a<=b) {...} >= ist größer gleich if(a>=b) {...} < ist kleiner if( ist größer if(a>b) {...} != ist ungleich if(a!=b) {...}Zudem gibt es noch Verknüpfungen über sogenannte logische Operatoren, die mehrere zu testende Bedingungen zulassen:
Tabelle 3: Logische Operationen Logische Operation Funktion Bedeutung Beispiel ! NICHT (NOT) Umkehrung des logischen Zustandes. Ergebnis ist wahr (true), wenn Operand falsch (false) ist. if(!a) {...} && UND (AND) Ergebnis ist wahr (true), wenn beide Operanden wahr sind. if((a<=b)&&(c==5)) {...} || ODER (OR) Ergebnis ist wahr (true), wenn einer der beiden Operanden wahr ist. if((a>=b)||(c<=6)) {...}Was sind Schleifen?
In einem Sketch kann zur Berechnung von Daten das Ausführen vieler einzelner wiederkehrender Schritte erforderlich sein. Wenn es sich bei diesen Schritten beispielsweise immer um gleichartige Befehlsausführungen handelt, ist es weder sinnvoll noch praktikabel, diese Befehle in großer Anzahl untereinander zu schreiben und sequentiell, also hintereinander, ausführen zu lassen. Aus diesem Grund wurde in der Datenverarbeitung ein spezielles programmtechnisches Konstrukt geschaffen, das die Aufgabe hat, ein Programmstück, bestehend aus einem oder mehreren Befehlen, mehrfach hintereinander auszuführen. Wir nennen dies eine Schleife.
Was ist eine Schleife?

Eine Schleife – auch Loop genannt – ist eine Kontrollstruktur in einer Programmiersprache. Sie wiederholt einen definierten Anweisungsblock – den sogenannten Schleifenkörper –, solange die Schleifenbedingung logisch wahr ist. Schleifen, deren Schleifenbedingung immer wahr ist oder in denen keine Schleifenbedingung definiert wurde, sind sogenannte Endlosschleifen.
Schauen wir uns an, wie eine Schleife grundsätzlich aufgebaut ist. Es gibt zwei unterschiedliche Schleifenvarianten:
kopfgesteuerte Schleifen und
fußgesteuerte Schleifen.
Beiden Varianten ist gemeinsam, dass sie eine Instanz besitzen, die die Kontrolle darüber übernimmt, ob und wie oft die Schleife durchlaufen werden muss. Dieser Instanz ist ein einzelner Befehl oder ein ganzer Befehlsblock (Schleifenkörper genannt) angegliedert, der durch die Instanz gesteuert und abgearbeitet wird.
Kopfgesteuerte Schleifen
Bei kopfgesteuerten Schleifen befindet sich die Kontrollinstanz im Schleifenkopf, der sich – wie der Name vermuten lässt – oben befindet. Das bedeutet wiederum, dass der Eintritt in den ersten Schleifendurchlauf von der Auswertung der Bedingung abhängt und gegebenenfalls nicht stattfindet. Die Schleife wird also möglicherweise überhaupt nicht ausgeführt.

Abb. 6: Grundsätzlicher Aufbau einer kopfgesteuerten Schleife
Die Verwendung des Plurals kurz vorher in der entsprechenden Überschrift ist schon ein Hinweis darauf, dass es verschiedene Typen von Kopfschleifen gibt, die in unterschiedlichen Situationen zum Einsatz kommen.
for-Schleife
Die for-Schleife kommt immer dann zum Einsatz, wenn vor Beginn des Schleifenaufrufs eindeutig feststeht, wie oft die Schleife durchlaufen werden soll. Werfen wir dazu einen Blick auf das Flussdiagramm, das der grafischen Wiedergabe des Programmflusses dient:

Abb. 7: Das Flussdiagramm einer for-Schleife
In der Schleife kommt eine Variable mit der Bezeichnung Laufvariable zum Einsatz. Sie wird in der Bedingung einer Bewertung unterzogen, die darüber entscheidet, ob und wie oft die Schleife durchlaufen wird. Der Wert dieser Variablen wird in der Regel im Schleifenkopf bei jedem neuen Durchlauf modifiziert, so dass die Abbruchbedingung irgendwann erreicht sein sollte, wenn du keinen Denkfehler gemacht hast. Hier ein kurzes Beispiel, das später in deinem Projekt verwendet wird:
for(int i = 0; i < 7; i++) pinMode(ledPin[i], OUTPUT);
while-Schleife
Die while-Schleife wird dann verwendet, wenn sich erst zur Laufzeit der Schleife ergeben soll, ob und wie oft sie zu durchlaufen ist. Wenn während des Schleifendurchlaufs zum Beispiel ein Eingang des Mikrocontrollers kontinuierlich abgefragt und überwacht wird und bei einem bestimmten Wert eine Aktion durchgeführt werden soll, muss dieser Schleifentyp verwendet werden. Wir wollen schauen, wie das entsprechende Flussdiagramm aussieht:

Abb. 8: Das Flussdiagramm einer while-Schleife
Die Abbruchbedingung befindet sich bei dieser Schleife ebenfalls im Kopf. Es wird dort jedoch keine Modifikation der in der Bedingung angeführten Variablen vorgenommen. Sie muss im Schleifenkörper erfolgen. Wenn dies vergessen wird, haben wir es mit einer Endlosschleife zu tun, aus der es kein Entrinnen gibt, solange der Sketch läuft. Auch hierzu ein kurzes Beispiel für eine while-Schleife:
while(i > 1) { // Kontrollinstanz Serial.println(i); i = i - 1; }
Wenn du in der Kontrollinstanz mit Werten und Variablen arbeitest, die beispielsweise vom Datentyp float sind, ist es aufgrund von Ungenauigkeiten von float sehr riskant, genau auf ein bestimmtes Ergebnis hin abzufragen. Das kann bedeuten, dass die Abbruchbedingung niemals erfüllt wird und der Sketch in einer Endlosschleife sein Dasein fristet. Verwende statt des Operators == zur Abfrage auf Gleichheit lieber die Operatoren <= oder >=.
Fußgesteuerte Schleife
Die fußgesteuerte Schleife wird so genannt, weil die Kontrollinstanz im Schleifenfuß untergebracht ist.

Abb. 9: Grundsätzlicher Aufbau einer fußgesteuerten Schleife
Hierbei handelt es sich um eine do...while-Schleife. Da die Auswertung der Bedingung erst am Ende der Schleife stattfindet, können wir zunächst festhalten, dass sie mindestens einmal ausgeführt wird.

Abb. 10: Das Flussdiagramm einer do-while-Schleife
Diese Schleife wird recht selten Verwendung finden, doch der Vollständigkeit halber möchte ich sie dir nicht vorenthalten. Die Syntax gleicht der einer while-Schleife, wobei du erkennen kannst, dass die Kontrollinstanz am Fuß der Schleife untergebracht ist:
do { Serial.println(i); i = i - 1; } while(i > 1); // Kontrollinstanz
Diese unterschiedlichen Schleifenformen wirst du zukünftig verwenden, wenn du deine Sketche schreibst.
Sei kommunikativ und sprich darüber
Wenn man sich als Programmierer eines Problems annimmt und codiert, ist es sinnvoll, sich hier und da Notizen zu machen. Manchmal hat man einen Geistesblitz oder eine geniale Idee und ein paar Tage später – mir geht es jedenfalls öfter so – fällt es dann schwer, sich an die einzelnen Gedankengänge detailliert zu erinnern. Was habe ich da bloß programmiert und warum habe ich es so und nicht anders gemacht? Natürlich kann jeder Programmierer eigene Strategien für das Ablegen geeigneter Notizen entwickeln: Collegeblock, Rückseite von Werbeprospekten, Word-Dokumente und so weiter. Alle diese Methoden haben jedoch entscheidende Nachteile:
Wo habe ich nur meine Notizen hingelegt?
Sind sie auch auf dem neuesten und aktuellsten Stand?
Jetzt kann ich nicht mal meine eigene Schrift lesen!
Wie kann ich meine Notizen meinem Freund geben, der auch an meiner Programmierung interessiert ist?
Das Problem ist die Trennung von Programmiercode und Notizen, die dann keine Einheit bilden. Wenn die Notizen verloren gehen, wird es für dich unter Umständen schwierig, alles noch einmal zu rekonstruieren. Und jetzt stell dir deinen Freund vor, der absolut keine Ahnung hat, was du mit deinem Code erreichen wolltest. Da muss eine andere Lösung her: Du kannst innerhalb deines Codes Anmerkungen beziehungsweise Hinweise hinterlegen, und das genau an der Stelle, für die sie gerade relevant sind. So hast du alle Informationen genau da, wo sie benötigt werden.
Einzeiliger Kommentar
Schau dir das folgende Beispiel aus einem Programm an:
int ledPinRotAuto = 7; // Pin 7 steuert rote LED (Autoampel) int ledPinGelbAuto = 6; // Pin 6 steuert gelbe LED (Autoampel) int ledPinGruenAuto = 5; // Pin 5 steuert grüne LED (Autoampel) ...
Hier werden Variablen deklariert und mit einem Wert initialisiert. Zwar sind recht aussagekräftige Namen ausgewählt, doch ich denke, es ist sinnvoll, noch einige kurze ergänzende Anmerkungen anzuführen. Hinter der eigentlichen Befehlszeile wird ein Kommentar eingefügt, der durch zwei Schrägstriche (Slashes) eingeleitet wird. Warum ist das notwendig? Ganz einfach: Der Compiler versucht, alle Befehle zu interpretieren und auszuführen. Nehmen wir einmal den ersten Kommentar:
Pin 7 steuert rote LED (Autoampel)