Forum: Mikrocontroller und Digitale Elektronik Paketorientierte Daten handhaben


von Mr.T (Gast)


Lesenswert?

Hallo Forengemeinde

ich habe eine allgemeine Frage zur uC Programmierung. Es geht um etwas 
ganz Grundsätzliches. Und zwar die Frage, wie man paketorientierte Daten 
elegant handhaben könnte.

Also man hat z.B. irgend eine Schnittstelle (z.B. seriell, CAN, ...) 
über die Daten ankommen, die in Paketen gruppiert sind.

Jetzt möchte man einerseits alle Datenpakete empfangen und einen 
Acknowledge zurückschicken. Das geht ja einfach (pseudocode):
1
for(;;)
2
{
3
  /* blockierender Aufruf wartet, bis ein Paket ankommt */
4
  empfange_paket();
5
6
  /* mach was mit den Daten */
7
  ...
8
9
  /* blockierender Aufruf wartet, bis alle Daten gesendet sind */
10
  sende_ack();
11
}

Jetzt wird es aber kompliziert, denn wenn man von sich aus auch Daten 
schicken möchte, und dann schauen möchte ob ein Acknowledge zurück 
kommt, wird es ungemütlich:

1
for(;;)
2
{
3
  /* blockierender Aufruf wartet, bis ein Paket ankommt */
4
  empfange_paket();
5
6
  /* mach was mit den Daten */
7
  ...
8
9
  /* blockierender Aufruf wartet, bis alle Daten gesendet sind */
10
  sende_ack();
11
12
  /* sende irgendwelche Daten zum anderen Teilnehmer */
13
  sende_daten();
14
  warte_auf_ack();
15
  if(ack_empfangen())
16
  {
17
    /* alles gut */
18
  }
19
  else if(nak_empfangen())
20
  {
21
    sende_daten(); /* daten erneut senden */
22
  }
23
}


Das funktioniert dann schon nicht mehr, weil der Aufruf zum Daten senden 
nur dann erfolgt, wenn zuvor auch etwas empfangen wurde. Ein weiteres 
Problem taucht auf, wenn nach dem Senden von Daten beim Warten auf das 
Acknowledge noch ein anderes Datenpaket eintrifft, das weder ACK noch 
NAK ist - dieses Paket möchte man ja behalten, aber kann es dann an 
dieser Stelle nicht gebrauchen.

Wie würde man in einem solchen Fall richtig vorgehen. Es gibt bestimmt 
ein Design Pattern hierzu, aber ich weiss den richtigen Begriff zum 
Suchen offenbar nicht.

von protokolliera (Gast)


Lesenswert?

Mr.T schrieb:
> Und zwar die Frage, wie man paketorientierte Daten
> elegant handhaben könnte.

Du willst das Rad neu erfinden.

Das Rad bzw. Zauberwort lautet TCP und braucht nicht neu
erfunden werden. TCP ist unabhängig von der Schnittstelle,
allerdings dürfte die Realisierung über CAN (Paketgrösse?)
schwierig werden.

von Georg (Gast)


Lesenswert?

Mr.T schrieb:
> /* blockierender Aufruf wartet, bis ein Paket ankommt */

So macht man das eben nicht. Man empfängt Zeichen per Interrupt-Routine, 
die das verfügbare Zeichen ausliest und in den Empfangsbuffer schreibt, 
die blockiert also nicht bzw. nur µsec. Ist das Paket komplett* setzt 
man ein Flag für das die ganze Zeit ungestört laufende Hauptprogramm, 
dass ein Paket angekommen ist. Genauso sendet man im Sendebuffer 
stehende Daten bis der Sendebuffer leer ist. Beides kann völlig 
unabhängig voneinander und vom Hauptprogramm laufen.

*Wie man das erkennt ist ein anderes Thema, das muss man sowieso lösen, 
z.B. indem man ein Paket mit CR abschliesst. Es gibt aber auch eine 
Zillion andere Möglichkeiten.

Georg

von Stefan F. (Gast)


Lesenswert?

Eigentlich geht es nicht nur um das Senden und Empfangen von Daten, 
sondern um deren asynchrone Verarbeitung. Das sind mindestens zwei 
separate Threads.

Beispiel für eine Smart-Home Station im Zimmer zur Regelung des Klimas:

Der eine Thread fragt regelmäßig einen Temperatursensor ab und sendet 
den Wert an eine Zentrale. Wenn diese den Empfang nicht bestätigt, geht 
eine rote "comm. error" LED an.

Der andere Thread wartet auf Kommandos von der Zentrale zum Steuern des 
Heizungsventils und antwortet mit Bestätigungen, nachdem das Ventil 
eingestellt wurde.

Der entscheidende Punkt ist, dass diese Vorgänge unabhängig 
voneinander arbeiten. Das macht man bei µC wahlweise mit einem 
Betriebssystem (z.B. FreeRTOS) oder sonst üblicherweise mit 
Zustandsautomaten: 
http://stefanfrings.de/multithreading_arduino/index.html

Weiterhin sollte die Kommunikation von der Verarbeitung entkoppelt sein. 
Üblicherweise mittels Interrupt-Routine.

Was von der Zentrale kommt kann ein Befehl sein, oder eine Bestätigung. 
Zwischen der seriellen Schnittstelle und den beiden Threads gehört ein 
Vermittler, der den Typ der Nachricht erkennt und die Nachricht dann 
entsprechend dem richtigen Thread zuweist.

Oder man benutzt eine gemeinsame Warteschlange, in welche diese 
Ereignisse eingetragen werden. Die Threads schauen dann zyklisch nach, 
ob eine für sie relevante Nachricht in der Warteschlange steht.

von Mr.T (Gast)


Lesenswert?

ich habe noch vergessen zu erwähnen, dass ich FreeRTOS bereits nutze. 
Meine Interruptroutine sammelt alle empfangenen chars in einem Puffer, 
und eine Empfangsfunktion liest Byte für Byte aus diesem Puffer und 
setzt Pakete zusammen.

Ich sehe folgende Problematik: wenn empfange_paket() blockierend ist, 
dann ist der Task so lange blockiert, bis mindestens ein Paket empfangen 
wurde. Wenn der Task aber in der Zwischenzeit was anderes machen soll, 
dann kann er das nicht, weil er blockiert ist.

Auf der anderen Seite, wenn empfange_paket() nicht blockierend ist, dann 
wird der Task zwar nicht unnötig blockiert, ABER dafür läuft er ständig, 
selbst wenn keine Daten zu verarbeiten sind.

von Johannes S. (Gast)


Lesenswert?

Mr.T schrieb:
> Wenn der Task aber in der Zwischenzeit was anderes machen soll,

Dann lasse doch einen weiteren Thread laufen.

von Stefan F. (Gast)


Lesenswert?

> wenn empfange_paket() nicht blockierend ist, dann wird der
> Task zwar nicht unnötig blockiert, ABER dafür läuft er ständig,
> selbst wenn keine Daten zu verarbeiten sind.

Ja ist halt so.

Wenn du die CPU Last in der Leerlauf-Schleife reduzieren willst, kannst 
du ja vTaskDelay() oder taskYIELD() benutzen oder wie auch immer man bei 
deinem OS Zeit an andere Threads abgibt.

von Johannes S. (Gast)


Lesenswert?

das Warten auf ein Paket im blokierenden Empfang kostet sicher weniger 
als das Polling, wo der Thread ja zyklisch aktiviert werden muss.
Es geht auch mit einem Thread wenn man eine allgemeine EventQueue 
verwendet, dann kann ein Event ein empfangenes Paket sein, ein Paket das 
gesendet werden soll oder beliebige andere Events. Das ist auch keine 
neue Erfindung, prominentes Beispiel ist die Windows Messagepumpe. Das 
ist dann aber kooperatives MT, wenn eine Aufgabe blockiert, dann werden 
die wartenden nicht ausgeführt.

von Mr.T (Gast)


Lesenswert?

Stefan ⛄ F. schrieb:
> Ja ist halt so.
>
> Wenn du die CPU Last in der Leerlauf-Schleife reduzieren willst, kannst
> du ja vTaskDelay() oder taskYIELD() benutzen oder wie auch immer man bei
> deinem OS Zeit an andere Threads abgibt.

es wäre halt schön, wenn ich die Mechanismen nutzen könnte, die das OS 
zur Verfügung stellt und den Task schlafen legen kann, wenn es nichts zu 
tun gibt.

Johannes S. schrieb:
> Mr.T schrieb:
>> Wenn der Task aber in der Zwischenzeit was anderes machen soll,
>
> Dann lasse doch einen weiteren Thread laufen.

ja, das kann man machen. Mich würde halt interessieren, ob das "sauberes 
Design" ist. Zwei Threads um ein paar Pakete zu verarbeiten? ich habe 
nichts dagegen, aber ich würde es gern "richtig" machen. Und ich weiss 
eben noch nicht, wie "richtig" geht ;-)

von Johannes S. (Gast)


Lesenswert?

ein Thread kostet erstmal nur RAM für den zusätzlichen Stack. Wenn der 
an einer OS Semaphore blockiert, dann kostet das üblicherweise keine 
Rechenzeit. Zyklisches Aktivieren der Task ist teuer weil dann ein 
Kontextwechsel nötig ist.
Dann hängt es natürlich noch vom Protokoll ab ob ein Paket sofort 
quittiert werden muss oder soetwas wie das windowing beim TCP 
(aufwändiger) möglich ist. Und ob Senden komplett asynchron zum Empfang 
dawzischen funken darf, einfacher wäre das Senden über einen Thread der 
seine Daten über eine Q bekommt.

von Mr.T (Gast)


Lesenswert?

Johannes S. schrieb:
> ein Thread kostet erstmal nur RAM für den zusätzlichen Stack. Wenn
> der
> an einer OS Semaphore blockiert, dann kostet das üblicherweise keine
> Rechenzeit. Zyklisches Aktivieren der Task ist teuer weil dann ein
> Kontextwechsel nötig ist.
> Dann hängt es natürlich noch vom Protokoll ab ob ein Paket sofort
> quittiert werden muss oder soetwas wie das windowing beim TCP
> (aufwändiger) möglich ist. Und ob Senden komplett asynchron zum Empfang
> dawzischen funken darf, einfacher wäre das Senden über einen Thread der
> seine Daten über eine Q bekommt.

Du scheinst da die Design Patterns gut zu kennen. Kannst du mir sagen, 
wo ich mich da einlesen kann, wie man sowas macht? wie lerne ich, wie 
das richtig geht? wie würde ein simpler Pseudocode aussehen, der die 
Pakete streng synchron abarbeitet?

von Johannes S. (Gast)


Lesenswert?

Literaturtipp habe ich nicht, ich habe einfach mit verschiedenen APIs 
gearbeitet. Bei UDP mit lwIP z.B. hat man schon eine Pufferung von 
Paketen. 'Richtig' ist wenn die Kommunikation auch mit Sonderfällen wie 
keine/langsame/schnellen/falschen Antworten klar kommt.
Also bei dem synchronen Ping Pong ist auch ein Timeout nötig damit der 
Empfänger nicht ewig auf eine Antwort wartet.

von Stefan F. (Gast)


Lesenswert?

Johannes S. schrieb:
> das Warten auf ein Paket im blokierenden Empfang kostet sicher weniger
> als das Polling, wo der Thread ja zyklisch aktiviert werden muss.

Nein. Blockieren kostet nämlich 100%, weil es blockiert.

von Mr.T (Gast)


Lesenswert?

Hmm OK ich habe mein Problem jetzt mit einem zweiten Thread gelöst.
Es funktioniert! Der zweite Thread empfängt die Pakete blockierend, 
prüft die CRC und je nach Paket werden die Daten dann am richtigen Ort 
gespeichert und über ein Eventflag (wird auch vom OS bereitgestellt) 
werden eventuell wartende andere Tasks informiert.
Das hat den Vorteil, dass man auch auf mehrere Eventflags gleichzeitig 
warten kann.

Ob das aber eine gute Implementation ist bin ich nicht sicher. In einem 
allgemeinen Fall für ein "richtiges" Netzwerkprotokoll könnte man das 
sicher nicht gebrauchen. Daher schon die akademische Frage, wie man es 
noch besser machen könnte. Aber ich schaue mir mal lwip mit UDP an, wie 
es dort implementiert ist.

Die Sache ist halt, dass ich die ISR, wo die Daten empfangen werden, 
möglichst kurz halten möchte, d.h. die ISR speichert die Empfangsdaten 
lediglich in einem Puffer und weckt dadurch den wartenden Empfangstask 
auf.

von Mr.T (Gast)


Lesenswert?

Stefan ⛄ F. schrieb:
> Nein. Blockieren kostet nämlich 100%, weil es blockiert.

blockieren kostet natürlich gar nichts, weil der Task, der blockiert 
ist, suspendiert ist und die CPU für andere Tasks frei gibt!

von Stefan F. (Gast)


Lesenswert?

Mr.T schrieb:
> blockieren kostet natürlich gar nichts, weil der Task, der blockiert
> ist, suspendiert ist und die CPU für andere Tasks frei gibt!

Ja. Ich bezog mich dabei auf einen ersten Ansatz, wo das ganze Programm 
nur einen Task hat. Wenn der blockiert, steht alles.

von Johannes S. (Gast)


Lesenswert?

Mr.T schrieb:
> blockieren kostet natürlich gar nichts, weil der Task, der blockiert
> ist, suspendiert ist und die CPU für andere Tasks frei gibt!

richtig. Auf einem Desktop OS laufen ja tausende Threads und wenn alle 
blockierenden 100 % CPU Last verursachen würden wäre das irgendwie blöd.

Statt Eventflags gingen eben auch Queues, daran kann ein Thread auch 
blockieren bis Daten vorliegen oder ein Timout abgelaufen ist. Die Qs 
könnten dann von verschiedenen Threads gefüttert werden, wenn der Client 
auch unaufgefordert Daten Senden darf. Das Ping Pong ist der einfache 
Fall wie z.B. im HTTP wo es immer Request-Response gibt.

Zum FreeRTOS gibt es ein Buch vom Autor R. Barry, da sind zumindest die 
RTOS Elemente beschrieben.

von Mr.T (Gast)


Lesenswert?

Johannes S. schrieb:
> Statt Eventflags gingen eben auch Queues, daran kann ein Thread auch
> blockieren bis Daten vorliegen oder ein Timout abgelaufen ist. Die Qs
> könnten dann von verschiedenen Threads gefüttert werden, wenn der Client
> auch unaufgefordert Daten Senden darf. Das Ping Pong ist der einfache
> Fall wie z.B. im HTTP wo es immer Request-Response gibt.

ich habe mir grade einen Beispielcode für STM32 und lwIP 
heruntergeladen. Und dort habe ich gesehen, dass die es genau gleich 
machen wie ich: es ist ein separater Task nur dazu da, die 
Ethernetpakete zu Empfangen. Soweit so gut, ich bin noch dabei den Code 
weiter zu analysieren, aber so wie es aussieht, scheint mein Approach 
doch nicht so verkehrt zu sein :-)

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.