UART und Datenpakete



  • Ich möchte zwei Controller über den UART miteinander verbinden. Jedoch möchte ich nicht Text übertragen, sondern Datenpakete.

    Bisher gehe ich dabei so vor, dass ich am Anfang jedes Datenpakets 2 Bytes mit der Größe des Datenpakets mitschicke.

    Ist dies so üblich, oder geht man da normalerweise anders vor?


  • Mod

    "Datenpakete" ist ein sehr allgemeiner Begriff, das kann alles mögliche bedeuten. Kommt eben auf deine Anforderungen an, du musst quasi ein eigenes Protokoll entwickeln (oder ein schon existierendes Protokoll nutzen, das deinen Anforderungen entspricht). Die Kombination aus einer Größeninformation mit darauf folgenden Nutzdaten ist durchaus eine sehr übliche. Andere Ansätze sind fest vorgegebene Größen oder die Markierung von Enden durch Sonderzeichen.



  • Dieser Thread wurde von Moderator/in SeppJ aus dem Forum C (alle ISO-Standards) in das Forum Rund um die Programmierung verschoben.

    Im Zweifelsfall bitte auch folgende Hinweise beachten:
    C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?

    Dieses Posting wurde automatisch erzeugt.



  • Über ein Ende-Symbol habe ich auch schon nachgedacht. Dann muss man aber sicherstellen dass dieses Symbol nicht im als teil des Datenpakets bzw. Nutzdaten vorkommt.

    Danke Sepp!



  • Alle mir bekannten binären seriellen Protokolle haben eine Header wo die Länge mit reinkodiert ist.
    Ist also sehr üblich würde ich sagen.

    Und du solltest dir auch Gedanken darüber machen was passiert wenn...
    - Der Empfänger mitten in einem Paket ansetzt (z.B. weil er später als der Empfänger gestartet wurde)
    - Der Empfänger ein paar Byte "verpasst" (Pufferüberlauf oder was auch immer)

    UART newbie schrieb:

    Über ein Ende-Symbol habe ich auch schon nachgedacht. Dann muss man aber sicherstellen dass dieses Symbol nicht im als teil des Datenpakets bzw. Nutzdaten vorkommt.

    Start- und Ende-Symbole sind auch gut, ja. Sogar sehr gut würde ich sagen.
    Weil dadurch das Handling von allen Sonderfällen super einfach und vor allem super zuverlässig wird.

    Sicherstellen dass die Spezialzeichen nicht im "Paketinhalt" vorkommen lässt sich ja relativ einfach. Die allereinfachste Variante ist 7 Byte auf 8 aufzuteilen. So bleibt dir pro Byte ein Bit übrig das im Paketinhalt z.B. immer 0 ist, und bei "Spezialsymbolen" immer 1. Gibt aber natürlich auch wesentlich kompliziertere Encodings, bei denen dafür weniger Overhead anfällt.



  • Beim Empfangen der Daten möchte ich diese Zwischenpuffern. Aktuell verwende ich einfach 16 Blöcke mit je 256 Byte Größe, d.h. es können insgesamt 16 Datenpakete zwischengepuffert werden, egal wie groß diese sind. Außerdem limitiert dies auch die maximale Größe meiner Datenpakete auf 256 Bytes.
    Dies möchte ich gerne ändern, indem ich sowas wie einen Ringbuffer verwende, jedoch soll jedes Datenpaket auch linear im Speicher liegen, und nicht wenn das obere Ende des Ringpuffers erreicht wird in zwei geteilt werden.

    Ich versuche dies nun so, dass jeder Eintrag in den Ringpuffer einen eintrag auf den Anfang des nächsten und vorherigen Elements besitzt. Passt ein Eintrag nicht mehr in den Ringbuffer, wird er nicht zerstückelt, sondern direkt an der stelle 0 eingetragen.

    Gibt es dafür evtl. effizientere Lösungsansätze?



  • Mir fällt jetzt keine einfachere/effizientere Lösung ein.

    Allerdings ist die Frage ob es nicht doch mit einem klassischen Ringpuffer (der einfach nur Bytes puffert) auch ginge.

    Der Teil der die Daten aus dem Ringpuffer raus nimmt kann ja zerstückelte Pakete dann mit 2x memcpy in einen linearen Puffer kopieren. Das restliche Programm kann dann mit den linearen Paketen weiter arbeiten.

    Alternativ kann man es auch so machen, dass man den Rinpuffer doppelt so gross macht wie nötig. Also wenn man N nutzbare Byte im Puffer braucht dann fordert man 2*N Byte an. Wenn der aktuellen Schreib-Index W [0 ... N-1] ist, dann kopiert man jedes Byte nach buffer[W] und nochmal nach buffer[W+N] .

    Dadurch kannst du an jedem beliebigen Index [0 ... N-1] anfangen und von dort linear bis zu N zusammenhängende Bytes (also den ganzen Puffer) lesen.



  • Der Teil der die Daten aus dem Ringpuffer raus nimmt kann ja zerstückelte Pakete dann mit 2x memcpy in einen linearen Puffer kopieren. Das restliche Programm kann dann mit den linearen Paketen weiter arbeiten.

    Genau das wollte ich vermeiden.

    Zur ursprünglichen Frage zurück:
    Ich denke ich werde nun auf eine Größenangabe verzichten. Ich sende Start- und Stop-Bytes. Um zu verhindern, dass dieses Byte nicht auch im datenteil des Pakets vorkommt, habe ich ein drittes Byte definiert (0x00). Wenn dieses Byte empfangen wird, dann heißt es, dass eigentlich ein byte gesendet werden sollte, dass bereits reserviert ist. Das nächste gesendete Byte gibt dann an was gesendet werden sollte.

    Das hat den vorteil das nur alle 3 von 266 gesendeten Datenbytes ein zusätzliches Byte gesendet werden muss. Und es hat wie bereits erwähnt den Vorteil, dass der Anfang und Ende einer Nachricht eindeutig erkannt wird.

    Was das Zwischenpuffern angeht: ich denke ich werde es nun einfach so beibehalten, dass die Pakete nur eine maximale Größe haben dürfen. Das würde dann auch eine eventuelle Fehlerüberprüfung vereinfachen.
    Ich tendiere immer ein wenig dazu alles so allgemein und flexibel wie möglich zu entwickeln, aber es gleichzeitig so effizient wir möglich zu machen. Leider steht das oft im gegensatz zueinander 😞

    Danke für eure Hilfe!



  • Dabei musst du halt beachten dass Worst-Case alle Bytes escaped werden müssen. Allerdings kannst du natürlich schlauerweise auch die bereits "un-escapeten" Daten in die Puffer schreiben. Dann müssten die nicht doppelt so gross sein.

    Und ... nur für den Fall dass du es übersiehst...
    Angenommen deine Spezialzeichen sind 0 (Escape), 1 (Start) und 2 (Stop).
    Dann solltest du nicht 0, 1 statt 1 schicken. Sondern z.B. 0, 0x81 oder sowas.
    Weil man sonst wissen muss dass vor dem 1er ein 0er war, damit man weiss dass der 1er nicht für "Start" steht.

    Und du solltest dir Werte aussuchen die du selten verschickst. Also vielleicht nicht gerade 0, 1 und 2 🙂



  • hustbaer schrieb:

    Mir fällt jetzt keine einfachere/effizientere Lösung ein.

    Och, je nach Anforderung kann man da beliebig effizienter werden.

    Zum Beispiel send(char* dta,int len) wird umgesetzt in statt len>>24,len>>16,len1>>8,len,dta[0],dta[1],…,dta[len-1] einfach in dta[0],dta[1],…,dta[len-1],0 mit der einzigen Ausnahmeregel, daß wenn im Eingabestrom 0 kommt, daraus 0 0 gemacht wird. Kräge das der Empfänger sinnvoll mit? Genausoviele waits und locks und Kontextwechsel? Jupp. Nur bei Datenlage: "fast keine 0 in Nutzdaten" sparsamer. Und bei vielen vielen kleine Paketen bis zu echt erheblich besser.



  • @volkard
    Ne. Guggsdu:

    UART newbie schrieb:

    Ich versuche dies nun so, dass jeder Eintrag in den Ringpuffer einen eintrag auf den Anfang des nächsten und vorherigen Elements besitzt. Passt ein Eintrag nicht mehr in den Ringbuffer, wird er nicht zerstückelt, sondern direkt an der stelle 0 eingetragen.

    Gibt es dafür evtl. effizientere Lösungsansätze?

    hustbaer schrieb:

    Mir fällt jetzt keine einfachere/effizientere Lösung ein.



  • hustbaer schrieb:

    Mir fällt jetzt keine einfachere/effizientere Lösung ein.

    Zur Kenntnis genommen.



  • @volkard
    bitte entweder ordentlich mitlesen, oder spar dir deinen Zynismus.

    Mein "mir fällt jetzt keine einfachere/effizientere Lösung ein" war NICHT auf die Sache mit "Länge in der Header mitschicken" bezogen.

    Dein Beitrag macht daher keinen Sinn.



  • Ja, das habe ich alles soweit bedacht.

    Dabei musst du halt beachten dass Worst-Case alle Bytes escaped werden müssen. Allerdings kannst du natürlich schlauerweise auch die bereits "un-escapeten" Daten in die Puffer schreiben. Dann müssten die nicht doppelt so gross sein.

    Damit kann man das ganze auf jeden Fall noch etwas effizienter machen, verkompliziert es jedoch auch ein wenig.
    Beim senden schiebe ich die Daten einfach ein ein Ringbuffer rein, der wann immer ein byte gesendet werden kann von der anderen Seite abgebaut wird.



  • Naja kommt drauf an ob es sich auszahlt.
    Die Vorteile sind

    1. Braucht weniger Speicher

    2. Die Messages in den Puffern können einfacher verarbeitet werden wenn es kein "variable length encoding" im Puffer mehr gibt. D.h. du kannst dann einfach an der Stelle Pufferanfang+X lesen wenn du das Byte mit Offet X braucht. Und das ohne dass du vor dem Verarbeiten der Message nochmal drüberlaufen und "un-escapen" musst. Und da du 2x memcpy aus nem zerstückelten Puffer nicht willst, gehe ich einfach mal davon aus dass du das auch nicht willst.

    Der Nachteil ist natürlich dass der Code im Interrupt-Handler minimal komplizierter wird. Mehr als ein "last byte was escape sequence" bool brauchst du da aber nicht - sollte akzeptabel sein.

    Und ... dass der Throughput durch das Escapen nicht mehr deterministisch ist hast du auch bedacht, oder?



  • Und ... dass der Throughput durch das Escapen nicht mehr deterministisch ist hast du auch bedacht, oder?

    Nein, das hab ich leider nicht bedacht! Aber der Durchsatz ist noch "schwach deterministisch", da sich der Durchsatz im schlimmsten Fall nur halbieren kann. Damit kann ich noch leben 🙂

    Was mir aber sehr wichtig ist, dass das Protokoll einfach zu verwenden ist. Ich setze den Code grade neu auf. Im Prinzip hab ich nurnoch 2 Klassen: Encoder und Decoder.

    Den Encoder fütter ich mit kompletten Datenpaketen (push), und die entsprechenden zu sendenden bytes kann man dann mit einer pop-funktion rausholen.
    Beim Decoder ist das entsprechend andersrum.

    Also fällt der 2. Vorteil beim senden nicht mehr ins Gewicht.



  • Obwohl, das ganze hat beim senden doch ein kleines Problem: Dadurch das die codierten Daten länger werden können als die nicht codierten Daten, kann man nicht mehr zu Begin sagen ob noch genug platz im Ringpuffer vorhanden ist. So kann man erst während des codierens feststellen ob es nicht mehr reicht.

    Ich finds nur grad erstaunlich wie viel Arbeit ein scheinbar so kleines Problem machen kann ... 🙂



  • Naja, ich hab den Eindruck dass du dir die Sache auch ordentlich schwerer machst, indem du versuchst sämtliche "unnötigen" Speicherzugriffe zu vermeiden.

    Wie ist denn das Verhältnis Speichergeschwindigkeit vs. CPU-Geschwindigkeit bei deinen Contollern?

    Bei normalen PCs wäre es heute noch total kontraproduktiv ein einmaliges Umkopieren von ein paar hundert Byte durch komplizierteren Code zu vermeiden. Weil der L1 Cache einfach so sauschnell ist. Bei High-End GPUs ist's wieder anders umgekehrt, da kann man sich einiges an Arithmetik leisten wenn man damit nen Speicherzugriff vermeiden kann.



  • Der Prozessor ist ein Cortex M0+

    Ich mach mir da jedoch grade so viele Gedanken, weil ich diesen Code dann auch für zukünftige Projekte verwenden möchte. Kennst du vllt. fertige Protokolle an denen ich mich orientieren könnte? Ich schiele grad mit einem Auge auf HDLC



  • Mir fällt grad noch was anderes ein:

    Wenn ich die Nachrichten direkt codiere, dann kann ich mir die interrupts sparen und es direkt über den DMA machen.


Anmelden zum Antworten