Semaphores

A semaphore is a synchronization primitive used for managing access to shared resources. In its simplest form, a semaphore is a variable that indicates whether a resource is currently in use. The operating system provides an API to create and manipulate semaphores in an atomic manner, ensuring safe access from multiple tasks.  

Semaphores are commonly used to protect shared resources such as global variables or hardware interfaces like UART or SPI. Each shared resource should have a dedicated semaphore. Tasks must attempt to take the semaphore before accessing the resource. If the semaphore is available, the task successfully takes it and gains exclusive access to the resource. If the semaphore is already held by another task, the requesting task is placed into the Blocked state until the semaphore becomes available. Once the task finishes using the resource, it must give the semaphore back, allowing other tasks to proceed. Failing to release a semaphore can lead to deadlock, where tasks remain indefinitely blocked and therefore unable to access the resource. 

 

Semaphores in FreeRTOS

The two most common types of semaphores are counting and binary semaphores. A counting semaphore is used when there are multiple instances of a resource available. Each time the semaphore is taken, the resource count is decremented. When the resource count reaches 0, any additional semaphore takes will result in the task blocking until a resource becomes available.

FreeRTOS also supports what is called a binary semaphore. A binary semaphore can exist in one of two states: available or taken. Because binary semaphores do not carry a count, they are ideal for scenarios where only one task should access a resource at a time. Due to its simplicity and effectiveness in managing single-instance resources, we will focus exclusively on binary semaphores.

Because the semaphore will be accessed by multiple tasks, it should be declared as a global variable. This ensures that all tasks have visibility to the same semaphore instance, allowing coordinated access to the shared resource.

You need to initialize the semaphore handle before starting the FreeRTOS scheduler. In this example, the semaphore is initially placed in the taken state, ensuring that the task waiting for data will block until the data is generated and the semaphore is released. This approach helps coordinate task execution and prevents premature access to incomplete or unavailable data. 

 

Semaphores – Synchronization

Let’s modify the ADC example from above to use semaphores to synchronize data being produced in one task and consumed in another. We’ll begin with the task that produces the data. In this task, a semaphore is given to signal that new data is available, effectively waking up the task that is waiting to consume it.

The receiving task has been rewritten to remain in a blocked state until the semaphore is received, indicating that the data is ready.

Using semaphores here is a significant improvement because task_adc_print() no longer wastes MCU cycles by polling a global variable. Instead, it remains blocked until task_adc_read() gives the semaphore. This not only improves efficiency but also avoids race conditions, since semaphore operations are atomic. 

Semaphores – Mutual Exclusion 

In addition to task synchronization, semaphores can also be used to provide mutual exclusion for shared hardware resources. In this example, two tasks attempt to use the same UART interface to print debug information. 

Before diving into the task implementations, here are a few important notes: 

  1. Aside from the different string stored in task_name, the two tasks are functionally identical.
  2. The standard printf() function is not used because Infineon’s implementation is already thread-safe and does not require semaphore protection. 
  3. However, the low-level cyhal functions used for UART communication are not thread-safe and must be protected to avoid concurrent access issues. 
  4. A non-blocking delay has been added to each task to exacerbate potential issues when resource management is not properly implemented.  You would not want to intentionally add this type of delay to your applications. 

 

If you examine the console output produced by these two tasks, you’ll notice that their messages become interleaved. This happens because the time required to transmit the entire message over the UART interface exceeds the time slice each task is given before a context switch occurs. As a result, one task may begin printing its message, get preempted, and then the other task resumes execution and starts printing its own message—leading to a jumbled output.

 

If we modify each of the tasks to claim ownership of the resource using a semaphore, then we ensure that each task completes its transmission of the string before the other task can gain access to the UART.

In this example, we want to initialize the semaphore so that it starts in the available state so that the first task to enter the Running state can successfully acquire it. This ensures that whichever task runs first gains exclusive access to the UART without blocking

 

Each function is then modified to acquire the semaphore before accessing the UART. If the semaphore is available, the task successfully claims it and gains exclusive access to the UART interface. If the semaphore is already held by another task, the requesting task transitions into the Blocked state and remains there until the semaphore is released. This mechanism ensures that only one task interacts with the UART at a time, preventing the data from being interleaved.

The resulting output would look like this: 

 

If either of these tasks had additional responsibilities to perform—even when the UART is currently in use by another task—you could restructure the code to check the semaphore with a timeout value other than portMAX_DELAY. If the semaphore is not successfully acquired within the specified time, the task could skip the UART operation and proceed with its other duties.