Arduino: Programmiertechniken/Statemaschine

Ansatz Bearbeiten

Microcontroller 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

 
Erste Version der Statemaschine


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 Bearbeiten

Zu 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 Bearbeiten

Schaubild der Statemaschine Bearbeiten

 
Einfache Statemaschine für Tastenauswertung

















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 Bearbeiten

Zu 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 Bearbeiten

Diese 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 Bearbeiten

Diese Version kann jetzt neben Klick auch Doppelklick und Halten erkennen und melden.

Schaubild der erweiterten Statemaschine Bearbeiten

 
Statemachine Taster mit Erkennung Klick, DoppelKlick und Halten




















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 Bearbeiten

In 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

 
Statediagramm einer Ampel


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.

Zurück Bearbeiten

Quellen Bearbeiten

  1. https://www.arduino.cc/reference/de/language/functions/time/delay/
  2. https://de.wikipedia.org/wiki/Prellen
  3. https://en.wikipedia.org/wiki/Switch_statement#Fallthrough