(Serielle Kommunikation zwischen Arduino und ESP32)
Am Beispiel eines Spieles, das wir hier einfach „Kugelwerfen“ nennen, wollen wir ein wenig mit der seriellen
Kommunikation zwischen zwei verschiedenen Mikrocontrollern experimentieren. Neben einigen wenigen Komponenten besteht die
Schaltung aus den Mikrocontrollern Arduino (Nano) und ESP32. Die beiden Mikrocontroller stammen von zwei verschiedenen Herstellern,
man kann sie beide jedoch mit der Software „Arduiono IDE“ programmieren.
Das Spiel selbst findet auf einer 8x8 RGB-Matrix statt. Die beiden Spieler stehen sich gegenüber und „werfen“ sich
gegenseitig Kugeln zu. Der erste Spieler, hier durch Arduino Nano imitiert, steht links auf dem Spielfeld und spielt mit grünen
Bällen. Der andere, ESP32, steht auf der rechten Seite des Spielfeldes und spielt mit roten Kugeln. Jede Spielrunde besteht aus
acht Würfen. Jeder Wurf wird mit Pluspunkten belohnt. Wird zum Beispiel eine Kugel auf das Feld (Spalte) vier geworfen, erhält der
Spieler vier Punkte. Es gewinnt der Spieler, der mehr Punkte angesammelt hat. Die Besonderheit des Spiels sind die Minuspunkte.
Wenn eine Kugel die Kugel des Gegners trifft, wird sie vom Spielfeld verbannt und dem Gegenspieler werden die davor gutgeschriebene
Punkte wieder abgezogen.
Die Spielzüge (Kugelwürfe) werden via Zufall bestimmt.
Das Spielfeld
Arduino Nano
Arduino Nano spielt von links nach rechts. Seine Kugeln sind grün. Der Mikrocontroller verfügt über eine serielle
Schnittstelle. Die Pins werden mit RX0 und TX1 bezeichnet. Einige andere Modelle von Arduino sind mit mehreren seriellen
Schnittstellen ausgestattet.
Der zweite Spieler, der auf der rechten Seite spielt, ist der Mikrocontroller ESP32 (ESP32 Dev KitC V4). Er spielt von
rechts nach links mit
roten Kugeln. Er kann bei Bedarf gleich drei seriellen Schnittstellen zur Verfügung stellen. In der Schaltung nutzen wie die Pins
mit Bezeichnung TX und RX. Dabei handelt es sich um die GPIO1 und GPIO3.
Mit einem 4-Zeilen und 20-Zeichen LCDisplay wird signalisiert, welcher der beiden Spieler mit dem nächsten Wurf an der
Reihe ist. Zusätzlich werden die aktuellen Koordinaten des Wurfs und die Punkte der Spieler angezeigt. Das Display kommuniziert
mit dem Mikrocontroller über den I2C Bus. Dies ermöglicht das FC-113 Modul, das an das Display hinten angelötet ist. Auf dem
Modul befindet sich ein Potentiometer, mit dem die Displayhelligkeit eingestellt werden kann.
Als Spielfeld kommt eine RGB-Matrix zum Einsatz. Auf dem so definierten Spielfeld werden die Kugel angezeigt, sodass
man durchgehend die aktuelle Situation erblicken kann. Wie bereits erwähnt spielt Arduino mit Grün von links nach rechts, ESP32
mit Rot von rechts nach links.
Die beiden Mikrocontroller-Module arbeiten mit unterschiedlichen Spannungen. Arduino Betriebsspannung beträgt 5VDC,
der ESP32 arbeitet mit 3,3VDC. An dieser Stelle soll man aufpassen. Wenn auf einen Pin von ESP32 eine Spannung von 5VDC gelangt,
kann er beschädigt werden. Als Abhilfe werden in solchen Fällen Levelkonverter eingesetzt. Sie passen die Spannungen
entsprechend an.
Spannungsregler
Der Arduino wird mit einer externen Spannung von 9VDC eingespeist. Mit einem Spannungsregler wird die hohe
Eingangsspannung auf 5VDC heruntergeregelt. Mit ihr werden dann das ESP32-Modul, die RGB-Matrix und das
Display eingespeist. Der Hauptbestandteil des Moduls ist der Regler LM2596.
Auf dem Schaltplan findet sich neben diversen Komponenten auch ein Kondensator (C1). Die Aufgabe des Kondensators
ist dafür zu sorgen, dass bei jedem Spiel (vor allem beim Einschalten) die Spieler jeweils mit anderen Zufallszahlen agieren.
Die Zufallsgeneratoren beider Mikrocontroller arbeiten zwar gut, starten jedoch nach Anlegen der Spannung immer mit den
gleichen Zufallszahlen. Mit der Funktion randomSeed(Wert) kann man das Problem umgehen. Die Funktion benötigt allerdings
einen Startwert, der möglichst bei jedem Einschalten einen anderen Wert hat. Diese Aufgabe übernimmt der Kondensator. Er lädt
sich über den Widerstand R3 auf. Die Spannung am Kondensator kann maximal 3,3V erreichen. Bei Betätigen des Tasters S1 erfolgt eine Entladung des
Kondensators über den Widerstand R4. Da man nicht exakt abschätzen kann, wie lange ein Mensch den Taster betätigt, baut sich
auf dem Kondensator eine Spannung von unbekanntem Wert auf. Sie wird den analogen Eingängen der beiden Mikrocontroller zugeführt.
Die Werte werden im Programm ausgelesen und der Funktion randomSeed() zugeführt.
Die Testschaltung
Testschaltung
Das Programm (ESP32)
// **********************************************************************************************
// Kugelwerfen
// Serielle Komminikation zwischen Arduino und ESP32
// ESP32 Sketch
// Arduino IDE 1.8.19
// **********************************************************************************************
int LED_PIN = 18; // Leuchtdiode
int Reihe, Spalte; // Mein Wurf via Zufalsszahlen
String Mein_Wurf; // Mein Wurf als String
String Sende_String, Antwort; // Kommunikation - Variablen
unsigned long Wartezeit = 500; // Antwortzeit
unsigned long Zeit_Start; // Zeitpunkt festhalten
int Spielfeld_Reihen [8]; // Reihen
void setup() {
Serial.begin (9600); // Serielle Schnittstelle
pinMode (LED_PIN, OUTPUT); // Status Leuchtdiode
}
// **********************************************************************************************
void loop() { // Hauptprogramm
if (Serial.available() > 0) { // Daten vom Partner vorhanden?
Sende_String = Serial.readStringUntil('*'); // Ja. Auslesen bis "*"
digitalWrite (LED_PIN, HIGH); // Leuchtdiode Ein
if (Sende_String == "Start") { // Neues Spiel, alte Variablen
for (int i=0; i<8; i++) { // 8 Reihen
Spielfeld_Reihen [i] = 0; // Neues Spiel = alles nullen
}
Sende_String = "Neuer_Wurf"; // Mein Wurf vorbereiten
randomSeed(analogRead(32)); // immer andere Zufallszahlen
}
}
if (Sende_String == "Neuer_Wurf") { // Mein Wurf
bool Mein_Wurf_OK = false;
Spalte = random(1,7); // Spalte via Zufall
while (Mein_Wurf_OK != true) {
Reihe = random(0,8); // Reihe via Zufall
if (Spielfeld_Reihen [Reihe] == 0) { // Reihen dürfen sich nicht
Spielfeld_Reihen [Reihe] = 1; // wiederholen
Mein_Wurf_OK = true;
}
}
Mein_Wurf = String(Reihe) + String(Spalte) + "*"; // Mein Wurf als String
Antwort = "";
while (Antwort != "OK") { // Senden bis Bestätigung
Serial.print (Mein_Wurf); // Meinen Wurf senden
delay (500); // Wartezeit
if (Serial.available() > 0) { // Bestätigung angekommen?
Zeit_Start = millis();
while ((Antwort != "OK") or ((Zeit_Start + Wartezeit) >millis())) {
Sende_String = Serial.readStringUntil('*');
if (Sende_String == "OK") { // Bestätigung erhalten
Antwort = "OK"; // Alles wiederholen
digitalWrite (LED_PIN, LOW); // Leuchtdiode Aus
}
}
}
}
}
}
// **********************************************************************************************
Der Spieler mit roten Kugeln (ESP32) hat nicht allzu viel zu tun. Das Programm überwacht die Schnittstelle und wartet
auf eine Anforderung von Arduino. Zwei Varianten sind hier möglich. Er muss einen neuen Kugelwurf generieren, wenn er die Nachricht
„Start“ oder „Neuer_Wurf“ erhält. Beide Anforderungen führen zu einem neuen Wurf. Bei der Nachricht „Start“ werden zusätzlich die
Spielfeldvariablen genullt. Nachdem via Zufall neue Zufallszahlen für Reihe und Spalte generiert und verschickt wurden, wartet
ESP32 auf eine Bestätigung mit „OK“ von Arduino. Sobald diese eintrifft, verlässt das Programm die interne Warteschleife und wartet
auf eine weitere Anforderung zum neuen Wurf.
Das Programm (Arduino Nano)
// **********************************************************************************************
// Kugelwerfen
// Serielle Komminikation zwischen Arduino und ESP32
// Arduino Sketch
// Arduino IDE 1.8.19
// **********************************************************************************************
#define LED_PIN 6 // DIN RGB Matrix
#define LED_COUNT 64 // Pixel Anzahl
#include<Adafruit_NeoPixel.h> // Bibliothek Matrix
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
#include<LiquidCrystal_I2C.h> // Bibliothek Display I2C-Bus
LiquidCrystal_I2C lcd(0x27,20,4); // Display definieren
int Start_Taster = 2; // Taster Spiel Start
int Status_LED = 9; // Status Leuchtdiode (mein Wurf)
int Spiel; // 0=kein Spiel / 1=Spiel
String Sende_String, Antwort; // Sende/Empf-Variablen
int Reihe, Spalte; // Spielfeld-Koordinaten
unsigned long Wartezeit = 500; // Antwortzeit
unsigned long Zeit_Start; // Zeitpunkt festhalten
int Wurf_Nr; // 8 Würfe pro Spiel
int Spieler; // 1-ESP32 / 2-Arduino
int Punkte_ESP32 [8], Punkte_Arduino [8]; // Punkte pro Reihe
int Gesamtpunkte_ESP32, Gesamtpunkte_Arduino;
void setup() {
strip.begin(); // Matrix
strip.show();
strip.setBrightness(40); // Matrix Helligkeit
lcd.init(); // Display initialisieren
lcd.backlight(); // LCD Hintergrundbeleuchtung
pinMode (Start_Taster, INPUT_PULLUP); // Start Taster als Pullup
pinMode(Status_LED, OUTPUT); // Status LED
Serial.begin(9600); // Serielle Kommunikation
lcd.setCursor(4,0); lcd.print("Kugelwerfen");
}
// **********************************************************************************************
void loop() {
// SPIEL BEGINN ----------------------------------------------
Sende_String ="Neuer_Wurf*";
if (digitalRead(Start_Taster) == LOW) { // Spiel Start
Spiel = 1; // Spiel läuft
Wurf_Nr = 0;
Gesamtpunkte_ESP32 = Gesamtpunkte_Arduino = 0;
lcd.clear();
Sende_String = "Start*"; // Start an den Partner
for (int i=0; i<8; i++) {
Punkte_ESP32 [i] = Punkte_Arduino [i] = 0; // Neues Spiel = alles nullen
}
randomSeed(analogRead(A0)); // Startwert Zufallsgenerator
Spielfeld_Start ();
}
// ESP32 IST AM ZUG ------------------------------------------
if (Spiel == 1) { // Spiel gestartet?
Spieler = 1; // ESP32 ist dran
Antwort = "";
Reihe = Spalte = -1;
LCD_Infotafel (); // Anzeige aktualisieren
while (Antwort != "OK") {
Serial.print (Sende_String); // Partner ist dran
delay (500);
if (Serial.available() > 0) { // Partner sendet seinen Wurf
Zeit_Start = millis();
while ((Antwort != "OK") or ((Zeit_Start + Wartezeit) >millis())) {
Sende_String = Serial.readStringUntil('*');
if (Sende_String.length() == 2) {
String Ziffer = Sende_String.substring(0,1);
Reihe = Ziffer.toInt();
Ziffer = Sende_String.substring(1,2);
Spalte = Ziffer.toInt();
Antwort = "OK";
Serial.print ("OK*"); // Bestätigung
Punkte_berechnen();
LCD_Infotafel (); // Anzeige aktualisieren
Wurf_auf_Matrix_Zeigen ();
}
}
}
}
}
// ARDUINO WURF ----------------------------------------------
if (Spiel == 1) {
Spieler = 2;
Reihe = Spalte = -1;
LCD_Infotafel ();
digitalWrite (Status_LED, HIGH); // Status LED an
delay(1000); // für LED
digitalWrite (Status_LED, LOW);
bool Mein_Wurf_OK = false;
Spalte = random(1,7); // Spalte via Zufall
while (Mein_Wurf_OK != true) {
Reihe = random(0,8); // Reihe via Zufall
if (Punkte_Arduino [Reihe] == 0) { // Reihen dürfen sich nicht
Mein_Wurf_OK = true; // wiederholen
}
}
Punkte_berechnen();
LCD_Infotafel ();
Wurf_auf_Matrix_Zeigen ();
Wurf_Nr++;
}
if (Wurf_Nr == 8) { // Spiel zu Ende
Spiel = 0;
lcd.setCursor(0,1); lcd.print("--------------------");
lcd.setCursor(0,2); lcd.print("Endstand: ");
}
}
// **********************************************************************************************
void LCD_Infotafel () { // Unterprogramm LCD-Anzeige
lcd.setCursor(4,0); lcd.print("Kugelwerfen"); // Titel
lcd.setCursor(1,1);
if (Spieler == 1) { // Spieler Anzeige
lcd.print("ESP32 ist dran ");
} else {
lcd.print("Arduino ist dran");
}
lcd.setCursor(1,2); lcd.print("Reihe: "); lcd.print(Reihe+1); // Wurf aktuell
lcd.setCursor(10,2); lcd.print("Spalte: "); lcd.print(Spalte+1);
lcd.setCursor(0,3); lcd.print("Arduino:"); // Aktueller Punktestand
lcd.setCursor(8,3); lcd.print(" ");
lcd.setCursor(8,3); lcd.print(Gesamtpunkte_Arduino);
lcd.setCursor(12,3); lcd.print("ESP32:");
lcd.setCursor(18,8); lcd.print(" ");
lcd.setCursor(18,8); lcd.print(Gesamtpunkte_ESP32);
delay (1000);
}
// **********************************************************************************************
void Spielfeld_Start () { // ESP32 spielt rechts
strip.clear(); // Arduino spielt links
for (int i=0; i<8; i++) {
strip.setPixelColor(i*8, 0, 255, 0); // GRÜN für Arduino
strip.setPixelColor((i*8)+7, 255, 0, 0); // BLAU für ESP32
}
strip.show();
}
// **********************************************************************************************
void Wurf_auf_Matrix_Zeigen () {
if (Spieler == 1) { // Wurfanzeige ESP32
int i;
for (i=0; i<(Spalte+1); i++) {
for (int j=1; j<5; j++) {
strip.setPixelColor(((Reihe+1)*8-(i+1)), 255, 0, 0);
strip.show();
delay (100);
strip.setPixelColor(((Reihe+1)*8-(i+1)), 0, 0, 0);
strip.show();
delay (100);
}
}
strip.setPixelColor(((Reihe+1)*8-i), 255, 0, 0);
strip.show();
}
if (Spieler == 2) { // Wurfanzeige Arduino
int i;
for (i=0; i<(Spalte+1); i++) {
for (int j=1; j<5; j++) {
strip.setPixelColor((Reihe*8+i), 0, 255, 0);
strip.show();
delay (100);
strip.setPixelColor((Reihe*8+i), 0, 0, 0);
strip.show();
delay (100);
}
}
strip.setPixelColor((Reihe*8+i-1), 0, 255, 0);
strip.show();
}
}
// **********************************************************************************************
void Punkte_berechnen() {
if (Spieler == 1) { // Punkte ESP32
Punkte_ESP32 [Reihe] = Spalte + 1;
if (Punkte_Arduino [Reihe] != 0) {
if ((8-Spalte) <= Punkte_Arduino [Reihe]) {
Gesamtpunkte_Arduino = Gesamtpunkte_Arduino - Punkte_Arduino [Reihe];
}
}
Gesamtpunkte_ESP32 = Gesamtpunkte_ESP32 + Punkte_ESP32 [Reihe];
}
if (Spieler == 2) { // Punkte Arduino
Punkte_Arduino [Reihe] = Spalte + 1;
if (Punkte_ESP32 [Reihe] != 0) {
if ((8-Punkte_ESP32 [Reihe]) <= Spalte) {
Gesamtpunkte_ESP32 = Gesamtpunkte_ESP32 - Punkte_ESP32 [Reihe];
}
}
Gesamtpunkte_Arduino = Gesamtpunkte_Arduino + Punkte_Arduino [Reihe];
}
}
// **********************************************************************************************
Der gesamte Spielverlauf ist nicht kompliziert. Arduino fordert sein Gegenüber mit „Start“ (beim Spielanfang)
oder „Neuer_Wurf“ (beim laufenden Spiel) dazu auf, einen neuen Wurf zu absolvieren. Sobald er die Koordinaten für den Wurf erhalten
hat, bestätigt er dies mit „OK“. Anschließend, nachdem der Wurf seines Gegners auf der Matrix angezeigt wurde, generiert er seine
eigenen Werte für Reihe und Spalte und führt auf der Matrix die Bewegung durch. Das Spiel endet, nachdem beide Spieler alle ihre
Kugel geworfen haben.
Für die serielle Übertragung werden in beiden Programme nur wenige Funktionen verwendet:
- Serial.begin(9600) – die Funktion initialisiert die serielle Schnittstelle mit Baudrate von 9600.
- Serial.print (Sende_String) : Mit Serial.print() werden Daten verschickt
- Serial.available() : Mit der Funktion wird die Schnittstelle abgefragt, ob neue Daten vorhanden sind.
Serial.available() > 0 liefert den Hinweis, dass neue Nachrichten angekommen sind und gelsen werden können.
- Serial.readStringUntil() : Mit der Funktion wird die Schnittstelle ausgelesen und zwar bis zu dem Zeichen, das in Klammern
angegeben wird. Serial.readStringUntil('*') liest die Daten bis zum Zeichen „*“. Das Zeichen selbst wird nicht
berücksichtigt.