Semaphores are a way to manage concurrency and synchronization, they are a simple and effective tool to control the access to shared resources and to coordinate the execution of multiple tasks. They are a fundamental concept in concurrent programming and are used in a wide range of applications, from low-level embedded systems to high-performance distributed systems.
Semaphores are often used to protect shared resources from concurrent access, which can lead to race conditions, data corruption, and other problems. By using semaphores, a task can request access to a shared resource and block if the resource is not available. Once the resource is released, the task can continue execution.
What is semaphore in FreeRTOS?
A semaphore is a synchronization object used in FreeRTOS (Free Real-Time Operating System) to signal the availability of a resource or to signal the completion of an event. It is a counting semaphore, which means it can have a value of 0 or a positive integer.
How Semaphore is used?
A semaphore is created using the xSemaphoreCreateCounting() function, which takes two arguments: the initial value of the semaphore and the maximum value the semaphore can take. Once a semaphore is created, it can be used in the following ways:
A task can "take" the semaphore by calling the xSemaphoreTake() function. If the semaphore's value is greater than 0, it is decremented by 1 and the task continues execution. If the semaphore's value is 0, the task is blocked until the semaphore is given by another task.
A task can "give" the semaphore by calling the xSemaphoreGive() function. If there are any tasks blocked on the semaphore, one of them will be unblocked and can continue execution. If there are no tasks blocked on the semaphore, the semaphore's value is incremented by 1.
A task can "take" the semaphore with a timeout by calling the xSemaphoreTake() function and passing a timeout value. If the semaphore's value is greater than 0, it is decremented by 1 and the task continues execution. If the semaphore's value is 0, the task is blocked until the semaphore is given by another task or the timeout expires.
Semaphores are useful in FreeRTOS for synchronizing access to shared resources, such as memory buffers or I/O devices, and for signaling the completion of events, such as the completion of a software timer or the reception of a message on a queue.
What are the types of semaphores?
There are two main types of semaphore: binary semaphores and counting semaphores.
Binary Semaphores: A binary semaphore is a semaphore that can have only two values: 0 or 1. When a binary semaphore is created, it is initialized with a value of 0 or 1. When a task "takes" a binary semaphore, it waits until the semaphore's value becomes 1, and then the value is set to 0. When a task "gives" a binary semaphore, the value is set to 1, releasing any task that may be waiting on the semaphore. Binary semaphores are often used to implement mutual exclusion, which ensures that only one task can access a shared resource at a time.
Counting Semaphores: A counting semaphore is a semaphore that can have any positive integer value. When a counting semaphore is created, it is initialized with a value. When a task "takes" a counting semaphore, the value is decremented by 1. If the resulting value is less than 0, the task is blocked until the semaphore is given by another task. When a task "gives" a counting semaphore, the value is incremented by 1, releasing any task that may be waiting on the semaphore. Counting semaphores are often used to implement counting resources, such as memory buffers or I/O devices, where multiple tasks can access the resource simultaneously.
Mutex semaphore, It is a specialized binary semaphore that is used to provide mutual exclusion in a priority-inheritance manner, it is useful in situations where a task with a lower priority is holding the semaphore and a higher priority task want to take the semaphore, in this case the priority of the lower priority task will be temporarily raised to the priority of the higher priority task.
Based on the use the type of Semaphores are:
Recursive Semaphores: A recursive semaphore is a semaphore that allows a task to take the semaphore multiple times without deadlocking. When a task takes a recursive semaphore, the semaphore's count is decremented. If the task already holds the semaphore, the count is not decremented and the task continues execution. When the task gives the semaphore, the count is incremented. If the count reaches zero, the semaphore is released and made available to other tasks. Recursive semaphores are useful in situations where a task may need to take the semaphore multiple times, such as in a nested function call.
Queue Set Semaphores: A queue set semaphore is a semaphore that is used to wait for multiple events at the same time. A queue set semaphore is created by calling the xQueueCreateSet() function and is added to a queue set by calling the xQueueAddToSet() function. Once a semaphore is added to a queue set, a task can wait for any of the semaphores in the set by calling the xQueueSelectFromSet() function. When a semaphore is given, the task that is waiting on the queue set is unblocked and can continue execution. Queue set semaphores are useful in situations where a task needs to wait for multiple events to occur before continuing execution.
Timed Semaphores: A timed semaphore is a semaphore that can be taken with a timeout. When a task takes a timed semaphore, it waits for the semaphore to be given or for the timeout to expire. If the semaphore is given before the timeout expires, the task continues execution. If the timeout expires, the task is unblocked and a value is returned to indicate that the timeout occurred. Timed semaphores are useful in situations where a task needs to wait for an event to occur, but should not wait indefinitely.
It's important to note that the different types of semaphores have different use cases and should be used appropriately to achieve the desired result.
What are the advantages of using Semaphore?
Semaphores have several advantages in a real-time operating system like FreeRTOS:
- Synchronization: Semaphores can be used to synchronize access to shared resources, such as memory buffers or I/O devices, ensuring that only one task can access the resource at a time. This prevents race conditions and data corruption.
- Signaling: Semaphores can be used to signal the completion of an event, such as the completion of a software timer or the reception of a message on a queue, allowing tasks to continue execution in a coordinated manner.
- Prioritization: Semaphores can be used to prioritize access to shared resources, ensuring that high-priority tasks are serviced first. This helps to guarantee that critical tasks are executed in a timely manner.
- Deadlock Prevention: With the use of Mutex semaphore, it can prevent deadlock by using the priority inheritance mechanism, where a task holding the semaphore temporarily raises its priority to the priority of the task requesting the semaphore, thus preventing lower priority task to block the higher priority task.
- Flexibility: Semaphores can be used in a variety of different situations, from simple mutual exclusion to more complex synchronization and signaling scenarios.
- Portability: Semaphores are a standard feature of most real-time operating systems, including FreeRTOS, making it easy to port code from one platform to another.
- Scalability: Semaphores can be used in both small and large systems, from single-task systems to systems with thousands of tasks, making them suitable for a wide range of applications.
Limitations of a Semaphore
While semaphores are a powerful synchronization tool, they do have some limitations:
- Complexity: Using semaphores can add complexity to a system, as tasks must be designed to properly acquire and release semaphores, and care must be taken to avoid deadlocks.
- Priority Inversion: Priority inversion is a problem that occurs when a low-priority task holds a semaphore that is needed by a high-priority task. The high-priority task is blocked, and the low-priority task continues to execute, causing the system's overall responsiveness to suffer. The priority-inheritance mechanism can be used to mitigate this problem.
- Limited Resource Management: Semaphores can be used to manage the allocation of shared resources, but they do not provide a way to manage the deallocation of resources. This can lead to resource leaks if tasks fail to properly release resources they have acquired.
- Limited Error Handling: Semaphores do not provide a way to handle errors that may occur while acquiring or releasing resources. If a task fails to acquire a semaphore, it may be blocked indefinitely, leading to a deadlock.
- Limited Error Detection: Semaphores do not provide a way to detect errors that may occur while acquiring or releasing resources. This can make it difficult to diagnose and fix problems that may arise in a system.
- Performance: In some cases, using semaphores can add overhead to the system and can affect the performance, particularly if many tasks are blocked waiting on semaphores.
It's important to consider these limitations when using semaphores and to use them appropriately in a system to achieve the desired results.
Thr example of using a binary semaphore to synchronize communication between two tasks in Arduino IDE with the ESP32:
#include <Semaphore.h>
SemaphoreHandle_t xSemaphore;
void senderTask(void *pvParameters) {
while (1) {
// Send message
xSemaphoreGive(xSemaphore);
vTaskDelay(1000);
}
}
void receiverTask(void *pvParameters) {
while (1) {
// Wait for message
if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
// Process message
Serial.println("Message received");
}
}
}
void setup() {
xSemaphore = xSemaphoreCreateBinary();
xTaskCreate(senderTask, "Sender", 1024, NULL, 1, NULL);
xTaskCreate(receiverTask, "Receiver", 1024, NULL, 2, NULL);
}
void loop() {
vTaskDelay(portMAX_DELAY);
}
In this example, two tasks are created: senderTask and receiverTask. The senderTask sends a message by giving the binary semaphore xSemaphore, while the receiverTask receives the message by taking the binary semaphore. When the receiverTask receives the message, it processes it and prints "Message received" to the serial port.
The sender task sends the message every second using vTaskDelay(1000) while the receiver task waits indefinitely for the semaphore using portMAX_DELAY.
It's important to note that this is a simplified example and it depends on the actual use case, you might need to adapt it to your specific requirements.
Conclusion
The semaphores are a powerful synchronization tool that can be used in real-time operating systems like FreeRTOS to control access to shared resources and to signal the completion of events. There are different types of semaphores available, including binary semaphores, counting semaphores, recursive semaphores, queue set semaphores, and timed semaphores, each with its own use cases and advantages.