Skip to content



FreeRTOS »

Notifications - Inter-task Communication and Synchronization

FreeRTOS supports some methods for Inter-task Communication and Synchronization. With a simple implementation, notification is 45% faster and uses less RAM.Notifications is a lightweight mechanism which can be used in many use cases which do not need a complex method to control or synchronize tasks.

Last update: 2022-06-29


STM32-Tutorials

Task Notifications#

RTOS task notification functionality is enabled by default, and can be excluded from a build by setting configUSE_TASK_NOTIFICATIONS to 0 in FreeRTOSConfig.h.

Each RTOS task has an array of task notifications. Prior to FreeRTOS V10.4.0, tasks only had a single task notification, not an array of notifications.

The constant configTASK_NOTIFICATION_ARRAY_ENTRIES sets the number of indexes in the task notification array.

Each task notification has a notification state that can be either:

  • taskNOT_WAITING_NOTIFICATION
  • taskWAITING_NOTIFICATION
  • taskNOTIFICATION_RECEIVED
typedef struct tskTaskControlBlock {
    ...
    #if ( configUSE_TASK_NOTIFICATIONS == 1 )
        volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
        volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    #endif
    ...
}

Notify#

Task notification is an event sent directly to a task, rather than indirectly to a task via an intermediary object such as a queue, event group or semaphore. Sending a task notification to a task will unblock that task if the task is in the blocked state specifically to wait for a notification.

BaseType_t xTaskGenericNotify() ( TaskHandle_t xTaskToNotify,
                                  UBaseType_t uxIndexToNotify,
                                  uint32_t ulValue,
                                  eNotifyAction eAction,
                                  uint32_t * pulPreviousNotificationValue
                                )
{
    pxTCB = xTaskToNotify;

    if( pulPreviousNotificationValue != NULL ) {
        *pulPreviousNotificationValue = pxTCB->ulNotifiedValue[ uxIndexToNotify ];
    }

    ucOriginalNotifyState = pxTCB->ucNotifyState[ uxIndexToNotify ];
    pxTCB->ucNotifyState[ uxIndexToNotify ] = taskNOTIFICATION_RECEIVED;

    switch( eAction ) {
        // update pxTCB->ulNotifiedValue[ uxIndexToNotify ] using ulValue
    }

    if( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) {
        listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
        prvAddTaskToReadyList( pxTCB );
        if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) {
            taskYIELD_IF_USING_PREEMPTION();
        }
    }
}

Waiting#

When a task is set to wait for a notification, it is put to delay task list and moved to blocked state.

Each notification within the array operates independently - a task can only block on one notification within the array at a time and will not be unblocked by a notification sent to any other array index.

BaseType_t xTaskGenericNotifyWait( UBaseType_t uxIndexToWaitOn,
                                   uint32_t ulBitsToClearOnEntry,
                                   uint32_t ulBitsToClearOnExit,
                                   uint32_t * pulNotificationValue,
                                   TickType_t xTicksToWait
                                 )
{
    if( pxCurrentTCB->ucNotifyState[ uxIndexToWait ] != taskNOTIFICATION_RECEIVED )
    {
        pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ] &= ~ulBitsToClearOnEntry;
        pxCurrentTCB->ucNotifyState[ uxIndexToWait ] = taskWAITING_NOTIFICATION;
        if( xTicksToWait > ( TickType_t ) 0 ) {
            prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
            portYIELD_WITHIN_API();
        }
        if( pulNotificationValue != NULL ) {
            *pulNotificationValue = pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ];
        }
    }
}

Limitation#

Unblocking an RTOS task with a direct notification is 45% faster and uses less RAM than unblocking a task using an intermediary object such as a binary semaphore.

However, it has some limitations that it only be used when there is only one task that can be the recipient of the event. This condition is however met in the majority of real world use cases, such as an interrupt unblocking a task that will process the data received by the interrupt.

Usage#

As Binary Semaphores#

A binary semaphore is a semaphore that has a maximum count of 1, hence the ‘binary’ name. A task can only ‘take’ the semaphore if it is available, and the semaphore is only available if its count is 1.

When a task notification is used in place of a binary semaphore the receiving task’s notification value is used in place of the binary semaphore’s count value, and the ulTaskNotifyTake() (or ulTaskNotifyTakeIndexed()) API function is used in place of the semaphore’s xSemaphoreTake() API function.

TheulTaskNotifyTake() function’s xClearOnExit parameter is set to pdTRUE, so the count value is returned to zero each time the notification is taken - emulating a binary semaphore.


Example:

This is an example of a transmit function in a generic peripheral driver. An RTOS task calls the transmit function, then waits in the Blocked state (so not using an CPU time) until it is notified that the transmission is complete. The transmission is performed by a DMA, and the DMA end interrupt is used to notify the task.

static TaskHandle_t xTaskToNotify = NULL;
const UBaseType_t xArrayIndex = 1;

// Transmit function
void vStartTransmission( uint8_t *pcData, size_t xDataLength ) {
    configASSERT( xTaskToNotify == NULL );
    xTaskToNotify = xTaskGetCurrentTaskHandle();
    vStartTransmit( pcData, xDatalength );
}

// A task that transmits data and the enters blocked state
void vTask(...) {
    StartTransmission( pcData, xDataLength );
    const TickType_t xMaxBlockTime = pdMS_TO_TICS( 500 );
    ulTaskNotifyTakeIndexed (
        xArrayIndex,
        pdTRUE, /* xClearCountOnExit */
        xMaxBlockTime 
    );
}

// The transmit end interrupt
void vTransmitEndISR( void ) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    prvClearInterruptSource();
    configASSERT( xTaskToNotify != NULL );
    vTaskNotifyGiveIndexedFromISR( xTaskToNotify,
                                   xArrayIndex,
                                   &xHigherPriorityTaskWoken );
    xTaskToNotify = NULL;
    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

As Counting Semaphores#

A counting semaphore is a semaphore that can have a count value of zero up to a maximum value set when the semaphore is created. A task can only ‘take’ the semaphore if it is available, and the semaphore is only available if its count is greater than zero.

When a task notification is used in place of a counting semaphore the receiving task’s notification value is used in place of the counting semaphore’s count value, and the ulTaskNotifyTake() (or ulTaskNotifyTakeIndexed()) API function is used in place of the semaphore’s xSemaphoreTake() API function.

The ulTaskNotifyTake() function’sxClearOnExit parameter is set to pdFALSE so the count value is only decremented (rather than cleared) each time the notification is taken - emulating a counting semaphore.


Example:

The value returned fromulTaskNotifyTake() is used to know how many outstanding ISR events must be processed, allowing the RTOS task’s notification count to be cleared back to zero each time ulTaskNotifyTake() is called.

const UBaseType_t xArrayIndex = 0;

/* The ISR does not process data directly.
   It unblocks the handling task, and increase notification value */
void vISR() {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    prvClearInterruptSource();
    vTaskNotifyGiveIndexedFromISR( xHandlingTask, 
                                   xArrayIndex, 
                                   &xHigherPriorityTaskWoken );
    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

/* A task that blocks waiting to be notified and processes data */
void vHandlingTask() {
    BaseType_t xEvent;
    const TickType_t xBlockTime = pdMS_TO_TICS( 500 );
    uint32_t ulNotifiedValue;
    while(1) {
        ulNotifiedValue = ulTaskNotifyTakeIndexed (
            xArrayIndex,
            pdTRUE,  /* xClearOnExit */
            xBlockTime
        );
        if( ulNotifiedValue == 0 ) {
            // no notification
        } else {
            // process outstanding interrupts
            while(ulNotifiedValue) {
                xEvent = xQueryPeripheral();
                if( xEvent != NO_MORE_EVENTS ) {
                    vProcessEvent(xEvent);
                    ulNotifiedValue--;
                } else {
                    break;
                }
            }
        }
    }
}

As Event Group#

An event group is a set of binary flags (or bits), to each of which the application writer can assign a meaning. An RTOS task can enter the Blocked state to wait for one or more flags within the group to become active. The RTOS task does not consume any CPU time while it is in the Blocked state.

When a task notification is used in place of an event group the receiving task’s notification value is used in place of the event group, bits within the receiving task’s notification value are used as event flags, and the xTaskNotifyWait() API function is used in place of the event group’sxEventGroupWaitBits() API function.

Likewise, bits are set using the xTaskNotify() and xTaskNotifyFromISR() API functions (with their eAction parameter set to eSetBits) in place of the xEventGroupSetBits() and xEventGroupSetBitsFromISR() functions respectively.

xTaskNotifyFromISR() has significant performance benefits when compared to xEventGroupSetBitsFromISR() because xTaskNotifyFromISR() executes entirely in the ISR, whereas xEventGroupSetBitsFromISR() must defer some processing to the RTOS daemon task.

Unlike when using an event group the receiving task cannot specify that it only wants to leave the Blocked state when a combination of bits are active at the same time. Instead, the task is unblocked when any bit becomes active, and must test for bit combinations itself.


Example:

Two ISR for transmitting and receiving will notify to a task handler.

#define TX_BIT    0x01
#define RX_BIT    0x02

static TaskHandle_t xHandlingTask;

void vTxISR( void ) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    prvClearInterrupt();
    xTaskNotifyFromISR( xHandlingTask,
                       TX_BIT,
                       eSetBits,
                       &xHigherPriorityTaskWoken );
    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

void vRxISR( void ) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    prvClearInterrupt();
    xTaskNotifyFromISR( xHandlingTask,
                       RX_BIT,
                       eSetBits,
                       &xHigherPriorityTaskWoken );
    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

static void prvHandlingTask( void *pvParameter ) {
    const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 500 );
    BaseType_t xResult;
    uint32_t ulNotifiedValue;
    while(1) {
        xResult = xTaskNotifyWait (
            pdFALSE,          /* Don't clear bits on entry. */
            ULONG_MAX,        /* Clear all bits on exit. */
            &ulNotifiedValue, /* Stores the notified value. */
            xMaxBlockTime
        );
        if( xResult == pdPASS ) {
            if( ( ulNotifiedValue & TX_BIT ) != 0 ) vProcessTX();
            if( ( ulNotifiedValue & RX_BIT ) != 0 ) vProcessRX();
        }
    }
}

As Mailbox#

RTOS task notifications can be used to send data to a task, but in a much more restricted way than can be achieved with an RTOS queue because:

  • Only 32-bit values can be sent
  • The value is saved as the receiving task’s notification value, and there can only be one notification value at any one time

The task’s notification value is the mailbox value.

Data is sent to a task using the xTaskNotify() (or xTaskNotifyIndexed()) and xTaskNotifyFromISR() (orxTaskNotifyIndexedFromISR()) API functions with their eAction parameter set to:

  • eSetValueWithOverwrite: the receiving task’s notification value is updated even if the receiving task already had a notification pending.

  • eSetValueWithoutOverwrite: the receiving task’s notification value is only updated if the receiving task did not already have a notification pending - as to update the notification value would overwrite the previous value before the receiving task had processed it.

A task can read its own notification value using xTaskNotifyWait() (or xTaskNotifyWaitIndexed()).

Example#

F411RE_FreeRTOS_Task_Notification.zip

Here is a simple project to demonstrate notification which help to save processor’s time.

  • A Button task monitors the input button on PC13 and notify the button’s state
  • A LED task turns on or off the LED on PA5 based on the Button’s state

Below example uses SEGGER SystemView to visualize the RTOS activities.

Performance comparision

Here is the result of process’s usage on 3 below implementation methods:

Method %Load %Idle Note
Button task and LED task use loops 94% (buton: 47%, led: 47%) 0% Consume all processor’s time
Button task loops to check input, then notifies LED task 96% (buton: 96%, led: 0%) 0% Consume all processor’s time
Remove button task, button interrupt notifies LED task 0.3% (isr: 0.2%, led: 0.1%) 90.6% Not consume processor’s time

Two looping tasks#

Button task and LED task use loops to process input and internal state.


Define tasks and shared variable:

volatile unsigned int button_pressed = 0;
TaskHandle_t buttonTaskHandler;
TaskHandle_t ledTaskHandler;

Button task

void Button_Task() {
  while(1) {
    if((GPIOC->IDR & GPIO_IDR_ID13) == 0) {
      button_pressed = 1;
    } else {
      button_pressed = 0;
    }
  }
}

LED task

void LED_Task() {
  while(1) {
    if(button_pressed) {
      GPIOA->BSRR |= GPIO_BSRR_BS_5;
    } else {
      GPIOA->BSRR |= GPIO_BSRR_BR_5;
    }
  }
}

Main program

int main(void)
{
  SystemInit();
  SEGGER_SYSVIEW_Conf();

  /* turn on clock for GPIOA, GPIOC */
  RCC->AHB1ENR |= (RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOCEN);

  /* set PA5 to output mode */
  GPIOA->MODER &= ~GPIO_MODER_MODE5_1;
  GPIOA->MODER |=  GPIO_MODER_MODE5_0;

  /* set PC13 to input mode */
  GPIOC->MODER &= ~GPIO_MODER_MODE13_1;
  GPIOC->MODER &= ~GPIO_MODER_MODE13_0;

  xTaskCreate(
          Button_Task,
          "Button_Task",
          configMINIMAL_STACK_SIZE,
          NULL,
          1,
          &buttonTaskHandler);

  xTaskCreate(
        LED_Task,
        "LED_Task",
        configMINIMAL_STACK_SIZE,
        NULL,
        1,
        &ledTaskHandler);

  vTaskStartScheduler();
    for(;;);
}

Result:

Two task’s loops consume all processor’s time. Can use vTaskDelay() to push tasks into blocked state and reserve processor for other tasks.

Two tasks using loop consume all processor’s time

One looping task uses Notification#

Button task loops to check input, then notifies LED task.


Define tasks and shared variable:

volatile unsigned int button_pressed = 0;
TaskHandle_t buttonTaskHandler;
TaskHandle_t ledTaskHandler;

Button task

void Button_Task() {
  volatile unsigned int button_changed = 0;
  while(1) {
    if((GPIOC->IDR & GPIO_IDR_ID13) == 0) {
      if(!button_pressed) {
        button_pressed = 1;
        button_changed = 1;
      }
    } else {
      if(button_pressed) {
        button_pressed = 0;
        button_changed = 1;
      }
    }

    if(button_changed) {
      SEGGER_SYSVIEW_PrintfTarget("pressed=%u", button_pressed);
      xTaskGenericNotify(
          ledTaskHandler /* xTaskToNotify */,
          0 /* uxIndexToNotify */,
          0 /* ulValue */,
          eNoAction /* eAction */,
          NULL /* pulPreviousNotificationValue */
      );
      button_changed = 0;
    }
  }
}

LED task

void LED_Task() {
  while(1) {
    xTaskGenericNotifyWait(
      0 /* uxIndexToWaitOn */,
      0 /* ulBitsToClearOnEntry */,
      0 /* ulBitsToClearOnExit */,
      NULL /* pulNotificationValue */,
      portMAX_DELAY /* xTicksToWait*/
    );
    if(button_pressed) {
      GPIOA->BSRR |= GPIO_BSRR_BS_5;
    } else {
      GPIOA->BSRR |= GPIO_BSRR_BR_5;
    }
  }
}

Main program

int main(void)
{
  SystemInit();
  SEGGER_SYSVIEW_Conf();

  /* turn on clock for GPIOA, GPIOC */
  RCC->AHB1ENR |= (RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOCEN);

  /* set PA5 to output mode */
  GPIOA->MODER &= ~GPIO_MODER_MODE5_1;
  GPIOA->MODER |=  GPIO_MODER_MODE5_0;

  /* set PC13 to input mode */
  GPIOC->MODER &= ~GPIO_MODER_MODE13_1;
  GPIOC->MODER &= ~GPIO_MODER_MODE13_0;

  xTaskCreate(
          Button_Task,
          "Button_Task",
          configMINIMAL_STACK_SIZE,
          NULL,
          1,
          &buttonTaskHandler);

  xTaskCreate(
        LED_Task,
        "LED_Task",
        configMINIMAL_STACK_SIZE,
        NULL,
        1,
        &ledTaskHandler);

  vTaskStartScheduler();
    for(;;);
}

Result:

The LED task is put to blocked state when it waits for a notification. However, the Button task still consumes all processor’s time in its loop.

Two tasks using notification reduce activation, and change the portion of processor’s time

Interrupt sends Notification#

Remove button task, button interrupt notifies LED task.


Define tasks and shared variable:

volatile unsigned int button_pressed = 0;
TaskHandle_t ledTaskHandler;

Interrupt Handler

void EXTI15_10_IRQHandler() {
  SEGGER_SYSVIEW_RecordEnterISR();
  if (EXTI->PR & (1UL << 13)) {
    /* clear pending bit */
    EXTI->PR |= (1UL << 13);
    if((GPIOC->IDR & GPIO_IDR_ID13) == 0) {
        button_pressed = 1;
    } else {
        button_pressed = 0;
    }
    SEGGER_SYSVIEW_PrintfTarget("pressed=%u", button_pressed);
    xTaskGenericNotifyFromISR(
        ledTaskHandler /* xTaskToNotify */,
        0 /* uxIndexToNotify */,
        0 /* ulValue */,
        eNoAction /* eAction */,
        NULL /* pulPreviousNotificationValue */,
        NULL /*pxHigherPriorityTaskWoken, set to NULL to not switch task immediately */
    );
  }
  SEGGER_SYSVIEW_RecordExitISR();
}

LED task

void LED_Task() {
  while(1) {
    xTaskGenericNotifyWait(
      0 /* uxIndexToWaitOn */,
      0 /* ulBitsToClearOnEntry */,
      0 /* ulBitsToClearOnExit */,
      NULL /* pulNotificationValue */,
      portMAX_DELAY /* xTicksToWait*/
    );
    if(button_pressed) {
      GPIOA->BSRR |= GPIO_BSRR_BS_5;
    } else {
      GPIOA->BSRR |= GPIO_BSRR_BR_5;
    }
  }
}

Main program

int main(void)
{
  SystemInit();
  SEGGER_SYSVIEW_Conf();

  /* turn on clock for GPIOA, GPIOC */
  RCC->AHB1ENR |= (RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOCEN);

  /* set PA5 to output mode */
  GPIOA->MODER &= ~GPIO_MODER_MODE5_1;
  GPIOA->MODER |=  GPIO_MODER_MODE5_0;

  /* set PC13 to input mode */
  GPIOC->MODER &= ~GPIO_MODER_MODE13_1;
  GPIOC->MODER &= ~GPIO_MODER_MODE13_0;

  /* turn on clock for SYSCONFIG */
  RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
  /* select PC13 as source for EXTI */
  SYSCFG->EXTICR[3] &= ~SYSCFG_EXTICR4_EXTI13_Msk;
  SYSCFG->EXTICR[3] |= SYSCFG_EXTICR4_EXTI13_PC;
  /* enable interrupt on EXTI13 */
  EXTI->IMR |= EXTI_IMR_IM13;
  /* set up PC13 interruption on 2 edges */
  EXTI->RTSR |= EXTI_RTSR_TR13;
  EXTI->FTSR |= EXTI_FTSR_TR13;

  /* enable NVIC */
  __NVIC_SetPriority(EXTI15_10_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY+1);
  __NVIC_EnableIRQ(EXTI15_10_IRQn);

  xTaskCreate(
        LED_Task,
        "LED_Task",
        configMINIMAL_STACK_SIZE,
        NULL,
        1,
        &ledTaskHandler);

  vTaskStartScheduler();
    for(;;);
}

Result:

The LED task does not react to the change of input right after receiving a notification. It is activated in next SysTick, which is a delay time so the reaction time is increased.

One task and an interrupt using notification reduce a lot of process’s time

Reduce reaction time#

If LED Task is requested to run immediately after the notification received, the reaction time of system is reduced.

Use pxHigherPriorityTaskWoken to know if there is a higher priority task need can run to call portYIELD_FROM_ISR().

void EXTI15_10_IRQHandler() {
  SEGGER_SYSVIEW_RecordEnterISR();
  BaseType_t aTaskWoken = pdFALSE;
  if (EXTI->PR & (1UL << 13)) {
    /* clear pending bit */
    EXTI->PR |= (1UL << 13);
    if((GPIOC->IDR & GPIO_IDR_ID13) == 0) {
        button_pressed = 1;
    } else {
        button_pressed = 0;
    }
    SEGGER_SYSVIEW_PrintfTarget("pressed=%u", button_pressed);
    xTaskGenericNotifyFromISR(
        ledTaskHandler /* xTaskToNotify */,
        0 /* uxIndexToNotify */,
        0 /* ulValue */,
        eNoAction /* eAction */,
        NULL /* pulPreviousNotificationValue */,
        &aTaskWoken /*pxHigherPriorityTaskWoken, set to NULL to not switch task immediately */
    );
    portYIELD_FROM_ISR(aTaskWoken);
  }
  SEGGER_SYSVIEW_RecordExitISR();
}

Result:

Task which is notified can be run immediately to reduce waiting time

Comments