cancel
Showing results for 
Search instead for 
Did you mean: 

FreeRTOS semaphore timeout does not work with gcc optimization

glapalme
Associate II

Hi everyone,

I’m working on a project using FreeRTOS 10.4.6 in combination with STM32CubeMX 6.11.0 and TouchGFX on an STM32H562 microcontroller.

I’ve encountered an issue where the xQueueReceive and xSemaphoreTake (or using the API osMessageQueueGet and  osSemaphoreAcquirefunctions do not reach their specified timeouts, regardless of the value passed. Every timeout value
other than 0 behaves like portMAX_DELAY and waits indifinetely. However, a xSemaphoreGive or a xQueueSend called from another task will works and wake up the current blocked task.

This behavior only occurs in release builds with higher GCC optimization levels (-O2, -O3, and -Os). In debug builds or when using lower optimization levels (-O0 or -O1), the functions behave as expected and return after the timeout.

System Info:

  • FreeRTOS version: 10.4.6
  • STM32CubeMX: 6.11.0
  • MCU: STM32H562
  • Additional: TouchGFX integration

Hypothesis:

It seems that the FreeRTOS scheduler may not be yielding correctly to check for timeouts in tasks that call xQueueReceive and xSemaphoreTake. However, functions like vTaskDelay and xTaskCheckForTimeOut work as expected, so this issue might be specific to blocking calls.

Specific Issue:

  • Occurs in release builds with GCC optimization levels -O2, -O3, and -Os.
  • No issue in debug builds or with lower optimizations (-O0, -O1).

Questions:

  • Are there any known issues with FreeRTOS and GCC optimizations that could cause blocking functions to behave incorrectly?
  • Can anyone suggest relevant compiler flags or FreeRTOS configurations to mitigate this problem?

Example Code:

Here’s a code snippet illustrating the issue. With optimization set to -Os, the printf("world") is never reached, while the tasks (with the same code) that I tested has priorities 24 and 48.

void taskDLM( void const *argument )
{
    // be sure that the binary semaphore is not available in the first loop execution
    osSemaphoreAcquire( downloadStartSemHandle, (TickType_t)0 );

    TickType_t xTicksToWait = 50/portTICK_PERIOD_MS;

    while ( 1 )
    {
        vTaskDelay((TickType_t)1000/portTICK_PERIOD_MS);
        printf("hello \n");
// A 50ms timeout is expected here since the binary semaphore is not available
        osSemaphoreAcquire( downloadStartSemHandle, xTicksToWait );
// code is never reached, unless another task `gives` the binary semaphore
        printf("world \n");
        vTaskDelay((TickType_t)1000/portTICK_PERIOD_MS);
    }
}

Here is my FreeRTOSConfig.h:

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/*-----------------------------------------------------------
 * Application specific definitions.
 *
 * These definitions should be adjusted for your particular hardware and
 * application requirements.
 *
 * These parameters and more are described within the 'configuration' section of the
 * FreeRTOS API documentation available on the FreeRTOS.org web site.
 *
 * See http://www.freertos.org/a00110.html
 *----------------------------------------------------------*/

/* USER CODE BEGIN Includes */
/* Section where include file can be added */
/* USER CODE END Includes */

/* Ensure definitions are only used by the compiler, and not by the assembler. */
#if defined(__ICCARM__) || defined(__ARMCC_VERSION) || defined(__GNUC__)
#include <stdint.h>
extern uint32_t SystemCoreClock;
/* USER CODE BEGIN 0 */
extern void configureTimerForRunTimeStats(void);
extern unsigned long getRunTimeCounterValue(void);
/* USER CODE END 0 */
#endif
#ifndef CMSIS_device_header
#define CMSIS_device_header "stm32h5xx.h"
#endif /* CMSIS_device_header */

/*-------------------- STM32H5 specific defines -------------------*/
#define configENABLE_TRUSTZONE                   0
#define configRUN_FREERTOS_SECURE_ONLY           0
#define configENABLE_FPU                         1
#define configENABLE_MPU                         0

#define configUSE_PREEMPTION                     1
#define configSUPPORT_STATIC_ALLOCATION          1
#define configSUPPORT_DYNAMIC_ALLOCATION         1
#define configUSE_IDLE_HOOK                      0
#define configUSE_TICK_HOOK                      0
#define configCPU_CLOCK_HZ                       ( SystemCoreClock )
#define configTICK_RATE_HZ                       ((TickType_t)1000)
#define configMAX_PRIORITIES                     ( 56 )
#define configMINIMAL_STACK_SIZE                 ((uint16_t)128)
#define configTOTAL_HEAP_SIZE                    ((size_t)220000)
#define configSTACK_ALLOCATION_FROM_SEPARATE_HEAP 0
#define configMAX_TASK_NAME_LEN                  ( 20 )
#define configGENERATE_RUN_TIME_STATS            1
#define configUSE_TRACE_FACILITY                 1
#define configUSE_STATS_FORMATTING_FUNCTIONS     1
#define configUSE_16_BIT_TICKS                   0
#define configUSE_MUTEXES                        1
#define configQUEUE_REGISTRY_SIZE                50
#define configCHECK_FOR_STACK_OVERFLOW           2
#define configUSE_RECURSIVE_MUTEXES              1
#define configUSE_MALLOC_FAILED_HOOK             1
#define configUSE_DAEMON_TASK_STARTUP_HOOK       1
#define configUSE_COUNTING_SEMAPHORES            1
#define configENABLE_BACKWARD_COMPATIBILITY      0
#define configUSE_PORT_OPTIMISED_TASK_SELECTION  0
#define configUSE_TASK_NOTIFICATIONS             1
#define configRECORD_STACK_HIGH_ADDRESS          1
#define configHEAP_CLEAR_MEMORY_ON_FREE          0
#define configUSE_MINI_LIST_ITEM                 1
#define configUSE_SB_COMPLETED_CALLBACK          0
/* USER CODE BEGIN MESSAGE_BUFFER_LENGTH_TYPE */
/* Defaults to size_t for backward compatibility, but can be changed
   if lengths will always be less than the number of bytes in a size_t. */
#define configMESSAGE_BUFFER_LENGTH_TYPE         size_t
/* USER CODE END MESSAGE_BUFFER_LENGTH_TYPE */

#define configRUN_TIME_COUNTER_TYPE              size_t

/* Co-routine definitions. */
#define configUSE_CO_ROUTINES                    0
#define configMAX_CO_ROUTINE_PRIORITIES          ( 2 )

/* Software timer definitions. */
#define configUSE_TIMERS                         1
#define configTIMER_TASK_PRIORITY                ( 2 )
#define configTIMER_QUEUE_LENGTH                 10
#define configTIMER_TASK_STACK_DEPTH             256

/* CMSIS-RTOS V2 flags */
#define configUSE_OS2_THREAD_SUSPEND_RESUME  1
#define configUSE_OS2_THREAD_ENUMERATE       1
#define configUSE_OS2_EVENTFLAGS_FROM_ISR    1
#define configUSE_OS2_THREAD_FLAGS           1
#define configUSE_OS2_TIMER                  1
#define configUSE_OS2_MUTEX                  1

/* Set the following definitions to 1 to include the API function, or zero
to exclude the API function. */
#define INCLUDE_vTaskPrioritySet             1
#define INCLUDE_uxTaskPriorityGet            1
#define INCLUDE_vTaskDelete                  1
#define INCLUDE_vTaskCleanUpResources        0
#define INCLUDE_vTaskSuspend                 1
#define INCLUDE_xTaskDelayUntil              1
#define INCLUDE_vTaskDelay                   1
#define INCLUDE_xTaskGetSchedulerState       1
#define INCLUDE_xTimerPendFunctionCall       1
#define INCLUDE_xQueueGetMutexHolder         1
#define INCLUDE_xSemaphoreGetMutexHolder     1
#define INCLUDE_uxTaskGetStackHighWaterMark  1
#define INCLUDE_xTaskGetCurrentTaskHandle    1
#define INCLUDE_eTaskGetState                1

/* Cortex-M specific definitions. */
#ifdef __NVIC_PRIO_BITS
 /* __BVIC_PRIO_BITS will be specified when CMSIS is being used. */
 #define configPRIO_BITS         __NVIC_PRIO_BITS
#else
 #define configPRIO_BITS         4
#endif

/* The lowest interrupt priority that can be used in a call to a "set priority"
function. */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY   15

/* The highest interrupt priority that can be used by any interrupt service
routine that makes calls to interrupt safe FreeRTOS API functions.  DO NOT CALL
INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER
PRIORITY THAN THIS! (higher priorities are lower numeric values. */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5

/* Interrupt priorities used by the kernel port layer itself.  These are generic
to all Cortex-M ports, and do not rely on any particular library functions. */
#define configKERNEL_INTERRUPT_PRIORITY 		( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 	( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

/* Normal assert() semantics without relying on the provision of an assert.h
header file. */
/* USER CODE BEGIN 1 */

#define configLIST_VOLATILE volatile

#ifdef SDEBUG

#define INCLUDE_xTaskGetIdleTaskHandle  1

#define configASSERT( x ) if ((x) == 0) {taskDISABLE_INTERRUPTS(); __asm( "BKPT #0\n" );}
#endif
/* USER CODE END 1 */

#define SysTick_Handler xPortSysTickHandler

/* USER CODE BEGIN 2 */
/* Definitions needed when configGENERATE_RUN_TIME_STATS is on */
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS configureTimerForRunTimeStats
#define portGET_RUN_TIME_COUNTER_VALUE getRunTimeCounterValue
/* USER CODE END 2 */

/* USER CODE BEGIN Defines */
/* Section where parameter definitions can be added (for instance, to override default ones in FreeRTOS.h) */
#if ( configUSE_TRACE_FACILITY == 1 )
	#include "trcRecorder.h"
#endif
/* USER CODE END Defines */

Thanks in advance for any help!

5 REPLIES 5
SofLit
ST Employee

Hello @glapalme and welcome to the community,

I've created a simple project in attachment based on STM32H573-DISCO board starting from the TouchGFX template and didn't reproduce the behavior with optimizations -Os and -O3 / FreeRTOS 10.4.6 

Replaced the printf by two variables that increment. The two variables are incrementing (almost at the same time in the Live Expression) meaning the semaphore timout has expired in 50ms and the current task is no more blocking:

void taskDLM(void *argument)
{
  /* USER CODE BEGIN taskDLM */
    // be sure that the binary semaphore is not available in the first loop execution
    osSemaphoreAcquire( downloadStartSemHandle, (TickType_t)0 );

    TickType_t xTicksToWait = 50/portTICK_PERIOD_MS;

    while ( 1 )
    {
        vTaskDelay((TickType_t)1000/portTICK_PERIOD_MS);
        var_1++;
        //printf("hello \n");
// A 50ms timeout is expected here since the binary semaphore is not available
        osSemaphoreAcquire( downloadStartSemHandle, xTicksToWait );
        var_2++;
// code is never reached, unless another task `gives` the binary semaphore
        //printf("world \n");
        vTaskDelay((TickType_t)1000/portTICK_PERIOD_MS);
    }
  /* USER CODE END taskDLM */
}

 

To give better visibility on the answered topics, please click on "Accept as Solution" on the reply which solved your issue or answered your question.

Hi SofLit, Thanks for your help.

Unfortunately, I’m still facing the issue even after replacing printf with variables. The semaphore timeout still doesn’t expire after 50ms, and the task remains blocked.

glapalme
Associate II

Hey everyone, I’ve identified the root cause of the issue when using optimization -O2 with the CMSIS-OS API. After stepping through the code, here’s what I found:

  1. Calling osSemaphoreAcquire with a timeout of 10 ms works fine. No issues at this stage.

    glapalme_0-1725999819539.png

     

  2. osSemaphoreAcquire then calls xQueueSemaphoreTake. In this function, the xTicksToWait parameter seems to be optimized out in the debugger, and I’m unable to view its value. 

    glapalme_1-1725999829576.png

     

  3. The real problem arises when xQueueSemaphoreTake calls xTaskCheckForTimeOut, passing the xTicksToWait pointer as the second argument.

    glapalme_2-1725999836076.png

Here in xTaskCheckForTimeOut, the debugger shows a completely unreasonable, huge value for *pxTicksToWait. This clearly causes issues because this value is used to calculate the remaining ticks before the timeout:


My workaround

To make it work, I introduced a volatile variable xCopyOfTicksToWait in the xQueueSemaphoreTake function to store a copy of the value of xTicksToWait.

// queue.c
// ...


BaseType_t xQueueSemaphoreTake( QueueHandle_t xQueue,
                                TickType_t xTicksToWait )
{
    BaseType_t xEntryTimeSet = pdFALSE;
    TimeOut_t xTimeOut;
    Queue_t * const pxQueue = xQueue;
    volatile TickType_t xCopyOfTicksToWait = xTicksToWait;

    #if ( configUSE_MUTEXES == 1 )
        BaseType_t xInheritanceOccurred = pdFALSE;
    #endif

    /* Check the queue pointer is not NULL. */
    configASSERT( ( pxQueue ) );

    /* Check this really is a semaphore, in which case the item size will be
     * 0. */
    configASSERT( pxQueue->uxItemSize == 0 );

    /* Cannot block if the scheduler is suspended. */
    #if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
    {
        configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xCopyOfTicksToWait != 0 ) ) );
    }
    #endif

    /*lint -save -e904 This function relaxes the coding standard somewhat to allow return
     * statements within the function itself.  This is done in the interest
     * of execution time efficiency. */
    for( ; ; )
    {
        taskENTER_CRITICAL();
        {
            /* Semaphores are queues with an item size of 0, and where the
             * number of messages in the queue is the semaphore's count value. */
            const UBaseType_t uxSemaphoreCount = pxQueue->uxMessagesWaiting;

            /* Is there data in the queue now?  To be running the calling task
             * must be the highest priority task wanting to access the queue. */
            if( uxSemaphoreCount > ( UBaseType_t ) 0 )
            {
                traceQUEUE_RECEIVE( pxQueue );

                /* Semaphores are queues with a data size of zero and where the
                 * messages waiting is the semaphore's count.  Reduce the count. */
                pxQueue->uxMessagesWaiting = uxSemaphoreCount - ( UBaseType_t ) 1;

                #if ( configUSE_MUTEXES == 1 )
                {
                    if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
                    {
                        /* Record the information required to implement
                         * priority inheritance should it become necessary. */
                        pxQueue->u.xSemaphore.xMutexHolder = pvTaskIncrementMutexHeldCount();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                #endif /* configUSE_MUTEXES */

                /* Check to see if other tasks are blocked waiting to give the
                 * semaphore, and if so, unblock the highest priority such task. */
                if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
                {
                    if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
                    {
                        queueYIELD_IF_USING_PREEMPTION();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }

                taskEXIT_CRITICAL();
                return pdPASS;
            }
            else
            {
                if( xCopyOfTicksToWait == ( TickType_t ) 0 )
                {
                    /* The semaphore count was 0 and no block time is specified
                     * (or the block time has expired) so exit now. */
                    taskEXIT_CRITICAL();
                    traceQUEUE_RECEIVE_FAILED( pxQueue );
                    return errQUEUE_EMPTY;
                }
                else if( xEntryTimeSet == pdFALSE )
                {
                    /* The semaphore count was 0 and a block time was specified
                     * so configure the timeout structure ready to block. */
                    vTaskInternalSetTimeOutState( &xTimeOut );
                    xEntryTimeSet = pdTRUE;
                }
                else
                {
                    /* Entry time was already set. */
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        }
        taskEXIT_CRITICAL();

        /* Interrupts and other tasks can give to and take from the semaphore
         * now the critical section has been exited. */

        vTaskSuspendAll();
        prvLockQueue( pxQueue );

        /* Update the timeout state to see if it has expired yet. */
        if( xTaskCheckForTimeOut( &xTimeOut, &xCopyOfTicksToWait ) == pdFALSE )
        {
            /* A block time is specified and not expired.  If the semaphore
             * count is 0 then enter the Blocked state to wait for a semaphore to
             * become available.  As semaphores are implemented with queues the
             * queue being empty is equivalent to the semaphore count being 0. */
            if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
            {
                traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );

                #if ( configUSE_MUTEXES == 1 )
                {
                    if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
                    {
                        taskENTER_CRITICAL();
                        {
                            xInheritanceOccurred = xTaskPriorityInherit( pxQueue->u.xSemaphore.xMutexHolder );
                        }
                        taskEXIT_CRITICAL();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                #endif /* if ( configUSE_MUTEXES == 1 ) */

                vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xCopyOfTicksToWait );
                prvUnlockQueue( pxQueue );

                if( xTaskResumeAll() == pdFALSE )
                {
                    portYIELD_WITHIN_API();
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                /* There was no timeout and the semaphore count was not 0, so
                 * attempt to take the semaphore again. */
                prvUnlockQueue( pxQueue );
                ( void ) xTaskResumeAll();
            }
        }
        else
        {
            /* Timed out. */
            prvUnlockQueue( pxQueue );
            ( void ) xTaskResumeAll();

            /* If the semaphore count is 0 exit now as the timeout has
             * expired.  Otherwise return to attempt to take the semaphore that is
             * known to be available.  As semaphores are implemented by queues the
             * queue being empty is equivalent to the semaphore count being 0. */
            if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
            {
                #if ( configUSE_MUTEXES == 1 )
                {
                    /* xInheritanceOccurred could only have be set if
                     * pxQueue->uxQueueType == queueQUEUE_IS_MUTEX so no need to
                     * test the mutex type again to check it is actually a mutex. */
                    if( xInheritanceOccurred != pdFALSE )
                    {
                        taskENTER_CRITICAL();
                        {
                            UBaseType_t uxHighestWaitingPriority;

                            /* This task blocking on the mutex caused another
                             * task to inherit this task's priority.  Now this task
                             * has timed out the priority should be disinherited
                             * again, but only as low as the next highest priority
                             * task that is waiting for the same mutex. */
                            uxHighestWaitingPriority = prvGetDisinheritPriorityAfterTimeout( pxQueue );
                            vTaskPriorityDisinheritAfterTimeout( pxQueue->u.xSemaphore.xMutexHolder, uxHighestWaitingPriority );
                        }
                        taskEXIT_CRITICAL();
                    }
                }
                #endif /* configUSE_MUTEXES */

                traceQUEUE_RECEIVE_FAILED( pxQueue );
                return errQUEUE_EMPTY;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
    } /*lint -restore */
}

Question

So, why is the original xTicksToWait in xQueueSemaphoreTake getting corrupted by the compiler?

Did you test the project I shared? Do you face the same behavior you described?

Do you have a simple project you can share that can reproduce from my side?

To give better visibility on the answered topics, please click on "Accept as Solution" on the reply which solved your issue or answered your question.

Hi,

I wonder if the issue is related to the CMSIS-OS wrappers. I suggest trying to use the FreeRTOS API directly. 

As background I use 10.3.1 provided by CubeIDE but avoid using the CMSIS-OS wherever possible - just use the FreeRTOS directly as I find it easier to relate the code to the FreeRTOS documentation. I doubt I will ever port the code to another RTOS. I am a novice so not 100% sure this is a good idea but it appears to work ok. 

If that fails maybe FreeRTOS support forums might provide some help.

Dan