In the previous tutorial, we have seen that every task needs to implement a waiting mechanism. Simple delays are involved for tasks requiring periodic activation. What if we want to trig a task execution based on an event coming from another task? There are several options to do that. You can use one of the available kernel objects to carry an information from one task to one (or several) other tasks. These available kernel objects are basically:
The above objects have different working principles, and are involved depending on your needs. You may also use direct Notifications (from one task to another), which is another mechanism that does not involve an interface object.
Let us start with the first one: the Binary Semaphore. It is called 'binary' because it has only two states:
The binary semaphore can be used to implement a basic synchronization mechanism between two tasks:
Let us illustrate this with our simple two-tasks project from previous tutorials. Let say that now, we want Task_2 to display the console message every time Task_1 has completed 10 LED toggles.
The first step is to declare the semaphore object as global variable. Let us call our semaphore xSem:
...
// FreeRTOS tasks
void vTask1 (void *pvParameters);
void vTask2 (void *pvParameters);
// Kernel objects
xSemaphoreHandle xSem;
...
Then, we need to create the semaphore. We must create the semaphore before any task tries to use it (otherwise it crashes). Let us do it in the initialization part of main() function:
...
// Start Trace Recording
vTraceEnable(TRC_START);
// Create Semaphore object (this is not a 'give')
xSem = xSemaphoreCreateBinary();
// Give a nice name to the Semaphore in the trace recorder
vTraceSetSemaphoreName(xSem, "xSEM");
// Create Tasks
xTaskCreate(vTask1, "Task_1", 256, NULL, 1, NULL);
xTaskCreate(vTask2, "Task_2", 256, NULL, 2, NULL);
// Start the Scheduler
vTaskStartScheduler();
...
Creating the semaphore object only initializes the data structure behind. This is not a 'give' action.
Now, the semaphore object xSem exists, and can be used anywhere in the application. As said before, Task_1 is the one that gives the semaphore every 10 toggles:
/*
* Task_1 toggles LED every 10ms
*/
void vTask1 (void *pvParameters)
{
uint16_t count;
count = 0;
while(1)
{
BSP_LED_Toggle();
count++;
// Release semaphore every 10 count
if (count == 10)
{
xSemaphoreGive(xSem); // <-- This is where the semaphore is given
count = 0;
}
// Wait
vTaskDelay(10);
}
}
And Task_2 is the one that tries to take the semaphore. It displays a console message only when xSem semaphore is available:
/*
* Task_2 sends a message to console when xSem semaphore is given
*/
void vTask2 (void *pvParameters)
{
uint16_t count;
count = 0;
// Take the semaphore once to make sure it is empty
xSemaphoreTake(xSem, 0);
while(1)
{
// Wait for Semaphore endlessly
xSemaphoreTake(xSem, portMAX_DELAY); //<-- This is where the semaphore is taken
// Reaching this point means that semaphore has been taken successfully
// Display console message
my_printf("Hello %2d from task2\r\n", count);
count++;
}
}
There are few things worth noting in the above code:
Time to take a look at the resulting trace. The figure below exhibits the expected behavior. Every 10 execution of Task_1, xSem is released (given), triggering Task_2 which displays the console message.
Because Task_2 has higher priority than Task_1, it executes as soon as the semaphore is given. This suspends the execution of Task_1 before it reaches its waiting function. Task_1 only resumes (i.e. reaches the its wait function) after Task_2 is done.
Task_2 starts with a successful semaphore take action (green label), then performs the message sending, then loops and try to take the semaphore again. Since it was already taken once, there is nothing to take anymore, and Task_2 enter the blocked state here (red label), until the next semaphore give action.
![]() |
![]() ![]() |
When invoked, the vTaskDelay() function turns the calling task into the blocked state for the given duration. It does not work if you want a perfectly periodic task wake-up as it does not account for time the task execution takes for itself. Moreover, if the task has variable execution time, the period between task wake-ups is also variable. The figure below is an oscilloscope capture of the actual LED pin toggling. One can see that every 10 toggles we have a 12ms period due to the message printing process that suspends Task_1.
One way to address this issue could be to invert the priority hierarchy between Task_1 and Task_2. Let's try this:
...
// Create Tasks
xTaskCreate(vTask1, "Task_1", 256, NULL, 2, NULL);
xTaskCreate(vTask2, "Task_2", 256, NULL, 1, NULL);
...
The result is as expected. Task_1 give the semaphore but now, it is allowed to complete before Task_2 sends the console message. The result is a more uniform delay between wake-ups of Task_1.
Still, this is not perfect because Task_1 has slightly (not visible here) varying execution time (depending whether it gives or not the semaphore), and even if it is small its execution time comes as an offset (small constant error) to the desired 10ms wake-up period.
The correct way to implement a uniform delay between task wake-ups is below:
/*
* Task1 toggles LED every 10ms
*/
void vTask1 (void *pvParameters)
{
portTickType xLastWakeTime;
uint16_t count;
count = 0;
// Initialize timing
xLastWakeTime = xTaskGetTickCount();
while(1)
{
BSP_LED_Toggle();
count++;
// Release semaphore every 10 count
if (count == 10)
{
xSemaphoreGive(xSem);
count = 0;
}
// Wait here for 10ms since last wakeup
vTaskDelayUntil (&xLastWakeTime, (10/portTICK_RATE_MS));
}
}
Using vTaskDelayUntil() function this way provides a precise, uniform delay between task wake-ups:
/*
* main.c
*
* Created on: 24/02/2018
* Author: Laurent
*/
#include "main.h"
// Static functions
static void SystemClock_Config (void);
// FreeRTOS tasks
void vTask1 (void *pvParameters);
void vTask2 (void *pvParameters);
// Kernel objects
xSemaphoreHandle xSem;
// Main program
int main()
{
// Configure System Clock
SystemClock_Config();
// Initialize LED pin
BSP_LED_Init();
// Initialize the user Push-Button
BSP_PB_Init();
// Initialize Debug Console
BSP_Console_Init();
// Start Trace Recording
vTraceEnable(TRC_START);
// Create Semaphore object (this is not a 'give')
xSem = xSemaphoreCreateBinary();
// Give a nice name to the Semaphore in the trace recorder
vTraceSetSemaphoreName(xSem, "xSEM");
// Create Tasks
xTaskCreate(vTask1, "Task_1", 256, NULL, 2, NULL);
xTaskCreate(vTask2, "Task_2", 256, NULL, 1, NULL);
// Start the Scheduler
vTaskStartScheduler();
while(1)
{
// The program should never be here...
}
}
/*
* Task_1 toggles LED every 10ms
*/
void vTask1 (void *pvParameters)
{
portTickType xLastWakeTime;
uint16_t count;
count = 0;
// Initialize timing
xLastWakeTime = xTaskGetTickCount();
while(1)
{
// Toggle LED only if button is pressed
if (BSP_PB_GetState())
{
BSP_LED_Toggle();
count++;
}
// Release semaphore every 10 count
if (count == 10)
{
xSemaphoreGive(xSem);
count = 0;
}
// Wait here for 10ms since last wakeup
vTaskDelayUntil (&xLastWakeTime, (10/portTICK_RATE_MS));
}
}
/*
* Task_2 sends a message to console when xSem semaphore is given
*/
void vTask2 (void *pvParameters)
{
portBASE_TYPE xStatus;
uint16_t count;
count = 0;
// Take the semaphore once to make sure it is empty
xSemaphoreTake(xSem, 0);
while(1)
{
// Wait here for Semaphore with 2s timeout
xStatus = xSemaphoreTake(xSem, 2000);
// Test the result of the take attempt
if (xStatus == pdPASS)
{
// The semaphore was taken as expected
// Display console message
my_printf("Hello %2d from task2\r\n", count);
count++;
}
else
{
// The 2s timeout elapsed without Semaphore being taken
// Display another message
my_printf("Hey! Where is my semaphore?\r\n");
}
}
}
Watch the console window while playing with the user push-button:
/******************************************************************************
* TRC_CFG_SNAPSHOT_MODE
*
* Macro which should be defined as one of:
* - TRC_SNAPSHOT_MODE_RING_BUFFER
* - TRC_SNAPSHOT_MODE_STOP_WHEN_FULL
* Default is TRC_SNAPSHOT_MODE_RING_BUFFER.
*
* With TRC_CFG_SNAPSHOT_MODE set to TRC_SNAPSHOT_MODE_RING_BUFFER, the
* events are stored in a ring buffer, i.e., where the oldest events are
* overwritten when the buffer becomes full. This allows you to get the last
* events leading up to an interesting state, e.g., an error, without having
* to store the whole run since startup.
*
* When TRC_CFG_SNAPSHOT_MODE is TRC_SNAPSHOT_MODE_STOP_WHEN_FULL, the
* recording is stopped when the buffer becomes full. This is useful for
* recording events following a specific state, e.g., the startup sequence.
*****************************************************************************/
#define TRC_CFG_SNAPSHOT_MODE TRC_SNAPSHOT_MODE_RING_BUFFER
Below is the usual case of successful semaphore taking from Task_2, right after Task_1 releases it:
![]() |
![]() ![]() |
In this tutorial, you have learned how to implement a basic synchronization mechanism between two tasks using a binary semaphore.