Semaphores are synchronization mechanisms used in embedded systems to manage resource access and coordinate tasks in a real-time operating system (RTOS). FreeRTOS, one of the most popular and lightweight real-time operating systems, supports semaphores to facilitate task synchronization and inter-task communication. In this article, we’ll dive into the concept of semaphores in FreeRTOS, their types, usage, and examples.
What is a Semaphore?
A semaphore is a variable or abstract data type that is used to control access to a shared resource in a concurrent system, such as a multitasking RTOS. It provides a way for tasks to signal each other or protect critical resources. Semaphores prevent race conditions by ensuring that only one task can access a resource at any given time.
Semaphores work through two primary operations:
- Take (also known as wait or P operation): A task requests access to the resource. If the semaphore’s value is zero, the task will be blocked (i.e., it enters a “waiting” state).
- Give (also known as signal or V operation): A task releases the resource, incrementing the semaphore value and potentially unblocking waiting tasks.
Types of Semaphores in FreeRTOS
FreeRTOS supports several types of semaphores, each designed for specific use cases:
- Binary Semaphore
- Counting Semaphore
- Mutex (Binary Semaphore with Priority Inheritance)
Let’s explore each of these in more detail.
Binary Semaphore
A Binary Semaphore is a semaphore that can have only two states: taken (0) or given (1). It is mainly used to signal between tasks or interrupt service routines (ISRs) and tasks, often for simple synchronization or event signaling. Binary semaphores are sometimes referred to as “event flags.”
When a task or ISR “gives” a binary semaphore, it sets the value to 1, signaling that a particular event has occurred or a resource is available. On the other hand, when a task “takes” the semaphore, it sets the value to 0, which may block other tasks from taking it until it is given again.
Functions:
- xSemaphoreCreateBinary(): Create a binary semaphore.
- xSemaphoreTake(): Take the binary semaphore (decrement the semaphore value, which blocks the task if the semaphore is unavailable).
- xSemaphoreGive(): Release the binary semaphore (increment the semaphore value, which unblocks any waiting task).
Implementing Binary Semaphore in FreeRTOS (ESP32)
Let’s implement a scenario where one task is waiting for an event signal (from another task) using a binary semaphore.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
SemaphoreHandle_t xBinarySemaphore;
void vTask1(void *pvParameters) {
while(1) {
printf("Task 1 is waiting for signal...\n");
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) {
printf("Task 1 received signal, continuing...\n");
}
}
}
void vTask2(void *pvParameters) {
while(1) {
printf("Task 2 is doing work...\n");
vTaskDelay(pdMS_TO_TICKS(5000)); // Simulate work
printf("Task 2 is sending signal...\n");
xSemaphoreGive(xBinarySemaphore); // Signal Task 1
vTaskDelay(pdMS_TO_TICKS(2000)); // Repeat every 2 seconds
}
}
void app_main() {
// Create the binary semaphore
xBinarySemaphore = xSemaphoreCreateBinary();
if (xBinarySemaphore == NULL) {
printf("Failed to create binary semaphore\n");
return;
}
// Create tasks
xTaskCreate(vTask1, "Task1", 2048, NULL, 1, NULL);
xTaskCreate(vTask2, "Task2", 2048, NULL, 1, NULL);
}
Explanation :
- Task 1 waits for the binary semaphore to be given by Task 2. It uses xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) to block until the semaphore is available (value = 1).
- Task 2 simulates some work, and then signals Task 1 by calling xSemaphoreGive(xBinarySemaphore). This allows Task 1 to continue execution.
In this example, Task 1 is blocked until Task 2 signals it using the binary semaphore, ensuring synchronization.
Practical example :
we will simulate a button press event, which triggers an ISR on the ESP32. The ISR will signal a task to perform some action upon detecting the button press. A Binary Semaphore will be used to synchronize between the ISR and the task, ensuring that the task performs an action only when the button is pressed.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
// GPIO pin for the button
#define BUTTON_PIN 0 // GPIO0 (Change this based on your setup)
#define LED_PIN 2 // GPIO2 (LED)
SemaphoreHandle_t xBinarySemaphore;
// Interrupt Service Routine (ISR) for button press
static void IRAM_ATTR button_isr_handler(void* arg) {
// Give the semaphore from ISR
xSemaphoreGiveFromISR(xBinarySemaphore, NULL);
}
// Task to toggle LED when button is pressed
void vTaskButtonHandler(void* pvParameters) {
while (1) {
// Wait for the button press signal (binary semaphore)
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) {
printf("Button pressed! Toggling LED...\n");
// Toggle LED
gpio_set_level(LED_PIN, !gpio_get_level(LED_PIN));
}
}
}
void app_main() {
// Initialize the LED pin as output
gpio_pad_select_gpio(LED_PIN);
gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);
// Initialize the button pin as input with pull-up
gpio_pad_select_gpio(BUTTON_PIN);
gpio_set_direction(BUTTON_PIN, GPIO_MODE_INPUT);
gpio_set_pull_mode(BUTTON_PIN, GPIO_PULLUP_ONLY); // Assuming active low button
// Create the binary semaphore
xBinarySemaphore = xSemaphoreCreateBinary();
if (xBinarySemaphore == NULL) {
printf("Failed to create binary semaphore\n");
return;
}
// Set up interrupt for the button press
gpio_set_intr_type(BUTTON_PIN, GPIO_INTR_NEGEDGE); // Trigger on falling edge (button press)
gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1); // Install ISR service
gpio_isr_handler_add(BUTTON_PIN, button_isr_handler, NULL); // Add ISR handler
// Create the task to handle button press
xTaskCreate(vTaskButtonHandler, "ButtonHandler", 2048, NULL, 1, NULL);
}
Counting Semaphore
A Counting Semaphore allows a range of values greater than 1, making it useful when you need to control access to a shared resource that has a limited number of instances (e.g., a pool of resources). A counting semaphore can be used to manage multiple resources, allowing tasks to “take” a semaphore without blocking, as long as the semaphore value is greater than 0.
For example, if you have 3 identical resources, you can initialize a counting semaphore with a count of 3. Tasks can take the semaphore as long as the count is greater than 0. Once the count reaches 0, other tasks will block until a resource is released (i.e., the semaphore is given).
Implementing Counting Semaphore in FreeRTOS (ESP32)
Let’s implement a scenario where several tasks share a pool of resources, and a counting semaphore is used to control access to the pool.
Basic syntax :
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
SemaphoreHandle_t xCountingSemaphore;
void vTask1(void *pvParameters) {
while (1) {
printf("Task 1 is trying to take a resource...\n");
if (xSemaphoreTake(xCountingSemaphore, portMAX_DELAY) == pdTRUE) {
printf("Task 1 acquired a resource, doing work...\n");
vTaskDelay(pdMS_TO_TICKS(3000)); // Simulate work
printf("Task 1 releasing the resource...\n");
xSemaphoreGive(xCountingSemaphore); // Release resource
}
}
}
void vTask2(void *pvParameters) {
while (1) {
printf("Task 2 is trying to take a resource...\n");
if (xSemaphoreTake(xCountingSemaphore, portMAX_DELAY) == pdTRUE) {
printf("Task 2 acquired a resource, doing work...\n");
vTaskDelay(pdMS_TO_TICKS(2000)); // Simulate work
printf("Task 2 releasing the resource...\n");
xSemaphoreGive(xCountingSemaphore); // Release resource
}
}
}
void app_main() {
// Create a counting semaphore with 3 available resources
xCountingSemaphore = xSemaphoreCreateCounting(3, 3);
if (xCountingSemaphore == NULL) {
printf("Failed to create counting semaphore\n");
return;
}
// Create tasks
xTaskCreate(vTask1, "Task1", 2048, NULL, 1, NULL);
xTaskCreate(vTask2, "Task2", 2048, NULL, 1, NULL);
}
Practical example :
In this example, imagine we have an embedded system with a limited number of UART serial ports available on the ESP32, and multiple tasks need to use those UART ports. Instead of allowing tasks to directly access the UART hardware, we will use a counting semaphore to control the number of tasks that can access the serial ports at any given time.
Let’s assume:
- The ESP32 has 2 UART interfaces available.
- Multiple tasks may need to write data to these UART interfaces concurrently, but only 2 tasks should be allowed to access the UART ports at a time.
- A counting semaphore will be used to manage access to these 2 UART ports.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
// Semaphore to manage access to UART ports (2 available)
SemaphoreHandle_t xUartSemaphore;
// Simulated function to send data over UART
void send_data_over_uart(const char *data, int uart_num) {
printf("Task %d sending data: %s\n", uart_num, data);
vTaskDelay(pdMS_TO_TICKS(2000)); // Simulate sending data over UART
}
// Task that simulates using a UART port
void vUartTask(void *pvParameters) {
int uart_num = *((int *)pvParameters);
while (1) {
// Try to take the semaphore to access a UART port
printf("Task %d trying to access UART\n", uart_num);
if (xSemaphoreTake(xUartSemaphore, portMAX_DELAY) == pdTRUE) {
printf("Task %d got access to UART\n", uart_num);
// Simulate sending data over UART
send_data_over_uart("Hello from task!", uart_num);
// Release the UART access after finishing
printf("Task %d releasing UART access\n", uart_num);
xSemaphoreGive(xUartSemaphore);
// Simulate some work done by the task
vTaskDelay(pdMS_TO_TICKS(500));
} else {
printf("Task %d could not get access to UART\n", uart_num);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
}
void app_main() {
// Create a counting semaphore with 2 available UART ports
xUartSemaphore = xSemaphoreCreateCounting(2, 2);
if (xUartSemaphore == NULL) {
printf("Failed to create counting semaphore\n");
return;
}
// Create 4 tasks that simulate using the UART ports
int uart_num_1 = 1, uart_num_2 = 2, uart_num_3 = 3, uart_num_4 = 4;
xTaskCreate(vUartTask, "UartTask1", 2048, &uart_num_1, 1, NULL);
xTaskCreate(vUartTask, "UartTask2", 2048, &uart_num_2, 1, NULL);
xTaskCreate(vUartTask, "UartTask3", 2048, &uart_num_3, 1, NULL);
xTaskCreate(vUartTask, "UartTask4", 2048, &uart_num_4, 1, NULL);
}
Mutex (Mutual Exclusion Semaphore)
A mutex is a special type of binary semaphore used to provide mutual exclusion for shared resources. It ensures that only one task can access a resource at a time and that the task holding the mutex has exclusive ownership of the resource. Additionally, FreeRTOS mutexes support priority inheritance, which helps prevent priority inversion problems (where lower-priority tasks block higher-priority tasks).
Implementing Mutex
In this example, we will simulate a scenario where multiple tasks need to access and modify a shared resource (e.g., a shared counter or a sensor reading). Since multiple tasks will be writing to and reading from this shared resource concurrently, we will use a mutex to ensure mutual exclusion—i.e., only one task can access the shared resource at any time.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
// Shared resource (counter)
int shared_counter = 0;
// Mutex to protect the shared resource
SemaphoreHandle_t xMutex;
// Task 1: Increment shared counter
void vTask1(void *pvParameters) {
while (1) {
// Try to take the mutex (blocking if it's already taken)
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
printf("Task 1 is incrementing the counter...\n");
shared_counter++;
printf("Task 1: Counter value: %d\n", shared_counter);
// Release the mutex after modifying the shared resource
xSemaphoreGive(xMutex);
}
vTaskDelay(pdMS_TO_TICKS(1000)); // Simulate some work
}
}
// Task 2: Increment shared counter
void vTask2(void *pvParameters) {
while (1) {
// Try to take the mutex (blocking if it's already taken)
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
printf("Task 2 is incrementing the counter...\n");
shared_counter++;
printf("Task 2: Counter value: %d\n", shared_counter);
// Release the mutex after modifying the shared resource
xSemaphoreGive(xMutex);
}
vTaskDelay(pdMS_TO_TICKS(1500)); // Simulate some work
}
}
void app_main() {
// Create the mutex
xMutex = xSemaphoreCreateMutex();
if (xMutex == NULL) {
printf("Failed to create mutex\n");
return;
}
// Create two tasks
xTaskCreate(vTask1, "Task 1", 2048, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 2048, NULL, 1, NULL);
}
Implementing Producer-Consumer with Mutex
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
// Shared buffer size
#define BUFFER_SIZE 5
// Shared buffer (simple integer array)
int shared_buffer[BUFFER_SIZE];
int buffer_index = 0; // This will point to the next free slot for the producer
// Semaphore and mutex
SemaphoreHandle_t xMutex; // Mutex to protect the shared buffer
SemaphoreHandle_t xEmptySlots; // Semaphore to track empty slots in the buffer
SemaphoreHandle_t xFullSlots; // Semaphore to track full slots in the buffer
// Producer Task
void vProducerTask(void *pvParameters) {
int data = 0;
while (1) {
// Wait for an empty slot in the buffer
xSemaphoreTake(xEmptySlots, portMAX_DELAY);
// Take the mutex before modifying the shared buffer
xSemaphoreTake(xMutex, portMAX_DELAY);
// Produce data and place it into the shared buffer
shared_buffer[buffer_index] = data;
printf("Produced: %d\n", data);
buffer_index = (buffer_index + 1) % BUFFER_SIZE;
data++; // Increment the produced data value
// Release the mutex after modifying the shared buffer
xSemaphoreGive(xMutex);
// Signal that there is a full slot in the buffer
xSemaphoreGive(xFullSlots);
// Simulate some work
vTaskDelay(pdMS_TO_TICKS(1000)); // Produce data every 1 second
}
}
// Consumer Task
void vConsumerTask(void *pvParameters) {
int data;
while (1) {
// Wait for a full slot in the buffer
xSemaphoreTake(xFullSlots, portMAX_DELAY);
// Take the mutex before modifying the shared buffer
xSemaphoreTake(xMutex, portMAX_DELAY);
// Consume data from the shared buffer
buffer_index = (buffer_index - 1 + BUFFER_SIZE) % BUFFER_SIZE; // Move to the previous index
data = shared_buffer[buffer_index];
printf("Consumed: %d\n", data);
// Release the mutex after modifying the shared buffer
xSemaphoreGive(xMutex);
// Signal that there is an empty slot in the buffer
xSemaphoreGive(xEmptySlots);
// Simulate some work
vTaskDelay(pdMS_TO_TICKS(1500)); // Consume data every 1.5 seconds
}
}
void app_main() {
// Initialize the shared buffer index to 0
buffer_index = 0;
// Create mutex for protecting shared buffer
xMutex = xSemaphoreCreateMutex();
if (xMutex == NULL) {
printf("Failed to create mutex\n");
return;
}
// Create semaphores for empty and full slots in the buffer
xEmptySlots = xSemaphoreCreateCounting(BUFFER_SIZE, BUFFER_SIZE); // Initially, the buffer is empty
xFullSlots = xSemaphoreCreateCounting(BUFFER_SIZE, 0); // Initially, the buffer is full (0 slots)
// Create producer and consumer tasks
xTaskCreate(vProducerTask, "Producer Task", 2048, NULL, 2, NULL);
xTaskCreate(vConsumerTask, "Consumer Task", 2048, NULL, 2, NULL);
}
Key Differences Between Binary Semaphores, Counting Semaphores, and Mutexes
Feature | Binary Semaphore | Counting Semaphore | Mutex (Mutual Exclusion) |
Usage | Event signaling between tasks | Resource pooling or access control | Mutual exclusion for critical sections |
Value Range | 0 or 1 | 0 to max value | 0 or 1 (but with priority inheritance) |
Priority Inheritance | No | No | Yes |
Task Ownership | No ownership | No ownership | Task ownership (only owner can release) |
Example Use | Simple event signaling | Shared resource pool | Protect shared resources from race conditions |
Conclusion
Semaphores in FreeRTOS are essential tools for managing synchronization and resource access in a real-time multitasking environment. By understanding the different types of semaphores—binary, counting, and mutex—you can implement efficient and safe communication and resource management between tasks. Proper use of semaphores helps avoid race conditions, ensures task coordination, and maintains the real-time constraints of your application.