Hallo,
ich habe da mal eine Prinzipielle Frage.
Ich setze das uC/OS-III RTOS ein. Ich habe mehrere Tasks am laufen;
unter anderem ein Task, der ein selber entworfenes
Kommunikationsprotokoll ausführt. Der UART-Driver empfängt die
eingehenden Bytes, verpackt diese in einen "Frame" und sendet ihn per
Message an die Mailbox des Protokoll-Tasks.
Diesem Task werden jedoch auch noch von anderen Tasks Messages
geschickt. Wenn z.B. ein anderer Task einen Frame senden möchte, dann
wird dieser auch als Message der Mailbox des Protokoll-Tasks übergeben.
Auch Retransmission-Timeouts etc. werden so gehandhabt.
Jetzt ist es so, dass beim uC/OS-III die Mailbox nur eine bestimmte
maximale Anzahl von Messages aufnehmen kann. Wenn jetzt aber z.B. sehr
viele Frames auf einmal eintreffen, oder wenn ein Task nicht gleich
jeden Frame sofort abholt, dann kann es passieren, dass die Mailbox des
Protokoll-Tasks überläuft. Jetzt ist die Frage: wie bestimme ich die
optimale Grösse dieser Mailbox? Je grösser sie natürlich ist, desto mehr
Memory wird verheizt.
Ausserdem stellt sich ein weiteres Problem.
Meine Messages, die ich dem Protokoll-Task sende, haben ungefähr
folgendes Format:
1 | typedef enum
| 2 | {
| 3 | msg_type_timeout,
| 4 | msg_type_rx_frame,
| 5 | msg_type_tx_frame
| 6 | } msg_type;
| 7 |
| 8 | typedef struct
| 9 | {
| 10 | msg_type type; /* type of message */
| 11 | union
| 12 | {
| 13 | struct
| 14 | {
| 15 | uint8 msg_id;
| 16 | } timeout;
| 17 |
| 18 | struct
| 19 | {
| 20 | uint8* tx_data; /* data to transmit */
| 21 | uint8 dst_addr; /* destination device */
| 22 | uint8 length; /* data length */
| 23 | } tx_data;
| 24 |
| 25 | struct
| 26 | {
| 27 | uint8* rx_data; /* received data */
| 28 | uint8 dst_addr; /* destination device */
| 29 | uint8 src_addr; /* source addr */
| 30 | uint8 length; /* data length */
| 31 | } rx_data;
| 32 | } data;
| 33 | } msg;
|
diese Messages werden dynamisch alloziiert (sowas ähnliches wie
malloc()). Anhand des msg_type kann der Protokoll-Task dann entscheiden,
welche felder der "data"-union relevant sind.
Zwar werden diese messages dynamisch alloziiert, dennoch muss ich ja
schon im Voraus genug Memory reservieren, damit ausreichend Messages
alloziiert werden können. Hier stellt sich ja dann dieselbe Frage: Wie
bestimme ich, wie viele Messages gleichzeitig maximal alloziiert sind?
Muss diese Menge grösser, kleiner oder gleich gross sein, wie die
Mailbox des Protokoll-Tasks?
Tobias Plüss schrieb:
> jeden Frame sofort abholt, dann kann es passieren, dass die Mailbox des
> Protokoll-Tasks überläuft. Jetzt ist die Frage: wie bestimme ich die
> optimale Grösse dieser Mailbox?
Was optimal ist, liegt oft im Auge des Betrachters.
In deinem Fall musst du dich fragen: Dürfen Messages verloren gehen und
wenn ja wieviele erlaube ich so im Durchschnitt.
Da es sich um ein dynamisches System handelt, sind solche Zahlen nur
sehr schwer auf theoretischem Weg zu ermitteln. Einfacher ist es, wenn
man im Programm erst mal eine Statistik mitführt und in ein paar
repräsentiven Testläufen die relevanten Zahlen ermittelt.
Optimal kann auch bedeuten: da darf nichts verloren gehen. In dem Fall
muss dann die Messagebox das maximale Aufkommen bewältigen können.
> diese Messages werden dynamisch alloziiert (sowas ähnliches wie
> malloc()).
Hoffentlich aus einem Pool und nicht über die aufwändige allgemeine
Allokierung.
> Zwar werden diese messages dynamisch alloziiert, dennoch muss ich ja
> schon im Voraus genug Memory reservieren, damit ausreichend Messages
> alloziiert werden können.
Scheint sowas wie ein Pool zu sein
> Hier stellt sich ja dann dieselbe Frage: Wie
> bestimme ich, wie viele Messages gleichzeitig maximal alloziiert sind?
So viele wie du allozierst, so viele sind alloziert.
(Klingt jetzt nach einer Milchmädchenrechnung. Aber mehr kann man da
wirklich nicht darüber sagen)
> Muss diese Menge grösser, kleiner oder gleich gross sein, wie die
> Mailbox des Protokoll-Tasks?
Wenn du weniger Messages verfügbar hast, als die Mailbox groß ist, wie
kann dann die Mailbox jemals voll werden?
Hi Karl-Heinz,
danke. Ja, ich vergass zu erwähnen, dass die dynamische alloziierung
über einen Pool geschieht! Das OS stellt da entsprechende Funktionen
bereit.
Die alloziierung muss ja schnell sein, denn schliesslich wird auch im
Interrupt Memory alloziiert, damit man eine Message senden kann (das ist
ein zulässiges Vorgehen, oder? Ich mache selten direkt was im Interrupt,
sondern sende grundsätzlich eine Message).
Optimal würde in meinem Fall heissen: Memory-Verbrauch für den Pool so
klein wie möglich, aber auch möglichst wenig verlorene Messages. Wenn
ein paar einzelne Messages verloren gehen, macht das nichts.
Also würdest du sagen, die Mailbox muss so gross sein, wie ich Messages
zur Verfügung habe - das heisst, umfasst mein Pool 100 Messages, dann
muss die Mailbox auch 100 fassen können. Klingt ansich logisch.
Allerdings kann es ja auch sein, dass ein Task eine Message alloziiert,
und noch bevor er diese senden kann, wird er von einem höher
priorisierten Task unterbrochen - dann kann er die Message nicht
absenden, somit ist es ja eigentlich auch theoretisch möglich, dass mehr
Messages alloziiert sind, als dass der Protokoll-Task überhaupt
empfangen könnte. Oder?
Später muss ich mir dann noch überlegen, was ein Task machen soll, wenn
er kein Memory bekommt. Bisher habe ich da einen Assert eingefügt. Aber
eine entsprechende Fehlerbehandlung wäre wohl angebrachter (ein Zähler,
der hochgezählt wird, und dann könne man z.B. im Debugger sehen, wenn
Messages verloren gehen?)
Tobias Plüss schrieb:
> Also würdest du sagen, die Mailbox muss so gross sein, wie ich Messages
> zur Verfügung habe - das heisst, umfasst mein Pool 100 Messages, dann
> muss die Mailbox auch 100 fassen können.
Das würde ich nicht sagen.
Ich würde sagen, wenn deine Laufzeitmessungen zeigen, dass von den 100
Messages im Durchschnitt nie mehr als zb 15 im Gebrauch sind (und in
Einzelfällen 20 oder 25), dann würde ich die Mailbox so um die 15
Messages groß machen.
Du kannst es drehen wie du willst. Ohne belastbare Zahlen kann man da
nur allgemeine Rumschwurbeln. Zahlen müssen her.
Tobias Plüss schrieb:
> Später muss ich mir dann noch überlegen, was ein Task machen soll, wenn
> er kein Memory bekommt. Bisher habe ich da einen Assert eingefügt. Aber
> eine entsprechende Fehlerbehandlung wäre wohl angebrachter (ein Zähler,
> der hochgezählt wird, und dann könne man z.B. im Debugger sehen, wenn
> Messages verloren gehen?)
Kann man machen.
Cool wäre auch eine LED, die bei einem Allokierungsfehler angeschaltet
wird und bei der nächsten gelungenen Allokierung wieder ausgeht.
Flackert die LED nur ab und zu ein bischen, bist du im grünen Bereich.
Ist die LED aber auf Dauerein, dann hat dein Programm ein Problem :-)
Tobias Plüss schrieb:
> unter anderem ein Task, der ein selber entworfenes
> Kommunikationsprotokoll ausführt.
Dann bist Du auch dafür verantwortlich, daß diese Protokoll
atombombenfest ist.
In der Regel hat ein Protokoll 2 Arten von Nachrichten. Einmal welche,
um den Datenfluß zu steuern und einmal die eigentlichen Datenpakete.
Die Steuer-Messages müssen sofort bearbeitet und beantwortet werden,
dafür ist ein extra Puffer reserviert. Und damit kann der Master
kontrollieren, ob er überhaupt was senden darf und ob das erfolgreich
empfangen wurde.
Z.B. kann der Master abfragen, wieviel Messagebuffer noch frei sind.
Der Puffer für die Datenpakete wird fest für die geplante Maximalzahl
reserviert, muß also nicht dynamisch zugewiesen werden.
Daß ein Paket verworfen werden muß, darf bei einem ordentlichen
Protokoll nie vorkommen.
Peter
Hallo Tobias,
du brauchst genau eine Mailbox, egal wie viele Tasks da reinschreiben.
Vorausgesetzt das der nachfolgende Task die Messages schneller
verarbeiten kann, als sie hereinkommen.
Soll eine Message in eine volle Mailbox gestellt werden, so wird der
Task blockiert und macht erst weiter wenn sie geleert wurde.
Also berechne die maximale Frequenz aller Ereignisse die zu einem
Eintrag führen und messe die Verarbeitungszeit deines Übergeordneten
Tasks.
Da du von einem UART-Protokoll redest, sollte die Voraussetzung erfüllt
sein.
Nur im umgekehrten Fall (Verarbeitung dauert länger) braucht mein eine
Que / FIFO oder sonstiges.
Volker
Hallo,
sorry dass es so lange gedauert hat bis ich antworten konnte.
Es scheint der Eindruck entstanden zu sein, dass ich mehrere Mailboxen
habe. Das ist natürlich nicht so, der Protokoll-Task hat nur eine
einzige Mailbox. Aber die Messages, die kommen aus einem Pool, von wo
sie dynamisch alloziiert werden.
Peter,
du hast natürlich recht, eigentlich darf es nie vorkommen, dass Pakete
verworfen werden. Wenn ich aber z.B. den Ethernet-MAC anschaue, den ich
auch Benutze bei meinem ARM-Controller, dann stelle ich fest, dass im
dümmsten Fall die Ethernet-Frames schneller eintreffen können, als der
Prozessor sie verarbeiten kann. Da kann man absolut nichts dagegen
machen, denn der Pufferspeicher für die Frames ist ja begrenzt.
Noch eine andere prinzipielle Frage.
Gewisse Daten, die ich übertrage, müssen innerhalb eines bestimmten
Timeouts beantwortet werden. Dieses Timeout realisiere ich, in dem beim
Senden der Daten ein Timer gestartet wird, der dann bei seinem ersten
Aufruf wieder an den Protokoll-Task eine entsprechende Meldung sendet
und diesen so über das Timeout informiert. Wenn die Daten vor Ablauf des
Timers empfangen und bestätigt wurden, wird der Timer angehalten,
ansonsten aber sendet er die Message, der Protokoll-Taks bemerkt dies
und kann die Daten noch einmal senden. Ist das eine zulässige Praxis,
oder gibts da bessere Methoden?
Allgemein: Gibt es eigentlich Literatur oder so, wie man solche
RTOS-Anwendungen aufbaut? Ich implementiere da immer irgendwie so nach
meinem Gusto, aber es gibt ja immer 1000 Lösungen, wie man was machen
kann. Das Timeout z.B. könnte ja auch mit einem Semaphor oder so gelöst
werden.
Tobias Plüss schrieb:
> dümmsten Fall die Ethernet-Frames schneller eintreffen können, als der
> Prozessor sie verarbeiten kann. Da kann man absolut nichts dagegen
> machen, denn der Pufferspeicher für die Frames ist ja begrenzt.
Richtig.
Allerdings ist da das ganze darüber liegende Protokoll auch darauf hin
ausgelegt. Der Empfänger muss dem Sender bestätigen, dass er das Paket
erhalten hat. Wenn er nicht bestätigt, dann schickt der Sender erneut.
Das alles bedeutet natürlich Overhead und funktioniert im Grunde nur
deshalb, weil zb TCP/IP von Anfang an darauf ausgelegt war, dass auf der
Datenverbindung von New York nach San Franzisko das direkte Kabel in der
Mitte irgendwo durch Atombombeneinschlag zerstört wurde.
Ob du dir diesen AUfwand auf deiner dann doch eher lokal begrenzten
Platine auf der sich mehrere Prozesse unterhalten ebenfalls antun
willst, na ich weiß nicht.
> Noch eine andere prinzipielle Frage.
> Gewisse Daten, die ich übertrage, müssen innerhalb eines bestimmten
> Timeouts beantwortet werden. Dieses Timeout realisiere ich, in dem beim
> Senden der Daten ein Timer gestartet wird, der dann bei seinem ersten
> Aufruf wieder an den Protokoll-Task eine entsprechende Meldung sendet
> und diesen so über das Timeout informiert. Wenn die Daten vor Ablauf des
> Timers empfangen und bestätigt wurden, wird der Timer angehalten,
> ansonsten aber sendet er die Message, der Protokoll-Taks bemerkt dies
> und kann die Daten noch einmal senden. Ist das eine zulässige Praxis,
> oder gibts da bessere Methoden?
Ist zulässig. TCP/IP macht das ja auch so. Das Problem: In der Message
muss sowas wie eine eindeutige Kennung enthalten sein, damit der
Empfänger erkennen kann: Ooops, diese Nachricht hatte ich schon
bearbeitet.
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
|