Tinkercad Übung 8 - LED Lauflicht (2)

Pausiere die LED Animation

In dieser Übung pausierst du die LED Animation mit Hilfe des Tasters.
  • Empfohlenes Vorwissen: Bedienung Tinkercad Circuits, Übung 7
  • Neue Inhalte: Verzweigungen (if, else if), Variablen, Zustandsmaschine

Schritt 1 - Animationsprogramm kopieren

Kopiere die LED Animation, die du für Übung 7 erstellt hast oder nutze diesen Bespiel-Entwurf:
https://www.tinkercad.com/things/f6sTWwhVT0R-kkg-robotik-ubung-8-lauflicht-2-starter
Abbildung 1 - LED Animation aus Übung 7

Wir wollen nun den Taster integrieren. Mir fallen 2 Möglichkeiten ein:
  1. Die Animation ist pausiert und jedes Mal und wenn man den Taster gedrückt hält, läuft die Animation.
  2. Die Animation läuft und solange man den Taster gedrückt hält, ist die Animation pausiert.

Schritt 2 - Taster auslesen und die gesamte Animation pausieren

Momentan läuft die Animation im loop()-Bereich und wird somit die ganze Zeit wiederholt. Innerhalb des loop()-Bereiches sind die einzelnen Animationsschritte definitiert, z.B.

// Animationsschritt 1:
digitalWrite(9, LOW);             // Pin 9 ausschalten
digitalWrite(4, HIGH);           // Pin 4 einschalten
digitalWrite(8, LOW);
digitalWrite(13, HIGH);
delay(ANIMATIONSGESCHWINDIGKEIT);

usw...

Im diesem Schritt wollen wir programmieren, dass die Animation nur läuft, wenn der Taster gedrückt (gehalten) wird.

Dafür benötigen wir eine Verzweigung, denn wir müssen ja auf den Zustand des Tasters irgendwie reagieren:
  • wenn Taster gedrückt: LED Animation
  • sonst: nichts
In EV3 würde das so aussehen:
Abbildung 2 - Verzweigung in EV3

Am Anfang der Hauptprogrammschleife loop() wird der Zustand des Tasters ausgelesen. Nur, wenn er gedrückt ist, wird die Animation ausgeführt, sonst passiert nichts.

Es hilft, sich erstmal als Pseudocode aufzuschreiben, was man machen will:

// wenn Bedingung wahr:
    // Programm-Befehle, die ausgeführt werden, wenn Bedingung wahr ist
// sonst:
    // Programm-Befehle, die ausgeführt werden, wenn Bedingung unwahr ist

In C++ sieht das dann so aus:

// wird immer ausgeführt
if (Bedingung)
{
    // Befehle, die ausgeführt werden, wenn Bedingung wahr ist
}
else 
{
    // Befehle, die ausgeführt werden, wenn Bedingung unwahr ist
}
// wird immer ausgeführt

Das englische "if/else" übersetzt sich ins Deutsche als "wenn/sonst". Die Bedingung steht in Klammern. Die Befehls-Bereiche stehen in geschweiften Klammern. Ist die Bedingung wahr, wird der Bereich nach if ausgeführt, ist sie unwahr der Bereich nach dem else.

Unsere Bedingung ist, ob der Taster-Zustand gedrückt ist. Eine Bedingung ist immer ein Vergleich. Einfach Vergleiche:
  • Ist 12 gleich 12? → 12 == 12 → wahr
  • Ist 12 gleich 13? → 12 == 13 → unwahr
Schau mal ins Cheat Sheet (siehe Übung 5), was dir die Programmiersprache C++ da für Möglichkeiten bietet. Das Cheat Sheet kannst du hier herunterladen:

Ausschnitt aus dem Cheat Sheet:
Abbildung 2 - Vergleichsoperatoren im Cheat Sheet

Du siehst, man kann auch vergleichen, ob eine Zahl größer, kleiner, größer gleich, kleiner gleich oder ungleich einer anderen Zahl ist.

Schau mal in der Arduino-Referenz. Auch dort sind die Vergleichsoperatoren beschrieben:

Wir wollen also die Frage stellen:

Ist Taster Zustand GEDRÜCKT? → Taster Zustand == GEDRÜCKT

Den Taster liest du mit digitalRead() aus. Schau mal in die Arduino-Referenz, was dieser Befehl zurückgibt:
  • HIGH: an, enspricht wahr, auf englisch true, logische 1
  • LOW: aus, entspricht unwahr, auf englisch false, logische 0
Wir wollen also die Frage stellen

Taster Zustand == HIGH oder
Taster Zustand == true oder
Taster Zustand == 1

Wie du den Zustand eines Tasters ausliest, hast du in Übung Übung 6 gelernt. Fertig sieht unsere Bedingung für einen Taster an Pin 2 also so aus:

digitalRead(2) == HIGH oder
digitalRead(2) == true oder
digitalRead(2) == 1

Eingesetzt in die Verzweigung:

if (digitalRead(2) == HIGH) {
    // Befehle, die ausgeführt werden, wenn Bedingung wahr ist
} else {
    // Befehle, die ausgeführt werden, wenn Bedingung unwahr ist
}

Auch if und else gibt es in der Arduino-Referenz. Schaue dort mal nach:

Baue dein Animationsprogramm so um, das die Animation nur ausgeführt wird, wenn der Taster an Pin 2 den Zustand HIGH oder true oder 1 hat so wie im folgenden Pseudocode beschrieben:

// wiederhole (loop()):
    // wenn Taster an Pin 2 gedrückt:
        // Animationschritt 1
        // Animationschritt 2
        usw...
    // sonst
        // nichts

Geschweifte Klammer auf { und zu } befinden sich mit auf den Tasten 7 und 0.

Tipp: Den else-Zweig darf man übrigens auch weglassen. Das sieht dann so aus:

if (Bedingung) {
  // Befehle, die ausgeführt werden, wenn Bedingung wahr ist
}

Starte die Simulation und betätige den Taster. Was passiert? Probiere alle Möglichkeiten des Vergleichens aus: HIGH, true und 1. Was passiert, wenn du stattdessen mit LOW, false und 0 vergleichst?

Tipp: Momentan musst du den Taster noch ein wenig gedrückt halten bis das Programm reagiert. Das werden wir gleich verbessern.

Abbildung 3 - Ganze Animation pausieren

Schritt 3 - Einzelne Animationsschritte pausieren

Du hast bestimmt festgestellt, dass das Programm momentan nur auf den Tastendruck reagiert, wenn die Animation mit dem letzten Animationsschritt fertig ist, bevor Animationsschritt 1 wieder gestartet wird. Das liegt daran, dass wir nur an dieser Stelle den Zustand des Tasters auslesen. Ist der Taster einmal gedrückt worden, wird die gesamte Animation am Stück durchgeführt. Dann erst kehrt das Programm zum Anfang von loop() zurück und es kann entschieden werden, ob die Animation weiter ausgeführt werden übersprungen werden soll.

Wir versuchen das mal zu beheben, indem wir einfach jeden Animationsschritt einzeln in eine Verzweigung verpacken:

Pseudocode:

// wiederhole (loop()):
    // wenn Taster an Pin 2 gedrückt:
        // Animationschritt 1
    // wenn Taster an Pin 2 gedrückt:
        // Animationschritt 2
    usw...

Echter C++ Code:

// solange der Taster gedrückt ist:
if (digitalRead(2) == HIGH) {
  // Animationsschritt 1:
  digitalWrite(9, LOW); // Pin 9 ausschalten
  digitalWrite(4, HIGH); // Pin 4 einschalten
  digitalWrite(8, LOW);
  digitalWrite(13, HIGH);
  delay(ANIMATIONSGESCHWINDIGKEIT);
}

if (digitalRead(2) == HIGH) {
  // Animationsschritt 2:
  digitalWrite(4, LOW);
  digitalWrite(5, HIGH);
  digitalWrite(13, LOW);
  digitalWrite(12, HIGH);
  delay(ANIMATIONSGESCHWINDIGKEIT);
}

usw...

Baue dein Programm so um wie oben gezeigt. Starte die Simulation und halte den Taster gedrückt.
Abbildung 4 - einzelne Schritte der Animation pausieren

Schritt 4 - Zustandsmaschine

Du hast bestimmt gerade gemerkt: Solange man den Taster gedrückt hält, ist alles in Ordung, aber denn man ihn nur kurz drückt, wird alles recht zufällig.

Grund: Wenn der Taster nicht gedrückt ist, werden alle Verzweigungsblöcke übersprungen, da die Bedingung digitalRead(2) == HIGH immer unwahr ist, egal wie oft man danach fragt. Das ganze befindet sich in loop(), der Hauptrogrammschleife, wird also immer wiederholt. Dadurch, dass aber alles übersprungen wird in der Schleife, gehen die einzelnen Wiederholungen sehr schnell.

Drückt man nun den Taster, wird der Animationsschritt ausgeführt, bei dem gerade zufällig die Bedingung digitalRead(2) == HIGH abgefragt wird. Lässt man den Taster sofort wieder los, wird der darauf folgende Animationsschritt wieder übersprungen und alle anderen auch.

Irgendwie muss die Programm-Struktur aus Schritt 2 mit der aus Schritt 3 verheiratet werden.
Die Lösung ist eine Zustandsmaschine. Eine Zustandsmaschine besteht aus mehreren Zuständen (in unserem Fall Animationsschritte). Aus einem Zustand kann man in den nächsten übertreten (in unserem Fall ganz einfach von Animationsschritt 1 nach 2 nach 3 usw.). Dafür bauen wir das Programm folgendermaßen um:

Pseudocode:

// wiederhole (loop()):
    // wenn Taster an Pin 2 gedrückt:
        // wenn Animationschritt 1
            // Animationschritt 1
            // weiter mit Animationsschritt 2
        // oder wenn Animationschritt 2
            // Animationschritt 2
            // weiter mit Animationsschritt 3
        // oder wenn Animationschritt 3
            // Animationschritt 3
            // weiter mit Animationsschritt 4
        usw...

Das soll so funktionieren:

Ist der Taster nicht gedrückt, wird alles übersprungen so wie bei dem Programm aus Schritt 2.
Ist der Taster gedrückt:
    1. Taster ist gedrückt.
    2. Wir befinden wir uns gerade bei Animationsschritt 1: Er wird ausgeführt und wir merken uns, dass als nächstes Animationsschritt 2 ausgeführt werden soll.
    3. Ende von loop(), wieder zum Anfang.
    4. Wir befinden wir uns gerade bei Animationsschritt 2: Er wird ausgeführt und wir merken uns, dass als nächstes Animationsschritt 3 ausgeführt werden soll.
    5. Ende von loop(), wieder zum Anfang.
    6. Taster nicht mehr gedrückt.
    7. Alles wird übersprungen, d.h. die LEDs zeigen den zuletzt eingestellten Zustand: Animationsschritt 2.
    8. Ende von loop(), wieder zum Anfang.
    9. Taster ist wieder gedrückt.
    10. Wir befinden wir uns gerade bei Animationsschritt 3: Er wird ausgeführt und wir merken uns, dass als nächstes Animationsschritt 4 ausgeführt werden soll.
Jetzt wird bei losgelassenem Taster wirklich bei einem Animationsschritt pausiert!

In EV3 sähe das so aus:

Abbildung 5 - Mehrfach-Verzweigung in EV3

In C++:

loop() {
  if (digitalRead(2) == HIGH) {
    if ( Ist Animationsschritt 1? ) {
       // Animationsschritt 1
       // weiter mit Animationsschritt 2
    } else if ( Ist Animationsschritt 2? ) {
       // Animationsschritt 2
       // weiter mit Animationsschritt 3
    } else if ( Ist Animationsschritt 3? ) {
       // Animationsschritt 3
       // weiter mit Animationsschritt 4

           usw...
    }
  }
}

Variablen

Irgendwie muss sich das Programm merken, welcher Animationsschritt gerade ausgeführt werden soll. Dafür kann eine Variable verwendet werden. Variablen leben im RAM des Mikrocontrollers. Man kann dort Werte zwischenspeichern und wieder auslesen.

Variablen definieren:

Eine Variable wird definiert wie eine Konstante, nur ohne const:

int animationsschritt = 1;
  • int ist der Datentyp. Den kennst du Schon aus Übung 7. Dieser sagt dem Compiler, dass wir eine Zahl abspeichern wollen.
  • animationsschritt ist der Name der Variable.
  • = 1 ist der Startwert der Variable. Man kann diesen auch weglassen. Ich empfehle aber, immer einen Startwert zu setzen.
Der Variablen einen Typ und einen Namen zuzuweisen, nennt man Deklaration. Ab jetzt darf man sie benutzen. Eine Variable muss immer weiter oben im Programm deklariert werden als sie benutzt wird. In der Variablen das erste Mal einen Wert abzuspeichern, nennt man Initialisierung.

Deklaration und Initialisierung können auch als 2 Befehlszeilen geschrieben werden:

int animationsschritt;
animationsschritt = 1;

Wert in eine Variable speichern:

Um einen Wert in einer Variable abzuspeichern, wird der Zuweisungs-Operator verwendet. Dieser speichert den Wert auf der rechten Seite in die Variable auf der linken Seite.

int animationsschritt = 1; // animationsschritt hat den Wert 1
animationsschritt = 12;    // animationsschritt hat den Wert 12

Das obige Beispiel nutzt den Zuweisungsoperator, um die Variable animationsschritt mit 1 zu initialisieren und ein weiteres Mal, um den Wert der Variable auf 12 zu ändern.

In EV3 sähe das Schreiben einer Variable so aus:
Abbildung 6 - Variable schreiben in EV3

Wert aus einer Variablen lesen:

Jetzt nutzt es uns wenig, einen Wert irgendwo abzuspeichern, ihn aber nie zu benutzen. Den Wert wieder auszulesen, würde in EV3 so aussehen:
Abbildung 7 - Variable lesen in EV3

In C++ schreibt man einfach den Namen der Variable hin:

animationsschritt

Variable vergleichen:

Um in einer Verzweigung auf einen bestimmten Variablen-Wert zu prüfen, schreibt man also einfach den Namen der Variable:

animationsschritt == 12 → wahr
animationsschritt == 13 → unwahr

Der Variablen-Name wird dann durch den Inhalt (Wert) ersetzt:

12 == 12 --> wahr
12 == 13 --> unwahr

Eingebaut in die Zustandsmaschine:

// Animationsschritt (wir starten mit 1)
int animationsschritt = 1;

loop() {
  if (digitalRead(2) == HIGH) {
    if (animationsschritt == 1) {
       // Animationsschritt 1
       animationsschritt = 2; // nächster Animationsschritt
    } else if (animationsschritt == 2) {
       // Animationsschritt 2
       animationsschritt = 3; // nächster Animationsschritt
    } else if (animationsschritt == 1) {
       // Animationsschritt 3
       animationsschritt = 4; // nächster Animationsschritt

       usw...
    }
  }
}

Baue das Programm so um, dass die einzelnen Animationsschritte Zustände einer Zustandsmaschine sind. Diese Zustandsmaschine wird übersprungen, wenn der Taster nicht gedrückt ist. Ist der Taster gedrückt, geht die Zustandsmaschine in den zuletzt gespeicherten Zustand (Animationsschritt) und führt diesen aus. Ist der Zustand fertig ausgeführt, wird der nächste Zustand abgespeichert. Ist der letzte Zustand fertig ausgeführt, wird der erste Zustand abgespeichert.

Starte die Simulation und halte den Taster gedrückt.
Was passiert, wenn du den Zustand des Taster mit LOW vergleichst anstatt mit HIGH?
Abbildung 8 - Animation pausieren mit Zustandsmaschine

Zusammenfassung

Du kannst nun Verzweigungen in C++ programmieren, weißt, dass eine Bedingung immer ein Vergleich ist und wie man diesen schreibt, kannst Zahlen in einer Variablen abspeichern, ändern und wieder auslesen und hast das Konzept einer Zustandsmaschine kennen gelernt.

Kommentare

Beliebte Posts aus diesem Blog

Tinkercad Übung 6 - LED mit Taster ansteuern

Tinkercad Übung 11 - LED dimmen

Tinkercad Übung 15 - Ultraschallsensor auslesen