Tinkercad Übung 14 - Textanzeige

Buchstaben auf einer selbstgebauten LED-Matrix anzeigen

In dieser Übung zeigst du über den Serial Monitor eingegebene Texte auf einer LED-Matrix an.
  • Empfohlenes Vorwissen: Bedienung Tinkercad Circuits, Prozeduren, Funktionen, Variablen, Serial Monitor, Datentypen, Übung 6Übung 9
  • Neue Inhalte: Logische Operatoren, Switch-Case, Daten vom Serial Monitor empfangen

Schritt 1 - Demo-Entwurf öffnen und inspizieren

Öffne und kopiere diesen Demo-Entwurf:
https://www.tinkercad.com/things/53VwA425qMA-kkg-robotik-ubung-14-starter
Abbildung 1 - selbstgebaute LED-Matrix
In dieser Schaltung sind an alle Pins des Arduino Unos LEDs mit Vorwiderständen angeschlossen. Sie sind so angeordnet, dass wir sie wie Pixel eines Displays nutzen können. So etwas nennt man LED-Matrix und kann auch fertig gekauft werden:

Abbildung 2 - echte RGB-LED-Matrix

Die im Schaltplan aufgebaute LED-Matrix ist monochrom, d.h. sie kann nur eine Farbe darstellen. Die Farben der einzelnen LEDs kannst du gern nach deinem Geschmack umstellen. Momentan können die einzelnen LEDs nur grün (an) oder schwarz (aus).

Die Anzahl der Pixel unserer Matrix ist durch die Anzahl der Pins am Arduino begrenzt. Wenn man mehr Pins benötigt, kann man beispielsweise einen Arduino Mega oder Arduino Due verwenden. Die Simulation bietet uns allerdings leider nur einen Arduino Uno.

Schau in das Demo-Programm. Dort sind alle LEDs bereits als Ausgang konfiguriert. Die Prozedur ein(pin) wird als Kurzschreibweise für digitalWrite(pin, HIGH) verwendet. Mit der Prozedur alle(zustand) können alle LEDs eintweder auf LOW oder HIGH eingestellt werden.

Teste, ob die Schaltung funktioniert, in dem du alle LEDs einschaltest. Nutze dafür alle(HIGH).
Vielleicht hast du beim Ändern der LED-Farben einige LEDs verschoben und sie sind jetzt nicht mehr richtig angeschlossen. Das kannst du hiermit testen.

Ansonsten sind für alle Großbuchstaben des englischen Alphabets (also ohne Umlaute und ß) und Zahlen bereits Prozeduren implementiert, die diese anzeigen. Sie beginnen immer mit einem Unterstrich, da es z.B. bereits einen Arduino-Befehl nahmens F() gibt und Prozedur-Namen auch nicht mit Ziffern beginnen dürfen.

Probiere mal aus ein Zeichen anzuzeigen, zum Beispiel _A().

Schritt 1 - Text anzeigen

Schreibe nun ein Programm, dass nacheinander die Buchstaben H A L L O anzeigt und danach wieder alle LEDs ausschaltet.
Lösungsvorschlag: https://www.tinkercad.com/things/krFvJxql6Sw-kkg-robotik-ubung-14-buchstaben-anzeige
Abbildung 3 - "Hallo" anzeigen

Schritt 2 - Echten Text darstellen

Die einzelnen Prozeduren

_H(); _A(); _L(); _L(); _O();

einzeln aufzurufen hilft uns leider nicht, da der Nutzer des Programms am Ende ja beliebige Texte eingeben können soll. Die Aufrufe dieser Prozeduren müssen also irgendwie variabel gestaltet werden.

In Übung 6 hast du bereits den Datentyp char benutzt, um ein einzelnes Zeichen an den Serial Monitor zur Anzeige zu senden. Diesen Datentyp wollen wir nun auch benutzen, um einzelne Zeichen in einer Variablen zu speichern, z.B. so:

char zeichen = 'H';

Diesen Datentyp findest du auch im Cheat Sheet (https://www.dropbox.com/s/k0hw31v07y1ykk6/Cheat%20Sheet.pdf?dl=0).
Abbildung 4 - Cheat Sheet Abschnitt Datentypen für Text

oder in der Arduino-Referenz (https://www.arduino.cc/reference/de/language/variables/data-types/char/).

Der Datentyp char ermöglicht es dir eine Prozedur zu schreiben, die als Parameter ein Zeichen aufnimmt und dieses auf der LED Matrix anzeigt. Benutzt werden könnte diese Prozedur dann z.B. so:

anzeigen('H');
anzeigen('A');
anzeigen('L');
anzeigen('L');
anzeigen('O');

Diese Prozedur vergleicht dann das gegebene Zeichen mit den Zeichen, für die es Anzeige-Prozeduren (z.B. _A()) gibt und ruft diese auf, z.B.

void anzeigen(char buchstabe) {
  alle(LOW);
  if (buchstabe == 'A') {
    _A();
  } else if (buchstabe == 'B') {
    _B();
  ... usw. ...
  }
}

Schreibe die anzeigen() Prozedur. Das ist ein bisschen Fleißarbeit, da ja Verzweigungen für alle bekannten Buchstaben und Zahlen zu schreiben sind.

Teste die Prozedur, indem du wieder H A L L O damit schreibst.
Starte die Simulation und teste, dass das Programm genauso wie vorher funktioniert.

Schritt 3 - Groß- und Kleinbuchstaben

Bisher versteht die Prozedur anzeigen() nur Großbuchstaben. Das Demo-Programm sieht auch keine Kleinbuchstaben vor (aber du kannst dafür natürlich auch gern Prozeduren hinzufügen). Der Nutzer des Programms könnte allerdings trotzdem versuchen Kleinbuchstaben zu verwenden. Wir könnten natürlich bei einem Kleinbuchstaben auch einfach den entsprechenden Großbuchstaben anzeigen, also

anzeigen('H'); zeigt dasselbe H auf der LED-Matrix wie
anzeigen('h');

Also müssen mehr Verzweigungen her, z.B.

  if (buchstabe == 'A') {
    _A();
  } else if (buchstabe == 'a') {
    _A();
  ... usw. ...
  }

Jetzt versteht die Prozedur sowohl den Großbuchstaben A als auch den Kleinbuchstaben a und macht in beiden Fällen das gleiche.

Das Programm wird dadurch fast doppelt so lang. Es gibt aber zum Glück Alternativen:

In Übung 9 hast du gelernt, dass die Bedingung in einer Verzweigung oder die Abbruchbedingung einer Schleife ein Vergleich sein kann, aber nicht sein muss. Wichtig ist, dass am Ende true (wahr) oder false (unwahr) herauskommt. So etwas nennt man einen logischen oder boolschen Ausdruck.

Ein einfacher Vergleich:

buchstabe == 'A'

Hat buchstabe den Wert 'A', hat dieser logische Ausdruck das Ergebnis true, sonst false. Man kann diese Ausdrücke aber auch verketten und damit komplexere Ausdrücke bauen.

In unserem Fall wollen wir die Prozedur _A() aufrufen, wenn buchstabe entweder den Wert 'A' oder den Wert 'a' hat, weil wir wollen ja in beiden Fällen das selbe tun.

Als Pseudocode:
// buchstabe gleich 'A' oder buchstabe gleich 'a'

In C++:

buchstabe == 'A' || buchstabe == 'a'

|| ist ein logischer Operator und bedeutet ODER.

Schau dir die anderen logischen Operatoren im Cheat Sheet an:
Abbildung 5 - Cheat Sheet logische Operatoren

Du siehst es gibt UND, ODER und NICHT (es gibt sogar noch  mehr):
  • a || b ist wahr, wenn entweder a oder b wahr sind. Das ist ein bisschen unintuitiv, weil es funktioniert nicht ganz genau so wie wir "oder" umgangssprachlich verwenden: a || b ist nämlich auch wahr, wenn sowohl a als auch b wahr sind.
  • a && b ist nur wahr, wenn sowohl a als auch b wahr sind, sonst unwahr. So verwenden wir "und" ja auch umgangssprachlich.
  • !a ist wahr, wenn a unwahr ist und umgekehrt. Damit kann man also das Ergebnis umdrehen (Negation).
Wie in der Artithmetik ist es erlaubt Klammern zu setzen, um kompliziertere Ausdrücke zu bauen:

// Max Mustermann hat am 01.02.2003 Geburtstag.
// Max bekommt Geburtstagsgeschenke, wenn er ( heut Geburtstag hat
// oder wenn heut Weihnachten ist ) und seine Eltern nicht vergessen
// haben Geschenke zu besorgen:

if (
  (
       (tag_heute == 1 && monat_heute == 2 && jahr_heute == 2003)
    || (tag_heute == 24 && monat_heute == 12)
  ) && !wurde_vergessen()
) {
  bekommt_geschenke();
}

vereinfacht geschrieben:

if (
  ( hat_heute_geburtstag() || ist_weihnachten() ) && !wurde_vergessen()
) {
  bekommt_geschenke();
}

Klammern werden wie immer von innen nach Außen aufgelöst.

Die Negation kann auch vor Klammern geschrieben werden, um deren Ergebnis zu negieren z.B.

if (
  !(
     ( hat_heute_geburtstag() || ist_weihnachten() ) && !wurde_vergessen()
   )
) {
  // die ganze Bedingung oben wurde negiert
} else {
  bekommt_geschenke();
}

Die ganze Bedingung wurde in Klammern gesetzt und dann negiert. Was vorher true war, ist jetzt false und umgekehrt. Damit bekommt Kevin die Geschenke nun im else-Zweig.

Exkurs Ende. Zurück zu unserer Buchstaben-Anzeige.

Erweitere die Bedingungen in der anzeigen()-Prozedur, sodass sie bei Großbuchstaben und Kleinbuchstaben dasselbe machen, also z.B. _A() ausführt, wenn entweder buchstabe den Wert 'A' oder den Wert 'a' hat. Das für alle Verzweigungen umzusetzen ist wieder ein wenig Fleißarbeit (und muss du auch nicht unbedingt machen, weil wir das Programm eh noch einmal umbauen werden).

Das |-Zeichen (Pipe) befindet sich mit auf der > und < Taste.

Lösungsvorschlag: https://www.tinkercad.com/things/353vTDEgyUk-kkg-robotik-ubung-14-text-ausgeben

Starte die Simulation und teste, dass das Programm genauso wie vorher funktioniert.

Schritt 4 - Switch-Case

Wenn du die anzeigen()-Prozedur wirklich für alle möglichen Buchstaben und Ziffern implementiert hast, besteht diese nun aus einer sehr langen if-elseif-elseif-Struktur:

void anzeigen(char buchstabe) {
  alle(LOW);
  if (buchstabe == 'a' || buchstabe == 'A') {
    _A();
  } else if (buchstabe == 'b' || buchstabe == 'B') {
    _B();
  ... usw. ...
  }
}

Jeder der gemachten Vergleiche benutzt die Variable buchstabe. Aus diesem Grund kann man auch eine andere Verzweigungs-Struktur verwenden:

Als Pseudocode:
// nehme Wert von buchstabe. Umschalten zwischen Fällen:
  // Fall 'a':
  // oder Fall 'A':
    // A ausgeben
  // Fall 'b':
  // oder Fall 'B':
    // B ausgeben

In C++:

void anzeigen(char buchstabe) {
  alle(LOW);
  switch (buchstabe) {
    case 'a':
    case 'A':
      _A(); break;
    case 'b':
    case 'B':
      _B(); break;
    ... usw. ...
    case default:
      // was normalerweise else wäre, also wenn nichts zutrifft
  }
  // nach dem switch-case
}

Zu deutsch: "switch"== "umschalten", "case" == "Fall"
Diese Struktur schaut sich einmal oben den Wert der Variable buchstabe an und vergleicht diesen dann mit den Fällen 'a''A''b' und 'B', und zwar so lange, bis ein break (abbrechen) ausgeführt wird. Das ist also ein bisschen anders als bei if-elseif-elseif, wo garantiert immer nur einer der Zweige ausgeführt wird.
  • buchstabe hat Wert 'A':
    1. case 'a'  unwahr  weiter
    2. case 'A'  wahr  _A() ausführen, Abbruch und zu "nach dem switch-case".
  • buchstabe hat Wert 'a':
    1. case 'a'→ wahr  fällt durch bis zum nächsten break, also _A() ausführen, Abbruch und zu "nach dem switch-case".
  • buchstabe hat Wert 'b':
    1. case 'a'→ unwahr  weiter
    2. case 'A'  unwahr  weiter
    3. case 'b'  wahr  fällt durch bis zum nächsten break, also _B() ausführen, Abbruch und zu "nach dem switch-case".
  • buchstabe hat Wert 'B':
    1. case 'a'  unwahr  weiter
    2. case 'A'  unwahr  weiter
    3. case 'b'  unwahr  weiter
    4. case 'B'  wahr  _B() ausführen, Abbruch und zu "nach dem switch-case".
Das else bei switch-case heißt übrigens default. Dieser Zweig wird betreten, wenn keiner der anderen Fälle zutrifft (oder wenn vergessen wurde ein break zu schreiben und wir bis dahin durchfallen):
  • buchstabe hat Wert '?':
    1. case 'a'→ unwahr  weiter
    2. case 'A'  unwahr  weiter
    3. case 'b'  unwahr  weiter
    4. case 'B'  unwahr  weiter
    5. case default ist immer wahr  betreten und ausführen
Die switch-case-Struktur findest du auch in der Arduino-Referenz (https://www.arduino.cc/reference/de/language/structure/control-structure/switchcase/) oder im Cheat Sheet:
Abbildung 6 - Cheat Sheet Switch...case

Baue die anzeigen()-Prozedur nun so um, dass sie anstatt den vielen Vergleichen eine switch-case-Struktur verwendet. Das sollte jetzt ein bisschen kompakter aussehen als vorher.

Lösungsvorschlag: https://www.tinkercad.com/things/e3f9hSBw0aO-kkg-robotik-ubung-14-switch-case
Starte die Simulation und teste, dass das Programm genauso wie vorher funktioniert.

Tipp: switch-case eignet sich hervorragend, um Zustandsmaschinen zu bauen.

Schritt 5 - Text vom Serial Monitor einlesen und anzeigen

Du  hast jetzt alles vorbereitet, um beliebige Texte Zeichen für Zeichen in der LED-Matrix anzuzeigen. Diese Texte soll der Nutzer jetzt über den Serial Monitor eingeben können. Das sieht dann am Ende so aus:
Abbildung 7 - Text im Serial Monitor eingeben

Dass 2 der LEDs verrückt spielen und immer so (halb) an sind, ist kein Fehler und wäre bei einem echten Arduino auch so. Das sind die Pins 0 und 1, die wir bisher bei allen anderen Projekten vermieden haben. Diese Pins sind mit RX und TX beschriftet, also ein Pin zum Senden von seriellen Daten und ein Pin zum Empfangen. Diese werden aktiv, sobald man Serial.begin() ausführt. Aus Mangel an Pins am Arduino Uno musste ich sie allerdings mit verwenden. Das Projekt 3 verwendet diese Pins, um Daten zwischen 2 Arduinos auszutauschen.

Wie du siehst, kann der Serial Monitor nicht nur Text empfangen und anzeigen, sondern auch an den Arduino senden. Dafür gibt man den Text in das Textfeld ein und drückt entweder auf die "Senden"-Schaltfläche oder die ENTER-Taste.

Dann wird der ganze Text Zeichen für Zeichen an den Arduino übermittelt und landet dort erstmal in einer Warteschlange. Aus dieser Warteschlange kann unser Arduino-Programm die einzelnen Zeichen wieder auslesen.
  • Wenn wir das nie tun und der Nutzer weiter Zeichen an den Arduino sendet, ist die Warteschlange irgendwann voll und es können keine weiteren Zeichen mehr auf dem Arduino zwischengespeichert werden. Es ist also wichtig, dass das Arduino-Programm regelmäßig die Zeichen aus der Warteschlange ausliest und diese somit abarbeitet.
  • Ist die Warteschlange leer, kann das Programm auf dem Arduino kein Zeichen lesen. Vermutlich hat der Nutzer einfach noch nichts an den Arduino gesendet.
Um zu prüfen, ob etwas in der Warteschlange bereit liegt, gibt es den Befehl:

int Serial.available()

Diese Funktion gibt uns die Anzahl der bereits empfangenen und somit wartenden Zeichen zurück.

Füge folgende Zeile in loop() hinzu. Vergiss nicht den Serial Monitor mit Serial.begin() zu initialisieren. Starte die Simulation und öffne den Serial Monitor. Sende ein paar Zeichen an den Arduino. Was beobachtest du?

Serial.println(Serial.available());

Abbildung 8 - Serial.available()

Um die Zeichen auszulesen und aus der Warteschlange zu entfernen, steht u.A. dieser Befehl zur Verfügung:

char Serial.read();

Diese Funktion gibt dir das zuerst empfangene Zeichen aus der Warteschlange zurück und entfernt es aus der Warteschlange. Führe Serial.read() auch in loop() zusammen mit Serial.available() aus und beobachte was passiert, wenn du nun Text an dern Arduino sendest:

Serial.println(Serial.available());
Serial.read();

Du siehst, die Warteschlange wird vom Arduino sehr schnell wieder geleert und meist gibt es nichts mehr zu lesen übrig,

Serial.read() entfernt allerdings nicht nur das älteste Zeichen aus der Warteschlange, sondern gibt es als Rückgabewert auch wieder zurück. Man kann dieses Zeichen dann in einer Variablen speichern:

char empfangenes_zeichen = Serial.read();

Das ergibt allerdings nur Sinn, wenn sich überhaupt ein Zeichen in der Warteschlange befindet, sonst wird die Zahl -1 zurückgegeben. Dafür hat die anzeigen()-Prozedur gar keine Verzweigung.

Du kannst Serial.available() benutzen, um nur ein Zeichen zu lesen, wenn mehr als 0 Zeichen in der Warteschlange darauf warten gelesen zu werden. Baue loop() entsprechend um. Übergebe das empfangene Zeichen an die anzeigen()-Prozedur.

Achte darauf, dass der Nutzer auch Leerzeichen ' ' zum Trennen der Wörter eingeben will.
Starte die Simulation, öffne den Serial Monitor und sende einen Text an den Arduino.
Versuche mal ganz schnell hintereinander sehr viel Text an den Arduino zu senden, sodass er mit dem Anzeigen nicht mehr hinterher kommt.

Lösungsvorschlag: https://www.tinkercad.com/things/kk0eQGPuYLk-kkg-robotik-ubung-14-serialread

Füge Prozeduren hinzu, die Umlaute und ß anzeigen können, damit der Nutzer "Döner ist offen." wie gewohnt eingeben kann.

Weitere Funktionen für den Serial Monitor findest du in der Arduino-Referenz:

Zusammenfassung

Du weißt nun, wie du logische Ausdrücke schreibst, also z.B. mehrere Vergleiche zu einer komplizierten Bedingung zusammenfügen kannst. Du hast eine alternative Schreibweise zu if-elseif-elseif-else kennen gelernt. Du kannst nun Daten vom Serial Monitor empfangen und auf dem Arduino auswerten.

Mit dem Serial Monitor beschäftigt sich auch Projekt 3 noch einmal auf andere Art.

Du weißt  jetzt alles, um das Projekt "Reaktionsspiel" und das Projekt "2 Arduinos kommunizieren" zu probieren.

Weiter zu Übung 15




Kommentare

Beliebte Posts aus diesem Blog

Tinkercad Übung 6 - LED mit Taster ansteuern

Tinkercad Übung 11 - LED dimmen

Tinkercad Übung 15 - Ultraschallsensor auslesen