Using FreeRTOS Mutexes to Synchronize Threads

This tutorial shows how to use FreeRTOS mutexes to avoid race conditions between different threads. We will show how preemption could affect the order in which the code executes and even how to modify the FreeRTOS kernel to make thread scheduling more uniform.

Before you begin, install VisualGDB 5.2 or later.

  1. Start Visual Studio and open the Embedded Project Wizard:01-newprj
  2. Select “Create a new project -> Embedded Binary”:02-msbuild
  3. Select the ARM toolchain and your device. In this tutorial we will use the UART interface to demonstrate race conditions, so pick a board that has a UART interface and select a device matching your board. In this tutorial we will use the STM32F411-Nucleo board that uses the UART interface from the on-board ST-Link 2.1:03-device
  4. On the Sample Selection page choose LEDBlink (FreeRTOS):04-rtos
  5. Finally pick debug settings that match your board and press “Finish” to generate a sample project. We recommend using OpenOCD for debugging most of the boards:05-nucleo
  6. The default FreeRTOS sample consists of 2 threads each one controlling one LED. We will change those threads to print “Hello” messages in English and French instead and show how preemption will mix the output from the 2 threads. Rename the first thread to EnglishThread and the second one to FrenchThread:06-renameVisualGDB will update the references automatically.
  7. Replace the thread function contents with the following code:
    static void EnglishThread(void const *argument)
    {
        for (int i = 0;; i++)
        {
            UARTPrintf("Hello %d\r\n", i);
        }
    }
     
    static void FrenchThread(void const *argument)
    {
        for (int i = 0;; i++)
        {
            UARTPrintf("Bonjour %d\r\n", i);
        }
    }

    The English thread will print messages like “Hello 1”, “Hello 2” and so on and the French one will say Bonjour instead of Hello.

  8. Replace the LED1 and LED2 thread names with ENGLISH and FRENCH:
        osThreadDef(ENGLISH, EnglishThread, osPriorityNormal, 0, configMINIMAL_STACK_SIZE);
      
         /*  Thread 2 definition */
        osThreadDef(FRENCH, FrenchThread, osPriorityNormal, 0, configMINIMAL_STACK_SIZE);
      
        /* Start thread 1 */
        LEDThread1Handle = osThreadCreate(osThread(ENGLISH), NULL);
      
        /* Start thread 2 */
        LEDThread2Handle = osThreadCreate(osThread(FRENCH), NULL);
  9. Add a UARTPrintf() function that will format the messages and print them byte by byte:
    #include <stdarg.h>
     
    static void UARTPrintf(const char *pFormat, ...)
    {
        char buffer[64];
        va_list args;
        va_start(args, pFormat);
        __disable_irq();
        int len = vsprintf(buffer, pFormat, args);
        __enable_irq();
        
        for (int i = 0;< len; i++)
        {
            __disable_irq();
            HAL_UART_Transmit(&g_UART, &buffer[i], 1, HAL_MAX_DELAY);        
            __enable_irq();
        }
        va_end(args);
    }
  10. Add a g_UART variable that will be used as a UART port handle:
    UART_HandleTypeDef g_UART;
  11. Finally add the following code to main() before the call to osKernelStart() that will initialize the UART port. Note that if you are using a different board, the UART number and the GPIO port and pin numbers will be different:
        __USART2_CLK_ENABLE();
        __GPIOA_CLK_ENABLE();
        
        g_UART.Instance        = USART2;
        g_UART.Init.BaudRate   = 115200;
        g_UART.Init.WordLength = UART_WORDLENGTH_8B;
        g_UART.Init.StopBits   = UART_STOPBITS_1;
        g_UART.Init.Parity     = UART_PARITY_NONE;
        g_UART.Init.HwFlowCtl  = UART_HWCONTROL_NONE;
        g_UART.Init.Mode       = UART_MODE_TX_RX;
        if (HAL_UART_Init(&g_UART) != HAL_OK)
            asm("bkpt 255");
        
        GPIO_InitTypeDef  GPIO_InitStruct;
      
        GPIO_InitStruct.Pin       = GPIO_PIN_3;
        GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
        GPIO_InitStruct.Pull      = GPIO_PULLUP;
        GPIO_InitStruct.Speed     = GPIO_SPEED_HIGH;
        GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
     
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
     
        GPIO_InitStruct.Pin = GPIO_PIN_2;
        GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
     
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  12. Enable the Raw Terminal on the COM port connected to your board (you can find the COM port number for the ST-Link 2.1 via Device Manager):term
  13. Press F5 to start debugging and observe how the messages from 2 threads are mixed together:07-mix
  14. We will now use the VisualGDB Real-time to show what exactly happens on the microsecond level and why the messages get mixed. Enable real-time watch via the Dynamic Analysis page of VisualGDB Project Properties:08-analysis
  15. Start debugging. Once the threads are running, add UARTPrintf(), HAL_UART_Transmit() and the two thread names to the real-time watch window. Then continue the program and stop it once real-time watch gathers enough data:09-threadsNotice how the ENGLISH and FRENCH threads run one after the other and the calls to UARTPrintf() seem to overlap.
  16. The current picture does not fully explain why the output is mixed, so we will add custom real-time watches that will show the sent words and characters. Modify the UARTPrintf() function as shown below:
    #include <CustomRealTimeWatches.h>
    struct EventStreamWatch g_SendChar;
    struct EventStreamWatch g_SendText;
     
    static void UARTPrintf(const char *pFormat, ...)
    {
        char buffer[64];
        va_list args;
        va_start(args, pFormat);
        __disable_irq();
        int len = vsprintf(buffer, pFormat, args);
        __enable_irq();
        EventStreamWatch_ReportEvent(&g_SendText, buffer);
        
        for (int i = 0;< len; i++)
        {
            __disable_irq();
            HAL_UART_Transmit(&g_UART, &buffer[i], 1, HAL_MAX_DELAY);       
            char ch[2] = { buffer[i], 0 };
            EventStreamWatch_ReportEvent(&g_SendChar, ch);
            __enable_irq();
        }
        va_end(args);
    }
  17. The EventStreamWatch_ReportEvent() function will only report the events if the g_SendChar and g_SendText watches are added to the real-time watch window. Add them once you start debugging and run the program again to collect enough events. You will see how g_SendText is called by the FRENCH thread, sends 2 characters, then the ENGLISH thread takes control and sends 2 characters from the English message:10-events
  18. This behavior can be easily fixed using a mechanism called Mutex. A mutex can be “taken” by one thread before doing a critical operation (like writing a message) and if any other thread wants to take the same mutex, it will have to wait until the first one releases it. A FreeRTOS mutex should be declared like this:
    SemaphoreHandle_t g_Mutex;
  19. Then you need to initialize it from main() before calling osKernelStart():
    g_Mutex = xSemaphoreCreateMutex();
  20. Finally both English and French threads can take the mutex before calling UARTPrintf() and release it after calling it:
    static void EnglishThread(void const *argument)
    {
        for (int i = 0;; i++)
        {
            xSemaphoreTake(g_Mutex, portMAX_DELAY);
            UARTPrintf("Hello %d\r\n", i);
            xSemaphoreGive(g_Mutex);
        }
    }
     
    static void FrenchThread(void const *argument)
    {
        for (int i = 0;; i++)
        {
            xSemaphoreTake(g_Mutex, portMAX_DELAY);
            UARTPrintf("Bonjour %d\r\n", i);
            xSemaphoreGive(g_Mutex);
        }
    }
  21. Run the program now. Note how the messages are no longer mixed, but the same thread often prints several messages in a row before the other one gets a chance to run:11-mutex
  22. Add the g_Mutex to real-time watch and observe what causes this:retakeIf the FRENCH thread is preempted immediately after it releases the mutex, the ENGLISH thread starts running and manages to take it. If the preemption happens later, the ENGLISH thread cannot output any text, as the FRENCH thread as taken the mutex again.
  23. If we look into the FreeRTOS kernel sources, we will see that releasing the mutex results in a call to xTaskRemoveFromEventList() that lets the thread waiting for the mutex take control only if its priority is higher than the current thread priority:
        if (pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority)
        {
            /* Return true if the task removed from the event list has a higher
            priority than the calling task.  This allows the calling task to know if
            it should force a context switch now. */
            xReturn = pdTRUE;
     
            /* Mark that a yield is pending in case the user is not using the
            "xHigherPriorityTaskWoken" parameter to an ISR safe FreeRTOS function. */
            xYieldPending = pdTRUE;
        }
  24. We could modify the FreeRTOS kernel so that when a mutex is released, the first thread waiting for it will get switched immediately. This will result in more context switches (and lower performance), but no thread will occupy the CPU for too long. Rename xTaskRemoveFromEventList() to xTaskRemoveFromEventListEx() and add an extra parameter to it:
    BaseType_t xTaskRemoveFromEventListEx(const List_t * const pxEventList, BaseType_t yieldIfSamePriority)

    Change the yield condition to also trigger when the thread priorities are the same and yieldIfSamePriority is true:

    if (pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority || (yieldIfSamePriority && pxUnblockedTCB->uxPriority == pxCurrentTCB->uxPriority))

    Add the following wrapper to task.h so that existing code calling xTaskRemoveFromEventList() can still work:

    static BaseType_t xTaskRemoveFromEventList(const List_t * const pxEventList) PRIVILEGED_FUNCTION
    {
        xTaskRemoveFromEventListEx(pxEventList, pdFALSE);
    }
  25. FreeRTOS mutexes are implemented as queues with zero element size. When a mutex is released, FreeRTOS posts a virtual message to the queue. When another thread wants to take the mutex, it reads the message from the queue, making it empty again. We will now add a flag to the queue object that will enable the new “immediate yield” behavior. Add a new member to the end of QueueDefinition in queue.c:
    typedef struct QueueDefinition
    {
        //...
        uint8_t YieldToSamePriorityTasks;
    } xQUEUE;

    Change xQueueGenericSend() to set yieldIfSamePriority to 1 if the new queue flag was set:

    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
    {
        if( xTaskRemoveFromEventListEx( &( pxQueue->xTasksWaitingToReceive ), pxQueue->YieldToSamePriorityTasks ) == pdTRUE )
        {
            queueYIELD_IF_USING_PREEMPTION();
        }
        //...
    }

    Finally add a new function that will enable the YieldToSamePriorityTasks mode on the queue and call it from main() after creating the mutex:

    void xQueueYieldToSamePriorityTasks(QueueHandle_t xQueue)
    {
        Queue_t * const pxQueue = (Queue_t *) xQueue;
        pxQueue->YieldToSamePriorityTasks = pdTRUE;
    }
    xQueueYieldToSamePriorityTasks(g_Mutex);
  26. If you run the code now, you will see that the switch between the threads happens much more often: 12-fair
  27. The Real-time watch clearly shows how releasing the mutex now immediately results in a switch to the other thread and the only reason why the same thread could print 2 messages consecutively is if the other thread gets preempted before it manages to acquire the mutex:13-owner
  28. If you disable the real-time watch now, the overhead of reporting thread switches and other events will be eliminated and you will see that the threads get to run almost exactly in a round-robin fashion:fairrate