3.6 UART / RS232 (🚧)

Synopsis: [🔗 de.wikipedia.org/wiki/Universal_Asynchronous_Receiver_Transmitter] [🔗 https://reference.arduino.cc/reference/en/language/functions/communication/serial/]
Nochmal anschauen: 2.6 Schieberegister

Ist die älteste und schwierigste einfache Serielle Schnittstelle. Die Datenübertragung erfolgt asynchron (eine Baud-Rate muss eingestellt werden) und hat Start- und Stopp-Bits ausserdem kann ein Paritätsbit übertragen werden.
Es ist die elementare serielle Schnittstelle für Arduino, darüber werden viele der Boards programmiert und darüber wird mit dem Seriellen Monitor kommuniziert.

Datenübertragung analysieren

Nun wollte ich mit dem Oszilloskop die Kommunikation mitschneiden und steckte den Tastkopf in D1/TX auf dem L152RE-Board und fand nur Rauschen.
In der Doku fand ich raus, dass die Pins D1/PA2/TX und D0/PA3/RX einfach abgeklemmt sind und sich erst mit Lötbrücken wieder anklemmen lassen: [Doku UM1724] Seite 25 6.8 USART Communication. Auf der ST-LINK-Platine können die Signale aber abgegriffen werden, zur Kommunikation wird USART2 verwendet.

void setup() {
  Serial.begin(9600); // USART2 PA2, PA3 mit ST-Link verbunden
}

void loop() {
  Serial.println("ab");
  delay(1000);
}

Übertragung mit Logic-Analyzer beobachten

Logic Analyzer verbinden
Logic Analyzer verbinden
Logic2 einstellen
Logic2 einstellen
Oszillogramm für Datenübertragung

Aufgaben

  1. Zeichnen Sie ein Blockdiagramm mit 2 Kommunikationsteilnehmern die mittels UARTs verbunden sind
  2. Erklären Sie die serielle Kommunikation
  3. Parity
    1. Ergänzen Sie jeweils zu gerader Parity (even)
      1. 01010101___
      2. 11001000___
      3. 00000001___
    2. Ergänzen Sie jeweils zu Ungerader Parity (odd)
      1. 01101010___
      2. 11100011___
      3. 00000000___
    3. Richtig oder fehlerhaft
      1. even 101011011
      2. odd 01011101
      3. even 111001111
    4. Erläutern sie, wie die Erkennung von Fehlern bei der Datenübertragung funktioniert
  4. Wie ist mit UARTs eine Vollduplex-Datenübertragung möglich?
  5. Wie kann bei der Datenübertragung mit UART auf eine Taktleitung verzichtet werden?
  6. Was versteht man unter einer asynchronen Datenübertragung?
  7. Welche Bytes wurden oben übertragen, verwenden Sie die ASCII-Tabelle
Lösung
  1. Siehe Forsa S. 17
  2. Die Datenbits werden seriell, d.h. nacheinander übertragen. Damit das funktioniert müssen Sender und Empfänger mit der gleichen Geschwindigkeit arbeiten.
  3. Parity
    1. Ergänzen Sie jeweils zu gerader Parity (even)
      1. 01010101__0_
      2. 11001000__1_
      3. 00000001__1_
    2. Ergänzen Sie jeweils zu Ungerader Parity (odd)
      1. 01101010__1_
      2. 11100011__0_
      3. 00000000__1_
    3. Richtig oder fehler
      1. even 101011011 ✅
      2. odd 01011101 ✅
      3. even 111001111 ❌
    4. Wenn bei der Übertragung ein Bit umkippt kann dies durch die Parität erkannt werden.
  4. Für Senden und Empfang ist jeweils eine Leitung vorhanden.
  5. Sender und Empfänger haben einen Taktgenerator, durch das Startbit kann der Empfänger sich auf den Sender synchronisieren.
  6. Sender und Empfänger arbeiten nicht mit dem selben Takt sondern der Empfänger hat eine eigene Taktbasis.
  7. ‚a‘,’b‘,CR (Carriage Return) und LF (Line Feet)

Vom Seriellen-Monitor Zeichen einlesen

Synopsis: [🔗 reference.arduino.cc/reference/en/language/functions/communication/serial/available/] [🔗 reference.arduino.cc/reference/en/language/functions/communication/stream/]

Die serielle Schnittstelle, mit der der µC programmiert wird kann auch zum Übertragen von Daten, Zeichen vom und zum µC verwendet werden. Dazu muss in setup() die Schnittstelle mit der gewünschten Baud-Rate (Übertragungsgeschwindigkeit) initialisiert werden.
Beim Empfangen von Zeichen (Bytes) werden in einen Puffer (64 Bytes) gespeichert. Die Operation Serial.available() gibt die Anzahl der empfangen Bytes zurück.
Mit der Operation Serial.read() kann ein Zeichen aus dem Puffer gelesen werden, das Zeichen wird dabei aus dem Puffer entfernt und die Anzahl der Zeichen im Puffer um 1 erniedrigt. Beispielcode:

Serial.available() gibt Anzahl der Bytes im Empfangspuffer zurück.
Serial.read() gibt ein Zeichen aus dem Empfangspuffer zurück und erniedrigt die Anzahl um eins.

void setup(){
  Serial.begin(9600); // Serielle Schnittstelle mit 9600 Baud starten
}
void loop(){
  char zeichen;
  if(Serial.available()){ // wenn Zeichen gesendet wurden
    zeichen = Serial.read(); // lese Zeichen
    Serial.printf("Zeichen: %2c Hex: %#x\n",zeichen,zeichen); // gib Zeichen und Code aus
  }
}

Die eingegebenen Zeichen werden aus der Eingabezeile des seriellen Monitors erst nach einem „Return“ zum µC gesendet, es kann eingestellt werden, ob am Ende noch weitere Bytes, Symbole gesendet werden sollen:

  • Kein Zeilenende, es wird kein weiters Byte mehr gesendet.
  • Neue Zeile, es wird das Symbol New Line, (NL, „\n“, 0xa, bzw. Line Feed, LF) gesendet, das eine neue Zeile bewirken soll, denke an eine Schreibmaschine bei der in eine neue Zeile gegangen wird. Ist der Normalfall.
  • Zeilenumbruch, es wird das Symbol Carriage Return (CR, 0xd) gesendet, das einen Wagenrücklauf bewirken soll, also wieder zum Anfang der Zeile.
  • Sowohl NL als auch CR, es werden beide Symbole gesendet.

Mehrere Zeichen in einen String einlesen

Es gibt auch Operationen, mit denen nicht nur einzelne Zeichen sondern das Empfangene auch gleich z.B. in einen String eingelesen wird.

void setup(){
  Serial.begin(9600); // Serielle Schnittstelle mit 9600 Baud starten
}
void loop(){
  String s;
  if(Serial.available()){ // wenn Zeichen gesendet wurden
    s = Serial.readString(); // lese Daten in String ein
    Serial.print(s); // gib String aus
  }
}

Beim Testen des Codes fällt auf, dass es 1 Sekunde dauert bis im Seriellen Monitor die Eingabe erscheint. Die readString()-Operation hat erst fertig wenn nach einem Timeout von 1 Sekunde kein weiteres Zeichen mehr empfangen wird, siehe [Stream.readString() 🔗].
Mit readStringUntil(‚\n‘) kann das Einlesen z.B. mit dem Terminal-Symbol ‚\n‘, New Line gestoppt werden, die Eingabe erscheint sofort, siehe [Stream.readStringUntil() 🔗].

Aufgabe: Von Seriell-Monitor einlesen und Töne ausgeben

Die Tasten a..l auf der Tastatur sollen die Töne ab c‘ ausgeben, dieser Code ist vorgegeben:

void setup(){
  Serial.begin(9600); // Serielle Schnittstelle mit 9600 Baud starten
  analogWriteResolution(16); // 16 Bit PWM-Auflösung
}
unsigned char buchstabe[]={'a','s','d','f','g','h','j','k','l'};
unsigned int  frequenz[]={262,277,293,311,330,349,370,392,415,440,466,494}; // Frequenzen
void loop(){
  String s;
  if(Serial.available()){ // wenn Zeichen gesendet wurden
    s = Serial.readStringUntil('\n'); // lese Daten in String ein
    Serial.print(s); // gib String aus
    for (int i=0; _____ ;i++){ // String durchgehen
      //Serial.println(s[i]);
      for (int k=0; _____ ;k++){ // finde Buchstaben sizeof(buchstabe) gibt die Länge des Arrays zurück
        if(  ____    ){           // wenn Buchstabe passt
          analogWriteFrequency(frequenz[k]);
          analogWrite(D11,32000); // PWM mit ca. 50%
          delay(200); // Ton halten
          analogWrite(D11,0); //Ton aus
          delay(20); // bisschen Abstand
          ______     // etwas effizienter
        }
      }
    }
  }
}
Lösungsvorschlag
void setup(){
  Serial.begin(9600); // Serielle Schnittstelle mit 9600 Baud starten
  analogWriteResolution(16); // 16 Bit PWM-Auflösung
}
unsigned char buchstabe[]={'a','s','d','f','g','h','j','k','l'};
unsigned int  frequenz[]={262,277,293,311,330,349,370,392,415,440,466,494}; // Frequenzen
void loop(){
  String s;
  if(Serial.available()){ // wenn Zeichen gesendet wurden
    s = Serial.readStringUntil('\n'); // lese Daten in String ein
    Serial.print(s); // gib String aus
    for (int i=0;i<s.length();i++){
      //Serial.println(s[i]);
      for (int k=0;k<sizeof(buchstabe);k++){ // finde Buchstaben sizeof(buchstabe) gibt die Länge des Arrays zurück
        if(s[i]==buchstabe[k]){              // wenn Buchstabe passt
          analogWriteFrequency(frequenz[k]); // stelle PWM-Frequenz ein
          analogWrite(D11,32000);            // PWM mit ca. 50%
          delay(200); // Ton halten
          analogWrite(D11,0); //Ton aus
          delay(20); // bisschen Abstand
          break;     // etwas effizienter
        }
      }
    }
  }
}

Verbindung zu weiterer Seriellen Schnittstelle

Die Standart-UART-Schnittstelle dient der Programmierung und Kommunikation mit dem seriellen Monitor. Wenn weitere UART-Module (z.B. Bluetooth) angeschlossen werden sollen wird eine zusätzliche Schnittstelle benötigt. Der µC hat dafür bereits die Hardware, die an bestimmten Ports betrieben werden kann siehe mezmedia.de/etc/hardware/stm32-nucleo-l152re/#Pinout. Beispiel zum Anschluss eines BT-Moduls am Sturm-Board:

HardwareSerial

Synopsis: [https://github.com/stm32duino/Arduino_Core_STM32/wiki/API#hardwareserial]

#define BTRX PB11 // USART3-RX
#define BTTX PB10 // USART3-TX
HardwareSerial SerialBT(BTRX,BTTX); //https://github.com/stm32duino/Arduino_Core_STM32/wiki/API#hardwareserial

SoftwareSerial

Synopsis: [SoftwareSerial Library]
Falls an den gewünschten Pins keine Hardware helfen kann, gibt es auch die Möglichkeit per Software die Schnittstelle an beliebigen Pins zu simulieren, Beispielcode:

#include <SoftwareSerial.h> // Wir verwenden Software Serial
#define BTRX PB11
#define BTTX PB10
SoftwareSerial SerialBT(BTRX, BTTX);

Ich will schnell 50 Werte nach Processing übertragen und dort auswerten!

Synopsis: https://processing.org/reference/libraries/serial/Serial.html🔗
Beim Versuch viele Messwerte über die serielle Schnittstelle nach Processing zu senden bin ich über ein Problem gestolpert.

Arduino Software zum Senden

#define ZAHLEN 500
#define P_USER PC13
void setup(){
  pinMode(P_USER,INPUT);
  Serial.begin(9600);
}

void loop(){
  unsigned char byte_0;  // left Byte;
  unsigned char byte_1;  // right Byte;
  if(!digitalRead(P_USER)){ // Übertragung starten
    for(int i=0;i<ZAHLEN;i+=10){ // lesen wie der Teufel und speichern
      byte_0 = i & 0x00FF; // left Byte; // Übertragung nach Processing Test
      byte_1 = (i & 0xFF00) >> 8;  // right Byte;
      Serial.write(byte_1);
      Serial.write(byte_0);
    }
    delay(500);
  }
}

Processing Software zum Empfangen

import processing.serial.*;   //verwende die 'Serial' Library
 
Serial myPort;   // Deklariere ein Object vom Typ 'Serial', mit dem Namen 'myPort'
//globale Variablen
int x = 0;
//stelle eine Verbindung zum Arduino/Mbed Controller her.
void setup() {
  size(600,600); // Zeichenfläche
  background(#FFFFFF); // Weisser Hintergrund
  ellipseMode(CENTER); // Tentrum der Ellipse als Referenzpunkt
  // Setup der Kommunikation über UART
  // Um den Namen der Schnittstelle herauszufinden, lassen wir uns von der Serial-Klasse zunächst
  // eine Liste der Namen der am PC vorhandenen Ports geben ('Serial.list()')
  for(int i=0;i<Serial.list().length;i++){ // alle Ports ausgeben
     println(Serial.list()[i]);
  }
  // Bei den meisten PCs ist der STM32/Arduino der letzte Port in der Liste. Wir versuchen den letzen in der Liste
  String portName = Serial.list()[Serial.list().length-1]; // finde den Namen des letzten Serialports heraus.
  int baudrate=9600;       // Diese Baudrate muss mit der in eurem MBed/Arduino-Programm übereinstimmen.
  // portName = "COM1";    // Feslegung auf Port COM1 sonst wird die erste Schnittstelle in der Liste verwendet.
  myPort = new Serial(this, portName, baudrate); // Erzeuge ein Serial Objekt und stelle eine Verbindung her
}
 
void draw() { // draw wird regelmäßig aufgerufen: Lese Daten und gib sie in Processing aus
  int  int_16bit = 0;
  while (myPort.available() > 0) { // wiederhole das folgende, solange (while) es neue Daten (myPort.available() > 0)gibt.
    char hByte = myPort.readChar(); // lese Byte aus Puffer
    char lByte = myPort.readChar();
    int_16bit = (hByte<<8) + lByte;
    if(int_16bit>4095){ // Übertragungsfehler entdeckt
      println("Fehler 0x"+hex(int_16bit,4)+" "+int_16bit+" ");
      break;
    }
    stroke(1);
    fill(#EA0707);
    ellipse(x,500-int_16bit/10,5,5);
    x += 5;
    if(x==width) {
      x=0;
      background(#FFFFFF);
    }
  }
}
Fehlerhafte Daten

Beim Empfang der Daten kam es immer wieder zu Aussetzern, es wurden Bytes verschluckt, die Ausgabe war fehlerhaft.

Irgendwann bin ich drauf gekommen, dass die Ursache an dem unregelmäßigen Aufruf von draw() liegt, bzw. dass Processing die gesendeten Daten nicht in seinen Puffer legt. Zwei Lösungen bieten sich dafür an:

  1. https://processing.org/reference/libraries/serial/Serial_bufferUntil_.html
  2. https://processing.org/reference/libraries/serial/Serial_buffer_.html

Weil alle Binärwerte potentiell gesendet werden können ist kein „Terminalsymbol“ für bufferUntil möglich.
Deshalb verwende ich einfach die Anzahl der gesendeten Bytes als Trigger für das Event.

Bislang wurden bei meinen Versuchen keine Fehler mehr angezeigt.
Allerdings würde ich mir schon ein sicheres Verfahren wünschen Binär-Daten, Objekte vom µC zu Processing zu übertragen, bei dem mit potentiellen Übertragungsfehlern sinnvoll umgegangen werden kann. Denke an Bluetooth..

import processing.serial.*;   //verwende die 'Serial' Library
 
Serial myPort;   // Deklariere ein Object vom Typ 'Serial', mit dem Namen 'myPort'
//globale Variablen
int x = 0;
int[] messwerte= new int[50]; // Platz für 50 Messwerte
boolean neueMessung=false;    // Flag für neue Messwerte angekommen
//stelle eine Verbindung zum Arduino/Mbed Controller her.
void setup() {
  size(600,600); // Zeichenfläche
  background(#FFFFFF); // Weisser Hintergrund
  ellipseMode(CENTER); // Tentrum der Ellipse als Referenzpunkt
  // Setup der Kommunikation über UART
  // Um den Namen der Schnittstelle herauszufinden, lassen wir uns von der Serial-Klasse zunächst
  // eine Liste der Namen der am PC vorhandenen Ports geben ('Serial.list()')
  for(int i=0;i<Serial.list().length;i++){ // alle Ports ausgeben
     println(Serial.list()[i]);
  }
  // Bei den meisten PCs ist der STM32/Arduino der letzte Port in der Liste. Wir versuchen den letzen in der Liste
  String portName = Serial.list()[Serial.list().length-1]; // finde den Namen des letzten Serialports heraus.
  int baudrate=9600;       // Diese Baudrate muss mit der in eurem MBed/Arduino-Programm übereinstimmen.
  // portName = "COM1";    // Feslegung auf Port COM1 sonst wird die erste Schnittstelle in der Liste verwendet.
  myPort = new Serial(this, portName, baudrate); // Erzeuge ein Serial Objekt und stelle eine Verbindung her
  myPort.buffer(100); // 50 Messwerte lesen bis Event ausgelöst wird
}
 
void draw() { // draw wird regelmäßig aufgerufen: Lese Daten und gib sie in Processing aus
  int  int_16bit = 0;
  if (neueMessung) { // wiederhole das folgende, solange (while) es neue Daten (myPort.available() > 0)gibt.
    for (int i=0;i<50;i++){
      stroke(1);
      fill(#EA0707);
      ellipse(x,500-messwerte[i]/10,5,5);
      x += 5;
      if(x==width) {
        x=0;
        background(#FFFFFF);
      }
    }
    neueMessung=false;
  }
}

void serialEvent(Serial myPort) { // Event für serielle Daten
  int i = 0;
  while (myPort.available() > 0){
    messwerte[i]= (myPort.readChar()<<8) + myPort.readChar(); // Bytes aus Puffer lesen
    //println(messwerte[i]);
    i++;
  }
  if(i<50){
    println("Fehler zu wenige Bytes: "+i);
  }
  else neueMessung = true;
}

😩 Übertragungsfehler mit Paritätsbit bei Arduino und Processing erkennen?

Eine Suche mit Perplexity ergab diese Aussagen:

  • „Bei Arduino gibt es keine direkte Hardware-Unterstützung, um Paritätsfehler zu erkennen, da die Standard-Serial-Bibliothek diese Funktionalität nicht bietet.“
  • „In Processing können Paritätsfehler bei der seriellen Kommunikation nicht direkt erkannt werden, da die Serial-Bibliothek von Processing keine eingebaute Unterstützung für Paritätsprüfung bietet.“

Das war wohl nix mit Parität in Hardware 😔. Es wird immer eine selbst gebaute Softwarelösung vor geschlagen…

ToDo: Lösung mit sinnvollem Protokoll bei fehlerhaften Übertragungen…

Exit-Room-Telefon

Synopsis: [mezdata.de/mez-entwicklung/090_exitroom-telefon/index.php] [wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR0299]

Info: Kommando 0x12 spielt aus MP3-Ordner ab…