In embedded systems development, especially when dealing with real-time operating systems like FreeRTOS, it's common to have multiple tasks running in parallel. These tasks often need to communicate with each other or coordinate access to shared resources. FreeRTOS provides semaphores as a powerful synchronization tool to handle such scenarios gracefully.
This blog post will explore semaphores in FreeRTOS, how they work, their typical use cases, and demonstrate their implementation using the ESP-WROVER-KIT, a development board based on the ESP32 microcontroller.
What is a Semaphore in FreeRTOS?
A semaphore is a signaling mechanism that allows tasks or interrupts to synchronize their execution or share resources safely. Think of it as a flag that a task can set or clear, allowing other tasks to proceed only when the flag is in a particular state.
There are two main types of semaphores in FreeRTOS:
Binary Semaphore: This type of semaphore has only two states: taken (0) and available (1). It is typically used for task synchronization. For instance, a binary semaphore can be used to signal one task from another, or from an interrupt service routine (ISR).
Counting Semaphore: This type of semaphore has a value that counts up to a maximum count. It is often used to manage access to a pool of resources. For example, if you have a fixed number of identical hardware peripherals, a counting semaphore can track how many are available at any time.
Unlike mutexes, semaphores do not provide ownership or priority inheritance, which is important when protecting critical sections of code.
FreeRTOS Semaphore API Overview
FreeRTOS provides a set of API functions to work with semaphores. Here’s an overview of the key functions you’ll frequently use:
Creating Semaphores:
SemaphoreHandle_t xSemaphoreCreateBinary(void);
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
Taking (acquiring) a Semaphore:
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
This function attempts to take a semaphore. If the semaphore is not available, the task will wait for a specified number of ticks.
Giving (releasing) a Semaphore:
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
This function makes a semaphore available to other tasks. It should be called after the task has completed its use of the shared resource or after an event that another task is waiting for.
Using Semaphores from an ISR:
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
This is a special version of xSemaphoreGive
designed to be called from an interrupt context.
Semaphore Use Case Example with ESP-WROVER-KIT
To demonstrate how semaphores work in practice, let's create a simple application that involves two tasks:
- A sensor task that simulates collecting data from a sensor every second.
- A UART task that waits for data to be ready and then transmits it over UART.
These tasks will be synchronized using a binary semaphore. The sensor task will give the semaphore after generating data, and the UART task will wait for the semaphore before transmitting.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
// Tag for logging
static const char *TAG = "SemaphoreExample";
// Binary semaphore handle
SemaphoreHandle_t xBinarySemaphore;
// Simulated sensor task
void sensor_task(void *pvParameters)
{
while (true)
{
ESP_LOGI("SensorTask", "Reading sensor data...");
vTaskDelay(pdMS_TO_TICKS(1000)); // simulate sensor read delay
// Signal that data is ready
xSemaphoreGive(xBinarySemaphore);
ESP_LOGI("SensorTask", "Semaphore given");
}
}
// Simulated UART transmission task
void uart_task(void *pvParameters)
{
while (true)
{
// Wait until semaphore is available
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE)
{
ESP_LOGI("UARTTask", "Transmitting data over UART...");
vTaskDelay(pdMS_TO_TICKS(500)); // simulate transmission delay
}
}
}
// App main entry point
void app_main(void)
{
// Create binary semaphore
xBinarySemaphore = xSemaphoreCreateBinary();
if (xBinarySemaphore != NULL)
{
ESP_LOGI(TAG, "Semaphore created successfully");
// Create the tasks
xTaskCreate(sensor_task, "SensorTask", 2048, NULL, 2, NULL);
xTaskCreate(uart_task, "UARTTask", 2048, NULL, 2, NULL);
}
else
{
ESP_LOGE(TAG, "Failed to create semaphore");
}
}
Below is explanation of the above program
Global Declaration
We begin by declaring the semaphore handle.
SemaphoreHandle_t xBinarySemaphore;
Sensor Task
This task reads or simulates sensor data and gives the semaphore to signal that data is ready.
void sensor_task(void *pvParameters)
{
while (true)
{
ESP_LOGI("SensorTask", "Reading sensor data...");
vTaskDelay(pdMS_TO_TICKS(1000)); // simulate sensor read delay
// Signal that data is ready
xSemaphoreGive(xBinarySemaphore);
ESP_LOGI("SensorTask", "Semaphore given");
}
}
UART Task
This task waits for the semaphore, and when it is available, proceeds to simulate UART transmission.
void uart_task(void *pvParameters)
{
while (true)
{
// Wait until semaphore is available
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE)
{
ESP_LOGI("UARTTask", "Transmitting data over UART...");
vTaskDelay(pdMS_TO_TICKS(500)); // simulate transmission delay
}
}
}
app_main Setup
In the app_main
function, create the binary semaphore and start the tasks.
void app_main(void)
{
// Create binary semaphore
xBinarySemaphore = xSemaphoreCreateBinary();
if (xBinarySemaphore != NULL)
{
ESP_LOGI(TAG, "Semaphore created successfully");
// Create the tasks
xTaskCreate(sensor_task, "SensorTask", 2048, NULL, 2, NULL);
xTaskCreate(uart_task, "UARTTask", 2048, NULL, 2, NULL);
}
else
{
ESP_LOGE(TAG, "Failed to create semaphore");
}
}
Using Semaphores with ISRs
One of the most powerful uses of binary semaphores is to synchronize a task with an interrupt. For instance, a GPIO interrupt triggered by a button press can signal a task to perform a specific action.
Here’s how that works conceptually:
ISR Handler Example:
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
You would then configure the GPIO and attach the ISR in your initialization code. The task waiting for this semaphore would then run immediately after the interrupt if it's the highest priority task that is ready.
Common Mistakes When Using Semaphores
Not checking for NULL: Always ensure the semaphore is successfully created before using it.
Blocking indefinitely in ISRs: Do not use xSemaphoreTake
in an interrupt context. Use only xSemaphoreGiveFromISR
.
Incorrect priority levels: If the task giving the semaphore is at a lower priority than the one taking it, it may result in unexpected behavior if portYIELD_FROM_ISR
is not used properly.
Deadlocks: If a task waits indefinitely for a semaphore that is never given, the system can hang. Always ensure your design logic guarantees the semaphore is eventually released.
Summary
Semaphores are essential tools for inter-task communication and synchronization in FreeRTOS. With binary semaphores, you can coordinate task execution and synchronize events like sensor readings or external interrupts. Counting semaphores help manage access to a set of identical resources.
In this blog post, we demonstrated how to use a binary semaphore with the ESP-WROVER-KIT to coordinate between a sensor task and a UART task. We also reviewed key FreeRTOS semaphore APIs and common usage patterns, including from within interrupt service routines.
As your embedded applications grow in complexity, using semaphores effectively will help you manage concurrency, prevent race conditions, and build reliable real-time systems on platforms like the ESP32.