Verifying STM32 FreeRTOS binary semaphore operation

I’ve written about FreeRTOS before, but this time I’ll look at how binary semaphores work.

Here is the development environment at the time of posting.

PC: Windows 10 OS
IDE: STM32CubeIDE Version1.3.0
Configurator: STM32CubeMX Version5.4.0
Board: STM32Nucleo-F401RE

In a multi-threaded program, you need to be careful when dealing with shared resources between multiple threads.
Shared resources are files, memory, etc. that are handled by multiple threads.

In this article, we will look at handling simple shared memory (variables).

Create an IDE project

Select File – New -STM32 Project, select NUCLEO-F401RE from the Boart Selector tab, and press the Next button.

The Project Name should be RtosSemaphore.
The language should be C, so press Finish.

It will ask you “Initialize all peripherals with their default Mode?
This kinod of project is associated with the STM32CubeMx perspectiove.
Press Yes when it asks “Do you want to open this perspective now?

Add FreeRTOS

Check Middleware – FREERTOS in Pinout & Configuration – Categories.
Select CMSIS_V1 from Mode , Interface under FREERTOS Mode and Configuration on the right.
This is the version of the OS.
Either one is fine, but we will use the more stable V1.

Adding tasks and semaphores

Under Mode, select Tasks & Queues in Configuration.
Click the Add button for Tasks above.
A small window will appear. Change the priority to osPriorityNormal and click OK.
The two threads will now have the same priority and will be switched by the time scheduler.

Next, select Timers & Semaphores, and click the Add button for Binary Semaphore in the middle row.
A small window will appear, so click OK.

Next, select TIM5 from SYS – Timebase Source under System Core in Pinout & Configuration – Categories.

In FreeRTOS, it is recommended to use a Timebase Source other than SysTick.

The term “task” here can be thought of as the same as “thread”.
The term “thread” will be used throughout this section.

Build

First, let’s build it and make sure there are no errors.

Open Core – Src – main.c from Project Explorer by double-clicking on it.

In my environment

Line 272: StartDefaultTask()
Line 290: StartTask02()

In my environment, two threads are created.
The creation of these threads is written near line 126, and the osKernelStart() near line 138 starts these threads running.

Connecting the board and trying to run it

Now, connect the NUCLEO-F401RE board to the PC with a USB cable and Run – Debug.

When the debugger starts, double-click on the line number of the for(;;) in the two threads to put a breakpoint.

Run – Resume and run the program to break.
The F8 key is useful for Resume.

When you run the program with Resume, it will break at the breakpoint of the other thread, and you will see that both threads are working.

Coding

Add variables a, b, c, and d, and implement the following code in the two threads.
As a reminder, please remove the osDelay(1); part.
The reason is that this will cause the threads to switch.

(This will prevent the thread from switching when accessing shared memory, making it difficult to identify the problem.

/* USER CODE BEGIN 0 */ int a,b,c
int a,b,c,d;
/* USER CODE END 0 */ int a,b,c,d

/** * @brief The application entry point
  * @brief The application entry point.
  * @retval int
  */ int main(void)
int main(void)
void StartDefaultTask(void const * argument)
{
  /* USER CODE BEGIN 5 */ /* Infinite loop
  /* Infinite loop */
  for(;;)
  {
    a++;
    b++;
    if (100000 < a)
    {
    	d = b + c;
    	if ( a ! = d)
    	{
    		b = c;
    	}
    }
  }
  /* USER CODE END 5 */ } 
}

void StartTask02(void const * argument)
{
  /* USER CODE BEGIN StartTask02 */ /* Infinite loop
  /* Infinite loop */
  for(;;)
  {
	a++;
	c++;
    if (100000 < a)
    {
    	d = b + c;
    	if ( a ! = d)
    	{
    		b = c;
    	}
    }
  }
  /* USER CODE END StartTask02 */
}

Place a breakpoint and execute

Implement the following code in two threads, and put breakpoints at two places at if (a ! = d) and execute it with breakpoints in two places.
Break when the value of a exceeds 100000.

Make sure that the value of a does not match d at this time.

In our environment, a = 100001, d = 112356. Quite a difference.

The value of a is going to be the same as d, which is the value of b + c.

Why does this happen?

The a++ part, in C it is one line, but in reality the instructions are written over multiple lines in assembler.

In Window - Show View - Disassembly, the C code is disassembled and displayed.
The a++ part looks like this

ldr r3,[pc,#64].
ldr r3,[r3,#0].
adds r3,#1
ldr r2,[pc,#60] adds r3,#1
str r3,[r2,#0].

The process is to bring the memory value into the register r3, add +1, and then write it back.
So, if a thread switch occurs during this process, a counting error will occur.

For example, suppose the first thread brings a value of 5 from memory.
It adds one more value to make it 6, and then switches to the second thread before saving it to memory.
Then the second thread brings back the value from memory, which is also 5.
It adds one more value to make it 6 and stores it in memory.
Then it switches to the first thread, which stores 6 in memory.
Originally, it should have been 7.

So let's use binary semaphores to access shared resources like this one.

Using Binary Semaphores

There are two types of semaphores: counting semaphores and binary semaphores.

This is not a very good analogy, but imagine a parking lot.
A place where five cars can park is a counting semaphore with a maximum value of five, while a place where only one car can park is a binary semaphore.

Shared files must be accessible only from one place at a time.
Use a binary semaphore in such a case.

When a resource is in use, it waits and becomes available when it is free.
When the resource is used, it will wait, and when it is free, it will notify the user that it is free.

In this way, a parking space for one car is shared among the threads that use it.

To wait, use the following function.
osSemaphoreWait(myBinarySem01Handle, osWaitForever);

The first argument specifies the handle of the semaphore, and the second argument specifies the waiting time.
Since it is WaitForever, the value is to wait forever.

The following function is used to announce the availability.
osSemaphoreRelease(myBinarySem01Handle);

Again, the first argument is the handle of the semaphore.

The code using the semaphore looks like the following.
The shared memory access part is sandwiched between Wait and Release.

void StartDefaultTask(void const * argument)
{
  /* USER CODE BEGIN 5 */ /* Infinite loop
  /* Infinite loop */
  for(;;)
  {
  osSemaphoreWait(myBinarySem01Handle, osWaitForever);
  a++;
  osSemaphoreRelease(myBinarySem01Handle);
  b++;
    if (100000 < a)
    {
    	d = b + c;
    	if ( a ! = d)
    	{
    		b = c;
    	}
    }
  }
  /* USER CODE END 5 */ } 
}

void StartTask02(void const * argument)
{
  /* USER CODE BEGIN StartTask02 */ /* Infinite loop
  /* Infinite loop */
  for(;;)
  {
  osSemaphoreWait(myBinarySem01Handle, osWaitForever);
  a++;
  osSemaphoreRelease(myBinarySem01Handle);
  c++;
    if (100000 < a)
    {
    	d = b + c;
    	if ( a ! = d)
    	{
    		b = c;
    	}
    }
  }
  /* USER CODE END StartTask02 */
}

After this, build and run the program, and you can see that a and d are equal.
How did you like it? In this article, we introduced a very common way to handle binary semaphores.

Leave a Reply