Reise nach Jerusalem mit einem Arduino
Ein weiteres Beispiel für eine "Finite State machine" - einen "Endlicher Zustandsautomat" auf Basis der "Reise nach Jerusalem".
Das Spiel sollte so ablaufen: ein Spielleiter startet die Musik per Knopfdruck. Solange die Musik spielt dürfen die Spieler ihren Buzzer nicht drücken. Die Musik verstummt nach einer zufälligen Zeit. Sobald die Musik verstummt, müssen die Spieler so schnell wie möglich ihren Buzzer drücken. Wer seinen Buzzer als letztes drückt scheidet aus. Nach jeder Runde startet der Spielleiter das Spiel. Das Spiel endet, wenn nur mehr 1 Spieler übrig ist. Gewinner ist der letzte Spieler.
- eine Arduino Uno
- 4 große Buzzer/Buttons für die Spieler
- 4 LEDs und
- 4 Vorwiderstände passend zu den LEDs
- 1 Piezo Lautsprecher für eine akustische Ausgabe
- 1 "Clear" Button für den Spielleiter
Die Verkabelung ist ganz übersichtlich: Jeder Button wird an einen eigenen GPIO sowie GND angeschlossen. Ebenso wird jede LED über einen entsprechenden Vorwiderstand (je nach LED Farbe und Größe so zwischen 220 - 470KOhm) an einen GPIO sowie GND angeschlossen.
Einen Probeaufbau kann man auf einem Breadboard machen:
Ein einfacher Ablaufplan für unsere State Machine
Zunächst machen wir uns einen einfachen Ablaufplan für unsere State Machine damit wir uns nicht verlieren.
Wenn das Spiel beginnt, warten wir auf einen Tastendruck von einem der 4 Buttons. Sobald der erste Button gedrück ist, wird die LED des Spielers aktiviert. Die anderen Buttons haben keine Funktion mehr.
Für eine Sekunde zeigen wir den schnellsten Spieler mit der zum Button passenden LED an. Optional können wir einen Piezo Lautsprecher ansteuern. Eine Sekunde soll reichen, das Piepen kann ziemlich nervig sein.
Dann Warten wir auf die Freigabe durch den Spielleiter. Erst wenn der Spielleiter seinen Clear Button drückt, wird die Sieger LED wieder abgeschaltet und das Spiel kann erneut beginnen. Den Spielleiter und den anderen Spielern wird diese Phase durch Blinken der LED angezeigt.
Man könnte den Zuständen folgende Verhaltensspezifikationen angeben:
Angewandt für unser Beispiel zeichnest du dir zunächst ein Zustandsübergangsdiagramm:. Ich gebe keines vor - aber es könnte ähnlich wie im Buzzerspiel aussehen.
Schritt für Schritt zur State Machine
Eigentlich ist es ein etwas komplexererr Ablauf und du siehst, mit delay() wirst du nicht weit kommen. Aber du kannst einen nicht blockierenden Code - ganz ohne delay() erstellen. Bau den Sketch schrittweise auf. Weiter unten gibts den kompletten Code.
Variablen und Pin Definitionen
Zunächst beginnst du mit den Pin Definitionen. Pins ändern sich nicht daher machst du sie konstant. Vorzeichenlose Variablen mit der Länge von einem Byte reichen für die Zuordnung von Pins. Die Töne der Liedes kommen in ein Array.
Um die Melodie starten, spielen und stoppen zu können erstellst du dir ein eigene Klasse die das zuvor definierte Lied in einer Dauerschleife abspielen kann. Prinzipiell verwendest du Praktikten die aus dem Beispiel "BlinkWithoutDelay" kommen. Du musst nur dafür sorgen dass die Methode run() regelmäßig aufgerufen wird. Das angelegte Objekt (melody Kleingeschrieben) kümmert sich dann um das Abspielen jeder einzelnen Note. Am Ende des Liedes beginnt es wieder von Vorne.
Anschließend erstellst du eine Klasse für die Spieler. Je Spieler benötigst du folgende Membervariablen:
private: bool state = true; // ist der Spieler noch im Spiel (true) oder bereits ausgeschieden (false) bool sits = false; // Sitzt der Spieler const uint8_t buzzer; // GPIO für eine Buzzer / Taster gegen GND const uint8_t led; // GPIO für eine LED
Mit private verhinderst du den Zugriff auf die Member-Variablen, daher musst du eigene Setter und Getter Methoden zur Verfügung stellen, wenn du die Werte verändern möchtest. Die public: Methoden stellen dein Interface zur Klasse Player dar.
Und schließlich musst du noch tatsächliche Instanzenen für deine Spieler anlegen. Du könntest die 4 Spieler "anton", "berta", "caesar", "dora" nennen. Echte Namen bringen dir hier aber keinen Vorteil, daher verwenden wir Arrays. Du musst beachten, dass C++ bei der Nummerierung mit 0 (für den ersten Spieler) beginnt. Du legst daher diese 4 Spieler als Array an und weist auch gleich die jeweiligen Pins zu:
Player player[] { // dann legen wir für unsere Spieler ein Array [] an und weisen die konkreten GPIOs zu {A0, 2}, // Spieler 0: Buzzer GPIO, LED GPIO {A1, 3}, // Spieler 1 {A2, 4}, // Spieler 2 {10, 5} // Spieler 3: aufpassen, beim letzten kein Komma mehr };
Ob 2, 4 oder 8 Spieler spielt nun fast keine Rolle mehr. Eine Erweiterung ist nur mehr durch die verwendbaren GPIOs und den verfügbaren Speicher begrenzt.
Du hast in deinem Zustandsübergangsdiagramm festgestellt, dass du 3 Schritte durchführen musst. Man könnte die Schritte 0, 1, 2 durchnummerieren, oder eine C++ Enumeration (Aufzählung) verwenden. Genau genommen eine enum class (eine "scoped enumeration"):
// eine Enumeration für die einzelnen Zustände der State Machine enum class State {WALK_AROUND, // Spiele Musik SIT_DOWN, // Musik aus - langsamsten Teilnehmer ermitteln WAIT_FOR_CLEAR // Warten auf clear durch Spielleiter } state; // eine Variable in der wir den aktuellen Status speichern
Damit überlässt du die Nummerierung dem Compiler und verwendest künftig nur die Status "State::WALK_AROUND", "State::SIT_DOWN", "State::WAIT_FOR_CLEAR". Da du den aktuellen Status auch irgendwo speichern musst, legst du auch eine Variable state an.
setup()
Der Anfang des setup() dürfte selbstsprechend sein. Eventuell brauchst du Debug Ausgaben auf dem Serial Monitor, daher startest du die serielle Schnittstelle (und begrüßt den Anwender).
Alle Buttons verwenden den internen Pullup und werden gegen GND geschaltet, sind im Idle daher HIGH und bei Tastendruck LOW. Du beginnst mit dem Button für den Spielleiter:
pinMode(clearPin, INPUT_PULLUP); // Taster gegen Masse schalten
Für die Button und LED Pins der Spieler legst du dir in der Spieler Klasse eine eigene begin() Methode an. Du rufst im setup() für jeden Spieler die begin() Methode auf. Du könntest das einzeln machen. Praktischerweise hast du deine Spieler in einem Array und kannst daher eine For Schleife verwenden. Im konkreten Fall verwendest du eine Range based for loop:
for (auto &i : player) i.begin(); // range based for ... i ist nun eine Referenz
das auto range for erspart dir das übliche "int=0, i < wie_viele_hatten_wir_noch; i++". auto übernimmt den Typ der Objekte player und in der for schleife hast du mit i eine Referenz (daher das &) auf das jeweilige Objekt player. Nun kannst du mit i.begin() den Pinmode für alle GPIOs setzen. Willst du den Sketch auf 8 Spieler erweitern, so reicht die Erweiterung des Array player[]. Das Setup kann unverändert bleiben!
loop()
Im loop arbeitet eine State Machine je nach Status den jeweiligen Abschnitt ab. Die State Machine hat aktuell nur 3 Status und du kannst dafür einfache switch case Anweisungen für jeden einzelnen Status verwenden. Zusätzlich benötigst du einige Variablen für die State Machine. Variablen die den Durchlauf "überleben" müssen deklarierst du static:
static uint8_t currentRound = 1; // current Round static uint32_t currentPlayTimeMS = 42; // current planed playtime in MS static uint8_t currentPresses = 0; // wie viele Spieler haben in dieser Runde schon gedrückt static uint32_t previousMillis; // letzter Aufruf uint32_t currentMillis = millis(); // aktuellen Zeitstempel merken
Das Gesamtbeispiel
Abschließend der ganze Beispielsketch zum rauskopieren:
/* Reise nach Jerusalem Basierend auf: Quiz Buzzersystem für 8 Spieler Jeder Spieler hat eine LED und einen Button Wenn er an den Strom angeschlossen wird, dann soll die hier angezeigte Melodie fuer eine zufällige Zeit anfangen zu spielen. In der ersten Runde sollten 3 Spieler weiter kommen - das heisst 3 Button werden gedrueckt und dann geht die Melodie weiter und die LED von dem einen Spieler der als letztes gedrueckt hat geht aus. Das Spiel sollte dann enden sobald nur noch 1 Spieler da ist. https://forum.arduino.cc/index.php?topic=674983.0 read buttons switch LEDs blink LED according "BlinkWithoutDelay" write non-blocking code copyright by noiasca Der Code kann genutzt oder verändert werden, solange der volle Copyright Vermerk und der Verweis auf die ursprüngliche Download-Quelle: https://werner.rothschopf.net/microcontroller/202103_arduino_reise_nach_jerusalem.htm als Kommentar erhalten bleibt. 2022-10-30 minor fix 2021-05-29 */ // define the GPIO const uint8_t clearPin = A3; // GPIO zum Zurücksetzen des Spiels (statt dem RESET Button) const uint8_t walkAroundMin = 4; // kürzeste Spielzeit der Musik (sec) const uint8_t walkAroundMax = 10; // längste Spielzeit der Musik (sec) const uint8_t sitdownMax = 30; // Beschränkung der Zeit für das "Niedersetzen" (sec) const uint8_t song[] {49, 33, 37, 33, 49, 33, 37, 33, 37, 41, 44, 37, 41, 44}; // ein Song als Melodie class Melody { private: uint32_t previousMillis; // Zeitmanagement für die Melodie uint8_t nextTone = 0; // welche Note im Song wird als nächstes gespielt bool state; // läuft der Song gerade const uint16_t interval = 500; // wie viele MS wird jede Note gespielt const uint8_t peepPin; // GPIO public: Melody(const uint8_t peepPin) : peepPin{peepPin} {} void start() { nextTone = 0; state = true; run(); } void stop() { noTone(peepPin); state = false; } void run() // Zeitsteuerung für die Melodie, muss laufend aufgerufen werden { uint32_t currentMillis = millis(); if (currentMillis - previousMillis > interval) // Es ist Zeit für die nächste Note { previousMillis = currentMillis; tone(peepPin, song[nextTone]); nextTone++; if (nextTone >= sizeof(song) / sizeof(song[0])) nextTone = 0; } } }; Melody melody(13); // ein Melodyobjekt mit Übergabe des GPIO class Player { // ein Klasse für die Spieler private: bool state = true; // ist der Spieler noch im Spiel (true) oder bereits ausgeschieden (false) bool sits = false; // Sitzt der Spieler const uint8_t buzzer; // GPIO für eine Buzzer / Taster gegen GND const uint8_t led; // GPIO für eine LED public: Player (const uint8_t buzzer, const uint8_t led) : buzzer(buzzer), led(led) {} void begin(void) { pinMode(buzzer, INPUT_PULLUP); // Buzzer sind Input, auch hier - alle Button schließen gegen Masse pinMode(led, OUTPUT); // LEDs sind Output digitalWrite(led, HIGH); // Zum Spielanfang einschalten } bool getState() { return state; } void setState(bool newState) { state = newState; digitalWrite(led, newState); } void setSits(bool newSits) { sits = newSits; } bool pressed() // drückt der Spieler gerade den Button? { if (digitalRead(buzzer) == LOW) return true; else return false; } bool getSits() { return sits; } }; Player player[] { // dann legen wir für unsere Spieler ein Array [] an und weisen die konkreten GPIOs zu {A0, 2}, // Spieler 0: Buzzer GPIO, LED GPIO {A1, 3}, // Spieler 1 {A2, 4}, // Spieler 2 {10, 5} // Spieler 3: aufpassen, beim letzten kein komma mehr }; constexpr size_t noOfPlayer = sizeof(player) / sizeof(player[0]); // einmal die Anzahl der Spieler ermitteln // eine Enumeration für die einzelnen Zustände der State Machine enum class State {WALK_AROUND, // Spiele Musik SIT_DOWN, // Musik aus - langsamsten Teilnehmer ermitteln WAIT_FOR_CLEAR // Warten auf clear durch Spielleiter } state; // eine Variable in der wir den aktuellen Status speichern void setup() { Serial.begin(115200); // die Serielle aktivieren, damit man sieht was passiert Serial.println(F("\nMusical chairs, also known as Trip to Jerusalem")); Serial.println(F("press start to play music ...")); pinMode(clearPin, INPUT_PULLUP); // internen Pullup verwenden, Taster gegen Masse schalten for (auto &i : player) i.begin(); // range based for ... i ist nun eine Referenz auf den jeweiligen player state = State::WAIT_FOR_CLEAR; // Spielbegin: auf eine Freigabe durch den Spielleiter warten } void loop() { static uint8_t currentRound = 1; // current Round static uint32_t currentPlayTimeMS = 42; // current planed playtime in MS static uint8_t currentPresses = 0; // wie viele Spieler haben in dieser Runde schon gedrückt static uint32_t previousMillis; // letzter Aufruf uint32_t currentMillis = millis(); // aktuellen Zeitstempel merken switch (state) { case State::WALK_AROUND : // Warten auf die Tastendrücke melody.run(); // dafür sorgen, dass die Musik gegebenenfalls die nächste Note spielt for (size_t i = 0; i < noOfPlayer; i++) // prüfen ob ein aktiver Spieler zu früh drückt { if (player[i].getState() == true && player[i].pressed() == true) { Serial.print(F("pressed to early index=")); Serial.println(i); player[i].setState(false); melody.stop(); // Musik spielt ja noch, müssen wir also stoppen state = State::WAIT_FOR_CLEAR; Serial.println(F("--> WAIT_FOR_CLEAR")); } } if (currentMillis - previousMillis > currentPlayTimeMS) // prüfen auf Zeitablauf { melody.stop(); state = State::SIT_DOWN; Serial.println(F("--> SIT_DOWN")); previousMillis = currentMillis; // verwenden wir auch für den Timeout zum Niedersetzen } break; case State::SIT_DOWN : // Musik ist aus, die Spielen sollen sich auf freien Plätzen niedersetzen for (size_t i = 0; i < noOfPlayer; i++) // prüfen ob ein aktiver Spieler gedrückt hat { if (player[i].getState() == true && player[i].getSits() == false && player[i].pressed() == true) { Serial.print(F("player has seated, player=")); Serial.println(i); player[i].setSits(true); currentPresses++; } } if (currentPresses == currentRound) // prüfen ob bis auf einen alle Spieler gedrückt haben { Serial.println(F("enough players have pressed")); // wer steht noch rum? for (size_t i = 0; i < noOfPlayer; i++) { if (player[i].getState() == true && player[i].getSits() == false) { player[i].setState(false); // Spieler rauswerfen } } state = State::WAIT_FOR_CLEAR; Serial.println(F("--> WAIT_FOR_CLEAR")); } if (currentMillis - previousMillis > sitdownMax * 1000UL) // Zeit ist abgelaufen { Serial.println(F("timeout")); state = State::WAIT_FOR_CLEAR; Serial.println(F("--> WAIT_FOR_CLEAR")); } break; case State::WAIT_FOR_CLEAR : // warten, bis Clear gedrückt wird if (digitalRead(clearPin) == LOW) // checken ob Neustart gedrückt wird { currentRound--; // wir zählen um eine Runde runter if (currentRound == 0) // sollten wir in Runde 0 angelangt sein, fangen wir ein neues Spiel an { Serial.println(F("start new game")); currentRound = noOfPlayer - 1; // minus 1 weil wir einen Spieler je Runde rauswerfen for (auto &i : player) i.setState(true); // alle Spieler wieder aktivieren } else { Serial.println(F("start new round")); } Serial.print(F("currentRound=")); Serial.println(currentRound); for (auto &i : player) i.setSits(false); // alle Spieler sollen auf stehen (= sie sitzen nicht mehr) currentPresses = 0; // noch hat keiner gedrückt currentPlayTimeMS = random(walkAroundMin * 1000UL, walkAroundMax * 1000UL); // Laufzeitzeit für Melodie festlegen previousMillis = currentMillis; // aktuelle Zeit zurücksetzen melody.start(); // Melodie einschalten state = State::WALK_AROUND; Serial.println(F("--> WALK_AROUND")); } break; } }
Zusammenfassung
- Du hast einen nicht blockierenden Code mit Hilfe einer Finite State Machine erarbeitet.
- Die einzelnen Status verwaltest du in einer enum class.
- Du hast gelernt, wie man von einem Status in den anderen mittels einer Aktion (ein Tastendruck) oder mittels Zeitablauf kommt.
- Du hast Objekte kennengelernt, um Daten zusammenzufassen und gesehen, dass sich Objekte auch als Array anlegen lassen.
- Du nutzt künftig die range based for loop.