In multi-tasking applications, all tasks except the task with lowest priority need to block, i.e. make an RTOS call that will block the task. The task will then after some time be unblocked either by an ISR, another task or the RTOS itself, depending on the blocking condition of the task. The tasks can be divided into two groups:
- Synchronous (or periodic or time-triggered) tasks, that will be unblocked with a fixed frequency, e.g. one task will be unblocked every 5th tick. Depending on if the RTOS supports this kind of functionality or not, the task will be unblocked by either the RTOS itself or by the clock tick ISR.
- Asynchronous tasks (event-triggered) tasks, that will be unblocked by either an ISR or by another task (or in some rare cases by the RTOS)
Why do you need Intertask Synchronization?
If an application only has one single task with maybe a few interrupts, task synchronization is not needed as there are no other tasks to synchronize and it also means that there is no need for an ISR to synchronize the task either, as the task will always be ready to execute.
But in a multi-tasking application with several tasks with different priorities and also a number of ISRs, then you need a way block and unblock the tasks.
Intertask Data Communication
Why do you need Intertask Data Communication?
In a single task application there will be no conflicts accessing data, but if the application also includes a number of ISRs or it is a multi-tasking application sharing data between several tasks and maybe some ISRs, this may cause a race situation, where you e.g. may have a buffer containing data and one task that is writing data to the buffer and another task that reads the data, and you do not want them to do this ”at the same time”. So each time a task wants to write or read data from the buffer it needs to have exclusive access to the buffer and no other task may access the buffer until the first task has finished using it.
Mechanisms for Intertask Synchronization
Intertask synchronization basically means that you use some kind of mechanism to unblock a task, i.e. make it ready to start to execute as soon as tasks with higher priority has finished their execution and are blocked. Different RTOS may have different or several mechanisms to handle this. The way a task should be synchronized by another task, an ISR or the RTOS itself should of course be specified in your design.
Events (or Event Flags)
Events are probably the easiest mechanism to use, but you must know exactly which task you would like send the events to, as events are part of a task. If you need to synchronize a task from several sources then events are very useful, as a task can wait for several events at the same time.
For more details read: Events
If you do not know which task you should synchronize, you can not use events. In this case a semaphore is the best mechanism to use. If you just want the synchronized task to execute once, even if you have given the semaphore several times, then you can use a binary semaphore. But if you want the task to execute exactly the same number as you have given the semaphore, then you should use a counting semaphore.
For more details read: Semaphores
Message Queues, Pipes, Mailboxes
(All these three mechanisms work very similar)
If you need to both synchronize and deliver data to another task at the same time, you should use a queue.
For more details read: Queues
Signals (UNIX like signals)
A signal will force a task to execute its signal routine, a separate routine that belongs to the task and that is not executed during the task’s normal execution. Signals are often used when for extraordinary situations in the application, e.g. when a task has caused an exception, to be able to in a controlled way delete a task or shut-down and maybe reset the system.
Mechanisms for Intertask Communication
Many times tasks need to be able to share data that should only be accessible by one task at a time, i.e. when one task wants to write or read data no other task should be able to do it until the task has finished doing that. This can solved in many ways depending on the circumstances. The solution you have to choose depends on how the data should be treated by the task that reads the data. Two simple examples will explain the different models.
- All data must be read. One task or ISR, the producer, is generating data, that should be handled by another task, the consumer. All data must be read and handled by the consumer, like e.g. in a communication protocol. As the producer may generate, at least temporarily, data faster than the consumer can handle data there is a need to be able to queue up data. In this case the producer should put data into a buffer and then send data into a message queue (mailbox or pipe). As seen above sending a message into the message queue will also synchronize the consumer task. If synchronization is not needed, e.g. the consumer task will poll for data, then an alternative solution can be to use either a ring buffer or a linked list.
- Only last inputted data needs to be read. Many times only the last or actual data is of interest, e.g. the producer task reads the actual temperatures of something or the actual status of something, and the consumer task is only interested in reading the actual values. In this case there is no need for queuing up data as historical values are of no interest, so using a message queue in this design will not work. Instead only one buffer is needed (if data can not be written or read in an atomic operation), but this buffer needs to be protected and there is also a need for synchronization between the producer and the consumer. If data just needs to be protected, then a semaphore can be used. If the producer and the consumer also need to synchronize each other, then you need two semaphores or use events. If the producer is an ISR, events and semaphores can not be used as the ISR can not use the waiting option, which means that even if the task is reading data, there is no way to stop the ISR from starting to write data into the buffer. In this case the ISR can inform the task that data is ready for reading by either using a semaphore or an event. And the task has to disable interrupts when it is reading data. If this is to time consuming, two buffers can be used instead and the ISR and task can toggle between these two buffers.
Conditional variables is a very sophisticated way of synchronizing, probably not used so often, but for some design problems it may be the perfect solution. To be able to use a condition variable it needs to be associated with a mutex semaphore. In principle you can have one or several tasks that wait for a variable to be set to a specific value. One example that can be easily solved with a conditional variable is when you have a data base containing some kind of data, and it is acceptable that many tasks read data at the same time, but when a task wants to write into the data base it needs exclusive access to the data base.