Serielle Kommunikation ist eine der elementaren Kommunikationstechniken. Dabei geht es grundsätzlich darum, ein Daten Bit pro Zeiteinheit sequentiell von einem Rechner zu einem Anderen zu übertragen. Diese Form der Kommunikation basiert auf dem UART Protokoll (Universal Asynchronous Receiver-Transmitter). Das UART-Protokoll ist eines zuverlässigsten seriellen Kommunikations-Protokolle, das bis heute noch verwendet wird. Gegenüber parallelen Schnittstellen benötigt die serielle Schnittstelle weniger Stromkreise und demzufolge einen geringeren Verkabelungsaufwand, was besonders bei größeren Distanzen bedeutsam ist.
Die Kommunikation über eine Serielle Schnittstelle wird zur Datenübertragung zwischen zwei Geräten verwendet wird. Auch als RS232 Schnittstelle bezeichnet. Diese Schnittstelle überträgt Daten bitweise über eine einzelne Leitung, wobei eine logische 1 durch einen negativen Spannungspegel und eine logische 0 durch einen positiven Spannungspegel repräsentiert wird. Die Übertragung erfolgt asynchron, das heißt, Sender und Empfänger müssen sich nicht gegenseitig synchronisieren.
Die Aufgabe der Datenübertragung übernimmt ein sogenannter UART Baustein.Dieser überträgt und empfängt die Daten als serieller digitaler Datenstrom mit einem fixen Rahmen, der aus einem Start-Bit, fünf bis maximal acht oder neun Datenbits, einem optionalen Parity-Bit zur Erkennung von Übertragungsfehlern und einem oder zwei Stopp-Bits besteht. Dieses Protokoll verwendet zwei Drähte, die als Tx (Senden) und Rx (Empfangen) bekannt sind, damit beide Komponenten kommunizieren können. Wichtig ist, dass die RX und TX Pins bei Sender (Computer) und Empfänger (bspw. Modem oder zweiter Computer) jeweils gekreuzt sein müssen,damit die Daten empfangen werden. Falls zwei Computer miteinander verbunden werden, werden daher sogenannte Nullmodem-Kabel eingesetzt. (Nullmodem, weil kein Modem dazwischen hängt), bei denen die RX und TX Leitungen gekreuzt sind.
Der eingehende Datenstrom wird von einem UART Chip auf dem Arduino in 8 Bit Pakete gebündelt und Byteweise in den Seriellen Datenpuffer (Serial Receive Buffer) abgelegt. Die ankommenden Daten-Bits werden in genau der Reihenfolge in der sie eingehen in den Buffer abgelegt. Der Buffer hat eine maximale Größe von 64 Bytes
Da der Empfänger den Takt des Senders mit jedem empfangenen Byte neu berechnet und sich so automatisch synchronisiert, können Schwankungen des Taktes zwischen Sender und Empfänger ausgeglichen werden. Deshalb wird in diesem Zusammenhang von einer asynchronen Datenübertragung gesprochen.
Im Arduino Uno sind zwei Pins: Pin0 (Rx) und Pin1 (Tx), für die serielle Kommunikation belegt. Diese Pins werden mit 3,3 Volt oder 5 Volt betrieben. Die erste wichtigste Funktion ist Serial.begin(Baud), sie startet die serielle Kommunikation mit der Festlegung der Datenrate für die serielle Datenübertragung. Die Funktion Serial.end() deaktiviert die serielle Kommunikation und gibt die RX- und TX-Pins frei, damit sie wieder als Ausgänge und Eingänge verwendet werden können.
Sobald die serielle Kommunikation aktiviert ist, wird jedesmal, wenn über RX Daten empfangen werden, wird die Funktion serialEvent() aufgerufen. Diese Interrupt-ähnliche Funktion wird nach jedem Durchlauf von loop() aufgerufen. Intern wird mit Serial.availiable() geprüft ob Daten im Puffer liegen. Wenn dies der Fall ist, können diese dann mit Serial.read() gelesen und weiter verarbeitet werden.
Die ankommenden Daten-Bits werden in genau der Reihenfolge in der sie eingehen in den Buffer abgelegt. Der Buffer hat eine maximale Größe von 64 Bytes. Um nun auf die Daten im Buffer zugreifen zu können, stellt die Bibliothek <SoftwareSerial.h> verschiedene Funktionen bereit, diese sind nachfolgend aufgeführt. Die ausführliche Erklärung Ihrer Funktion findet sich auf den Web-Seiten des Arduino.
print()
, aber mit automatischem Zeilenumbruch.readString()
).Beispielsweise erlaubt es die Funktion Serial.read() das erste Zeichen im Buffer auszulesen und es von dort zu entfernen. Um zu erkennen wann ein Datenstrom beendet ist, werden üblicherweise Terminierungssymbole angehängt. Dabei handelt es sich entweder um Carriage Return oder Line Feed Symbole, die es erlauben zu erkennen wenn Eingaben beendet wurden.
Eine weitere Funktion Serial.available() gibt an wieviele Zeichen sich aktuell im Datenpuffer befinden. Die Funktion verändert nicht den Inhalt sondern gibt nur an ob Zeichen vorhanden sind. D.h. wenn eine Zahl >0 als Ergebnis erscheint sind Daten vorhanden.
Ein einfaches Beispiel soll erläutern wie dieser Mechanismus in der Praxis funktioniert. Das Einlesen der Zeichen erfolgt in meinem Beispile nicht in der "loop" sondern in der oben besprochenen Funktion SerialEvent(). In dieser Funktion wird geprüft ob Daten übertragen wurden, dann werden diese in einem Puffer gespeichert und nach dem Erkennen eines Zeilenende-Zeichens zur Prüfung ausgegeben und anschliessend wird die Funktion evalSerialData() aufgerufen. In dieser Funktion wird dann die Auswertung der Daten vorgenommen. Es liegt also eine klare Trennung zwischen dem Empfangen und der Auswertung der Daten vorgenommen.
String input = "";
void setup() {
Serial.begin(9600);
}
void loop() {
// Hier passiert dein Hauptprogramm
}
void serialEvent() {
while (Serial.available()) {
char c = (char)Serial.read();
input += c;
if (c == '\n') {
Serial.println("Empfangen: " + input);
input = "";
}
}
}// end serialEvent
Eine robustere Variante die einerseits nur aufgerufen wird, wenn auch Daten im Puffer stehen und dafür sorgt, dass die Größe des definierten Puffers nicht überschritten wird und auch alle Zeilen-Ende-Zeichen berücksichtigt, sieht wie folgt aus:
#define BUFFER_SIZE 64 // Maximale Länge der Eingabe
char buffer[BUFFER_SIZE]; // Puffer für serielle Eingabe
int bufferCount = 0; // Zähler für empfangene Zeichen
void setup() {
Serial.begin(9600); // Serielle Kommunikation starten
Serial.println("Bereit für Eingabe...");
}
void loop() {
// Hauptprogramm – kann leer bleiben, da serialEvent() automatisch aufgerufen wird
}
// Diese Funktion wird automatisch aufgerufen, wenn serielle Daten verfügbar sind
void serialEvent() {
while (Serial.available()) {
char ch = Serial.read();
// Zeichen nur speichern, wenn Puffer nicht voll ist
if (bufferCount < BUFFER_SIZE - 1) {
buffer[bufferCount++] = ch;
}
// Zeilenende erkannt? Dann auswerten
if (ch == '\n' || ch == '\r') {
buffer[bufferCount] = '\0'; // Nullterminierung für String
Serial.print("Empfangen ("); Serial.print(bufferCount);
Serial.println(" Zeichen):");
Serial.println(buffer);
evalSerialData(); // Eingabe auswerten
bufferCount = 0; // Puffer zurücksetzen
}
}
}
// Beispielhafte Auswertung der seriellen Eingabe
void evalSerialData() {
// Steuerzeichen am Ende entfernen
while (bufferCount > 0 && (buffer[bufferCount - 1] == '\r' || buffer[bufferCount - 1] == '\n')) {
buffer[--bufferCount] = '\0';
}
// Jetzt vergleichen
if (strcmp(buffer, "LED ON") == 0) {
Serial.println("Befehl erkannt: LED einschalten");
} else if (strcmp(buffer, "LED OFF") == 0) {
Serial.println("Befehl erkannt: LED ausschalten");
} else {
Serial.println("Unbekannter Befehl");
}
}
Die Funktion strcmp() ist eine String-Vergleichsfunktion, die zwei Zeichenketten miteinander vergleicht. Sie gehört zur Standardbibliothek < string.h> und ist ein echtes Arbeitstier, wenn es darum geht, herauszufinden, ob zwei Strings gleich sind. Sie bietet die ideale Möglichkeit einen Parser zu bauen, der verschiedene Befehle auswerten kann.
//---------------------------------------------------------
// PHOF PARSER Arduino UNO
// Liest Befehle vom Monitor ein, parst diese
// und führt entsprechende Aktionen aus
// neue Befehle hinzufügen durch weitere strcmp()-Blöcke ergänzen.
//---------------------------------------------------------
void evalSerialData() {
// Steuerzeichen am Ende entfernen
while (bufferCount > 0 && (buffer[bufferCount - 1] == '\r' || buffer[bufferCount - 1] == '\n')) {
buffer[--bufferCount] = '\0';
}
Serial.print("Eingabe erkannt: ");
Serial.println(buffer);
// Zerlege Eingabe in Tokens
char *command = strtok(buffer, " ");
char *arg1 = strtok(NULL, " ");
char *arg2 = strtok(NULL, " ");
if (command == NULL) {
Serial.println("Kein Befehl erkannt.");
return;
}
// LED ON / OFF
if (strcmp(command, "LED") == 0) {
if (arg1 != NULL && strcmp(arg1, "ON") == 0) {
digitalWrite(LED_BUILTIN, HIGH);
Serial.println("LED eingeschaltet.");
} else if (arg1 != NULL && strcmp(arg1, "OFF") == 0) {
digitalWrite(LED_BUILTIN, LOW);
Serial.println("LED ausgeschaltet.");
} else {
Serial.println("Ungültiger LED-Befehl.");
}
}
// SET TEMP "Wert"
else if (strcmp(command, "SET") == 0 && arg1 != NULL && strcmp(arg1, "TEMP") == 0 && arg2 != NULL) {
int temp = atoi(arg2);
Serial.print("Temperatur gesetzt auf: ");
Serial.print(temp);
Serial.println(" °C (simuliert)");
}
// STATUS?
else if (strcmp(command, "STATUS?") == 0) {
Serial.println("Systemstatus: OK. LED ist " + String(digitalRead(LED_BUILTIN) ? "AN" : "AUS"));
}
else {
Serial.println("Unbekannter Befehl.");
}
}
Datenübertragung mit FTDI Adapter: Ein FTDI Adapter ist ein Modul der Firma FTDI. Die Firma Future Technology Devices International wurde unter ihrem Kürzel FTDI für ihre USB-UART-Interface-Chips bekannt, mit denen es möglich ist, eine serielle Schnittstelle vom Typ RS-232 über einen weiteren Pegelwandler-Schaltkreis mit einem Universal Serial Bus (USB) zu verbinden.
Im folgenden Beispiel wird gezeigt, wie man einen solchen Adapter nutzen kann, wenn man gleichzeitig zwei Monitor-Ausgaben bedienen möchte. Im vorliegenden Versuch wurde ein Arduino Mega über die übliche IDE mit dem integrierten Monitor auf einem Mac programmiert. Standardmässig wird dabei der Serial(0) auf dem integrierten Monitor ausgegeben. Serial1 wird über einen FTDI Adapter mit einem Windows PC verbunden. Dafür kommt ein Arduino Mega zum Einsatz.Das einfache Programm gibt dann die Nachrichten auf den beiden Monitor-Programmen aus.
Der Test-Aufbau ist dem nachfolgenden Bild zu entnehmen. Wir haben einen Arduino Mega, bei dem wir die eingebaute Serielle-Schnittstelle" zur Verbindung mit der IDE auf dem MAc verwenden. Parallel dazu nutzen wir Serial1, um über den FTI232 Adapter den Arduino mit dem PC verbinden. Auch hier ist wichtig, dev TX1-Pin vom Arduino mit der Rx-Pin des FTDI Adapters und natürlich den Rx1-Pin des Arduino analog mit dem Tx-Pin des FTDI-Adapters zu verbinden. Dann müssen wir auf der PC Seite ein entsprechendes Terminal Programm starten und dabei die identischen Konfigrations-Parameter wie auf der Arduino Seite verwenden. Die einfachen test-Programme zeigen das Grundprinzip und können natürlich bielig auf die jeweilige Anwendung erweitert werden.
Hier nun ein erstes Test-Progtramm. Der Arduino empfängt die vom PC eingegeben Daten und sendet die empfangenen Daten wieder ins Terminal-Fenster zurück.
void setup() {
Serial.begin(9600);
Serial1.begin(9600); // Kommunikation mit FTDI (PC)
}
void loop() {
if (Serial1.available()) {
String empfangen = Serial1.readStringUntil('\n');
empfangen.trim(); // Entfernt \r und Leerzeichen
// Echo zurück an den PC
Serial1.println("PC hat gesendet: " + empfangen);
Serial.println("Arduino hat empfangen: " + empfangen);
}
}
Eine erweiterete Version die längere Texte erst nach der Eingabe von Return überträgt und das Eregbnis der Kommunikation auf beiden Seiten darstellt:
int bufferCount; // Anzahl der eingelesenen Zeichen
char buffer[80];
int bufferCount1; // Anzahl der eingelesenen Zeichen
char buffer1[80];
void setup() {
Serial.begin(9600);
Serial1.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
Serial.println("Hello via Serial 0 on Mac ---- Test I/O");
Serial1.println("Hello via Serial 1 to PC ---- Test I/O");
}
void loop() {
}
void serialEvent() {
char ch = Serial.read();
if (bufferCount < sizeof(buffer) - 1) {
buffer[bufferCount++] = ch;
}
if (ch == 13) { // Return gedrückt
buffer[bufferCount] = '\0'; // Nullterminierung
Serial.println(buffer);
Serial1.println(buffer);
bufferCount = 0; // Zähler zurücksetzen
}
}
void serialEvent1() {
char ch1 = Serial1.read();
if (bufferCount1 < sizeof(buffer1) - 1) {
buffer1[bufferCount1++] = ch1;
}
if (ch1 == 13) {
buffer1[bufferCount1] = '\0';
Serial1.println(buffer1);
Serial.println(buffer1);
bufferCount1 = 0;
}
}
Nun kann man auf der PC Seite auch ein Python Programm entwicklen, das Daten an den Arduino sendet:
import serial
import time
# Verbindung zum Arduino herstellen
arduino = serial.Serial(port='COM3', baudrate=9600, timeout=1) # COM-Port anpassen!
time.sleep(2) # kurze Pause für Initialisierung
# Nachricht senden
nachricht = "Hallo Arduino\n"
arduino.write(nachricht.encode()) # Senden als Byte-String
# Antwort lesen
antwort = arduino.readline().decode().strip()
print("Antwort vom Arduino:", antwort)
arduino.close()
Das passende Arduino Programm zum Empfangen der Python Daten könnte im wesentlichen so aufgebaut werden:
void setup() {
Serial1.begin(9600); // Kommunikation mit Python über FTDI
}
void loop() {
if (Serial1.available()) {
String empfangen = Serial1.readStringUntil('\n');
empfangen.trim();
Serial1.println("Arduino hat empfangen: " + empfangen);
}
}
Damit müssten die wesentlichen Grundlagen der Seriellen Kommunikation mit den Arduinos vorgestellt sein. Natürlich gibt es noch wesentlich mehr Details rund um das Thema Serielle Kommunikation. Mir ging es aber im hauptsächlich darum, aufzuzeigen, wie einfach und effizient es ist, mit den in der Arduino IDE vorhandenen Funktionen mit dem Arduino und von Arduino zu Arduino zu kommunizieren. Und natürlich findet man erfreulicherweise im Internet viele weiterführende Beiträge.