Das Titelbild dieses Beitrags ist von rawpixel.com – de.freepik.com

Die SoftwareSerial-Klasse verfügt genauso wie die Serial-Klasse über die available()-Methode, die die Anzahl der Zeichen zurückgibt, die bereits empfangen, aber noch nicht gelesen wurden. Es gibt jedoch einen interessanten Unterschied. Ein Aufruf von SoftwareSerial.available() ist deutlich langsamer als ein Aufruf von Serial.available(). Wir werden nach dem tieferen Grund für dieses seltsame Verhalten suchen und ich werde drei Möglichkeiten aufzeigen, um das Problem zu beheben.

EDIT: In der Arduino Version 1.8.17 wurde das Problem beseitigt.

Beobachtung

Während ich an der Implementierung einer neuen seriellen Bibliothek arbeitete, bemerkte ich, dass ein Aufruf von SoftwareSerial.available() viel Zeit in Anspruch nimmt (aus MCU-Sicht). Ich habe die Zeit gemessen und mit der Standardmethode verglichen, indem ich das folgende Programm verwendet habe:

#include <SoftwareSerial.h>

SoftwareSerial SoftSerial(8,9);

void setup()
{
  SoftSerial.begin(19200);
  Serial.begin(19200);
  DDRC = 0x03;
}

void loop() {
  PORTC = 0x01;
  Serial.available();
  PORTC = 0;
  PORTC = 0x02;
  SoftSerial.available();
  PORTC = 0;
  Verspätung(100);
}

Schließt man einen Logikanalysator an die Portpins PC0 und PC1 an, sieht man, dass ein Aufruf der SoftwareSerial-Methode zehnmal langsamer ist als ein Aufruf der Standardmethode!

Laufzeit für den Aufruf von Serial.available() und SoftSerial.available()

Die Ursache

Das führt natürlich zu der Frage, ob dies unvermeidbar ist. Das Gute ist, dass man den gesamten Quellcode inspizieren kann, um herauszufinden, was dahinter steckt. Die Implementierung der Standardmethode findet man in HardwareSerial.cpp im Arduino-Core-Verzeichnis:

int HardwareSerial::available(void)
{
  return ((unsigned int)(SERIAL_RX_BUFFER_SIZE +
                         _rx_buffer_head -
                         _rx_buffer_tail)) % SERIAL_RX_BUFFER_SIZE;
}

Die Implementierung der available()-Methode in der SoftwareSerial-Bibliothek sieht sehr ähnlich aus:

int SoftwareSerial::available()
{
  if (!isListening())
    return 0;

  return (_receive_buffer_tail + _SS_MAX_RX_BUFF -
          _receive_buffer_head) % _SS_MAX_RX_BUFF;
}

Der Hauptunterschied scheint die zusätzliche if-Anweisung mit einem Aufruf der isListening()- Methode zu sein. Allerdings enthält der Hauptteil dieser Methode nur einen einzigen Vergleich und kann deshalb nicht für eine zehnfache Verlangsamung verantwortlich sein. Der verbleibende Unterschied besteht darin, dass in der Standardmethode der Typ in unsigned int umgewandelt wird. Kann das einen so großen Unterschied machen?

Das tut es in der Tat. Die modulo-Operation in der HardwareSerial-Klassen führt zu folgendem Assembler-Code (der linke Operand der modulo-Operation befindet sich bereits in r24:r25 und der rechte Operand ist die Kompilierzeitkonstante 64):

  andi r24, 0x3F ; 63
  eor r25, r25

Dies ist ziemlich clever und funktioniert auf ähnliche Weise für alle Fälle, in denen der linke Operand eine ganze Zahl ohne Vorzeichen und der rechte Operand eine positive Potenz von zwei ist. Im Gegensatz dazu sieht der für die SoftwareSerial-Klasse generierte Code wie folgt aus:

  ldi r22, 0x40 ; 64
  ldi r23, 0x00 ; 0
  call 0xdae an; 0xdae <__divmodhi4>

Auch hier ist der linke Operand bereits im Registerpaar r24:r25. Dann wird die Zahl 64 in das Registerpaar r22:r23 geladen und die Modulo-Unterroutine aufgerufen (beachten Sie, dass die AVRs keine Division-Anweisungen kennen). Der Grund, warum die einfache Maskierungsoperation nicht ausreicht, ist, dass durch die C ++ – Typkonvertierungsregeln alle Summanden in int konvertiert werden, was zu einem Vorzeichen-behafteten Wert führt, bei dem die obige Optimierung nicht das richtige Ergebnis liefern würde, wenn wir eine negative Zahl hätten. Die ganzzahlige Division in Software ist kostspielig und aus diesem Grund sind die 15 μs keine Überraschung. Der Aufruf der allgemeinen ganzzahligen Modulofunktion ist jedoch offensichtlich überhaupt nicht notwendig, da der linke Operand niemals negativ werden kann.

Ein potenzielles Problem

Kann dieses Verhalten zu Problemen führen? Meistens wird man wohl nie merken, dass der Aufruf von SoftwareSerial.available() mehr Zeit in Anspruch nimmt als nötig. Wenn das Timing jedoch eng wird, kann es zu Problemen kommen.

Ein problematisches Szenario könnte wie folgt aussehen. Das Programm empfängt Bytes mit 57600 bps. Dies impliziert, dass ein Bit 17,36 μs benötigt. Die Interrupt-Routine für den Empfang von Bytes wird so geschrieben, dass sie in das Stopbit wartet, bevor sie zurückkehrt, was bedeutet, dass weniger als 17,36 μs pro empfangenem Byte verfügbar sind. Wird aus der Interrupt-Routine zu spät zurückgekehrt, wird das Programm wahrscheinlich nicht in der Lage sein, ein Byte für jedes empfangene Byte zu lesen. Mit anderen Worten, nach einiger Zeit wird der Input-Puffer vermutlich überlaufen. Um dies zu demonstrieren, habe ich das folgende Programm geschrieben:

#include <SoftwareSerial.h>

SoftwareSerial mySerial = SoftwareSerial(8, 9);

boolean available;
unsigned long sum = 0;
  
void setup()
{
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);   
  DDRC |= 0x03;
  mySerial.begin(57600);

  while (!mySerial.overflow()) {
    PORTC = 0x01;
    available = mySerial.available();
    PORTC = 0x00;
    if (available) {
      PORTC = 0x02;
      sum += mySerial.read();
      PORTC = 0x00;
    }
  }
  digitalWrite(LED_BUILTIN, LOW);
}

void loop() { }

Auf der Hostseite generiert ein Python-Skript die zu empfangenden Bytes, die mithilfe eines FT232R-Boards (unter Verwendung des pyserial-Moduls) an den Arduino gesendet werden:

#!/usr/bin/env python3
Seriell importieren

serialport = '/dev/cu.usbserial-1410' # or whatever port you use

while (1):
    ser = serial.Serial(serialport, 57600; stopbits=1)
    while (1):
        ser.write(b'1');

Die Kompilierzeitkonstante _SS_MAX_RX_BUFF wurde auf 8 gesetzt, um schnell einen Fehler zu provozieren. Der Logikanalysator, genauso angeschlossen wie beim oben beschriebenen Experiment, zeigte dann folgendes Bild, nachdem 11 Bytes empfangen wurden:

Pufferüberlauf nach Empfang von 11 Byte

Es ist interessant zu sehen, dass die Ausführungszeiten der available()- und der read()-Methoden aufgrund der Ausführung der Interrupt-Routinen gestreckt werden. Auf jeden Fall ist es leicht zu erkennen, dass wir nach dem Empfang von 11 Bytes einen Pufferüberlauf haben, da bisher nur 3 Bytes gelesen wurden.

Wenn wir die Kommunikationsgeschwindigkeit auf 38400 bps reduzieren würden, haben wir die gleichen Probleme ein paar Bytes später. Mit 19200 bps tritt in unserem Setup kein Pufferüberlauf mehr auf. Ebenso, wenn wir 1,5 Stopbits verwenden, gibt es auch für 57600 bps kein Problem.

Drei Möglichkeiten, das Problem zu beheben

Ich habe einen Pull Request zur Behebung des Problems eingereicht (18. Oktober 2021). Hoffentlich wird dieses Problem bald verschwinden. Bis gibt es drei Möglichkeiten, das Problem zu beheben.

  1. Anstatt zu prüfen, ob neue Eingaben verfügbar sind, kann man direkt die read()-Methode aufrufen (was Sie sowieso tun würden). Wenn der Rückgabewert -1 ist, weiß man, dass kein neues Byte gesendet wurde. Zu beachten ist, dass der Rückgabewert der read()-Methode eine ganze Zahl (ein Zwei-Byte-Wert mit Vorzeichen) ist!
  2. Man kann die available()-Methode in der SoftwareSerial-Klasse direkt ändern (vorausgesetzt, man findet den Ort in der Installation, an dem die SoftwareSerial-Bibliothek gespeichert ist).
  3. Man kann eine neue MySoftwareSerial-Klasse definieren (möglicherweise in einer neuen Bibliothek), die alles von der SoftwareSerial-Klasse erbt, aber die available()– Methode überschreibt.

Mit jedem der vorgeschlagenen Fixes läuft das obige Programm bei 57600 bps. Mit 115200 bps stoßen wir dennoch sehr bald auf einen Pufferüberlauf.

Zusammenfassung

Die available()-Methode der SoftwareSerial-Bibliothek benötigt viel mehr Zeit als nötig, was bei der Kommunikation mit 38400 bps oder schneller zu Pufferüberlaufproblemen führen kann. Anscheinend ist das bisher niemandem aufgefallen, seit die Bibliothek vor ein paar Jahren als Standardbibliothek adaptiert wurde. Hoffentlich wird das Problem bald gelöst, indem mein Pull Request integriert wird. Bis dahin kann man eine der drei oben vorgeschlagenen Fixes verwenden.

EDIT: Der Pull Request wurde akzeptiert, daher sollte das Problem in Arduino Version 1.8.17 und höher nicht mehr vorhanden sein.