FreeRTOS-Warteschlangen und IPC-Verwirrung

Ich kämpfe mit meinem ersten richtigen freeRTOS-Projekt. Ich verwende im Grunde einen ATmega328P-Mikrocontroller und ein nRF24L01 + -Funkgerät als "Knoten". Ich habe zwei dieser Knoten und benutze sie, um miteinander zu sprechen. Ich habe freeRTOS erfolgreich auf meinen Mikrocontroller portiert, sodass ich Tasks verwenden kann, um LEDs zu blinken und mit meinem Funktreiber, den ich auch geschrieben habe, sehr einfache Daten zu senden/empfangen. Der Teil, mit dem ich zu kämpfen habe, ist, wie ich meinen Code und meine Aufgaben am besten strukturiere, um so etwas wie eine Netzwerkschicht über meinem Funktreiber zu erstellen.

Hier waren meine ersten Gedanken:

  1. Wenn der Data-Ready-Interrupt des Radios ausgelöst wurde, würde ich eine globale boolesche Variable setzen, um meiner "radio_task()"-Funktion zu signalisieren, Daten aus dem FIFO des Radios zu lesen.
  2. Ich hätte eine Funktion namens "radio_task()", die eigentlich KEINE freeRTOS-Aufgabe wäre, die die ausgehende Warteschlange überprüft und meinen Funktreiber (in Kombination mit freeRTOS ReceiveFromQueue()) verwendet, um das vordere Paket in die Warteschlange zu senden, wenn da ist einer. Diese Funktion würde auch den globalen booleschen Wert prüfen und ein Paket aus dem FIFO des Funkgeräts lesen, falls es eines gäbe. Es würde dann dieses gelesene Paket an die eingehende Warteschlange sendenToQueue().
  3. Ich hätte dann ein tatsächliches freeRTOS namens nwk_task(), das eine Zustandsmaschine hätte, um verschiedene Netzwerkwartungen und -wartungen usw. durchzuführen. Innerhalb der Zustandsmaschine gäbe es einige Aufrufe einer nwk_send()-Funktion, die sendToQueue() für die ausgehende Warteschlange wäre . Ebenso müssten einige ReceiveFromQueue()-Aufrufe in der eingehenden Warteschlange vorhanden sein, die alle von mir empfangenen Pakete verarbeiten würden. Dann würde diese freeRTOS-Aufgabe schließlich meine Funktion radio_task() aufrufen
  4. Ich hätte möglicherweise andere Tasks laufen, die Sensordaten überwachen und vielleicht auch nwk_send() aufrufen würden, wenn bestimmte Ereignisse basierend auf Sensordaten usw. auftreten.

Ich denke, das ist kein schrecklicher Ansatz, aber ich stoße auf ein paar Schwierigkeiten, von denen ich dachte, dass ich fragen würde. Fragen:

  1. Ich habe eine Typedef für mein nwk_packet_t erstellt, die eine Struktur ist, die aus einer Adresse, einem statisch definierten Array, das die maximale Größe für ein Funkpaket darstellt, und einer data_length besteht, die angibt, wie viel des Arrays tatsächlich für dieses Paket verwendet wird. Wie erstelle ich eine freeRTOS-Warteschlange, die meine nwk_packet_t enthält? Gibt es auch eine bessere Möglichkeit, mein nwk_packet_t zu erstellen, als das Array mit statischer Größe zu verwenden? Ich habe über die Verwendung eines Zeigers nachgedacht, aber dann muss ich sicherstellen, dass das Array, das ich sende, nicht überschrieben wird, bis es tatsächlich aus der Warteschlange gezogen wird, oder?
  2. Ich bin verwirrt darüber, wo die Blockierung auftreten würde / muss. Durch die Verwendung des globalen booleschen Flags in meiner datenbereiten ISR erlaube ich, dass Interrupts häufiger auftreten, aber bedeutet das, dass ich portENTER_CRITICAL() verwenden muss, wenn ich tatsächlich Daten aus den Warteschlangen pushe/pulle?
  3. Wie würde ein erfahrener Embedded-Programmierer die Aufgaben und Warteschlangen strukturieren, um eine Netzwerkschicht über meinem grundlegenden Funktreiber zu ermöglichen?

Entschuldigung für mein großes Unverständnis zu einigen dieser Themen, dies ist mein erster Schritt in die Programmierung auf Betriebssystemebene. Alles andere, was ich getan habe, war sehr niedriges Niveau und grundlegendes C. Danke für Ihre Hilfe!

BEARBEITEN: Eine Sache, die ich vergessen habe zu erwähnen, ist, dass mein Radio einige Hardware-Bestimmungen zum Bestätigen hat. Sie können eine Nachricht so einstellen, dass sie mit einer bestimmten Anzahl von Wiederholungen automatisch bestätigt wird, und die Hardware wird all das handhaben. Es kann jedoch immer noch eine Nachricht fehlschlagen, die Sie vom Radio lesen können, oder das Radio einen Interrupt auslösen lassen, wenn dies geschieht. Ich hätte also gerne eine Vorstellung von einem Nachrichtenerfolg und einem Nachrichtenfehler, der eine Art Rückruf ausführt, aber ich habe keine Ahnung, wie ich das in meine Struktur einbauen soll. Irgendwelche Ideen dazu würden dazu passen, Sachen aus den Warteschlangen usw. zu entfernen?

Danke noch einmal.

leider ist der code nicht offen....

Antworten (2)

Kommunikationsstapel :(

Kommunikationsstapel in einfachem C :((

Dies ist eine Zusammenfassung, wie ich es mache, obwohl es sicherlich nicht der einzige Weg ist:

Die App erstellt zunächst ein „generalPool“-Array von Pufferstrukturen (BS:) mit fester Größe. Während des Laufs werden niemals mehr Puffer zugewiesen und keine Puffer freigegeben. Die BS hat Platz für Daten, Datenlänge, nächste/vorherige Indexbytes und eine „Befehls“-Aufzählung, die beschreibt, was der Puffer ist (und andere Dinge, aber das trübt das Problem). Indizes zu diesem Array werden für alle Inter-Thread- und Treiberkommunikationen verwendet (ich verwende Indizes in Bytegröße anstelle von Zeigern, da weniger als 256 BS vorhanden sind und ich RAM-Einschränkungen habe). Die nächsten/vorherigen Bytes werden initialisiert, um eine doppelt verknüpfte Liste zu bilden, und die Aufrufe zum Abrufen/Einfügen eines Index werden durch einen Mutex geschützt.

Inter-Thread-Kommunikationen werden durchgeführt, indem ein BS-Index aus dem generalPool abgerufen, nach Bedarf geladen, die Aufzählung gesetzt und der Index dann in eine Producer-Consumer-Warteschlange verschoben wird. Der Thread am anderen Ende entfernt den Index aus der Warteschlange und schaltet typischerweise die Aufzählung ein, um die BS-Nachricht zu verarbeiten. Nach der Bearbeitung kann der Consumer-Thread die BS neu bündeln oder zur weiteren Bearbeitung an einer anderen Stelle in die Warteschlange stellen (z. B. Logger).

Da die BS diese nächsten/vorherigen Bytes hat, benötigt die Erzeuger-Verbraucher-Warteschlangenklasse keinen eigenen Speicherplatz – sie hat erste und letzte Bytes und kann so die BS auf ähnliche Weise wie der Pool miteinander verbinden.

OK, jetzt Treiber:

Ich habe die Interrupt-Verschachtelung deaktiviert, sodass jeweils nur ein Interrupt ausgeführt werden kann. Dadurch kann ich einen BS-Index „DriverQueue“ erstellen. Die DriverQueue verfügt über tatsächlichen Speicherplatz für die Indexbytes – sie verwendet nicht die Next/Prev-Links. Dadurch können BS-Indizes sicher an einem Ende hinzugefügt und am anderen entfernt werden, und zwar durch einen beliebigen Interrupt und einen Thread.

Ich habe eine 'CommsPool' DriverQueue. Dies wird beim Start mit einigen BS vorbelegt, die aus dem generalPool extrahiert wurden. Diese BS werden für empfangene Daten verwendet.

Ich habe eine 'commsTx' DriverQueue für jeden tx-Interrupt. Auf ihnen werden ausgehende Daten in eine Warteschlange gestellt.

Ich habe eine 'commsRx' DriverQueue für alle RX-Interrupts. Eingehende Daten werden darauf in eine Warteschlange gestellt.

Ein 'commsThread' verarbeitet die Kommunikation auf höherer Ebene, indem er eine Zustandsmaschine initialisiert und betreibt, ähnlich Ihrer Idee. Im Ruhezustand wartet es auf ein 'CommsEvent'-Semaphor.

Die rx-Interrupts erhalten BS aus dem CommsPool, laden sie mit Daten von der Hardware, setzen die Befehlsaufzählung auf 'RxX' (X ist die Kommunikationskanal-/Interrupt-ID-Nummer), schieben den BS-Index in die gemeinsame commsRx-Warteschlange und signalisieren Kommunikationsereignis.

Die tx-Interrupts erhalten BS von ihrem eigenen, privaten commsTx, laden die Daten in die Hardware, setzen die Befehlsaufzählung auf 'TxUsed', schieben den BS-Index in die gemeinsame commsRx-Warteschlange und signalisieren CommsEvent.

Der commsThread ist für die Verwaltung aller E/A verantwortlich. Es hat eine 'commsRq'-Eingabewarteschlange für Kommunikationsanforderungs-BS von anderen Threads. Dies ist jedoch keine blockierende Warteschlange - nur Thread-sicher. Es blockiert nicht, da der commsThread auch die commsEvent-Signale von den Interrupt-Handlern verarbeiten muss.

Jeder Thread, der Dinge kommunizieren möchte, lädt einen BS mit geeigneten Daten und Befehlen, stellt sie in die Warteschlange von commsRq und signalisiert CommsEvent, wodurch der commsThread geweckt wird.

Der Commsthread weiß nicht, warum er aufgeweckt wurde, also fragt er zuerst die commsRx-Warteschlange ab, um zu sehen, ob darin ein BS ist. Wenn ja, verarbeitet es es - wenn es ein 'RxX' ist, verarbeitet es es durch seinen State-Engine-Code/Daten, wenn es ein 'TxUsed' ist, überprüft es zuerst den CommsPool, um zu sehen, ob es 'aufgefüllt' werden muss, und pusht es dort, wenn es nötig ist, andernfalls schiebt es es zurück in den generalPool, um es woanders wiederzuverwenden.

Sobald der commsThread die Treiberwarteschlangen entsprechend gehandhabt hat, fragt er die commsRq-Warteschlange ab, um zu sehen, ob es neue Kommunikationsanforderungen von anderen Threads gibt. Wenn dies der Fall ist, wird die Anforderung aus der Warteschlange entfernt und durch ihren Zustandsmaschinencode/Daten verarbeitet.

Danach prüft der commsThread erneut, um zu sehen, ob eine Auffüllung des CommsPools erforderlich ist, und wenn der CommsPool nicht voll ist, füllt er ihn mit mehr BS aus dem allgemeinenPool auf.

Der commsThread springt dann zurück, um erneut auf das Semaphor zu warten. Die Semaphore stellt sicher, dass der commsThread genau so oft ausgeführt wird, wie es erforderlich ist, um alle Eingaben von anderen Threads und den Interrupt-Handlern zu verarbeiten, nicht mehr und nicht weniger. Wenn der Thread jemals aufwacht und nichts zu tun findet, ist dies ein Fehler.

So mache ich es jedenfalls :) Es bietet einen guten Durchsatz und eine effiziente RAM-Nutzung. Erzeuger-Verbraucher-Warteschlangen zwischen Threads benötigen keinen internen Speicher. Nur ein Thread (und damit nur ein RAM-verbrauchender Stack:) ist für die gesamte Unterbrechungsverwaltung und Tx/Rx-Datenverarbeitung erforderlich. Nach der Initialisierung sind keine mallocs/frees erforderlich. Es gibt kein geschäftiges Warten oder die Notwendigkeit einer periodischen Überprüfung von Flags. Es ist kein Kopieren der Daten erforderlich (außer in/aus Hardware – unvermeidlich). Timeout-Aktionen können entweder durch ein zeitgesteuertes Warten auf die Semaphore (bevorzugt, wenn Ihr Betriebssystem dies unterstützt) oder durch das periodische 'Einfügen' eines 'TimeTick'-BS in die Eingabewarteschlange von einem anderen Thread behandelt werden. Zurückgegebene BS können zur Debug-Anzeige einfach beispielsweise zu einem Logger oder Terminal "umgeleitet" werden, bevor sie an den allgemeinen Pool zurückgegeben werden.

Wie auch immer Sie dies tun, Sie sollten in Betracht ziehen, zu C++ zu wechseln. C wird nur unordentlich für alles andere als einfachen geradlinigen Code. C++ ermöglicht beispielsweise die Implementierung der BS als Klasseninstanzen mit Methoden zum Einströmen von Daten und zum "automatischen Erweitern" einer BS durch Abrufen und Verknüpfen einer anderen BS, wenn eine BS voll wird, wodurch eine "zusammengesetzte" Datennachricht erzeugt wird.

Ich habe einiges weggelassen. Vielleicht kennen Sie zum Beispiel bereits das Elend von TX-Interrupts - nachdem der TX im Leerlauf war, müssen sie oft "vorbereitet" werden, indem die ersten Bytes in einen FIFO geladen werden, damit der TX-Interrupt erneut startet :(

Auch Hinweis: Meine UART-Debug-Terminal-Eingabeaufforderung sieht aus wie 'A:96>'. Die Zahl (hier 96) ist die aktuelle Anzahl von BS im allgemeinen Pool. Wenn diese Zahl zu sinken beginnt, weiß ich, dass ich ein Leck habe :)

Interessant, ich muss das noch mehrmals lesen und darüber nachdenken, bevor ich sinnvolle Folgefragen stellen kann. +1 Vielen Dank für Ihre Antwort.
Ja. Vielleicht sollte ich es auch mal lesen :)

Eine andere Möglichkeit, es mit mehr FreeRTOS-Elementen zu tun ... schließlich, wenn Sie FreeRTOS umgehen, warum sollten Sie es dann überhaupt verwenden?

Warum sollte Ihre radio_task nicht eine FreeRTOS-Aufgabe sein? Es kann ein Semaphor blockieren, und Ihr ISR kann xSemaphoreGiveFromISR() zum Entsperren verwenden. Siehe http://www.freertos.org/a00113.html Es kann dann mit Warteschlangen für TX- und RX-Daten verbunden werden.

Sie sollten wahrscheinlich auch eine separate Aufgabe haben, um mit TX- und RX-Warteschlangen umzugehen. Sie würden die Warteschlangen blockieren, also sitzen sie im Grunde da, bis es ein Paket zu erledigen gibt. Da dies wahrscheinlich asynchron zueinander ist, warum nicht 2 Aufgaben haben. Sie müssen wahrscheinlich den Funkstatus verfolgen, damit Ihre TX- und RX-Tasks einfach rotieren können, bis sie verfügbar sind Kontrolle während des Wartens zurück). RX und TX als getrennte Tasks zu behalten, bedeutet, dass Sie den Status des anderen nicht verfolgen müssen, es vereinfacht den Programmablauf. Zum Beispiel blockieren Sie für TX das Warten auf etwas Neues in der TX-Warteschlange, ziehen es dann ab, massieren es bei Bedarf, drehen es dann, bis das Radio zum Senden bereit ist, senden es dann und geben dann den Paketspeicher frei. dann zurück nach oben, um die Warteschlange zu blockieren. RX muss überhaupt nicht überprüft werden. Dasselbe gilt für den RX-Thread, sehr einfacher Ablauf.

Ihre Datenpaketwarteschlangen wären eigentlich Warteschlangen von Zeigern auf Daten, die Sie mit pvPortMalloc() und vPortFree() von FreeRTOS mallocieren würden. Ja, es ist C, Sie müssen sich selbst um die malloc/frees kümmern. Nicht so schwer ... aber stellen Sie sicher, dass Sie einen der Heap-Manager verwenden, der kostenlos unterstützt (2, 3 oder 4, nicht 1 - Sie können einen der 4 Heap-Manager anschließen). Wenn Ihre Pakete immer dieselbe (maximale) Größe haben, wird eine Fragmentierung verhindert.

So habe ich FreeRTOS auf einem CC430 für ein einfaches drahtloses Mesh-Netzwerk verwendet.

Das ist sehr interessant, danke für dein Feedback, Marc. Ich wollte dieses Projekt schon seit einiger Zeit wieder aufgreifen und Ihre Ideen sorgen für neue Motivation. Ich werde einige der von Ihnen angesprochenen FreeRTOS-Konzepte untersuchen. Haben Sie den Code für Ihr Projekt irgendwo verfügbar? Es wäre wirklich hilfreich, ein Beispiel zu sehen, wie Sie das alles gemacht haben.