Issue resuming AND sending a message to a task from an ISR (Cortex-M4)

Hello,

I am observing a strange behavior in my application running on a Cortex-M4.

I have a task (Task_A with priority 1) that reads and process commands from a queue. At a given moment, after processing a command, the task is suspended - vTaskSuspend(Task_A).
When there is an event (for example a button pressed) I want to resume the task and send a command in his queue. I detect and process the event form an ISR. This is, the pseudo code of my ISR:

void MyISR(void)
{
// 1. prepare the command
MyCommand xCmd = { … };

// 2. first resume the task.
xTaskResumeFromISR(m_xTask);

// 3. then send the command to task input queue
xQueueSendToBackFromISR(m_xInQueue, &xCmd, NULL);

}

What I observe using a FreeRTOS aware debugger is that:

  1. when the system enter in the ISR code then Task_A is in SUSPENDED state.
  2. xTaskResumeFromISR returns with success and Task_A is now in READY state
  3. xQueueSendToBackFromISR returns with success and m_xInQueue has one element.
  4. The IDLE task is the RUNNING task.

At this moment, when the application returns from the ISR Task_A never goes in RUNNING state and the control is always in the IDLE task.

Note: this is the beginning of Task_A control loop

for ( ; ; ) {
if (pdTRUE == xQueueReceive(m_xInQueue, &xCommand, portMAX_DELAY)) {
switch (xCommand.nCmdID) {
[…]

xQueueReceive never returns.

I also noted that if I do not suspend Task_A (comment vTaskSuspend(Task_A) ), but I leave the task blocked in the queue, then the ISR works as I expect and the task does its job.

Am I doing something wrong in the ISR?

Best regards,
Stefano

So why suspend/resuming the task at all ? Is there a reason ?

Don’t suspend and resume the task. The task will suspend on the queue if it is empty and will wake when you send it a command using xQueueSendTobackFromISR.

It is a rare situation that you need to us TaskSuspend / TaskResume, for example you might suspend a GPS task if you turned the GPS off.

In addition to not doing suspend/resume, you should also use pxHigherPriorityTaskWoken parameter of xQueueSendToBackFromISR so that the control returns to Task_A from the ISR. This page provides details about xQueueSendToBackFromISR: https://www.freertos.org/xQueueSendToBackFromISR.html

Thanks.

Your ISR does not force a context switch from within the ISR (the last parameter to the “FromISR” function is NULL so you don’t know if a context switch is needed or not), so the expected behavior is for the ISR to return to the Idle task, but for there to be a switch to the higher priority task the next time the secheduler executes - which in the worst case will be the next tick interrupt.

Do you have configUSE_PREEMPTION set to 1 in FreeRTOSConfig.h? If not the idle task would have to call Yield for the higher priority task to run. The idle task will call yield if configIDLE_SHOULD_YIELD is set to 1.

Hi Richard,

yes, configUSE_PREEMPTION is set to 1.
I also tried to force the context switch using the last parameter as in the API documentation, but the result is the same. The item stays in the queue and there is never a context switch.
A (minor) thing I noted is that taskYIELD_FROM_ISR(), reported in the documentation, is no more defined, but there is instead portYIELD_FROM_ISR() and portEND_SWITCHING_ISR(xSwitchRequired). The last one is what I tested with no success.

Normally I don’t use the pxHigherPriorityTaskWoken parameter since FreeRTOS V7.3.somenthing (long time ago and my memory is not so good).

If, according to you, with that ISR pseudo code I must have a context switch, then I have to dig more in the application, because, by experience, when it seems that the scheduler does something unexpected, then the issue is very probably in the application code :slight_smile:

Best regards,
Stefano

To replay to Hartmut, Rik and Gaurav,

I agree with you. I have no reason to suspend the task, but, instead, it is better to wait the next message.
The task was originally designed to do one job after a reset, if some conditions are true. Then its purpose is finished.
With the evolution of the application, the same job can be done not only after a reset, but also when there are specific events. The idea was to keep the task suspended just as an extra precaution, to avoid side effect if other application tasks, out of my control, send commands to Task_A by mistake.

Thanks for you answer,
Stefano

I’m sure you’d agree that sometimes it’s better to rework/maintain code :wink:
The wrong messages are still hanging around and might hit the task sooner or later…
As you know, of course. Good luck !

Hello,
I would like to share a simple demo (not the full application) to reproduce this scenario, but I can’t attach document to this forum because I am a new user :slight_smile:

So I will try to describe the demo code (based on FreeRTOS v10.2.1) and share few snippets

The demo has:

  • 1 user task
  • 1 user queue
  • IDLE task and Timer Service task

in main.c file

#define MSG_SUSPEND      1
#define MSG_DO_SOMETHING 2

typedef struct _Msg {
  uint8_t nID;
} Msg;

TaskHandle_t g_xTask = NULL;
QueueHandle_t g_xQueue = NULL;

int main(void)
{
  // board initialization.
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();

  g_xQueue = xQueueCreate(10, sizeof(Msg));
  vQueueAddToRegistry(g_xQueue, "MyQueue");
  xTaskCreate(TaskCode, "MyTask", configMINIMAL_STACK_SIZE, &g_xTask, 1, &g_xTask);

  vTaskStartScheduler();
  while (1)
  {
  }
}

void TaskCode( void * pvParameters )
{
  TaskHandle_t *pxTask = (TaskHandle_t*)pvParameters;
  UNUSED(pxTask);
  Msg xMsg = {0};
  xMsg.nID = MSG_SUSPEND;
  xQueueSendToBack(g_xQueue, &xMsg, portMAX_DELAY);

  for (;;) {
    if (xQueueReceive(g_xQueue, &xMsg, portMAX_DELAY) == pdTRUE) {
      switch (xMsg.nID) {
        case MSG_SUSPEND:
          vTaskSuspend(g_xTask);
          break;

        case MSG_DO_SOMETHING:
          xQueueSendToBack(g_xQueue, &xMsg, portMAX_DELAY);
          vTaskDelay(pdMS_TO_TICKS(1000));
          break;
      }
    }
  }
}

The ISR code is this:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  BaseType_t xRes = pdTRUE;
  if (GPIO_Pin == BUTTON_USER_Pin) {
    // wake up the task
    xRes = xTaskResumeFromISR(g_xTask);
    assert_param(xRes);
    Msg xMsg = {
        .nID = MSG_DO_SOMETHING,
    };
    xRes = xQueueSendToBackFromISR(g_xQueue, &xMsg, NULL);
    assert_param(xRes);
  }
  // END_OF_ISR
}

This simple demo reproduces what I am observing in my real application. When the task is suspended I trigger the ISR code and before leaving the ISR (// END_OF_ISR) the situation is as reported in the following task and queue table:

MyTask is READY but it is never rescheduled. The RUNNING task is always the IDLE.

Best regards,
Stefano

Make sure you have configASSERT defined so that it detects configuration errors.

One thing I DON’T see in this code is setting the interrupt priority of the GPIO, so it will default to a priority of 0 which is ‘above’ (higher priority, lower numerical value) than the level you will have configured for the maximum ISR priority that can talk to FreeRTOS in FreeRTOSConfig.h, which can cause data corruption in FreeRTOS.

Hi Richard,

thank you for you input.

configASSERT is defined and the IRQ priority is set to 14.

I can share the demo project, but, as I wrote, I can’t attach document to this forum. If there is any other way, it is not a problem for me to share the code, if someone is interested.

This is the code to configure the GPIO:

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOE_CLK_ENABLE();
  HAL_PWREx_EnableVddIO2();

  /*Configure GPIO pins : BUTTON_USER_Pin  */
  GPIO_InitStruct.Pin = BUTTON_USER_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);

  // other GPIO configuration...

    /* EXTI interrupt init*/
  HAL_NVIC_SetPriority(EXTI0_IRQn, 14, 0);
  HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}

This is the full FreeRTOSConfig.h file:

#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 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.
 *----------------------------------------------------------*/

/* Ensure stdint is only used by the compiler, and not the assembler. */
#if defined(__ICCARM__) || defined(__CC_ARM) || defined(__GNUC__)
 #include <stdint.h>
 extern uint32_t SystemCoreClock;
#endif

#define configUSE_PREEMPTION                    1
#define configUSE_IDLE_HOOK                     1
#define configUSE_TICK_HOOK                     0
#define configCPU_CLOCK_HZ                      ( SystemCoreClock )
#define configTICK_RATE_HZ                      ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES                    ( 7 )
#define configMINIMAL_STACK_SIZE                ( ( uint16_t ) 128 )
#define configTOTAL_HEAP_SIZE                   ( ( size_t ) ( 40 * 1024 ) )
#define configMAX_TASK_NAME_LEN                 ( 16 )
#define configUSE_TRACE_FACILITY                1
#define configUSE_16_BIT_TICKS                  0
#define configIDLE_SHOULD_YIELD                 1
#define configUSE_MUTEXES                       1
#define configQUEUE_REGISTRY_SIZE               100
#define configUSE_RECURSIVE_MUTEXES             0
#define configUSE_COUNTING_SEMAPHORES           1
#define configGENERATE_RUN_TIME_STATS           0
#ifdef DEBUG
#define configUSE_MALLOC_FAILED_HOOK            1
#define configCHECK_FOR_STACK_OVERFLOW          2
#else
#define configUSE_MALLOC_FAILED_HOOK            0
#define configCHECK_FOR_STACK_OVERFLOW          0
#endif
/* Tasks debug through STMOD+ pins. */
#define configUSE_APPLICATION_TASK_TAG          0

/* 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               (configMAX_PRIORITIES-1)
#define configTIMER_QUEUE_LENGTH                100
#define configTIMER_TASK_STACK_DEPTH            ( configMINIMAL_STACK_SIZE * 2 )

/* 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           1
#define INCLUDE_vTaskSuspend                    1
#define INCLUDE_vTaskDelayUntil                 1
#define INCLUDE_vTaskDelay                      1
#define INCLUDE_xQueueGetMutexHolder            1
#define INCLUDE_xTaskGetSchedulerState          1
#define INCLUDE_eTaskGetState                   1
#define INCLUDE_xTaskAbortDelay                 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        /* 15 priority levels */
#endif

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

/* 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  2

/* 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. */
#define configASSERT( x ) if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }

/* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS
   standard names. */
#define vPortSVCHandler    SVC_Handler
#define xPortPendSVHandler PendSV_Handler

/* IMPORTANT: This define MUST be commented when used with STM32Cube firmware,
              to prevent overwriting SysTick_Handler defined within STM32Cube HAL */
/* #define xPortSysTickHandler SysTick_Handler */

#if (configUSE_APPLICATION_TASK_TAG == 1)
 #define traceTASK_SWITCHED_IN() HSD_traceTASK_SWITCHED_IN((int)pxCurrentTCB->pxTaskTag)
 #define traceTASK_SWITCHED_OUT() HSD_traceTASK_SWITCHED_OUT((int)pxCurrentTCB->pxTaskTag)

//#define traceTASK_SWITCHED_IN() BSP_DEBUG_PIN_On((int)pxCurrentTCB->pxTaskTag)
//#define traceTASK_SWITCHED_OUT() BSP_DEBUG_PIN_Off((int)pxCurrentTCB->pxTaskTag)
#endif

/* The size of the global output buffer that is available for use when there
are multiple command interpreters running at once (for example, one on a UART
and one on TCP/IP).  This is done to prevent an output buffer being defined by
each implementation - which would waste RAM.  In this case, there is only one
command interpreter running, and it has its own local output buffer, so the
global buffer is just set to be one byte long as it is not used and should not
take up unnecessary RAM. */
#define configCOMMAND_INT_MAX_OUTPUT_SIZE 1
/* USER CODE END Defines */

#endif /* FREERTOS_CONFIG_H */

And these are the hook functions definition:

void vApplicationStackOverflowHook( TaskHandle_t pxTask, char *pcTaskName )
{
  ( void ) pcTaskName;
  ( void ) pxTask;

  /* Run time stack overflow checking is performed if
  configCHECK_FOR_STACK_OVERFLOW is defined to 1 or 2.  This hook
  function is called if a stack overflow is detected. */
  taskDISABLE_INTERRUPTS();
  for( ;; );
}

void vApplicationMallocFailedHook( void )
{
  /* vApplicationMallocFailedHook() will only be called if
  configUSE_MALLOC_FAILED_HOOK is set to 1 in FreeRTOSConfig.h.  It is a hook
  function that will get called if a call to pvPortMalloc() fails.
  pvPortMalloc() is called internally by the kernel whenever a task, queue,
  timer or semaphore is created.  It is also called by various parts of the
  demo application.  If heap_1.c or heap_2.c are used, then the size of the
  heap available to pvPortMalloc() is defined by configTOTAL_HEAP_SIZE in
  FreeRTOSConfig.h, and the xPortGetFreeHeapSize() API function can be used
  to query the size of free heap space that remains (although it does not
  provide information on how the remaining heap might be fragmented). */
  taskDISABLE_INTERRUPTS();

  for( ;; );
}

/**
 * This function block the program execution if an exception occurs in the FreeRTOS kernel code.
 *
 * @param ulLine [IN] specifies the code line that has triggered the exception.
 * @param pcFileName [IN] specifies the full path of the file that has triggered the exception.
 */
void vFreeRTOSAssertCalled( unsigned long ulLine, const char * const pcFileName )
{
  /* Parameters are not used. */
  ( void ) ulLine;
  ( void ) pcFileName;

#ifdef FREERTOS_CONFIG_ASSERT_MUST_BLOCK
#else
volatile unsigned long ulSetToNonZeroInDebuggerToContinue = 0;


  taskENTER_CRITICAL();
  {
    while( ulSetToNonZeroInDebuggerToContinue == 0 )
    {
      /* Use the debugger to set ulSetToNonZeroInDebuggerToContinue to a
      non zero value to step out of this function to the point that raised
      this assert(). */
      __asm volatile( "NOP" );
      __asm volatile( "NOP" );
    }
  }
  taskEXIT_CRITICAL();
#endif
}

void vApplicationIdleHook( void )
{
  __NOP();
}

Best regards,
Stefano

FreeRTOS_Cortex-M4_Test.zip (1.4 MB)

I see that something is changed and now I can attach document.
That is the demo code:

If need I can try to adapt the demo to other IDE and/or board (always Cortex-M4).

Best regards,
Stefano

Invoking MX_GPIO_Init enabling the GPIO interrupt before starting the (ISR handler) task is dangerous b/c the ISR might use g_xTask / g_xQueue being NULL if an interrupt occurs too early/unexpectedly. Just a hint.

Hi Hartmut,

that code is aromatically generated by our project configuration tool. I agree that is not “a best practice” in a multi-tasking application, but it is not the root of the reported behavior.
Even if I move the HAL_NVIC_EnableIRQ(EXTI0_IRQn); instruction inside the MyTask control loop, the result is the same.

Thank you for your note.

Best regards,
Stefano

A colleague (Gaurav - he posts here regularly) has replicated the issue, so hopefully we will have an answer soon. Can you please let me know your configPORT_OPTIMISED_TASK_SELECTION setting - or better still - post your FreeRTOSConfig.h file here. Thanks.

Thank you for reporting this issue. There is a bug in our code where we miss the context switch if the return value of xTaskResumeFromISR (which indicates if a context switch is required) is ignored. This is fixed in the following PR: https://github.com/FreeRTOS/FreeRTOS-Kernel/pull/207

You should consider using the return value of xTaskResumeFromISR and pxHigherPriorityTaskWoken parameter of xQueueSendToBackFromISR to ensure that the control returns to the task from the ISR:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  BaseType_t xContextSwitchRequired = pdFALSE;
  BaseType_t xHighPriorityTaskWasWoken = pdFALSE;

  if (GPIO_Pin == B_Pin) {
    // wake up the task
    xContextSwitchRequired = xTaskResumeFromISR(g_xTask);

    Msg xMsg = {
        .nID = MSG_DO_SOMETHING,
    };
    xQueueSendToBackFromISR(g_xQueue, &xMsg, &xHighPriorityTaskWasWoken);

    xContextSwitchRequired = ( ( xContextSwitchRequired == pdTRUE ) || ( xHighPriorityTaskWasWoken == pdTRUE ) ) ? pdTRUE : pdFALSE;
    portYIELD_FROM_ISR( xContextSwitchRequired );
  }
}

Thank you once again for bringing this to our attention.

Hi Gaurav,

thank you for your support, your explanation and this recommendation / workaround. It works fine.

@rtel my FreeRTOSConfig.h is in this previous comment.
But I am not using configPORT_OPTIMISED_TASK_SELECTION and I can’t find it neither in the source code of FreeRTOS nor in the documentation. What is it?

Best regards and thanks for this real Open Source, well designed and rock solid real time scheduler,
Stefano

Sorry for confusion, I got the name wrong, it should have been configUSE_PORT_OPTIMISED_TASK_SELECTION - info is here https://www.freertos.org/a00110.html#configUSE_PORT_OPTIMISED_TASK_SELECTION