Verarbeiten von Nachrichten in RTOS am Beispiel von FreeRTOS

Logo_FreeRTOSGuten Tag. Dieser Artikel beschreibt eine mögliche Implementierung des Handler-Musters für FreeRTOS für den Nachrichtenaustausch zwischen Threads. Der Artikel richtet sich in erster Linie an Personen, die Betriebssysteme in Projekten für Mikrocontroller, Heimwerker und Studierende von RTOS und Mikrocontrollern einsetzen.
Es wird davon ausgegangen, dass der Leser mit den grundlegenden Begriffen in Bezug auf RTOS vertraut ist, wie Warteschlange und Ablauf. Weitere Informationen zu FreeRTOS finden Sie in den qdx- Beiträgen FreeRTOS: Einführung und FreeRTOS: Interprozesskommunikation .
Diejenigen, die an Projekten für Mikrocontroller mit FreeRTOS teilgenommen haben, sind möglicherweise auf die Tatsache gestoßen, dass die Standard-API eher schlecht ist, was dazu führt, dass zusätzlicher Code geschrieben werden muss, der sich größtenteils wiederholt. In meinem Fall fehlten Tools für die Interaktion zwischen Flows, nämlich das Fehlen eines Unified Messaging-Systems. In der Regel wird die eine oder andere Form der Warteschlange verwendet, um Informationen zwischen Threads und der Synchronisation auszutauschen. Darüber hinaus ist die Art der in der Warteschlange enthaltenen Informationen jedes Mal anders, wodurch die Möglichkeit der Wiederverwendung von Code verringert wird.
Wenn Sie ein einheitliches Nachrichtenformular verwenden, können Sie häufig mehrere Threads in einem Worker-Thread kombinieren, der empfangene Nachrichten in einer Warteschlange verarbeitet.

Die Idee ähnelt der Verwendung der Handler-Klasse in Android, sodass die Namen (einschließlich der Namen der Klassenfelder und -strukturen) unverschämt von dort übernommen werden.
Der Ansatz basiert auf der Verwendung eines einzelnen Threads zum Verarbeiten mehrerer Nachrichtentypen, der Nachrichten aus der Warteschlange extrahiert, den entsprechenden Handler aufruft und mit der nächsten Nachricht fortfährt.
Der Thread ist in der Warteschlange blockiert. Wenn also keine Nachrichten vorhanden sind, wird die Steuerung auf andere Threads übertragen. Sobald eine neue Nachricht in die Warteschlange gestellt wird, wird der Thread entsperrt und die Nachricht verarbeitet. Nachrichten können von Interrupt-Handlern, anderen Threads, anderen Handlern oder für sich selbst in die Warteschlange gestellt werden.

Wie jeder Thread kann auch Worker Thread (oder Looper) durch einen anderen Thread mit höherer Priorität ersetzt werden. Durch die Verwendung mehrerer Loopers mit unterschiedlichen Prioritäten können die wichtigsten Meldungen zeitnah verarbeitet werden. Idealerweise ein Thread mit einer eindeutigen Priorität für jeden Handler (leider wird es immer einen Kompromiss geben).
Warum ist das alles notwendig?

Erstens bietet dieser Ansatz Flexibilität. Auf diese Weise können Sie komplexe gekapselte Objekte erstellen, die auf viele Ereignisse reagieren. Ein Beispiel aus der jüngsten Praxis ist die Klasse der RFID-Lesegeräte, die ursprünglich nur mit der Befehlszeile arbeiten sollte. Infolgedessen verwandelte sich der Handler in einen Zustandsautomaten, und Nachrichten aus dem Ordner, dem Zeitgeber, dem Bewegungssensor und der Batteriestandsüberwachung wurden den Nachrichten über die Befehlszeile hinzugefügt.
Diagramm


Implementierungsbeispiel

Betrachten Sie das Obige mit einem einfachen C ++ - Programm. Ich werde die Thread-Klasse nicht beschreiben, es reicht zu erwähnen, dass die Nachkommen von Thread die run () -Methode überschreiben sollten, die den Hauptteil des Threads darstellt.

Jede Nachricht ist eine Struktur:

struct MESSAGE {
    /** Handler responsible for handling this message */ 
    Handler *handler; 
    /** What message is about */ 
    char what; 
    /** First argument */ 
    char arg1; 
    /** Second argument */ 
    char arg2; 
    /** Pointer to the allocated memory. Handler should cast to the proper type, 
     * according to the message.what */ 
    void *ptr; 
};

Beispiel für die Implementierung eines Looper-Streams:

Looper::Looper(uint8_t messageQueueSize, const char *name, unsigned short stackDepth, char priority): Thread(name, stackDepth, priority) {
    messageQueue = xQueueCreate(messageQueueSize, sizeof(Message));
}
void Looper::run() {
    Message msg;
    for (;;) {
        if (xQueueReceive(messageQueue, &msg, portMAX_DELAY)) {
            // Call handleMessage from the handler
            msg.handler->handleMessage(msg);
        }
    }
}
xQueueHandle Looper::getMessageQueue(){
    return messageQueue;
}

Ein Beispiel für die Implementierung von abstract Handler (nicht alle Methoden):

Handler::Handler(Looper *looper) {
    messageQueue = looper->getMessageQueue();
}
bool Handler::sendMessage(char what, char arg1, char arg2, void *ptr) {
    Message msg;
    msg.handler = this;
    msg.what = what;
    msg.arg1 = arg1;
    msg.arg2 = arg2;
    msg.ptr = ptr;
    return xQueueSend(messageQueue, &msg, 0);
}

Beispiel für die Implementierung eines Handlers:

Sie müssen eine virtuelle Methode überschreiben, die Looper aufruft.
void ExampleHandler::handleMessage(Message msg) {
#ifdef DEBUG
    // Лог особенно актуален для сложных классов, например конечных автоматов
    debugTx->putString("ExampleHandler.handleMessage(");
    debugTx->putInt(msg.what, 10);
    debugTx->putString(")\n");
#endif
    TxBuffer *responseTx;
    switch (msg.what) {
    case EVENT_RUN_SPI_TEST:
        responseTx = (TxBuffer*)msg.ptr;
        testSpi();
        // Пример использования прикрепленного указателя
        responseTx->putString("Some response\n");
        break;
    case EVENT_BLINK:
         // Пример использования аргументов сообщения
        led->blink(msg.arg1, msg.arg2);
        break;
    }
}

Eine beispielhafte Implementierung von main:

main wird zum Erstellen von Threads, Handlern und anderen Initialisierungen verwendet.
int main( void ) {
    // Создание потока
    Looper looper = Looper(10, "LPR", 500, configNORMAL_PRIORITY);
    // Создание на нем обработчика
    ExampleHandler exampleHandler = ExampleHandler(&looper);
    // Создание интерпретатора команд
    CommandInterpreter interpreter = CommandInterpreter();
    // Регистрация обработчика. Теперь когда интерпретатор
    // получит команду Strings_SpiExampleCmd, он пошлет в
    // обработчик сообщение с темой EVENT_RUN_SPI_TEST
    interpreter.registerCommand(Strings_SpiExampleCmd, Strings_SpiExampleCmdDesc, &exampleHandler, EVENT_RUN_SPI_TEST);
    interpreter.registerCommand(Strings_BlinkCmd, Strings_BlinkCmdDesc, &exampleHandler, EVENT_BLINK);
    vTaskStartScheduler();
    /* Should never get here, stop execution and report error */
    while(true) ledRGB.set(PINK);
    return 0;
}


Beispielquellen
Fazit

Dieser Ansatz bietet mehrere Vorteile:
  • Sie können mehrere Komponenten zur Wiederverwendung schreiben, z. B. einen Befehlszeileninterpreter oder einen Interrupt-Handler für Schaltflächen, die Nachrichten an registrierte Handler senden
  • Jeder Handler wird zusammen mit den Nachrichtencodes in einer separaten Datei beschrieben
  • Das Erweitern eines vorhandenen Handlers oder das Hinzufügen eines neuen ist einfacher als das Erstellen eines neuen Threads
  • Da Nachrichten auf demselben Thread ausgeführt werden, besteht keine Möglichkeit eines Rennens
  • Die Verwendung eines einzelnen Threads reduziert die Speicherkosten pro Stapel erheblich
  • Während des Entwicklungsprozesses können Handler einfach durch einen Zustandsautomaten ersetzt werden, der aus mehreren Handlern besteht (einer für jeden Zustand).
  • Die Zeit, die für die Verarbeitung mehrerer Nachrichten in einem Thread aufgewendet wird, ist kürzer als wenn jeder Nachrichtentyp in einem separaten Thread verarbeitet wurde, da keine Kontextwechsel vorhanden sind

Message Handler (Handler) haben einige Einschränkungen:
  • Handler sollten den Stream nicht blockieren. (Wenn der Stream blockiert wird, wartet die gesamte Nachrichtenwarteschlange und der Stream ist inaktiv.)
  • Die Verarbeitung von Nachrichten sollte nicht zu lange dauern
  • Es ist schwieriger, die Reaktionszeit auf das Ereignis vorherzusagen, da die Nachrichtenverarbeitung abwechselnd und nicht pseudo-simultan (nach Zeitscheibe) erfolgt.

Natürlich können nicht alle Threads das vorgeschlagene Modell verwenden. Wenn es erforderlich ist, eine harte Echtzeit zu gewährleisten, können nicht mehrere Handler auf demselben Thread ausgeführt werden (einer ist möglich). Die Praxis zeigt jedoch, dass alle anderen Abläufe recht einfach sind und praktisch keine Interaktion mit anderen Abläufen erfordern. Hierbei handelt es sich entweder um Streams, die etwas lesen (von einem seriellen Anschluss oder USB) und Nachrichten an den zuständigen Handler senden, oder um Streams, die zeitaufwändige Vorgänge ausführen (Anzeige). Die Hauptlogik der Firmware kann mit Hilfe von Handlern erfolgreich beschrieben werden.
Vielen Dank für Ihre Aufmerksamkeit.

Jetzt auch beliebt: