Best Practices und Muster des Kommunikationsprotokolls


19

Jedes Mal, wenn ich ein serielles Protokoll entwerfe, das zwischen zwei Arduinos verwendet werden soll, habe ich das Gefühl, ein Rad neu zu erfinden. Ich frage mich, ob es Best Practices oder Muster gibt, denen die Leute folgen. Diese Frage bezieht sich weniger auf den eigentlichen Code als vielmehr auf das Format der Nachrichten.

Wenn ich zum Beispiel einem Arduino sagen wollte, dass es dreimal seine erste LED blinken soll, könnte ich Folgendes senden:

^L1,F3\n
  • '^': startet einen neuen Befehl
  • 'L': Definiert den Befehl. (L: Richtet diesen Befehl auf eine LED aus.)
  • '1': Ziel die erste LED
  • ',': Kommandozeilen-Trennzeichen, neuer Wert in dieser Nachricht folgt
  • 'F': Flash-Unterbefehl
  • '3': 3 mal (LED dreimal blinken)
  • '\ n': Beendet den Befehl

Gedanken? Wie nähern Sie sich normalerweise dem Schreiben eines neuen seriellen Protokolls? Was ist, wenn ich eine Anfrage von Arduino 1 an Arduino 2 senden und dann eine Antwort erhalten wollte?

Antworten:


13

Es gibt viele Möglichkeiten, ein serielles Protokoll zu schreiben, je nachdem, welche Funktionalität Sie wünschen und wie viel Fehlerprüfung Sie benötigen.

Einige der häufigsten Dinge, die Sie in Punkt-zu-Punkt-Protokollen sehen, sind:

Ende der Nachricht

Die einfachsten ASCII-Protokolle haben nur ein Ende der Nachrichtenzeichenfolge, häufig \roder \nweil dies gedruckt wird, wenn die Eingabetaste gedrückt wird. Bei binären Protokollen wird möglicherweise 0x03ein anderes gemeinsames Byte verwendet.

Beginn der Nachricht

Das Problem, nur das Ende der Nachricht zu haben, ist, dass Sie nicht wissen, welche anderen Bytes bereits empfangen wurden, wenn Sie Ihre Nachricht senden. Diese Bytes würden dann der Nachricht vorangestellt und dazu führen, dass sie falsch interpretiert wird. Wenn der Arduino beispielsweise gerade aus dem Ruhezustand erwacht ist, befindet sich möglicherweise etwas Müll im seriellen Puffer. Um dies zu umgehen, haben Sie einen Start der Meldungssequenz. In Ihrem Beispiel ^häufig in binären Protokollen0x02

Fehlerüberprüfung

Wenn die Nachricht beschädigt werden kann, müssen wir einige Fehler überprüfen. Dies kann eine Prüfsumme oder ein CRC-Fehler oder etwas anderes sein.

Escape-Charaktere

Möglicherweise wird durch die Prüfsumme ein Steuerzeichen hinzugefügt, z. B. das Byte "Beginn der Nachricht" oder "Ende der Nachricht", oder die Nachricht enthält einen Wert, der einem Steuerzeichen entspricht. Die Lösung besteht darin, ein Escape-Zeichen einzuführen. Das Escape-Zeichen wird vor einem geänderten Steuerzeichen platziert, sodass das tatsächliche Steuerzeichen nicht vorhanden ist. Wenn beispielsweise ein Startzeichen 0x02 ist, können wir mit dem Escape-Zeichen 0x10 den Wert 0x02 in der Nachricht als Bytepaar 0x10 0x12 (Byte-XOR-Steuerzeichen) senden.

Paketnummer

Wenn eine Nachricht beschädigt ist, können wir ein erneutes Senden mit einer Nack- oder Retry-Nachricht anfordern. Wenn jedoch mehrere Nachrichten gesendet wurden, kann nur die letzte Nachricht erneut gesendet werden. Stattdessen kann dem Paket eine Nummer zugewiesen werden, die sich nach einer bestimmten Anzahl von Nachrichten überschlägt. Wenn diese Nummer beispielsweise 16 ist, kann das sendende Gerät die letzten 16 gesendeten Nachrichten speichern. Wenn eine Nachricht beschädigt ist, kann das empfangende Gerät unter Verwendung der Paketnummer ein erneutes Senden anfordern.

Länge

In binären Protokollen wird häufig ein Längenbyte angezeigt, das dem empfangenden Gerät angibt, wie viele Zeichen in der Nachricht enthalten sind. Dies fügt eine weitere Ebene der Fehlerprüfung hinzu, als ob die richtige Anzahl von Bytes nicht empfangen worden wäre, als ob ein Fehler aufgetreten wäre.

Arduino-spezifisch

Bei der Ausarbeitung eines Protokolls für Arduino ist die erste Überlegung, wie zuverlässig der Kommunikationskanal ist. Wenn Sie über die meisten Funkmedien, XBee, WiFi usw. senden, sind bereits Fehlerüberprüfungen und Wiederholungsversuche eingebaut, und es macht keinen Sinn, diese in Ihr Protokoll aufzunehmen. Wenn Sie einige Kilometer über RS422 senden, ist dies erforderlich. Die Dinge, die ich einschließen würde, sind der Beginn der Nachricht und das Ende der Nachricht Zeichen, wie Sie haben. Meine typische Implementierung sieht ungefähr so ​​aus:

>messageType,data1,data2,…,dataN\n

Die Begrenzung der Datenteile durch ein Komma ermöglicht ein einfaches Parsen, und die Nachricht wird unter Verwendung von ASCII gesendet. ASCII-Protokolle eignen sich hervorragend, da Sie Nachrichten in den seriellen Monitor eingeben können.

Wenn Sie ein binäres Protokoll wünschen, um möglicherweise die Nachrichtengrößen zu verkürzen, müssen Sie ein Escape-Verfahren implementieren, wenn ein Datenbyte mit einem Steuerbyte identisch sein kann. Binäre Steuerzeichen sind besser für Systeme, bei denen das gesamte Spektrum der Fehlerüberprüfung und -wiederholung gewünscht wird. Falls gewünscht, kann die Nutzlast weiterhin ASCII sein.


Ist es nicht möglich, dass der Müll vor dem tatsächlichen Beginn des Nachrichtencodes einen Code für den Beginn der Nachrichtensteuerung enthält? Wie würden Sie damit umgehen?
CMCDragonkai

@CMCDragonkai Ja, dies ist eine Möglichkeit, insbesondere für Einzelbyte-Steuercodes. Wenn Sie jedoch während des Parsens einer Nachricht auf einen Startsteuercode stoßen, wird die Nachricht verworfen und das Parsing neu gestartet.
geometrikal

9

Ich habe keine formellen Kenntnisse über serielle Protokolle, aber ich habe sie einige Male benutzt und mich mehr oder weniger mit diesem Schema abgefunden:

(Paket-Header) (ID-Byte) (Daten) (fletcher16-Prüfsumme) (Paket-Fußzeile)

Ich mache normalerweise die Header 2 Bytes und die Footer 1 Bytes. Mein Parser gibt alles aus, wenn ein neuer Paket-Header angezeigt wird, und versucht, die Nachricht zu analysieren, wenn eine Fußzeile angezeigt wird. Wenn die Prüfsumme fehlschlägt, wird die Nachricht nicht verworfen, sondern es wird so lange hinzugefügt, bis das Fußzeilenzeichen gefunden wurde und eine Prüfsumme erfolgreich ist. Auf diese Weise muss die Fußzeile nur ein Byte lang sein, da Kollisionen die Nachricht nicht stören.

Die ID ist willkürlich, manchmal ist die Länge des Datenabschnitts das untere Halbbyte (4 Bits). Ein zweites Längenbit könnte verwendet werden, aber ich kümmere mich normalerweise nicht darum, da die Länge nicht bekannt sein muss, um eine korrekte Analyse durchzuführen. Das Anzeigen der richtigen Länge für eine bestimmte ID ist also nur eine Bestätigung, dass die Nachricht korrekt war.

Die fletcher16-Prüfsumme ist eine 2-Byte-Prüfsumme mit nahezu der gleichen Qualität wie CRC, ist jedoch viel einfacher zu implementieren. Einige Details hier . Der Code kann so einfach sein:

for(int i=0; i < bufSize; i++ ){
   sum1 = (sum1 + buffer[i]) % 255;
   sum2 = (sum2 + sum1) % 255;
}
uint16_t checksum = (((uint16_t)sum1)<<8) | sum2;

Ich habe auch ein Anruf- und Antwortsystem für kritische Nachrichten verwendet, bei dem der PC etwa alle 500 ms eine Nachricht sendet, bis er eine OK-Nachricht mit einer Prüfsumme der gesamten ursprünglichen Nachricht als Daten (einschließlich der ursprünglichen Prüfsumme) erhält.

Dieses Schema ist natürlich nicht gut geeignet, um in ein Terminal wie in Ihrem Beispiel eingegeben zu werden. Ihr Protokoll scheint ziemlich gut für die Beschränkung auf ASCII zu sein, und ich bin sicher, es ist einfacher für ein schnelles Projekt, bei dem Sie in der Lage sein möchten, Nachrichten direkt zu lesen und zu senden. Für größere Projekte ist es schön, die Dichte eines Binärprotokolls und die Sicherheit einer Prüfsumme zu haben.


Da "[Ihr] Parser alles ausgeben wird, wenn ein neuer Paket-Header angezeigt wird", frage ich mich, ob dies keine Probleme verursacht, wenn der Header zufällig in Daten auftritt.
humanityANDpeace

@humanityANDpeace Der Grund für das Verwerfen ist, dass ein abgeschnittenes Paket niemals korrekt analysiert wird. Wann entscheiden Sie also über den Müll und fahren fort? Die einfachste und meiner Erfahrung nach gute Lösung ist das Verwerfen eines fehlerhaften Pakets, sobald der nächste Header eingeht. Ich habe ohne Probleme einen 16-Bit-Header verwendet, aber Sie könnten ihn verlängern, wenn die Gewissheit wichtiger ist Bandbreite.
BrettAM

Was Sie als Header bezeichnen, ist also eine Art magische 16-Bit-Kombination. dh 010101001 10101010, richtig? Ich bin damit einverstanden, dass es nur eine Änderung von 1/256 * 256 ist, aber es deaktiviert auch die Verwendung dieser 16-Bit-Datei in Ihren Daten. Andernfalls wird sie als Header falsch interpretiert und Sie verwerfen die Nachricht, richtig?
humanityANDpeace

@humanityANDpeace Ich weiß, es ist ein Jahr später, aber Sie müssen eine Escape-Sequenz einführen. Vor dem Senden prüft der Server die Nutzdaten auf spezielle Bytes und maskiert sie dann mit einem anderen speziellen Byte. Clientseitig müssen Sie die ursprüngliche Nutzlast wieder zusammensetzen. Dies bedeutet, dass Sie keine Pakete mit fester Länge senden können und erschwert die Implementierung. Es stehen viele serielle Protokollstandards zur Auswahl, die sich alle mit diesem Problem befassen. Hier ist eine sehr gute Lektüre zum Thema .
RubberDuck

1

Wenn Sie sich für Standards interessieren, werfen Sie einen Blick auf die ASN.1 / BER-TLV-Codierung. ASN.1 ist eine Sprache zur Beschreibung von Datenstrukturen, die speziell für die Kommunikation entwickelt wurden. BER ist eine TLV-Methode zum Codieren der Daten, die mit ASN.1 strukturiert wurden. Das Problem ist, dass die ASN.1-Codierung bestenfalls schwierig sein kann. Einen vollwertigen ASN.1-Compiler zu erstellen ist ein Projekt für sich (und ein besonders kniffliges, denke Monate ).


Es ist wahrscheinlich besser, nur die TLV-Struktur beizubehalten. TLV besteht im Wesentlichen aus drei Elementen: einem Tag, einer Länge und einem Wertefeld. Das Tag definiert den Typ der Daten (Textzeichenfolge, Oktettzeichenfolge, Ganzzahl usw.) und die Länge die Länge des Werts .

In BER gibt das T auch an, ob der Wert eine Menge von TLV-Strukturen selbst (ein konstruierter Knoten) oder direkt ein Wert (ein primitiver Knoten) ist. Auf diese Weise können Sie einen Baum in Binärform erstellen, ähnlich wie bei XML (jedoch ohne den XML-Overhead).

Beispiel:

TT LL VV
02 01 FF

ist eine Ganzzahl (Tag 02) mit einer Länge von 1 (Länge 01) und -1 (Wert FF). In ASN.1 / BER sind die Ganzzahlen vorzeichenbehaftete Big-Endian-Zahlen, aber Sie können natürlich auch Ihr eigenes Format verwenden.

TT LL (TT LL VV, TT LL VV VV)
30 07  02 01 FF  02 02 00 FF

ist eine Sequenz (eine Liste) mit der Länge 7, die zwei Ganzzahlen enthält, eine mit dem Wert -1 und eine mit dem Wert 255. Die beiden Ganzzahlcodierungen bilden zusammen den Wert der Sequenz.

Sie können dies auch einfach in einen Online-Decoder werfen, nicht wahr?


Sie können in BER auch eine unbestimmte Länge verwenden, um Daten zu streamen. In diesem Fall müssen Sie Ihren Baum jedoch korrekt analysieren. Ich würde es für ein fortgeschrittenes Thema halten, Sie müssen zum Beispiel wissen, wie man zuerst die Breite und zuerst die Tiefe analysiert.


Wenn Sie ein TLV-Schema verwenden, können Sie sich grundsätzlich jede Art von Datenstruktur vorstellen und diese codieren. ASN.1 geht noch viel weiter und bietet Ihnen eindeutige Bezeichner (OIDs), Auswahlmöglichkeiten (ähnlich wie C-Gewerkschaften), Einschlüsse anderer ASN.1-Strukturen usw. usw., aber das kann für Ihr Projekt zu viel bedeuten. Die wahrscheinlich bekanntesten ASN.1-definierten Strukturen von heute sind die von Ihrem Browser verwendeten Zertifikate.


0

Wenn nicht, haben Sie die Grundlagen abgedeckt. Ihre Befehle können sowohl von Menschen als auch von Maschinen erstellt und gelesen werden, was ein großes Plus ist. Sie können eine Prüfsumme hinzufügen, um einen fehlerhaften oder während der Übertragung beschädigten Befehl zu erkennen, insbesondere wenn Ihr Kanal ein langes Kabel oder eine Funkverbindung enthält.

Wenn Sie industrielle Stärke benötigen (Ihr Gerät darf niemanden verletzen oder zum Erliegen bringen; Sie benötigen hohe Datenraten, Fehlerbehebung, Erkennung fehlender Pakete usw.), lesen Sie einige der Standardprotokolle und Entwurfspraktiken.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.