FreeRTOS Binary Semaphores – Process Synchronization

Keywords: Embedded systems, ARM, FreeRTOS, Semaphore, Tasks Synchronization, Mutex

Code Link: Tutorial Source Code Github – Keil

In the introductory tutorial we mentioned that Binary Semaphores are used for process Synchronization. In this tutorial we will explain what does process synchronization means and how freeRTOS Binary Semaphores can be used to achieve process Synchronization.

Note: In this tutorial, terms “Task” and “Process” are used interchangeably.

When it comes to process synchronization, two requirements need to be fulfilled!

  • Mutual Exclusion
  • Order of Execution

Mutual Exclusion means no two or more tasks/process are allowed to access same resource simultaneously. Order of Execution reflects the sequence at which the processes are allowed to execute.

For example consider an array/buffer shared by two Tasks in a hypothetical embedded system. One task say “sensor-Task” reads data from sensors and fill it in the array. Another task say “LCD-Task” is responsible for reading sensors data from buffer, process it and display on LCD. In this system the LCD-Task should only run once the data is available in the array buffer i.e. The LCD-Task should wait until the array buffer is filled by sensor data acquiring Task —> ORDER.

second, while the sensors data is being filled in the buffer, the LCD-Task is not allowed to read half filled data i.e. These task should only execute one after another —> Mutual Exclusion.

Synchronization via Binary Semaphore:

Binary semaphores can be used in an interesting way to achieve synchronization by simply reversing the semaphore inherent properties of incrementing and decrementing the internal counter – N (introductory tutorial). Instead of initializing semaphore with some positive value, initializing it with 0 and reversing the Take() and Give() Operations sequence to Give() and Take().

Consider two task say Task-1 and Task-2, Task-2 is only allowed to run once Task-1 completes. The scheduler can schedule these two tasks in any order. So in order for these two tasks to run in the desired sequence, Semaphore initialized with 0 can be used. i.e. Task-2 should call semaphore Take() operation while Task-1 needs to call Semaphore Give() operation. Don’t worry, the example tutorial explains it.

Suppose Task-2 starts running before Task-1, as soon as it will call semaphore Task() call, the Task-2 will be sent to block state as the semaphore is initialized with 0 and can’t be Taken. Now since Task-2 is in block state, Task-1 will be allowed to run. Task-1 does some work like acquiring data and calls semaphore Give() operation. Now that the semaphore is available (as given by semaphore Task-1), Task-2 will come out of block state to Take semaphore (making the counter again 0) thus going to run state. At this point any new call from Task-2 will again block the task until again its unblocked by Task-1.

This ideally solves an interesting problem normally faced in embedded systems described bellow.

Most of the embedded systems uses interrupts to handle various on chip and off-chip events. As an example, interrupt generated by a serial buffer indicating data is ready to be received and processed. The data processing may be time consuming and if done inside ISR, it MAY starve normal application flow.

In embedded systems it is highly recommended to keep the ISR as short as possible to avoid blocking/starving of normal application flow especially in time critical systems. The problem becomes more sever if interrupts are arriving at high rate like in case of Serial communication at 115200kbps. The case bacomes even more severe if Interrupt Nesting and Interrupt Re-entrant are allowed.

Tutorial Scenario:

This tutorial is a demonstration of Task synchronization with interrupt via binary semaphore to tackle problems mentioned earlier. A freeRTOS task is created that keeps waiting for the signal (Binary Semaphore Give()) to be received from interrupt. The task remains in block state on binary semaphore until it is unblocked by interrupt which in this tutorial is generated by a user button on STM32F4-Discovery board. The problem mentioned above is tackled in the following way.

  • Task remains in block state consuming no CPU cycles
  • Task only executes after Interrupt ISR returns – Synchronization
  • Inside ISR, only task is unblocked on binary semaphore (Signal) – Thus ISR remains short.

Follow are the tutorial steps.

Steps:

1. The first step is to create a binary semaphore to be used later for interrupt-to-task synchronization.

smphrHandle = xSemaphoreCreateBinary();

Where smphrHandle is semaphore handle declared globally and used to reference semaphore later in code.

SemaphoreHandle_t smphrHandle;

xSemaphoreCreateBinary() is already explained in the introductory tutorial.

Once the binary semaphore creation is successful, a freeRTOS task is created and Scheduler is started otherwise an error message is sent to the debug port. At this point the binary semaphore contains value=1 in its internal counter variable – N.

if (smphrHandle != NULL) {
    
  xTaskCreate (vTask, "Task", 200, NULL, 1, NULL);
    
  vTaskStartScheduler();  
    
}else {
    
  printf ("Failed to create Semaphore! :-(\n");
    
  while (1);
}

Note: If you don’t know how freeRTOS tasks are created, we have a dedicated tutorial for freeRTOS Tasks. Link to the tutorial is given bellow.

2. Next step is to define Task Function i.e. vTask for the task created in the first step. The vTask definition is given bellow.

void vTask(void * pvParams) {

  /* 
    initially take semaphore to bring the
    binary semaphore counter value to 0.
  */
  xSemaphoreTake(smphrHandle, portMAX_DELAY);

  for (;;) {
    /* Lock Task here until unlocked by ISR*/
    xSemaphoreTake(smphrHandle, portMAX_DELAY);
    
    printf("Button Pressed, Task Unlocked!.\n");
  }
}

The xSemaphoreTake() API sends a Task to Block State if semaphore is not available i.e. the internal counter variable of smphrHandle value is 0.

In order to synchronize Task with Interrupt, the vTask Task needs to remains in Block State. In order to achieve this the first call to xSemaphoreTake(smphrHandle, portMAX_DELAY); statement brings the binary semaphore value to 0 (in step 1, it was 1) before the vTask enters the main task functionality infinite loop.

Now that the binary semaphore has been taken, the next call to xSemaphoreTake(smphrHandle, portMAX_DELAY) in infinite loop will block the Task for indefinite time until the semaphore is given by the ISR.

The second call to xSemaphoreTake(smphrHandle, portMAX_DELAY) in the infinite loop will send the vTask to block state preventing it from printing the printf message.

3. The next step is to configure User button GPIO on STM32F4-Discovery as an input and its interrupt configuration on button press. We already have a dedicated tutorial for STM32F4 interrupts and demo tutorial. Therefor the code will not be repeated here. The link to the tutorial is given bellow. Kindly go through the link bellow and relate it to the source code link given at the start of this tutorial.

4. The next step is to define an ISR for button interrupt. On STM32F4-Discovery User button is connected to PA.0 which produces interrupt on EXTI0 and subsequently calls ISR defined by void EXTI0_IRQHandler void functions definition.

Note: See the STM32F4 Interrupt tutorial for more detail. Link given above.

void EXTI0_IRQHandler (void) {

    int txStatus = 0;
    BaseType_t xHigherPriorityTaskWoken;
    /*
        Clear the pending interrupt
    */
    __setbit (EXTI->PR, 0);

    txStatus = xSemaphoreGiveFromISR(smphrHandle, &xHigherPriorityTaskWoken);

    if (pdPASS == txStatus) {
      portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
    }
}

The __setbit (EXTI->PR, 0); clears the interrupt so that next interrupt/button press can be detected. txStatus = xSemaphoreGiveFromISR(smphrHandle, &xHigherPriorityTaskWoken); statement (as explained in the introductory tutorial) Gives back semaphore (increments semaphore internal counter). If giving back the semaphore caused a higher priority Task to unblock i.e. A low priority Task was running and a Task with higher priority was waiting (blocked) for the smphrHandle to be available and mean while the interrupt occurs than the xHigherPriorityTaskWoken flag will be set to indicate to the user to Yield from ISR immediately. xSemaphoreGiveFromISR returns pdPASS if giving semaphore was successful.

If giving back semaphore was successful, an immediate return from ISR is required DIRECTLY to the high priority Task being unblocked by semaphore Give(). In freeRTOS in order to switch directly to the that higher priority task being unblocked, portEND_SWITCHING_ISR and portYIELD_FROM_ISR are used and depends upon the freeRTOS Port used. In case of STM32F4 (ARM Cortex-M) the first one is used. portEND_SWITCHING_ISR will cause the kernel to directly jump to the high priority task instead of returning from ISR to the low priority task and then waiting for the context switch upon which the high priority will be selected. This minimize the delay between unblocking a Task from an ISR and subsequently achieving perfect synchronization.

The following videos demonstrate the message sent from blocked task vTask to debug window upon button press/interrupt.

For complete source code refer to Github link given at the start of this tutorial.

Note: The reason why single button press causes multiple messages to display on debug serial window is due to the button Debounce Effect that causes the ISR to be invoked multiple times.

Note: We have routed printf messages to ST-Link debugger via ARM-ITM. There is a dedicated tutorial on how to redirect printf messages to debugger. Link to the tutorial is given bellow.

Click the full screen button for more clear view.

References:

[1] – FreeRTOS Official Website



111 thoughts on “FreeRTOS Binary Semaphores – Process Synchronization”

Leave a Reply

Your email address will not be published. Required fields are marked *