Idee: Im Forum taucht immer wieder der Rat auf: „Ersetze dalay() durch millis().“ und „Das kann man mit einem Endlichen Automaten lösen.“ Hier ein Beispiel, das beides vereint.
Aufgabenstellung: Es sollte sich um eine allgemein bekannte Situation handeln, die mit einfachen Mitteln nachvollzogen werden kann. Meine Wahl fällt auf eine Ampelschaltung, weil sie jeder kennt. Asynchron dazu soll ein Blaulicht blitzen.
Material: Neben einem Arduino, ich verwende einen UNO, vier LEDs mit je einem Vorwiderstand.
Quellen: Viele nette Menschen, die ihr Wissen im Internet zur Verfügung stellen so wie in diesem Forum.
Sketch 1 mit delay()
Zunächst sehen wir uns den Sketch an, wie ihn jeder Anfänger schnell schreibt:
void setup() {
// Definiert die Pins als Aus- oder Eingang
pinMode(2, OUTPUT);
pinMode(3, OUTPUT);
pinMode(4, OUTPUT);
}
void loop() {
// Ampelschaltung
digitalWrite(2, HIGH);
digitalWrite(3, LOW);
digitalWrite(4, LOW);
delay(3000);
digitalWrite(2, HIGH);
digitalWrite(3, HIGH);
digitalWrite(4, LOW);
delay(1000);
digitalWrite(2, LOW);
digitalWrite(3, LOW);
digitalWrite(4, HIGH);
delay(3000);
digitalWrite(2, LOW);
digitalWrite(3, HIGH);
digitalWrite(4, LOW);
delay(1000);
}
Sollte der Arduino nichts weiter machen, wären wir fertig. Die Funktion delay() unterbricht das Programm für eine definierte Zeit, während der keine anderen Aktionen möglich sind. Lange for- und while-Schleifen können genauso blockierend sein. Eine weitere zeitkritische Funktion wie ein Blaulicht oder eine Reaktion auf Taster ist auf diese Weise nicht zu realisieren. Dies geht nur, wenn loop() nicht blockiert wird.
Sketch 2 mit Konstanten
Ohne weiteren Kommentar kann man nur durch Verstehen des Codes erschließen, dass die rote LED an Pin 2 angeschlossen werden soll. Wollen wir die LED später mit einem anderen Anschluss verbinden, weil wir beispielsweise auf eine anderen Arduino umgestiegen sind, so müssten wir das ganze Programm durchsuchen, um die relevanten Stellen zu finden. Bei langen Programmen kann dies mühsam und fehlerträchtig sein. Daher werden Konstanten definiert, die mit ihrem Namen gleich auch ihre Bedeutung verraten.
// Ampel 1 Belegung der Ausgaenge
const byte RotPin = 2;
const byte GelbPin = 3;
const byte GruenPin = 4;
//
const boolean ein = HIGH;
const boolean aus = LOW;
const int ZEITROTPHASE = 3000;
const int ZEITGELBPHASE = 1000;
void setup() {
// Definiert die Pins als Aus- oder Eingang
pinMode(RotPin, OUTPUT);
pinMode(GelbPin, OUTPUT);
pinMode(GruenPin, OUTPUT);
}
void loop() {
// Ampelschaltung
digitalWrite(RotPin, HIGH);
digitalWrite(GelbPin, LOW);
digitalWrite(GruenPin, LOW);
delay(ZEITROTPHASE);
digitalWrite(RotPin, HIGH);
digitalWrite(GelbPin, HIGH);
digitalWrite(GruenPin, LOW);
delay(ZEITGELBPHASE);
digitalWrite(RotPin, LOW);
digitalWrite(GelbPin, LOW);
digitalWrite(GruenPin, HIGH);
delay(ZEITROTPHASE);
digitalWrite(RotPin, LOW);
digitalWrite(GelbPin, HIGH);
digitalWrite(GruenPin, LOW);
delay(ZEITGELBPHASE);
}
Sketch 2 mit Konstanten
Ohne weiteren Kommentar kann man nur durch Verstehen des Codes erschließen, dass die rote LED an Pin 2 angeschlossen werden soll. Wollen wir die LED später mit einem anderen Anschluss verbinden, weil wir beispielsweise auf eine anderen Arduino umgestiegen sind, so müssten wir das ganze Programm durchsuchen, um die relevanten Stellen zu finden. Bei langen Programmen kann dies mühsam und fehlerträchtig sein. Daher werden Konstanten definiert, die mit ihrem Namen gleich auch ihre Bedeutung verraten.
Sketch 3 mit Endlichem Automaten und millis()
Stell Dir vor, wir verabreden uns zu einem Treffen in fünf Minuten. Was passiert?
- Du schaust auf die Uhr, um die aktuelle Zeit zu ermitteln und merkst Dir diese als die Zeit, an der wir uns verabredet haben.
- Du wartest, bis die aktuelle Zeit auf Deiner Uhr abzüglich der Zeit, an der wir uns verabredet haben, mit den fünf Minuten übereinstimmt. Wir treffen uns.
Zu 1.: Zeit_der_Verabredung = aktuelle_Zeit
Zu 2.: Wenn aktuelle_Zeit abzüglich Zeit_der_Verabredung gleich fünf Minuten dann "Hallo!"
Deine Uhr zeigt die Sekunden, Minuten und Stunden seit Mitternacht an. millis() zeigt die Millisekunden seit Reset an. Für die Verabredung "in fünf Minuten" ist das aber nicht relevant, wann die Zeitzählung startet. Deine Uhr könnte Winterzeit zeigen, meine Sommerzeit oder auch vollkommen falsch gehen, wir würden uns dennoch in fünf Minuten treffen, weil es sich um eine relative Zeitangabe handelt.
Das jetzt in millis (Intervall = fünf Minuten):
Zu 1.: Zeit_der_Verabredung = millis();
Zu 2.: if (millis() - Zeit_der_Verabredung == Intervall) {Serial.println("Hallo");}
Für den (schlechten) Fall, Dein Programm sollte länger als eine Millisekunde für loop() benötigen, schreibt man besser:
Zu 2.: if (millis() - Zeit_der_Verabredung >= Intervall) {Serial.println("Hallo");}
Die Ampelphasen ROT, ROTGELB, GRUEN, GELB sind die Zustände des Endlichen Automaten. Die Variable zustand durchläuft alle Phasen, wobei durch Bedingungen, hier die Zeit, die Zustandsänderungen ausgelöst werden. Mittels enum werden die Zustände beginnend bei 0 fortlaufend durchnummeriert.
// Ampel Belegung der Ausgaenge
const byte RotPin = 2;
const byte GelbPin = 3;
const byte GruenPin = 4;
//
const boolean ein = HIGH;
const boolean aus = LOW;
//
const int ZEITROTPHASE = 3000;
const int ZEITGELBPHASE = 1000;
unsigned long ampelMillis;
unsigned long ampelIntervall;
//
enum ZUSTAENDE {ROT, ROTGELB, GRUEN, GELB};
byte zustand = ROT;
void setup() {
// Definiert die Pins als Ausgang
pinMode(RotPin, OUTPUT);
pinMode(GelbPin, OUTPUT);
pinMode(GruenPin, OUTPUT);
}
void loop() {
// Ampelschaltung
if (millis() - ampelMillis >= ampelIntervall) {
switch (zustand) {
case ROT:
digitalWrite(RotPin, HIGH);
digitalWrite(GelbPin, LOW);
digitalWrite(GruenPin, LOW);
zustand = ROTGELB;
ampelMillis = millis();
ampelIntervall = ZEITROTPHASE;
break;
case ROTGELB:
digitalWrite(RotPin, HIGH);
digitalWrite(GelbPin, HIGH);
digitalWrite(GruenPin, LOW);
zustand = GRUEN;
ampelMillis = millis();
ampelIntervall = ZEITGELBPHASE;
break;
case GRUEN:
digitalWrite(RotPin, LOW);
digitalWrite(GelbPin, LOW);
digitalWrite(GruenPin, HIGH);
zustand = GELB;
ampelMillis = millis();
ampelIntervall = ZEITROTPHASE;
break;
case GELB:
digitalWrite(RotPin, LOW);
digitalWrite(GelbPin, HIGH);
digitalWrite(GruenPin, LOW);
zustand = ROT;
ampelMillis = millis();
ampelIntervall = ZEITGELBPHASE;
break;
}
}
}