Arduino: Programmiertechniken/Statemaschine

AnsatzBearbeiten

Microcontroller können Aufgaben nur nacheinander ausführen. Wenn der µC mehrere Aufgaben „gleichzeitig“ machen soll, darf er nicht mit „delay() [1]“ einfach warten.
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. Für den Microcontroller ist es wie als ob der Taster mehrmals gedrückt wird. Mit der Statemaschine soll dieses Prellen ausgeblendet werden bzw. soll das Programm für das Prellen blind gemacht werden.

Schaubild einer einfachen StatemaschineBearbeiten


Quellcode dieser einfachen StatemaschineBearbeiten

/*
  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 StatemaschineBearbeiten

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 StatemaschineBearbeiten

  • Der Tastendruck wird erst beim Loslassen des Tasters erzeugt
  • Die States haben keinen Namen

Version 2Bearbeiten

Schaubild der StatemaschineBearbeiten

















Code der StatemaschineBearbeiten

/*
  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 StatemaschineBearbeiten

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 VersionBearbeiten

  • In der Statemaschine sind noch einige Punkte "hardcodiert"
  • Sie kann nur für einen Taster verwendet werden

Version 3Bearbeiten

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.

CodeBearbeiten

/*
  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 VersionBearbeiten

  • Der Code der Statemaschiene ist mehrfach verwendbar.
  • Die Tastenereignisse haben jetzt auch Namen.
  • Die Tastenereignisse werden nach der Entprell = Debounce Zeit gemeldet

Nachteile dieser VersionBearbeiten

  • Es müssen pro Taster 3 Variablen und ein #define angelegt werden.

Version 4Bearbeiten

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

Schaubild der erweiterten StatemaschineBearbeiten




















CodeBearbeiten

/*
  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 VersionBearbeiten

  • Es können 3 verschiedene "Drückarten" erkannt werden

Nachteil dieser VersionBearbeiten

  • Das "Klick" Ereignis wird mit 300ms Verzögerung gemeldet, da noch gewartet wird ob es ein Doppelklick werden soll.

Die berühmte AmpelBearbeiten

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.

StatediagrammBearbeiten


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.

CodeBearbeiten

/*
  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 ProgrammierungBearbeiten

  • 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ückBearbeiten

QuellenBearbeiten

  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