Hello! Welcome back to the course.
In our last lesson, we explored how vTaskDelay() allows us to create non-blocking delays, a cornerstone of cooperative multitasking in FreeRTOS. We saw how tasks can yield CPU time, allowing other tasks to run. However, those tasks were running independently, only coordinated by time.
What happens when tasks need to directly interact? For instance, how does a task that monitors a sensor signal a different task to start a motor? This lesson introduces semaphores, a fundamental tool in FreeRTOS for achieving exactly this kind of synchronization. You'll learn how to use them to make your tasks work together intelligently.
By the end of this lesson, you will be able to implement task synchronization using both binary and counting semaphores to coordinate task execution based on events.
1. What are Semaphores?
At its core, a semaphore is a protected variable that acts as a signaling mechanism. It allows one task or an Interrupt Service Routine (ISR) to signal an event, and another task to wait for that event without wasting CPU cycles.
There are two main types of semaphores in FreeRTOS:
- Binary Semaphore: Can only have two states, "taken" (value 0) or "given" (value 1). It's like a single flag or a gate. It's perfect for signaling the occurrence of a single, unique event.
- Counting Semaphore: Can hold a value from 0 up to a predefined maximum. It's useful for counting multiple events or managing a pool of identical resources.
A common point of confusion is the difference between a semaphore and a mutex (which we'll cover in detail later). The key distinction is their purpose:
- Mutex (Mutual Exclusion): Used for protecting a shared resource. A task "takes" the mutex to lock the resource, uses it, and then "gives" it back. It follows an ownership model.
- Semaphore: Used for synchronization or signaling. One task or ISR "gives" the semaphore to signal an event, and a different task "takes" it to process the event.
Introduction to RTOS Part 7 - Semaphore | Digi-Key Electronics
To get a solid conceptual overview, let's start with this video from Digi-Key. It does an excellent job of explaining what semaphores are, how they differ from mutexes, and how they are typically used.
Please watch from the beginning until 05:18. Pay close attention to: The explanation of why the 'multiple restrooms' analogy is flawed. The concept of using semaphores as a signaling mechanism. The introduction to the producer-consumer design pattern.
2. Binary Semaphores: Signaling Events
Binary semaphores are the simplest and most common synchronization tool. They are ideal when one part of your system needs to trigger an action in another. A classic embedded systems example is an ISR signaling a task.

The basic workflow is:
- A task that needs to wait for an event tries to "take" the semaphore. If the semaphore is not available (its count is 0), the task enters the Blocked state.
- When the event occurs (e.g., a button is pressed, data arrives), the ISR or another task "gives" the semaphore, setting its count to 1.
- The waiting task is unblocked, moves to the Ready state, takes the semaphore (resetting its count to 0), and executes its work.
An ESP32 Example: Button Press Interrupt
Let's look at a practical example. The following tutorial from Random Nerd Tutorials demonstrates how to use a binary semaphore to toggle an LED when a button is pressed. An ISR will handle the button press, and a separate task will handle the LED.
ESP32 FreeRTOS Semaphores Arduino IDE Getting ...
This guide provides a complete, hands-on example for the ESP32.
Read the sections 'Introducing Semaphores', 'Semaphores Basic Functions', and all of 'Example 1: Binary Semaphore – Toggling an LED with a Button Press'. Focus on understanding the roles of these key functions in the provided code: xSemaphoreCreateBinary() xSemaphoreGiveFromISR() xSemaphoreTake()
Key API Functions
From the tutorial, you saw these critical functions:
-
Creation:
SemaphoreHandle_t mySemaphore = xSemaphoreCreateBinary();- This creates a binary semaphore and returns a handle to it. It is created in the "taken" state (count is 0).
-
Giving from an ISR:
xSemaphoreGiveFromISR(mySemaphore, &higherPriorityTaskWoken);- This is the special, "ISR-safe" version for giving a semaphore. ISRs must be very short and fast, and this function is optimized for that context.
- The
&higherPriorityTaskWokenparameter is important. If giving the semaphore unblocks a task with a higher priority than the currently running one, FreeRTOS will set this variable to true. The ISR should then callportYIELD_FROM_ISR()to force an immediate context switch to the higher-priority task.
-
Taking in a Task:
xSemaphoreTake(mySemaphore, portMAX_DELAY);- This function attempts to take the semaphore.
- The second parameter is the timeout.
portMAX_DELAYmeans the task will wait indefinitely (block) until the semaphore becomes available. You can also specify a timeout in ticks, after which the function will returnpdFALSEif the semaphore was not taken.
Test your understanding!
In the Random Nerd Tutorials example, the buttonISR() uses xSemaphoreGiveFromISR(). Why is it critical to use this specific function instead of the regular xSemaphoreGive() inside an ISR? What could go wrong if you used the regular version?
Show answer
The regular xSemaphoreGive() is designed to be called from a task context. It might try to block or interact with the scheduler in ways that are unsafe within an interrupt context. Using a non-ISR-safe function inside an ISR can lead to deadlocks, data corruption, or unpredictable system crashes because interrupts have strict timing and context constraints. xSemaphoreGiveFromISR() is specifically designed to be lightweight and safe for this environment.
3. Counting Semaphores: Managing Multiple Resources
Counting semaphores extend the binary concept by allowing the count to go above 1. They are perfect for scenarios where you need to track multiple events or manage a pool of finite, identical resources.
Imagine you have a pool of 5 network connections. A counting semaphore can be initialized with a count of 5. Any task that needs a connection must first "take" the semaphore. If the count is greater than 0, it succeeds, and the count is decremented. If the count is 0, the task blocks until another task releases a connection and "gives" the semaphore back, incrementing the count.
An ESP32 Example: Counting Button Presses
Let's build on the previous example. Instead of the LED toggling on every press, we can use a counting semaphore to "queue up" button presses and have the LED blink for each press that has been registered.
ESP32 FreeRTOS Semaphores Arduino IDE Getting ...
The same Random Nerd Tutorials article has a great follow-up example for counting semaphores.
Read 'Example 2: Counting Semaphore'. Pay attention to how xSemaphoreCreateCounting() is used and how the task consumes the semaphore count to perform multiple blinks.
Key API Functions
-
Creation:
mySemaphore = xSemaphoreCreateCounting(maxCount, initialCount);- Creates a counting semaphore.
maxCount: The maximum value the semaphore's count can reach.giveoperations will fail if the count is already at max.initialCount: The starting value of the semaphore.
-
Getting the Count:
UBaseType_t count = uxSemaphoreGetCount(mySemaphore);- Returns the current count of the semaphore. This can be useful for debugging or application logic.
4. Advanced Application: The Producer-Consumer Pattern
One of the most powerful uses for counting semaphores is in the producer-consumer pattern. This is a classic concurrent programming model that decouples tasks that produce data from tasks that consume it, using a shared buffer.
- Producer(s): One or more tasks that generate data and place it into a shared buffer.
- Consumer(s): One or more tasks that retrieve data from the buffer and process it.
- Synchronization:
- A mutex protects the buffer from being written to and read from at the same time (resource protection).
- A counting semaphore (
sem_filled) tracks the number of items in the buffer. A consumer musttakefrom this semaphore before reading, ensuring it doesn't read from an empty buffer. - A counting semaphore (
sem_empty) tracks the number of empty slots. A producer musttakefrom this semaphore before writing, ensuring it doesn't overflow the buffer.
This pattern is extremely common in embedded systems for handling things like serial data processing, sensor data acquisition, and logging.
The following article explains this exact pattern and provides a complete solution for an ESP32. While it uses a mutex (which we'll cover soon), focus on the role of the two counting semaphores.
Read the section titled 'Explanation'. This section breaks down the solution code and clearly explains the purpose of the mutex, the sem_empty semaphore, and the sem_filled semaphore in both the producer and consumer tasks.
This producer-consumer example gives you a glimpse into how different synchronization primitives are combined to solve complex, real-world problems.
Conclusion
You've now learned how to use FreeRTOS semaphores to coordinate the actions of different tasks. This is a massive step up from having independent, time-sliced tasks. You can now build systems where tasks react to events from the outside world (via ISRs) or from each other.
Key Takeaways:
- Semaphores are synchronization primitives used for signaling between tasks or between an ISR and a task.
- Binary semaphores act as a simple flag (0 or 1) to signal a single event. They are great for event notification, like from an ISR.
- Counting semaphores can count multiple events or manage a pool of resources.
- Use ISR-safe functions (e.g.,
xSemaphoreGiveFromISR) when interacting with FreeRTOS from an interrupt. - The producer-consumer pattern is a powerful model that uses counting semaphores and mutexes to safely pass data through a shared buffer.
In this lesson, we saw how semaphores signal that an event has happened or that a resource is available. However, they don't carry any data themselves. In the producer-consumer example, the data was passed through a global buffer. What if you want to send the actual data from the producer to the consumer directly and safely?
That's the job of FreeRTOS Queues, and they are the subject of our next lesson.