
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 *handler; char what; char arg1; char arg2; 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)) {
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
An example of a main implementation:
main is used to create threads, handlers, and other initialization.
int main( void ) {
')
Sample sourcesConclusion
Using this approach has several advantages:
- You can write several components for reuse, such as a command line interpreter or a button interrupt handler that will send messages to registered Handlers.
- each handler is described in a separate file, along with message codes
- expanding an existing handler or adding a new one is easier than creating a new thread
- since messages run on the same thread, there is no possibility of racing
- using one thread significantly reduces stack memory costs
- in the development process, Handler can be easily replaced with a finite state machine, which consists of several Handlers (one for each state)
- the time spent on processing several messages by one thread is less than if each message type was processed by a separate thread due to the lack of context switches
Message handlers (Handler) are subject to some restrictions:
- handlers should not block the flow (if blocking occurs, the entire message queue will wait, and the flow will idle)
- also processing the message should not take too much time
- It is more difficult to predict the reaction time to an event due to the fact that the processing of messages proceeds in turn, rather than psedvo-simultaneously (by time slice)
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.