Arduino: Programmiertechniken/Statemaschine
Ansatz
BearbeitenMicrocontroller können Aufgaben nur nacheinander ausführen – ein Befehl nach dem anderen. Wenn der µC mehrere Aufgaben „gleichzeitig“ machen soll, darf er nicht mit „delay() [1]“ einfach warten, denn dann kann es (zu) lange dauern, bis die anderen Aufgaben ebenfalls behandelt werden.
Der Ansatz um das zu umgehen sind Statemaschinen.
Hier wird mit einer ganz einfachen Statemaschine mit nur 2 Zuständen (= States) begonnen.
Aufgabe der Statemaschine ist die Auswertung eines Tasters. Taster haben allerdings die negative Eigenschaft zu "prellen". Das bedeutet, dass der Kontakt sich beim Schließen mehrmals kurz hinter einander öffnet und schließt bis der Kontakt zur Ruhe kommt. Dies geschieht in einer Zeit von 200µs bis 10ms [2]. Für den Menschen ist es z.B. an einer Lichtquelle nicht wahrnehmbar, aber der Microcontroller arbeitet so schnell, dass er dieses Prellen erkennt und so reagiert, als ob der Taster mehrmals gedrückt wird. Mit der Statemaschine kann dieses Prellen ausgeblendet werden bzw. soll das Programm für das Prellen blind gemacht werden.
Schaubild einer einfachen Statemaschine
Bearbeiten
Quellcode dieser einfachen Statemaschine
Bearbeiten/*
Hardware: Nano
Taster an D4 nach GND
LED_BUILTIN als Indikator
*/
// constants won't change. Used here to set a pin number:
const int ledPin = LED_BUILTIN;// the number of the LED pin
const int tasterPin = 4;
// Variables will change:
byte buttonState = 0;
byte buttonEvent = 0;
unsigned long previousMillis = 0;
const long debouncetime = 50;
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(tasterPin, INPUT_PULLUP);
}
void loop() {
switch (buttonState) {
case 0:
if (!digitalRead(tasterPin)) {
buttonState = 1;
previousMillis = millis();
}
break;
case 1:
if (digitalRead(tasterPin)) {
buttonState = 0;
if (millis() - previousMillis > debouncetime) {
buttonEvent = 1;
}
}
break;
}
//Auswertung Taster
if (buttonEvent == 1) {
buttonEvent = 0; //Ereignis zurücksetzten
digitalWrite(ledPin, !digitalRead(ledPin)); //LED toggel
}
}
Funktion der einfachen Statemaschine
BearbeitenZu Beginn ist der State 0. Wenn jetzt der Taster gedrückt wird (= LOW) wird (unabhängig vom Prellen) sofort in den State 1 gewechselt.
Im State 1 wird jetzt geprüft, ob der Eingang wieder HIGH ist. Wenn er High ist, wird wieder in den State 0 gewechselt. Nur wenn der Status für mehr als die "debouncetime" aktiv war, wird zusätzlich noch die Variable buttonEvent auf 1 gesetzt um den Tastendruck zu melden.
Nachteile der einfachen Statemaschine
Bearbeiten- Der Tastendruck wird erst beim Loslassen des Tasters erzeugt
- Die States haben keinen Namen
Version 2
BearbeitenSchaubild der Statemaschine
Bearbeiten
Code der Statemaschine
Bearbeiten/*
Hardware: Nano / Uno
Taster an D4 nach GND
LED_BUILTIN als Indikator
*/
// constants won't change. Used here to set a pin number:
const int ledPin = LED_BUILTIN;// the number of the LED pin
const int tasterPin = 4;
// Variables will change:
byte buttonEvent = 0;
unsigned long previousMillis = 0;
const long debouncetime = 50;
enum ButtonStates {
btnUnpressed, //0
btnPressed, //1
btnWaitForRelease //2
};
ButtonStates bState = btnUnpressed;
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(tasterPin, INPUT_PULLUP);
}
void loop() {
switch (bState) {
case btnUnpressed:
if (!digitalRead(tasterPin)) {
bState = btnPressed;
previousMillis = millis();
}
break;
case btnPressed:
if (digitalRead(tasterPin)) {
bState = btnUnpressed;
} else if (millis() - previousMillis > debouncetime) {
buttonEvent = 1;
bState = btnWaitForRelease;
}
break;
case btnWaitForRelease:
if (digitalRead(tasterPin)) {
bState = btnUnpressed;
}
break;
}
//Auswertung Taster
if (buttonEvent == 1) {
buttonEvent = 0; //Ereignis zurücksetzten
digitalWrite(ledPin, !digitalRead(ledPin)); //LED toggel
}
}
Funktion der Statemaschine
BearbeitenZu Beginn ist der State btnUnpressed. Wenn jetzt der Taster gedrückt wird (= LOW) wird (unabhängig vom Prellen) sofort in den State "btnPressed" gewechselt.
Im State btnPressed wird jetzt geprüft, ob der Eingang wieder HIGH ist. Wenn er High ist, wird wieder in den State "btnUnpressed" gewechselt. Wenn der Taster für mehr als die "debouncetime" LOW war, wird die Variable buttonEvent auf 1 gesetzt um den Tastendruck zu melden und in den Status btnWaitForRelease gewechselt.
Im State btnWaitForRelease wird jetzt geprüft, ob der Eingang wieder HIGH ist. Wenn er High ist, wird wieder in den State "btnUnpressed" gewechselt.
Nachteile dieser Version
Bearbeiten- In der Statemaschine sind noch einige Punkte "hardcodiert"
- Sie kann nur für einen Taster verwendet werden
Version 3
BearbeitenDiese Version soll jetzt für mehrere Taster verwendet werden. Dabei soll der Code nur einmal geschrieben werden. Das kann mit einer Funktion gelöst werden.
Code
Bearbeiten/*
Hardware: Nano / Uno
Taster an D4 nach GND
Taster an D5 nach GND
LED_BUILTIN als Indikator
*/
// constants won't change. Used here to set a pin number:
const int ledPin = LED_BUILTIN; // the number of the LED pin
const long DebounceTime = 50;
enum ButtonStates {
btnUnpressed, //0
btnPressed, //1
btnWaitForRelease //2
};
enum ButtonEvents {
btnNone,
btnClick
};
//Button 1
#define B1Pin 4
ButtonStates B1State = btnUnpressed;
ButtonEvents B1Event = btnNone;
uint32_t B1DebunceTime;
//Button 2
#define B2Pin 5
ButtonStates B2State = btnUnpressed;
ButtonEvents B2Event = btnNone;
uint32_t B2DebunceTime;
void BtnHandler(byte btn, ButtonEvents &bEvent, ButtonStates &bState, uint32_t &DebunceT) {
bool bPressed;
bPressed = !digitalRead(btn); //Wenn gedrückt = LOW
switch (bState) {
case btnUnpressed:
if (bPressed) {
DebunceT = millis();
bState = btnPressed;
}
break;
case btnPressed:
if (!bPressed ) {
bState = btnUnpressed;
}
if ((millis() - DebunceT > DebounceTime)) {
bEvent = btnClick;
bState = btnWaitForRelease;
}
break;
case btnWaitForRelease:
if (!bPressed) {
bState = btnUnpressed;
}
break;
}
}
void setup() {
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
pinMode(B1Pin, INPUT_PULLUP);
pinMode(B2Pin, INPUT_PULLUP);
}
void loop() {
BtnHandler(B1Pin, B1Event, B1State, B1DebunceTime);
BtnHandler(B2Pin, B2Event, B2State, B2DebunceTime);
//Debug
if (B1Event != btnNone) {
Serial.print("B1Event: ");
Serial.println(B1Event);
}
if (B2Event != btnNone) {
Serial.print("B2Event: ");
Serial.println(B2Event);
B2Event = btnNone; //Ereignis zurücksetzen
}
//Auswertung Taster
if (B1Event == btnClick) {
B1Event = btnNone; //Ereignis zurücksetzen
digitalWrite(ledPin, !digitalRead(ledPin)); //LED toggel
}
}
Vorteile dieser Version
Bearbeiten- Der Code der Statemaschiene ist mehrfach verwendbar.
- Die Tastenereignisse haben jetzt auch Namen.
- Die Tastenereignisse werden nach der Entprell = Debounce Zeit gemeldet
Nachteile dieser Version
Bearbeiten- Es müssen pro Taster 3 Variablen und ein #define angelegt werden.
Version 4
BearbeitenDiese Version kann jetzt neben Klick auch Doppelklick und Halten erkennen und melden.
Schaubild der erweiterten Statemaschine
Bearbeiten
Code
Bearbeiten/*
Hardware: Nano / Uno
Taster an D4 nach GND
Taster an D5 nach GND
LED_BUILTIN als Indikator
*/
// constants won't change. Used here to set a pin number:
const int ledPin = LED_BUILTIN; // the number of the LED pin
enum ButtonStates {
btnUnpressed, //0
btnPressed, //1
btnWaitForRelease, //2
btnWaitForDoubleClick, //3
btnDoubleClickPressed //4
};
enum ButtonEvents {
btnNone,
btnClick,
btnDoubleClick,
btnHold
};
//Button 1
#define B1Pin 4
ButtonStates B1State = btnUnpressed;
ButtonEvents B1Event = btnNone;
uint32_t B1DebunceTime;
//Button 2
#define B2Pin 5
ButtonStates B2State = btnUnpressed;
ButtonEvents B2Event = btnNone;
uint32_t B2DebunceTime;
void BtnHandler(byte btn, ButtonEvents &bEvent, ButtonStates &bState, uint32_t &DebunceT) {
#define DebounceTime 50
#define HoldTime 500
#define WaitForDoubleTime 300
bool bPressed;
bPressed = !digitalRead(btn); //Wenn gedrückt = LOW
switch (bState) {
case btnUnpressed:
if (bPressed) {
DebunceT = millis();
bState = btnPressed;
}
break;
case btnPressed:
if (!bPressed && (millis() - DebunceT < DebounceTime)) {
bState = btnUnpressed;
} else if (!bPressed && (millis() - DebunceT > DebounceTime)) {
bState = btnWaitForDoubleClick;
DebunceT = millis();
} else if (bPressed && (millis() - DebunceT > HoldTime)) {
bState = btnWaitForRelease;
bEvent = btnHold;
}
break;
case btnWaitForDoubleClick:
if (!bPressed && (millis() - DebunceT > WaitForDoubleTime)) {
bState = btnUnpressed;
bEvent = btnClick;
} else if (bPressed) {
bState = btnDoubleClickPressed;
DebunceT = millis();
}
break;
case btnDoubleClickPressed:
if (bPressed && (millis() - DebunceT > DebounceTime)) {
bState = btnWaitForRelease;
bEvent = btnDoubleClick;
} else if (!bPressed && (millis() - DebunceT < DebounceTime)) {
bState = btnWaitForDoubleClick;
}
break;
case btnWaitForRelease:
if (!bPressed) {
bState = btnUnpressed;
}
break;
}
}
void setup() {
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
pinMode(B1Pin, INPUT_PULLUP);
pinMode(B2Pin, INPUT_PULLUP);
}
void loop() {
BtnHandler(B1Pin, B1Event, B1State, B1DebunceTime);
BtnHandler(B2Pin, B2Event, B2State, B2DebunceTime);
//Debug
if (B1Event != btnNone) {
Serial.print("B1Event: ");
Serial.println(B1Event);
}
if (B2Event != btnNone) {
Serial.print("B2Event: ");
Serial.println(B2Event);
}
//Auswertung Taster
if (B1Event == btnClick) {
digitalWrite(ledPin, !digitalRead(ledPin)); //LED toggel
}
B1Event = btnNone; //Ereignis zurücksetzen
B2Event = btnNone; //Ereignis zurücksetzen
}
Vorteile dieser Version
Bearbeiten- Es können 3 verschiedene "Drückarten" erkannt werden
Nachteil dieser Version
Bearbeiten- Das "Klick" Ereignis wird mit 300ms Verzögerung gemeldet, da noch gewartet wird ob es ein Doppelklick werden soll.
Die berühmte Ampel
BearbeitenIn diesem Beispiel soll mit jedem Tastendruck die Ampel weitergeschaltet werden.
Begonnen wird mit Rot.
Mit "Halten" soll die Ampel in den gelben Blinkmodus
Mit Doppelklick in einen Automodus der alle 4 Sekunden weiterschaltet.
Es wird dazu die Tastenauswertung Version 4 benutzt.
Statediagramm
Bearbeiten
Man sieht hier 3 Kreise. Der äußere Kreis ist der Automatikmode. Er kann mit Doppelklick aus dem manuellen Mode aktiviert werden. Ein Klick geht zurück in den manuellen Mode.
Der Blinkmode kann mit Halten aktiviert werden Ein Klick geht zurück in den manuellen Mode.
- Aus allen manuellen Modes gibt es 3 Ausgänge
- Halten --> Blinken
- Klicken --> nächste Phase
- Doppelklick --> Automode
- Aus allen automatischen Modes gibt es 2 Ausgänge
- Klicken --> gleiche manuelle Phase
- Zeit --> nächste Phase
Wenn dieser Plan existiert, ist der Rest nur eine Fleißaufgabe. Die 10 States anlegen (enum), ein Switch case mit 10 Cases, und darin ein paar if Abfragen.
Code
Bearbeiten/*
Hardware: Nano / Uno
Taster an D4 nach GND
Serial Ausgabe als Indikator
*/
// constants won't change. Used here to set a pin number:
const int ledPin = LED_BUILTIN; // the number of the LED pin
enum ButtonStates {
btnUnpressed, //0
btnPressed, //1
btnWaitForRelease, //2
btnWaitForDoubleClick, //3
btnDoubleClickPressed //4
};
enum ButtonEvents {
btnNone,
btnClick,
btnDoubleClick,
btnHold
};
enum Ampel {
man_Rot,
man_Rot_Gelb,
man_Gruen,
man_Gelb,
auto_Rot,
auto_Rot_Gelb,
auto_Gruen,
auto_Gelb,
blink_Gelb,
blink_aus
};
//Button 1
#define B1Pin 4
ButtonStates B1State = btnUnpressed;
ButtonEvents B1Event = btnNone;
uint32_t B1DebunceTime;
Ampel AmpelState = man_Rot;
Ampel AmpelStateOld = blink_aus;
uint32_t AmpelTime;
void BtnHandler(byte btn, ButtonEvents &bEvent, ButtonStates &bState, uint32_t &DebunceT) {
#define DebounceTime 50
#define HoldTime 500
#define WaitForDoubleTime 300
bool bPressed;
bPressed = !digitalRead(btn); //Wenn gedrückt = LOW
switch (bState) {
case btnUnpressed:
if (bPressed) {
DebunceT = millis();
bState = btnPressed;
}
break;
case btnPressed:
if (!bPressed && (millis() - DebunceT < DebounceTime)) {
bState = btnUnpressed;
} else if (!bPressed && (millis() - DebunceT > DebounceTime)) {
bState = btnWaitForDoubleClick;
DebunceT = millis();
} else if (bPressed && (millis() - DebunceT > HoldTime)) {
bState = btnWaitForRelease;
bEvent = btnHold;
}
break;
case btnWaitForDoubleClick:
if (!bPressed && (millis() - DebunceT > WaitForDoubleTime)) {
bState = btnUnpressed;
bEvent = btnClick;
} else if (bPressed) {
bState = btnDoubleClickPressed;
DebunceT = millis();
}
break;
case btnDoubleClickPressed:
if (bPressed && (millis() - DebunceT > DebounceTime)) {
bState = btnWaitForRelease;
bEvent = btnDoubleClick;
} else if (!bPressed && (millis() - DebunceT < DebounceTime)) {
bState = btnWaitForDoubleClick;
}
break;
case btnWaitForRelease:
if (!bPressed) {
bState = btnUnpressed;
}
break;
}
}
void AmpelStateM() {
switch (AmpelState) {
case man_Rot:
case man_Rot_Gelb:
case man_Gruen:
if (B1Event == btnDoubleClick) {
AmpelState = AmpelState + 4;
AmpelTime = millis();
} else if (B1Event == btnClick) {
AmpelState = AmpelState + 1;
} else if (B1Event == btnHold) {
AmpelState = blink_Gelb;
AmpelTime = millis();
}
break;
case man_Gelb:
if (B1Event == btnDoubleClick) {
AmpelState = AmpelState + 4;
AmpelTime = millis();
} else if (B1Event == btnClick) {
AmpelState = man_Rot;
} else if (B1Event == btnHold) {
AmpelState = blink_Gelb;
AmpelTime = millis();
}
break;
case auto_Rot:
case auto_Rot_Gelb:
case auto_Gruen:
if (millis() - AmpelTime > 4000) {
AmpelState = AmpelState + 1;
AmpelTime = millis();
} else if (B1Event == btnHold) {
AmpelState = blink_Gelb;
AmpelTime = millis();
} else if (B1Event == btnClick) {
AmpelState = AmpelState - 4;
}
break;
case auto_Gelb:
if (millis() - AmpelTime > 4000) {
AmpelState = auto_Rot;
AmpelTime = millis();
} else if (B1Event == btnHold) {
AmpelState = blink_Gelb;
AmpelTime = millis();
} else if (B1Event == btnClick) {
AmpelState = AmpelState - 4;
}
break;
case blink_Gelb:
if (millis() - AmpelTime > 1000) {
AmpelState = blink_aus;
AmpelTime = millis();
} else if (B1Event == btnClick) {
AmpelState = man_Gelb;
}
break;
case blink_aus:
if (millis() - AmpelTime > 1000) {
AmpelState = blink_Gelb;
AmpelTime = millis();
} else if (B1Event == btnClick) {
AmpelState = man_Gelb;
}
break;
}
if (AmpelStateOld != AmpelState) {
AmpelStateOld = AmpelState;
switch (AmpelState) {
case man_Rot:
case auto_Rot:
Serial.println(F("Rot"));
break;
case man_Rot_Gelb:
case auto_Rot_Gelb:
Serial.println(F("Rot/Gelb"));
break;
case man_Gruen:
case auto_Gruen:
Serial.println(F("Gruen"));
break;
case man_Gelb:
case auto_Gelb:
case blink_Gelb:
Serial.println(F("Gelb"));
break;
case blink_aus:
Serial.println(F("Aus"));
break;
}
}
}
void setup() {
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
pinMode(B1Pin, INPUT_PULLUP);
}
void loop() {
BtnHandler(B1Pin, B1Event, B1State, B1DebunceTime);
AmpelStateM();
//Debug
if (B1Event != btnNone) {
Serial.print("B1Event: ");
Serial.println(B1Event);
}
//Auswertung Taster
if (B1Event == btnClick) {
digitalWrite(ledPin, !digitalRead(ledPin)); //LED toggel
}
B1Event = btnNone; //Ereignis zurücksetzen
}
Besonderheiten bei der Programmierung
Bearbeiten- Bei den Case Fällen wird das sogenannte "Fallthrough" [3] benutzt.
- Bei den States wird die Eigenart der enum ausgenutzt, dass sie im Hintergrund nur Zahlen sind mit denen man rechnen kann.