Protokolldesign – Kommunikation

Öfter sehe ich Fragen in den Foren, die sich um prinzipielles Protokolldesign drehen. Dabei sollen Daten zwischen zwei Teilnehmern ausgetauscht werden und finden keinen (guten) Ansatz. Dies hat auch meist wenig mit den Programmiererfahrungen zu tun. Zuletzt hatte ich einen gestandenen Hard- und Softwareentwickler eines großen Akkuschrauberherstellers am Telefon: “Nachrichten beliebiger länger über eine UART senden geht so einfach?”

JA!

Wenn ihr diesen Blogbeitrag gelesen habt, dann solltet ihr eine Übersicht darüber bekommen haben. Ich werde die möglichen Konzepte dafür nur anschneiden und Beispiele nennen, für weitere tiefergehende Informationen werde ich auf andere Seiten verweisen.

Sychrone Kommunikation

Fangen wir nach dem OSI-Modell ganz unten an: Die Bitübertragungsschicht sollte geklärt werden, bevor das weitere Konzept erstellt wird. Ob eine UART, SPI oder I2C Schnittstelle verwendet wird hängt ganz wesentlich mit dem Entwurf zusammen.

Bei SPI und I2C haben wir eine Schnittstelle nach dem Master/Slave prinzip. Das heißt: Wenn der Master etwas anfragt, dann darf der Slave nur solange senden, wie es dem Master genügt. Allein schon deshalb, weil der Master den Takt/Clock vorgibt.

Daher sind diese Protokoll oft so ausgelegt, dass alle Nachrichten eine feste Länge besitzen. Nur für wenige Pakettypen gibt es Ausnahmen in denen der Master einfach längere Daten abfragen oder senden kann.

Beispiel SPI Temperatursensor

Der Slave wird mit Hilfe der entsprechenden Chip Select (CS) Leitung auf den gemeinsamen Bus geschalten. Der Master beginnt Takt und Daten zu senden. Es ist demnach eindeutig wann eine Nachricht beginnt! Es kann ein Nachrichtenanfang über das CS Signal erkannt werden! (Framing)

Erste Byte: Adressbyte/-register -> Temperaturregister lesen

Zweite Byte: Temperatur High Byte

Dritte Byte: Temperatur Low Byte

Der 16 Bit Temperaturwert ist nun beim Master angekommen.

Beginner Tipp: Der Master muss drei Bytes senden und wird drei Bytes empfangen, da die SPI wie ein Schieberegister arbeitet. Es ist dabei klar, dass nur das Erste gesendete Byte des Masters relevant für den Slave ist (Leseregister) und nur die letzten zwei empfangenen Bytes gültige Temperaturdaten enthalten. Der Rest wird einfach ignoriert und dazu verwendet das Clock Signal weiter zu takten.

Einige SPI-Slaves können nun sofort eine weitere Nachricht verarbeiten, andere erwarten ein kurzes Abschalten des CS. Meine Empfehlung ist jedoch, nach jedem Telegramm das CS zu verwenden. Ein Temperaturlogger der 24 Stunden am Tag jede Sekunde die Temperatur abfragt, könnte auf der Taktleitung schnell ein Glitch bekommen. Damit wäre die gesamte Kommunikation um ein Bit verschoben. Ohne sich über das CS zu synchronisieren würde hier nur noch ein Reset der Baugruppe helfen.

Beispiel I2C Temperatursensor

Da es keinen CS-Leitung gibt, lauschen alle I2C-Slaves auf dem Bus bis sie aufgerufen werden. Dies passiert wenn der Master die Startkennung gefolgt von der passenden Adresse (z.B. 7-Bit Adresse) sendet (Framing). Danach folgt die passende Registeradresse, wie bei der SPI. Bei I2C gibt es nun einige Control-Bits, um die Kommunikation etwas sicherer zu gestalten. Es gibt ein z.B. ein Bit um ein Lese oder Schreibvorgang anzukündigen und der Slave muss nach jedem gelesenen/geschriebenen Byte ein ACK-Bit senden bzw. setzen. So kann der Master eher die Kommunikation abbrechen, wenn der Slave nicht mehr antwortet und muss nicht die gesamte Nachricht absetzen.

Die Kommunikation ist bei der I2C-Schnittstelle durch die Hardware schon etwas weiter für den Benutzer abstrahiert. Eine einfach Möglichkeit einen nicht antworteten Slave bei einer SPI zu erkennen ist schon schwieriger. Wenn der Temperaturwert nur 0x00 0x00 oder 0xFF 0xFF sein sollte wäre das ein Hinweis.

Asynchrone Kommunikation

Hiermit beenden wir den einfachen Teil und kommen zur “Königsklasse”: die asynchrone Kommunikation mit gleichberechtigten Kommunikationsteilnehmern im Vollduplexbetrieb. Eine UART oder auch CSMA CD, wie Ethernet, wäre dafür ein Beispiel. Dies bringt ganz neue Probleme mit sich:

  • Der Takt aller Kommunikationsteilnehmer sollte recht genau sein. Dies erfordert oft aber nicht immer einen Schwingquarz oder ähnliche Maßnahmen.
  • Es darf zeitgleich gesendet und empfangen werden. Wenn eine Anfrage gesendet wird, kann gerade noch eine verspätete Antwort oder andere Anfrage eingehen. Die Zugehörigkeit ist also erstmal undefiniert.
  • Das von mir schon mehrmals erwähnte Framing ist nicht mehr Problem der Bitübertragungsschicht. Mit Framing ist das Erkennen eines zusammengehörenden Nachrichtenpaketes gemeint.
  • Ein anderer Teilnehmer interessiert sich recht wenig dafür ob gerade Zeit ist, diese Nachricht überhaupt verarbeiten zu können. Ausnahme bildet hier das Handshake-Verfahren bei der UART. Bei der Hardwarevariante werden zusätzliche Leitungen zur Flusskontrolle eingesetzt. Man kann der Gegenstelle also mitteilen: “Halt stopp! Ich komme nicht hinterher”. Was auf den ersten Blick nach einer guten Lösung klingt, ist auch kein Wundermittel für alle Probleme. Irgendwann muss auch die ausgebremste Gegenstelle ihre Daten verwerfen, wenn diese nicht mehr senden kann.

Bleiben wir also bei allen nachfolgenden Betrachtungen bei einer UART mit RX- und TX-Leitungen zwischen zwei Teilnehmern.

Meist wird einer dieser Teilnehmer durch das Protokolldesign die Rolle des Masters und der andere die Rolle des Slaves einnehmen. Mit dem Unterschied zur SPI oder I2C, dass der Slave, wenn er denn was wichtiges zu sagen hat, einfach darauf los senden könnte.

Tipp: Bei den Protokollen wo der Master den Clock treibt (SPI/I²C), wird oft ein extra IRQ Pin verwendet, damit der Slave darauf hinweisen kann, dass er was wichtiges mitzuteilen hat.

#1

Betrachten wir die erste Möglichkeit des Framings und damit zum Telegrammaustausch.

Alle Nachrichten haben eine feste oder sogar die selbe Länge. Wir definieren:

  • per Protokolldef. soll es einen Master und einen Slave geben.
  • der Master sendet immer zwei Byte
  • der Slave antwortet mit einer Nachrichtenlänge abhängig der gesendeten Bytes des Masters

z.B.

  1. erste Byte: 0x01 für lesender Befehl
  2. zweite Byte: 0x02 für Temperaturregister

Der Slave hat nun zwei Bytes zu senden. Diese werden vom Master empfangen und als Temperaturwerte behandelt. Das Framing wird für den Master bei jeder neuen Nachricht begonnen. Das Framing des Slaves basiert auf dem Intervall von zwei Bytes.

Probleme bei dieser Variante:

  • der Slave kann z.B. beim Start der Schaltung desychronisieren und verwechselt damit die Reihenfolge der zwei Bytes. Der Slave sollte daher über einen zeitbasierenden Reset verfügen: Länger als X ms kein Byte erhalten? Dann kommt als nächstes sicher das erste Byte! Der Master sollte um diese Funktion wissen und etwas Zeit zwischen der Kommunikation lassen.
  • Das Framing des Masters kann durch die asynchrone Kommunikation ebenfalls schief gehen. Sendet der Master gerade seine Anfrage und der Slave beantwortet gerade noch die vorangegangen, kommt es zu Race Conditions die schwer bis gar nicht lösbar sind.
  • Bekommt der Master in der Zeit X keine Antwort oder zu wenig Bytes, muss ein Timeout erfolgen und darf nicht in einer Endlosschleife ewig warten. Dies gilt übrigens für ausnahmslos alle nachfolgenden Beispiele und Möglichkeiten.

Lösungen und Vorteile:

  • Gemütlich kommunizieren! Genug Zeit für die Nachrichtensychronisation belassen und schon treten die oberen Probleme kaum mehr auf.
  • Einfaches Protokolldesign für den Master.
  • Ist jeder Befehl des Masters immer nur ein Byte groß, ist ein Framing beim Slave nicht nötig -> ebenfalls sehr einfaches Protokolldesign des Slaves.
slave_out_of_sync

slave device desychronized

Ein Beispiel für dieses Protokolldesign ist mir bei einem Motortreiber bereits begegnet und ebenfalls bei dieser Relaiskarte:

http://www.produktinfo.conrad.com/datenblaetter/175000-199999/197720-an-01-ml-8FACH_RELAISKARTE_24V_7A_de_en_fr_nl.pdf

#2

Um zu vermeiden, dass der Slave seine Synchronisation über längere Zeit ohne “Anhaltspunkte” halten muss, kann ein Startbyte und/oder ein Endebyte an die Nachricht angehängt werden.

Dies Framing-Bytes sollten entweder einzigartig oder zumindest leichter wiederzufinden sein. Dazu könnte die Länge der Nutzdaten mit versendet werden. Dies führt zu dem Vorteil, dass Nachrichten ganz unterschiedlicher Länge versendet werden können. Die Empfänger vergleichen auf das Startbyte und geht bei dem nachfolgendem Byte von der Länge der Nachricht bis zum Endebyte aus.

Die Start- und Endebytes dienen dabei der Verifikation und im Fall einer desynchronisierung wird der Empfänger im Datenstrom ein Startbyte suchen.

Nachteile dieser Variante:

  • Das Protokoll kann sich auf ein falsches Byte in den Nutzdaten einschwingen und damit wird ein falsches Längenbyte ansetzen.
  • Ist daraufhin die Länge der Zeichen fälschlich hoch, kann die synchronisierung einige Pakete dauern.
  • Die erneute synchronisierung ist nicht sichergestellt und hängt von den Nutzdaten ab.

Folgende Tabelle stellt eine beispielhafte Nachricht mit binärem Inhalt vor:

Startbyte

Länge Befehl Nutzdaten Endebyte
0x01 0x02 0x11 0x01

0x02

Angenommen dies Nachricht wird mehrmals versendet und der Empfänger verliert bei „Befehl“ das Framing, dann versucht er erneut das Startbyte zu finden:

  1. Befehl 0x11 ist kein Startbyte
  2. Nutzdaten 0x01 ist das erkannte Startbyte
  3. Endebyte wird als Länge angenommen, also 2
  4. das neue Endebyte muss sich nun bei Befehl befinden
  5. dort ist kein Endebyte -> Nachricht nicht gültig
  6. neues Startbyte suchen -> Nutzdaten ist das erkannte Startbyte
  7. usw.

Wie zu erkennen ist, würde bei diesem wiederkehrenden Nachrichteninhalte keine erneute Synchronisation erfolgen.

Obwohl ein zusätzliches Endebyte in Kombination mit dem Startbyte und der Länge eine recht niedrige Wahrscheinlichkeit ergibt, dass eine falsche Nachricht durch kommt, ist dieses Protokolldesign nicht bei sensiblen Daten und Anwendungen zu empfehlen.

Da es nur einfache Vergleiche und Zähler enthält, ist es von der Rechenzeit her sehr sparsam und für die schwächsten Mikrocontroller noch gut geeignet.

Bekannte Beispiele sind Protokolle für die LED-Ansteuerung:

tpm2: http://www.ledstyles.de/index.php/Thread/18969-tpm2-Protokoll-zur-Matrix-Lichtsteuerung/

oder mit fester Länge bei unterschiedlichen Befehlen:

minidmx: http://www.dmx.snoefler.de/

#3

Ein sehr sicheres Framing erhält man durch Einsatz von eindeutigen Start und Endebytes. Bei einem String ist Beispielsweise die nachfolgende 0x00 immer ein eindeutiges Zeichen für das Ende der Zeichenkette (im Speicher). Ebenfalls sind Zeilenumbrüche wie Carriage Return oder Line Feed (<CR><LF>) ein guter Marker für das Ende einer versendeten Nachricht.

Dies wird zum Beispiel bei den AT-Commands von Funkmodulen und bei den NMEA GPS Module so gehandhabt.

http://www.telit.com/fileadmin/user_upload/products/Downloads/2G/Telit_AT_Commands_Reference_Guide_r23.pdf

https://de.wikipedia.org/wiki/NMEA_0183

Damit dürfen leider keine binären Daten übertragen werden, sonst besteht die Gefahr, dass diese Terminierungszeichen darin auftauchen.

#3.1

Also Lösung könnte man die Zahlenwerte in ein String wandeln. Alternativ besteht die Möglichkeit die gesamte binäre Nachricht in ein HEX Zeichenfolge in ASCII Darstellung zu wandeln.

Also z.B.: 64F3ABE1<LF>

Nachteilig wäre der etwas höhere Rechenaufwand und die doppelte Bandbreite, da aus einem Byte zwei werden.

Umgesetzt wird dies ebenfalls bei GSM-AT-Modems, bei der SMS-Codierung als “HEX” ist es dem fehlerbehafteten PDU-Format vorzuziehen.

#4

Eines der stabilsten Protokolle wird leider viel zu selten eingesetzt. Es geht um das Byte Stuffing. Dabei gibt es wieder ein Ende- und/oder Startbyte das einmalig ist. Alle weiteren Daten können binär übertragen werden. Wenn jedoch eines der eindeutigen Zeichen vorkommt, werden diese durch ein sogenanntes Escape-Zeichen und einem definierten Zeichen ersetzt. Durch das zusätzliche Escape-Zeichen, muss dieses Zeichen ebenfalls ersetzt werden. So wird das Protokoll absolut eindeutig.

Ein kleines Problem ist, dass nicht genau gesagt werden kann wie viel Overhead dieses Protokoll hat, da es immer auf die Nutzdaten ankommt. Eine Nachricht die viele Start-, Ende- oder Escapebytes enthält, wird länge alles Nachrichten ohne. Für dieses Problem gibt es ebenfalls Lösungsansätze auf die ich hier jedoch nicht eingehen werde.

Hier ist eine kleine bebilderte Umschau zum Bit und Byte Stuffing:

https://web.cs.wpi.edu/~rek/Undergrad_Nets/B06/BitByteStuff.pdf

Tipp: Oft wird das Startbyte gar nicht gebraucht. Im Gegensatz zum Endebyte könnt ihr es gern weglassen. Das Endebyte ist jedoch nötig um die Nachricht abzuschließen und verarbeiten zu können. Würde man sich nur am Startbyte orientieren, müsste zum Auswerten der vorangegangenen Nachricht bis zur nächsten Nachricht gewartet werden.

Empfangsparser erstellen

Das Empfangen von Zeichen sollte immer mit einer Statemachine getan werden. In vielen Anwendungen ist es wichtig, dass der Task nicht blockierend arbeitet und auch weitere Aufgaben vom Controller verarbeitet werden können.

Oft ist es Ratsam eine kleine Software FIFO die empfangenen UART-Zeichen einlesen zu lassen, per Interrupt oder auch DMA. In der Hauptschleife wird nun geprüft ob neue Zeichen zur Verfügung stehen. Sollten Zeichen vorhanden sein, werden diese in den Protokollparser gegeben.

statemachine

example state machine of a receiver

Wie man sich evtl. denken kann, benötigt dieser Parser einen Puffer der die Größte zu erwartende Nachricht speichern kann. Andernfalls würde der Speicher überlaufen. Dies ist natürlich zuvor immer zu prüfen.

Steht nach dem Startbyte die Nachrichtenlänge, müsste diese eingelesen werden und es wird heruntergezählt. Bei 0 sollte dann ein Endebyte empfangen werden oder es ist etwas schief gegangen.

Sicherung

#1

Viele kleine Mikrocontroller haben schon eine eingebaute CRC-Einheit. Eine an die Nachricht angehängte CRC sollte ein Minimum sein, wenn sensible Anwendungen erstellt werden. Ungültige Nachrichten mit fehlerhaft übertragenem Inhalt werden im einfachsten Fall verworfen.

#2

Somit kommen wir zu einer zweiten Sicherung im Protokolldesign: die Retransmits. Sollte eine Nachricht nach einer bestimmte Zeit nicht beantwortet werden, kann die Nachricht wiederholt werden. Dies muss nicht für jede Nachricht gelten und kann auch dem Benutzer überlassen werden.

#3

Das wiederholte Senden von Nachrichten hat jedoch eine weitere Tücke. Der Sender kann nicht sicherstellen ob die Nachricht bei der Gegenstelle nicht ankam oder ob nur die positive Bestätigung (acknowledge) verloren gegangen ist.

Sollten Nachteile dadurch entstehen, wenn die Gegenstelle zwei aufeinanderfolgende gültige Nachrichten erhält, sollte eine Sequenz ID eingeführt werden. Daran kann erkannt werden, ob die Nachricht nur ein Retransmit darstellt und nur die ACK-Nachricht nachgeholt werden muss oder die übliche Reaktion durchgeführt werden muss.

Beispiel:

Der Slave multipliziert eine Zahl immer mit dem Inhalt der Nachricht des Masters und sendet eine Bestätigung, falls die Multiplikation erfolgt ist. Wenn nun das ACK des Slaves verloren geht, sendet der Master die selbe Nachricht erneut, ohne zu wissen das die Multiplikation bereits erfolgt ist. Eine Sequenz ID lässt den Slave erkennen ob er diese Nachricht schon mit einem ACK beantwortet hat oder noch bearbeiten muss.

Eine andere gute Lösung ist es, den Master einfach das Produkt (Faktor * Faktor = Produkt) abfragen zu lassen, um erneut synchronisieren zu können, diesmal jedoch auf einer ganz anderen Ebene.

 

Ich habe sicher noch einige Varianten vergessen Ich hoffe jedoch die wichtigsten Protokolldesignschritte herausgestellt zu haben. Falls ihr noch Fragen habt, schreib einfach einen Kommentar.

Advertisements
Dieser Beitrag wurde unter Allgemein veröffentlicht. Setze ein Lesezeichen auf den Permalink.

2 Antworten zu Protokolldesign – Kommunikation

  1. Michael Fink schreibt:

    Vielen Dank für diesen interessanten Artikel und den weiterführenden Links. Ich bin derzeit an der Planung eins UART Protokolls zur Kommunikation zw. Linuxserver & MC. Ich denke ich lehne mich da an das erwähnte TPM2 Protokoll an, dies scheint für mein Anwendungsfall recht geeignet zu sein.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s