RTOS 02 - InterTask Communication
In this second part of the series, I will elaborate on inter-task communication in RTOS. If you didn’t read part one have a look at it if you want to know the basics of RTOS in a basic way.
Let’s go to our good old restaurant. So now there are three workers to take care of the works. Person1 takes care of buying ingredients from the local market. Person2 prepares the meals (Rice and Pizza). Person3 may deal with customers and serving. At the moment they work independently. One person doesn’t care about another person’s work. He will keep doing what he is supposed to do. But that may not be the real case. After bringing items Person1 needs to inform Person2 that he bought these things and that he has stored them on the shelves. Person2 needs to inform Person3 about what he prepared each day and how much is left. So there is a need for a better communication mechanism. If Person1 didn’t inform Person2 about his updates, Person2 may stop cooking and lookout for a possible update from Person1. That may lead to the deadlock as I explained in the previous article. Else, even before Person2 has finished preparing, Person3 may go and serve customers with half-cooked rice and then customers may never come again. That may lead to the race condition as I explained in the previous article.
Intertask communication mechanisms
There are many intertask mechanisms used in FreeRTOS.
- Task notifications
- Mutex
- Semaphore
- Queue
- EventGroups
There are two main reasons why we need to have better communication.
- Notify different tasks about one’s context (task-related information) or to provide some data
- To access shared data
I will use task notifications and mutex to show you how the above two are achieved using the simplest possible example.
Task Notifications
Events can be sent to a task using an intermediary object, like a local seller between farmer and customer. Examples of such objects are mutex, semaphores, queues, and event groups. Task notifications are a method of sending an event directly to a task without the need for such an intermediary object. A notification sent to a task can optionally perform an action, such as update, overwrite, or increment the task’s notification’s value. In that way, task notifications can be used to send data to a task.
As you can see here the task3 will only trigger when task2 notifies him. Person3 will only serve after Person2 says that meals are ready. Task3 has a task handle. Other tasks can use this handle to send notifications. Those who know the task handler can even suspend task3 from further executions.
A notification sent to a task will remain pending until it is cleared by the task calling xTaskNotifyWait() or ulTaskNotifyTake(). If the task was already in the Blocked state to wait for a notification when the notification arrives, then the task will automatically be removed from the Blocked state (unblocked) and execute.
Here I will show how multiple notifications can be sent using task notifications.
Using different bits of the unsigned 32-bit integer (as defined in the ESP-IDF), tasks may send multiple notifications. The task who is waiting for such notifications may use a switch statement to get the notification. So once Person2 prepared rice he will inform Person3 that rice is available. Now, Person3 can inform customers that rice is available and serve them. The same goes for pizza.
There are many different parameters that are important but since it is not my focus to discuss each APIs, I will leave it to you to refer further if you are interested. however, if one task needs to send some large structured data to another, the queue mechanism should be used.
Mutex
Mutex means mutual exclusion among tasks when they access a shared resource. A mutex can be implemented as a token. When a task seeks to access a resource shared with another one, it has to obtain the token to do so. This is to avoid the race condition. In this case, using the token to allow only the task having the token to access the shared data can make things going without issues. This shows the structure of a program where task1 and task2 want to reach out to shared data (In this case simply reaching for a common function). This time it is more of a practical example.
xSemaphoreTake/Give APIs are used to obtain the token and release the token. I called task2 first. So as you can see first task2 will acquire the common function and print humidity. Once task2 is done with that, it will release the flag. Now, task1 can use the above function and while task2 waits, task1 uses it twice because task2 waits double the time of task1. As both of them release the flags once the job is done, the program will never go to the deadlock condition.
That’s it for now. I hope you enjoyed this article. Queues, Semaphores, and event groups are actually used in different ways to achieve the above requirements. If you are interested I recommend reading some API guides to learn about it. As I explained at the end of the first article you can use RTOS extensively for many complex programs. When the requirements are rather simple super-loop approach works fine. However, there are many other interesting things such as memory allocation using static or dynamic memory, software timers, APIs to debug using JTAGs, and many more.
Thank you for reading and stay tuned!.