📜 ⬆️ ⬇️

Processing messages in RTOS on the example of FreeRTOS

Logo_FreeRTOS Hello. This article describes one of the possible implementations of the Handler pattern for FreeRTOS, intended for exchanging messages between threads. The article is intended primarily for people using operating systems in projects for microcontrollers, DIY enthusiasts and people studying RTOS and microcontrollers.
It is assumed that the reader is familiar with the basic terms related to RTOS, such as queue and flow. You can learn more about FreeRTOS in qdx FreeRTOS posts : introduction and FreeRTOS: interprocess communication .
Those who participated in projects for microcontrollers using FreeRTOS may have come across the fact that the standard API is rather poor, which leads to the need to write additional code, which is largely repeated. In my case, there was a lack of tools for interaction between threads, namely the lack of a unified messaging system. Usually, some form of queue is used to exchange information between threads and synchronization. In this case, the type of information contained in the queue is different each time, which reduces the possibility of code reuse.
Using the unified form of a message often allows you to combine several threads into one Worker Thread, which processes received messages in turn.

The idea is similar to the use of the Handler class in Android, so the names (including the name of the fields of classes and structures) are shamelessly borrowed from there.
The approach is based on using one thread to process several types of messages, which extracts messages from the queue, calls the appropriate handler, and proceeds to the next message.
A thread is blocked on the queue, so if there are no messages, control is transferred to other threads. As soon as a new message is placed in the queue, the thread is unblocked and the message is processed. Messages can be sent to the queue by interrupt handlers, other threads, other Handlers, or to themselves.

Like any thread, Worker Thread (or Looper) can be preempted by another thread with a higher priority. Using multiple Loopers with different priorities allows for timely processing of the most important messages. Ideally, one thread with a unique priority for each Handler (unfortunately, there will always be a compromise).
Why all this is necessary

First of all, this approach provides flexibility. This allows you to create complex encapsulated objects that respond to many events. An example from recent practice is the RFID reader class, which initially assumed to work only with the command line. In consequence, Handler turned into a state machine, and messages from the command line, a timer, a motion sensor and a battery level monitor were added to messages from the command line.
Diagram


Implementation example

Consider the example of a simple program in C ++. I will not give a description of the class Thread, it suffices to mention that the heirs of Thread must override the run () method, which is the body of the thread.

Each message is a structure:

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; }; 

An example implementation of the Looper stream:

 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; } 

An example implementation of an abstract Handler (not all methods):

 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); } 

Handler implementation example:

It is necessary to override one virtual method, which the Looper will call.
 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; } } 

An example of a main implementation:

main is used to create threads, handlers, and other initialization.
 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; } 

')
Sample sources
Conclusion

Using this approach has several advantages:

Message handlers (Handler) are subject to some restrictions:

Of course, not all threads can use the proposed model. If it is necessary to provide hard real-time, it will not be possible to have several Handlers on one stream (one is possible). However, practice shows that all other threads are quite simple and practically do not require interaction with other threads. These are either streams that read something (from the serial port or USB) and send messages to the responsible handler, or streams that perform time-consuming operations (display). The basic logic of the firmware can be successfully described with the help of Handlers.
Thanks for attention.

Source: https://habr.com/ru/post/143979/


All Articles