ESP32 with FreeRTOS: Getting Started with Semaphores (Arduino IDE)
Learn how to use FreeRTOS semaphores with the ESP32, using the Arduino IDE. Semaphores are like signals (or flags) that allow you to synchronize tasks and manage events.
- Suitable forB.Tech/Diploma IoT/ESP32 students
- Build time1-2 weeks
- LevelBeginner
- Available atAjvika Labs, Kakinada
About this project
In this guide, we’ll introduce you to and explain how to use FreeRTOS semaphores with the ESP32, using the Arduino IDE. Semaphores are like signals (or flags) that allow you to synchronize tasks and manage events. They can be used to indicate that an event has occurred or that a resource is available. Unlike queues, semaphores don’t carry data.
There are two types of semaphores:
binary semaphores
and
counting semaphores
. In this tutorial, we’ll create and explore two different examples to show you how these two types of semaphores work.
New to FreeRTOS?
Start with this tutorial:
ESP32 with FreeRTOS (Arduino IDE)—Getting Started Guide: Creating Tasks
Introducing Semaphores
Semaphores are signaling tools in FreeRTOS used to coordinate tasks. They are commonly used to indicate that an event has occurred or that a resource is available. Unlike queues, semaphores don’t carry data, only a “count”, or “flag”, or “signal” (or whatever you want to call it) used to trigger actions when something happens.
Simple example of how a binary semaphore works
They allow tasks to wait for events, such as a button press or motion detection, or to signal that an event has occurred. This makes them especially useful in scenarios involving interrupts and task synchronization.
Since semaphores don’t store data, they consume less memory than queues and are ideal for lightweight event signaling between tasks.
There are two types of semaphores:
Binary semaphore
: signals a single event. It is a synchronization tool that can be either empty (0) or full (1). It’s like a signal that a task is waiting for before it can proceed.
Counting semaphore
: tracks multiple events (it can be the same event several times). It’s like a queue of events up to a maximum count that you define. Contrary to FreeRTOS queues, these don’t carry data. Only a synchronization signal. You’ll better understand how this works later in the example.
Semaphores Basic Functions
Here are some basic functions of binary and counting semaphores
when using the ESP32 with Arduino IDE. We’ll explore these functions in practical examples next.
Creating a Binary Semaphore
To create a binary semaphore, use the
xSemaphoreCreateBinary()
function. It returns a
SemaphoreHandle_t
handle if successful, or
NULL
if the creation fails.
Creating a Counting Semaphore
To create a counting semaphore, use the
xSemaphoreCreateCounting()
function. It returns a
SemaphoreHandle_t
handle if successful, or
NULL
if the creation fails. Pass as an argument the maximum count.
Taking a Semaphore (Getting from the Semaphore)
Use the
xSemaphoreTake(semaphore, timeout)
function in a task to wait for or take a semaphore. For a binary semaphore, it blocks until the semaphore is available (state 1), setting it to 0 when taken.
For a counting semaphore, it decrements the count if greater than 0, or blocks if the count is 0. The timeout parameter specifies how long to wait (in ticks);
portMAX_DELAY
means wait indefinitely. This means the task will be blocked until there’s a semaphore value to take.
Giving a Semaphore
To give a semaphore use the
xSemaphoreGive()
function if inside a task, or
xSemaphoreGiveFromISR()
if used in ISRs (interrupt service routine functions).
For a binary semaphore, it sets the state to 1, unblocking a waiting task (or ignored if the semaphore is already 1). For a counting semaphore, it increments the count up to
maxCount
, unblocking a waiting task (ignored if at
maxCount
).
Example 1: Binary Semaphore – Toggling an LED with a Button Press
In this section, we’ll build a simple example to demonstrate how a binary semaphore works and how to implement it in a practical application. This example will toggle an LED once when the pushbutton is pressed. To signal the button press, we’ll use a semaphore. Simultaneously, we’ll have another task blinking an LED to demonstrate that we can have multiple tasks running simultaneously.
So, here’s an overview of the example:
We’ll add an interrupt to a pushbutton. When the pushbutton is pressed, the corresponding ISR will give the semaphore (will set it to 1).
There’s another task, called
LEDToggleTask()
, that will be waiting for the semaphore to toggle the state of the LED. When the semaphore is given by the ISR, this task will run and the semaphore will be reset to 0. Only when the semaphore is set to 1, when the button is pressed, this task will run again.
Simultaneously, we have another task called
LEDBlinkTask()
that will increase and decrease the brightness of another LED.
Parts Required
Here’s a list of the parts required for this example:
ESP32 Board
of your choice – read
Best ESP32 Development Boards
2x
LED
2x
220 Ohm resistor
or similar value
Pushbutton
Breadboard
Jumper wires
You can use the preceding links or go directly to
MakerAdvisor.com/tools
to find all the parts for your projects at the best price!
Wiring the Circuit
Wire the following circuit:
Red LED connected to
GPIO 2
Blue LED connected to
GPIO 4
Pushbutton connected to
GPIO 23
You can follow the next schematic diagram.
Code
Upload the following code to the Arduino IDE.
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#define BUTTON_PIN 23
#define LED1_PIN 2 // Toggled LED
#define LED2_PIN 4 // Blinking LED
#define DEBOUNCE_DELAY 200
SemaphoreHandle_t buttonSemaphore = NULL;
volatile uint32_t lastInterruptTime = 0;
void IRAM_ATTR buttonISR() {
uint32_t currentTime = millis();
if (currentTime - lastInterruptTime > DEBOUNCE_DELAY) {
BaseType_t higherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(buttonSemaphore, &higherPriorityTaskWoken);
lastInterruptTime = currentTime;
if (higherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
}
void LEDToggleTask(void *parameter) {
pinMode(LED1_PIN, OUTPUT);
bool ledState = false;
for (;;) {
if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY)) {
ledState = !ledState;
digitalWrite(LED1_PIN, ledState ? HIGH : LOW);
Serial.print(\"LEDToggleTask: LED1 \");
Serial.println(ledState ? \"ON\" : \"OFF\");
}
}
}
void LEDBlinkTask(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
for (;;) {
digitalWrite(LED2_PIN, HIGH);
Serial.println(\"LEDBlinkTask: LED2 ON\");
vTaskDelay(250 / portTICK_PERIOD_MS);
digitalWrite(LED2_PIN, LOW);
Serial.println(\"LEDBlinkTask: LED2 OFF\");
vTaskDelay(250 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
// Defining the button as an interrupt
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
buttonSemaphore = xSemaphoreCreateBinary();
if (buttonSemaphore == NULL) {
Serial.println(\"Failed to create semaphore!\");
while (1);
}
xTaskCreatePinnedToCore(
LEDToggleTask, // Task function
\"LEDToggleTask\", // Task name
3000, // Stack size
NULL, // Task parameters
2, // Higher priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
LEDBlinkTask, // Task function
\"LEDBlinkTask\", // Task name
3000, // Stack size
NULL, // Task parameters
1, // Medium priority
NULL, // Task handle
1 // Core ID
);
}
void loop() {
}
How Does the Code Work?
Start by defining the pins for the pushbutton, for the toggled LED, and for the blinking LED.
#define B
There are two types of semaphores:
binary semaphores
and
counting semaphores
. In this tutorial, we’ll create and explore two different examples to show you how these two types of semaphores work.
New to FreeRTOS?
Start with this tutorial:
ESP32 with FreeRTOS (Arduino IDE)—Getting Started Guide: Creating Tasks
Introducing Semaphores
Semaphores are signaling tools in FreeRTOS used to coordinate tasks. They are commonly used to indicate that an event has occurred or that a resource is available. Unlike queues, semaphores don’t carry data, only a “count”, or “flag”, or “signal” (or whatever you want to call it) used to trigger actions when something happens.
Simple example of how a binary semaphore works
They allow tasks to wait for events, such as a button press or motion detection, or to signal that an event has occurred. This makes them especially useful in scenarios involving interrupts and task synchronization.
Since semaphores don’t store data, they consume less memory than queues and are ideal for lightweight event signaling between tasks.
There are two types of semaphores:
Binary semaphore
: signals a single event. It is a synchronization tool that can be either empty (0) or full (1). It’s like a signal that a task is waiting for before it can proceed.
Counting semaphore
: tracks multiple events (it can be the same event several times). It’s like a queue of events up to a maximum count that you define. Contrary to FreeRTOS queues, these don’t carry data. Only a synchronization signal. You’ll better understand how this works later in the example.
Semaphores Basic Functions
Here are some basic functions of binary and counting semaphores
when using the ESP32 with Arduino IDE. We’ll explore these functions in practical examples next.
Creating a Binary Semaphore
To create a binary semaphore, use the
xSemaphoreCreateBinary()
function. It returns a
SemaphoreHandle_t
handle if successful, or
NULL
if the creation fails.
Creating a Counting Semaphore
To create a counting semaphore, use the
xSemaphoreCreateCounting()
function. It returns a
SemaphoreHandle_t
handle if successful, or
NULL
if the creation fails. Pass as an argument the maximum count.
Taking a Semaphore (Getting from the Semaphore)
Use the
xSemaphoreTake(semaphore, timeout)
function in a task to wait for or take a semaphore. For a binary semaphore, it blocks until the semaphore is available (state 1), setting it to 0 when taken.
For a counting semaphore, it decrements the count if greater than 0, or blocks if the count is 0. The timeout parameter specifies how long to wait (in ticks);
portMAX_DELAY
means wait indefinitely. This means the task will be blocked until there’s a semaphore value to take.
Giving a Semaphore
To give a semaphore use the
xSemaphoreGive()
function if inside a task, or
xSemaphoreGiveFromISR()
if used in ISRs (interrupt service routine functions).
For a binary semaphore, it sets the state to 1, unblocking a waiting task (or ignored if the semaphore is already 1). For a counting semaphore, it increments the count up to
maxCount
, unblocking a waiting task (ignored if at
maxCount
).
Example 1: Binary Semaphore – Toggling an LED with a Button Press
In this section, we’ll build a simple example to demonstrate how a binary semaphore works and how to implement it in a practical application. This example will toggle an LED once when the pushbutton is pressed. To signal the button press, we’ll use a semaphore. Simultaneously, we’ll have another task blinking an LED to demonstrate that we can have multiple tasks running simultaneously.
So, here’s an overview of the example:
We’ll add an interrupt to a pushbutton. When the pushbutton is pressed, the corresponding ISR will give the semaphore (will set it to 1).
There’s another task, called
LEDToggleTask()
, that will be waiting for the semaphore to toggle the state of the LED. When the semaphore is given by the ISR, this task will run and the semaphore will be reset to 0. Only when the semaphore is set to 1, when the button is pressed, this task will run again.
Simultaneously, we have another task called
LEDBlinkTask()
that will increase and decrease the brightness of another LED.
Parts Required
Here’s a list of the parts required for this example:
ESP32 Board
of your choice – read
Best ESP32 Development Boards
2x
LED
2x
220 Ohm resistor
or similar value
Pushbutton
Breadboard
Jumper wires
You can use the preceding links or go directly to
MakerAdvisor.com/tools
to find all the parts for your projects at the best price!
Wiring the Circuit
Wire the following circuit:
Red LED connected to
GPIO 2
Blue LED connected to
GPIO 4
Pushbutton connected to
GPIO 23
You can follow the next schematic diagram.
Code
Upload the following code to the Arduino IDE.
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#define BUTTON_PIN 23
#define LED1_PIN 2 // Toggled LED
#define LED2_PIN 4 // Blinking LED
#define DEBOUNCE_DELAY 200
SemaphoreHandle_t buttonSemaphore = NULL;
volatile uint32_t lastInterruptTime = 0;
void IRAM_ATTR buttonISR() {
uint32_t currentTime = millis();
if (currentTime - lastInterruptTime > DEBOUNCE_DELAY) {
BaseType_t higherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(buttonSemaphore, &higherPriorityTaskWoken);
lastInterruptTime = currentTime;
if (higherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
}
void LEDToggleTask(void *parameter) {
pinMode(LED1_PIN, OUTPUT);
bool ledState = false;
for (;;) {
if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY)) {
ledState = !ledState;
digitalWrite(LED1_PIN, ledState ? HIGH : LOW);
Serial.print(\"LEDToggleTask: LED1 \");
Serial.println(ledState ? \"ON\" : \"OFF\");
}
}
}
void LEDBlinkTask(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
for (;;) {
digitalWrite(LED2_PIN, HIGH);
Serial.println(\"LEDBlinkTask: LED2 ON\");
vTaskDelay(250 / portTICK_PERIOD_MS);
digitalWrite(LED2_PIN, LOW);
Serial.println(\"LEDBlinkTask: LED2 OFF\");
vTaskDelay(250 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
// Defining the button as an interrupt
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
buttonSemaphore = xSemaphoreCreateBinary();
if (buttonSemaphore == NULL) {
Serial.println(\"Failed to create semaphore!\");
while (1);
}
xTaskCreatePinnedToCore(
LEDToggleTask, // Task function
\"LEDToggleTask\", // Task name
3000, // Stack size
NULL, // Task parameters
2, // Higher priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
LEDBlinkTask, // Task function
\"LEDBlinkTask\", // Task name
3000, // Stack size
NULL, // Task parameters
1, // Medium priority
NULL, // Task handle
1 // Core ID
);
}
void loop() {
}
How Does the Code Work?
Start by defining the pins for the pushbutton, for the toggled LED, and for the blinking LED.
#define B
Technologies used
Hardware components
- ESP32
- Arduino
Software & tools
- Arduino IDE
What you get
- Working project / hardware kit, tested before delivery
- Complete source code with explanation
- Project demo and guidance for your viva
- Project report, PPT and IEEE paper support (on request)
Delivery & Payment
Delivery takes a minimum of two weeks from the date your inquiry is confirmed.
Payment is in two parts:
• 50% advance to start the project
• Remaining 50% before delivery
Note:
• Documentation and PPT are not included by default
• If you need documentation/PPT, it can be arranged for extra time and cost
We'll confirm the exact timeline and cost with you after reviewing your requirements.