How to use Binary Semaphore
This is the third tutorial in the series of Free RTOS, and in this tutorial, we are going to use binary semaphore in STM32.
Before reading this, I would recommend that you go through the basics of Free RTOS and Task operations. The links are listed below.
- Introduction to FREE RTOS –> https://controllerstech.com/introduction-to-free-rtos-in-stm32/
- Task Operations in FREE RTOS –> https://controllerstech.com/free-rtos-tutorial-2-0-with-stm32/
Semaphores are basically used to synchronize tasks with other events in the system. In FreeRTOS, semaphores are implemented based on queue mechanism. There are 4 types of semaphores in FreeRTOS:-
- Binary Semaphore
- Counting Semaphore
- Mutex
- Recursive
HOW DOES IT WORK
The working of Binary Semaphore is pretty straight forward. A Binary Semaphore is called Binary because either it is there (‘1’) or it is not (‘0’). There is no third condition in it. So, a Task either have the semaphore or it simply doesn’t. For a Task, we can create a condition that it must have the semaphore, in order to execute itself. Therefore, if the Task don’t have the semaphore, it have no other option but to wait for it to be released by the Task currently having the semaphore.
Let’s say there is a LOW Priority Task running in the critical section. A HIGH Priority Task can preempt the LPT at any point in time. But if the LPT have the semaphore, and it doesn’t release it until it comes out of the critical section, than HPT have no other option but to wait for the semaphore, and it can’t preempt the LOW Task.
I will show the working as we go further in this tutorial. Let’s see the setup part first
SETUP
In the CUBEIDE Setup, first I have created 3 different Tasks with different Priorities as shown in the picture below
Next, I have created a binary semaphore as shown in the picture below
The rest of the setup is usual, so I am not going to go in depth there. you can download the code at the end of this post, and check the setup yourself.
Some Insight into the CODE
As mentioned above, I have created 3 different Tasks with different priorities, and now is the time to write those Tasks.
Below is the Medium Priority Task
void Startnormaltask(void const * argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for(;;)
{
char *str1 = "Entered MediumTask\n";
HAL_UART_Transmit(&huart2, (uint8_t *) str1, strlen (str1), 100);
char *str2 = "Leaving MediumTask\n\n";
HAL_UART_Transmit(&huart2, (uint8_t *) str2, strlen (str2), 100);
osDelay(500);
}
/* USER CODE END 5 */
}
Basically, there is no semaphore needed to run the Medium Priority Task. I have created this task to show you guys that the semaphore is not compulsory for any task to run.
This Normal Task is not waiting for any semaphore, so it can run independently, preempting the LOWER Task when needed.
void Starthightask(void const * argument)
{
for(;;)
{
char *str1 = "Entered HighTask and waiting for Semaphore\n";
HAL_UART_Transmit(&huart2, (uint8_t *) str1, strlen (str1), 100);
osSemaphoreWait(BinSemHandle, osWaitForever);
char *str3 = "Semaphore acquired by HIGH Task\n";
HAL_UART_Transmit(&huart2, (uint8_t *) str3, strlen (str3), 100);
char *str2 = "Leaving HighTask and releasing Semaphore\n\n";
HAL_UART_Transmit(&huart2, (uint8_t *) str2, strlen (str2), 100);
osSemaphoreRelease(BinSemHandle);
osDelay(500);
}
}
As you can see above, the High Task waits for the semaphore before executingosSemaphoreWait(BinSemHandle, osWaitForever);
the first parameter of the function is the semaphore handler and second parameter is the wait time. Here the Task is going to wait forever.
After the execution is finished, the semaphore is released by the High Task using the function osSemaphoreRelease(BinSemHandle);
This way the semaphore becomes available, and any other task waiting for this semaphore, can acquire it.
void Startlowtask(void const * argument)
{
for(;;)
{
char *str1 = "Entered LOWTask and waiting for semaphore\n";
HAL_UART_Transmit(&huart2, (uint8_t *) str1, strlen (str1), 100);
osSemaphoreWait(BinSemHandle, osWaitForever);
char *str3 = "Semaphore acquired by LOW Task\n";
HAL_UART_Transmit(&huart2, (uint8_t *) str3, strlen (str3), 100);
while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13)); // wait till the pin go low
char *str2 = "Leaving LOWTask and releasing Semaphore\n\n";
HAL_UART_Transmit(&huart2, (uint8_t *) str2, strlen (str2), 100);
osSemaphoreRelease(BinSemHandle);
osDelay(500);
}
}
Just like HIGH Task, LOW Task also waits for the semaphore. Once it acquires it, than it waits for the button to be pressed. And after the button is pressed, the semaphore is released.
Let’s see How this actually works in reality
RESULT
- At the beginning, Scheduler have 3 tasks to run. So it will run the HIGH Priority Task first.
- High Task will acquire the semaphore, execute itself, and release the semaphore.
- Next, MEDIUM Priority Task will run, and go in the suspension for 500 ms
- Now it’s time for the LOW Priority Task to run. It will acquire the semaphore, and than waits for the button to be pressed.
- High Task will come out from the suspension and preempt the LOW task. It will try to acquire the semaphore, which is currently held by LOW Task. It have no other option but to wait for the Semaphore.
- MEDIUM Task will come out of the suspension, and preempt the LOW Task. It doesn’t need any Semaphore to run, so it will keep running every 500 ms.
The MEDIUM Task will keep running forever, untill I press the button. See the picture below.
The following operations are taking place above.
- MEDIUM Task was running preempting the LOW Task.
- The button was pressed and the control goes back to the LOW Task. Which will complete it’s further execution, and release the Semaphore.
- The HIGH Task was waiting for this Semaphore. It will acquire it, execute the remaining statements, and release the Semaphore again.
- MEDIUM Task will run now.
- Again scheduler have HIGH Task and LOW Task waiting for execution, so it will run the HIGH Task. Which will acquire and release the Semaphore.
- Medium Task is still in the suspended state as 500ms has not been passed yet. So only the LOW Task is available, and it will run. It will acquire the Semaphore and again wait for the button.
- HIGH Task is still waiting, as it’s 500 ms of suspension has not been over yet. MEDIUM Task will come out of the suspension and it will preempt the LOW Task.
- Now the HIGH Task will come out from the suspension, and try to take the Semaphore, which is still held by the LOW Task. So HIGH Task will wait for it to be released by the LOW Task.
- MEDIUM Task will resume and preempt the LOW Task. And it will keep running every 500ms.
As you can see, the HIGH Priority Task can’t preempt the LOWER Priority Task if it doesn’t have the Semaphore. This way we can do safe Task operations in the critical section, without worrying about the HIGH Task preempting the LOW Task.
Of course Binary Semaphore is not the answer to every problem in any RTOS. There are some problems associated with it. The most common one is Priority Inversion. Basically, when a HIGH Priority Task have to wait for the LOWER Priority Task to finish, it’s called Priority Inversion. We will discuss about it in another tutorial.