Inter-task Communication
[Getting Started]

FreeRTOS.org provides: The FreeRTOS eBook provides additional information on queues, binary semaphores, mutexes and counting semaphores, along with simple worked examples in a set of accompanying example projects.



Queues

Queues are the primary form of intertask communications. They can be used to send messages between tasks, and between interrupts and tasks. In most cases they are used as thread safe FIFO (First In First Out) buffers with new data being sent to the back of the queue, although data can also be sent to the front.

Queues can contain 'items' of fixes size - with the size of each item and the maximum number of items the queue can hold both being defined when the queue is created.

Items are placed into a queue by copy, not by reference. It is therefore preferable to keep the size of each item placed into the queue to a minimum. Placing items into a queue by copy greatly simplifies your application design as two tasks cannot access the data simultaneously. The queue takes care of all mutual exclusion issues for you.

If you wish to queue large items then it might be preferable to instead queue a pointer to each item - but when doing so care must be taken to ensure that your application clearly defines which task and/or interrupt is the 'owner' of the data.

Queue API functions permit a block time to be specified. The block time indicates the maximum number of 'ticks' that a task should enter the Blocked state to either wait for data to become available on a queue (in the case that a task is reading from a queue and the queue is already empty), or wait for space to become available on a queue (in the case where a task is writing to a queue, but the queue is already full). Should more than one task block on the same queue then the task with the highest priority will be the task that is unblocked first.

See the Queue Management section of the user documentation for a list of queue related API functions. Searching the files in the FreeRTOS/Demo/Common/Minimal directory will reveal multiple examples of their usage. Note that interrupts must NOT use API functions that do not end in "FromISR".


Writing to and reading from a queue. In this example the queue was created to hold 5 items, and the queue never becomes full.



Binary Semaphores

Binary semaphores are used for both mutual exclusion and synchronisation purposes.

Binary semaphores and mutexes are very similar but have some subtle differences: Mutexes include a priority inheritance mechanism, binary semaphores do not. This makes binary semaphores the better choice for implementing synchronisation (between tasks or between tasks and an interrupt), and mutexes the better choice for implementing simple mutual exclusion. The description of how a mutex can be used as a mutual exclusion mechanism holds equally for binary semaphores. This sub section will only describe using binary semaphores for synchronisation.

Semaphore API functions permit a block time to be specified. The block time indicates the maximum number of 'ticks' that a task should enter the Blocked state when attempting to 'take' a semaphore, should the semaphore not be immediately available. If more than one task blocks on the same semaphore then the task with the highest priority will be the task that is unblocked the next time the semaphore becomes available.

Think of a binary semaphore as a queue that can only hold one item. The queue can therefore only be empty or full (hence binary). Tasks and interrupts using the queue don't care what the queue holds - they only want to know if the queue is empty or full. This mechanism can be exploited to synchronise (for example) a task with an interrupt.

Consider the case where a task is used to service a peripheral. Polling the peripheral would be wasteful of CPU resources, and prevent other tasks from executing. It is therefore preferable that the task spends most of its time in the Blocked state (allowing other tasks to execute) and only execute itself when there is actually something for it to do. This is achieved using a binary semaphore by having the task Block while attempting to 'take' the semaphore. An interrupt routine is then written for the peripheral that just 'gives' the semaphore when the peripheral requires servicing. The task always 'takes' the semaphore (reads from the queue to make the queue empty), but never 'gives' it. The interrupt always 'gives' the semaphore (writes to the queue to make it full) but never takes it. The source code provided on the xSemaphoreGiveFromISR() documentation page should make this clearer.

Task prioritisation can be used to ensure peripherals get services in a timely manner - effectively generating a 'differed interrupt' scheme. An alternative approach is to use a queue in place of the semaphore. When this is done the interrupt routine can capture the data associated with the peripheral event and send it on a queue to the task. The task unblocks when data becomes available on the queue, retrieves the data from the queue, then performs any data processing that is required. This second scheme permits interrupts to remain as short as possible, with all post processing instead occurring within a task.

See the Semaphores/Mutexes section of the user documentation for a list of semaphore related API functions. Searching the files in the FreeRTOS/Demo/Common/Minimal directory will reveal multiple examples of their usage. Note that interrupts must NOT use API functions that do not end in "FromISR".


Using a semaphore to synchronise a task with an interrupt. The interrupt only ever 'gives' the semaphore, while the task only ever 'takes' the semaphore.



Counting Semaphores

Just as binary semaphores can be thought of as queues of length one, counting semaphores can be thought of as queues of length greater than one. Again, users of the semaphore are not interested in the data that is stored in the queue - just whether or not the queue is empty or not.

Counting semaphores are typically used for two things:

  1. Counting events.

    In this usage scenario an event handler will 'give' a semaphore each time an event occurs (incrementing the semaphore count value), and a handler task will 'take' a semaphore each time it processes an event (decrementing the semaphore count value). The count value is therefore the difference between the number of events that have occurred and the number that have been processed. In this case it is desirable for the count value to be zero when the semaphore is created.

  2. Resource management.

    In this usage scenario the count value indicates the number of resources available. To obtain control of a resource a task must first obtain a semaphore - decrementing the semaphore count value. When the count value reaches zero there are no free resources. When a task finishes with the resource it 'gives' the semaphore back - incrementing the semaphore count value. In this case it is desirable for the count value to be equal the maximum count value when the semaphore is created.

See the Semaphores/Mutexes section of the user documentation for a list of semaphore related API functions. Searching the files in the FreeRTOS/Demo/Common/Minimal directory will reveal multiple examples of their usage. Note that interrupts must NOT use API functions that do not end in "FromISR".



Mutexes

Mutexes are binary semaphores that include a priority inheritance mechanism. Whereas binary semaphores are the better choice for implementing synchronisation (between tasks or between tasks and an interrupt), mutexes are the better choice for implementing simple mutual exclusion (hence 'MUT'ual 'EX'clusion).

When used for mutual exclusion the mutex acts like a token that is used to guard a resource. When a task wishes to access the resource it must first obtain ('take') the token. When it has finished with the resource it must 'give' the token back - allowing other tasks the opportunity to access the same resource.

Mutexes use the same semaphore access API functions so also permit a block time to be specified. The block time indicates the maximum number of 'ticks' that a task should enter the Blocked state when attempting to 'take' a mutex if the mutex is not immediately available. Unlike binary semaphores however - mutexes employ priority inheritance. This means that if a high priority task blocks while attempting to obtain a mutex (token) that is currently held by a lower priority task, then the priority of the task holding the token is temporarily raised to that of the blocking task. This mechanism is designed to ensure the higher priority task is kept in the blocked state for the shortest time possible, and in so doing minimise the 'priority inversion' that has already occurred.

Priority inheritance does not cure priority inversion! It just minimises its effect in some situations. Hard real time applications should be designed such that priority inversion does not happen in the first place.


Using a mutex to guard access to a shared resource.



Recursive Mutexes

A mutex used recursively can be 'taken' repeatedly by the owner. The mutex doesn't become available again until the owner has called xSemaphoreGiveRecursive() for each successful xSemaphoreTakeRecursive() request. For example, if a task successfully 'takes' the same mutex 5 times then the mutex will not be available to any other task until it has also 'given' the mutex back exactly five times.

This type of semaphore uses a priority inheritance mechanism so a task 'taking' a semaphore MUST ALWAYS 'give' the semaphore back once the semaphore it is no longer required.

Mutex type semaphores cannot be used from within interrupt service routines.



Copyright (C) 2010 Real Time Engineers Ltd.
Any and all data, files, source code, html content and documentation included in the FreeRTOS distribution or available on this site are the exclusive property of Real Time Engineers Ltd.. See the files license.txt (included in the distribution) and this copyright notice for more information. FreeRTOSTM and FreeRTOS.orgTM are trade marks of Real Time Engineers Ltd..