Try the new version: www.pomad.fr/POMAD_2024/welcome

4. Binary Semaphores

Submitted by admin on Thu, 07/15/2021 - 22:38

 

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:

  • Semaphores (binary semaphores, counting semaphores)
  • Mutexes
  • Message Queues
  • Event Groups

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.

 

1. The Binary Semaphore

Let us start with the first one: the Binary Semaphore. It is called 'binary' because it has only two states:

  • The semaphore exists (it has been 'given')
  • The semaphore doesn't exist (it was never given, or it has already been 'taken')

The binary semaphore can be used to implement a basic synchronization mechanism between two tasks:

  • Task_A gives the semaphore when something has been completed
  • Task_B is waiting for this semaphore. Task_B takes the semaphore when it becomes available and then resume execution.
  • Once a binary semaphore has been taken, it is 'dead' (i.e. you can't take it twice) until it is given again.

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 OS 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 LED toggling:

/*
 *	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++;
	}
}

 

Save , build , start a debug session and run the program .

 

There are few things worth noting in the above code:

  • The xSemaphoreTake() function takes two arguments. The first one is the semaphore name, the second one is a timeout. If the task couldn't take the semaphore for that amount of time, it resumes (i.e. execute next lines) whatever. In the example above, the first take attempt (before the loop) is only there to kill xSem semaphore if for any reason it was already given. The timeout is set to zero, therefore the task does not actually waits here if the semaphore was not given.
  • The second take attempt (inside the loop) sets the timeout to portMAX_DELAY, which you may consider as an infinite wait time. The task will wait here forever if the semaphore is never given.
  • The semaphore take action tells the scheduler that Task_2 is waiting for the semaphore xSem to be given by some other task. This is therefore an OS-aware waiting mechanism, and we don't need a delay function anymore. The scheduler gives CPU to other tasks while Task_2 waits for xSem.

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.

image_000.png

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 its waiting 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.

image_001.png

gitlab commit  Commit name "Binary semaphore"
push  Push onto Gitlab

 

2. More on delays

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.

image_002.png

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.

image_003.png

image_004.png

 

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 wakeup events 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:

image_005.png

 

3. Using timeouts

Most of the time, waiting endlessly for a semaphore is not what you want. In the example above, if for some reason Task_1 does not release the semaphore, Task_2 will never execute. Usually, you want to be able to handle such abnormal situation.

To illustrate this, let us change a bit our code so that:

  • Task_1 only perform the LED toggling and count increment when the user button (the blue one) is pressed
  • Task_2 tries to take the semaphore. But if nothing is coming within a 2 seconds delay, then it displays an alert message
/*
 * 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:

image_006.png

Catching both cases of semaphore taking attempts in the trace recorder is a little tricky because the 2s timeout is larger than the recorder capacity (limited by the small amount of RAM we have).

You'll need to turn the snapshot mode to circular buffer. In this mode, you'll get the n last seconds of program execution (instead of the n first seconds when using the stop-when-full recording mode).

/******************************************************************************
 * 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:

image_007.png

And now, this is what happen when the semaphore was not available for more than 2 seconds:

image_008.png

 

If you have Tracealyzer opened, try this: Views→Communication Flow

image_009.png

Yes, Tracealyzer is able to construct this graph based on what's in the recorder data. Isn't that wonderful? Imagine this on more complex projects.

 

gitlab commit  Commit name "Binary semaphore with timeout"
push  Push onto Gitlab

 

4. Summary

In this tutorial, you have learned how to implement a basic synchronization mechanism between two tasks using a binary semaphore.

 

Add new comment

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.