Skip to content

Task Notifications

Every FreeRTOS task has a built-in 32-bit notification value. Another task (or an ISR) can write to that value and optionally wake the receiving task. The receiver blocks until a notification arrives, then reads it and continues.

No object is created with xSemaphoreCreate or xQueueCreate. The notification mechanism is built into the task’s control block and uses negligible extra memory.


Task notifications are the right tool for 1:1 signaling — exactly one sender, exactly one receiver.

Use caseGood fit?
ISR wakes a processing taskYes
Task A tells Task B that new data is readyYes
Multiple tasks all notifying one taskYes (notifications accumulate)
One task notifying multiple tasksNo — use an event group or semaphore
Sending data along with the signalSometimes — the 32-bit value can carry a small payload

Compared to a binary semaphore used for the same purpose, task notifications are:

  • faster (no object to look up, fewer kernel instructions)
  • smaller (no heap allocation — the value lives in the TCB)
  • simpler to write in the common 1:1 pattern

The trade-off: you cannot broadcast to multiple receivers, and you cannot inspect the notification state from outside the receiving task as easily as with a semaphore.


Simplified API: xTaskNotifyGive / ulTaskNotifyTake

Section titled “Simplified API: xTaskNotifyGive / ulTaskNotifyTake”

For the common “signal that work is ready” pattern, FreeRTOS provides two simplified functions.

// From a task:
xTaskNotifyGive(xTargetTaskHandle);
// From an ISR:
BaseType_t higher = pdFALSE;
vTaskNotifyGiveFromISR(xTargetTaskHandle, &higher);
portYIELD_FROM_ISR(higher);

Each call increments the receiver’s notification count by one.

// Block until at least one notification arrives.
// pdTRUE clears the count after returning (binary behavior).
// portMAX_DELAY blocks forever.
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

ulTaskNotifyTake returns the notification count at the moment it woke up. With pdTRUE, it clears the count before returning. With pdFALSE, it decrements the count by one (counting semaphore behavior).


#include "FreeRTOS.h"
#include "task.h"
static TaskHandle_t xConsumerHandle = NULL;
// Producer: does work, then tells the consumer new data is ready
void ProducerTask(void *pvParameters)
{
(void)pvParameters;
for (;;) {
prepareNextFrame(); // fill the buffer
xTaskNotifyGive(xConsumerHandle); // wake the consumer
vTaskDelay(pdMS_TO_TICKS(33)); // ~30 Hz production rate
}
}
// Consumer: blocks until notified, then processes
void ConsumerTask(void *pvParameters)
{
(void)pvParameters;
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // wait for notification
renderFrame(); // process the buffer
}
}
int main(void)
{
xTaskCreate(ConsumerTask, "Consumer", 512, NULL, 2, &xConsumerHandle);
xTaskCreate(ProducerTask, "Producer", 256, NULL, 2, NULL);
vTaskStartScheduler();
for (;;) {}
}

The consumer blocks at ulTaskNotifyTake using zero CPU. It only runs when the producer calls xTaskNotifyGive. This is event-driven rendering: the consumer processes exactly when there is something to process.


static TaskHandle_t xProcessingHandle = NULL;
// ADC conversion complete — signal the processing task
void ADC0SS3_IRQHandler(void)
{
ADCIntClear(ADC0_BASE, 3);
BaseType_t higher = pdFALSE;
vTaskNotifyGiveFromISR(xProcessingHandle, &higher);
portYIELD_FROM_ISR(higher);
}
void ProcessingTask(void *pvParameters)
{
(void)pvParameters;
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
readAndProcessSample();
}
}

This is equivalent to the binary semaphore ISR→task pattern, but without allocating or initializing a semaphore handle. The ISR is slightly faster and the code is more concise.


The simplified pair (xTaskNotifyGive / ulTaskNotifyTake) is sufficient for most cases. For more control, use the full API.

BaseType_t xTaskNotify(
TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction // how to update the notification value
);

The eAction parameter controls what happens to the notification value:

eActionEffect
eNoActionWake the task; do not change the value
eSetBitsOR ulValue into the notification value
eIncrementIncrement the value by one (same as xTaskNotifyGive)
eSetValueWithOverwriteUnconditionally set the value to ulValue
eSetValueWithoutOverwriteSet the value only if the task has already consumed the previous one

The receiver uses xTaskNotifyWait to access the full 32-bit value:

BaseType_t xTaskNotifyWait(
uint32_t ulBitsToClearOnEntry, // clear these bits when entering the wait
uint32_t ulBitsToClearOnExit, // clear these bits when waking up
uint32_t *pulNotificationValue, // receives the value at wake-up
TickType_t xTicksToWait
);

Example: Passing a small value through the notification

Section titled “Example: Passing a small value through the notification”
// Sender: encode a command as the notification value
typedef enum { CMD_START = 1, CMD_STOP = 2, CMD_RESET = 3 } Command;
xTaskNotify(xWorkerHandle, CMD_RESET, eSetValueWithOverwrite);
// Receiver: read the command
uint32_t cmd;
xTaskNotifyWait(0, UINT32_MAX, &cmd, portMAX_DELAY);
switch (cmd) {
case CMD_START: ... break;
case CMD_STOP: ... break;
case CMD_RESET: ... break;
}

PropertyTask notificationBinary semaphore
Object requiredNo — built into TCBYes — must create and store a handle
Heap allocationNoneYes
SpeedFasterSlightly slower
Number of receiversExactly oneAny number
Can be given before receiver existsNo — need the TaskHandleYes — semaphore exists independently
ISR supportvTaskNotifyGiveFromISRxSemaphoreGiveFromISR

Use a task notification when you have one sender and one receiver and need the lightest possible mechanism. Use a semaphore when multiple tasks might wait on the same event, or when the sender runs before the receiver exists.


To notify a specific task, you need its TaskHandle_t. There are two ways:

Pass the handle at creation:

TaskHandle_t xRenderHandle = NULL;
// Create the task and save the handle
xTaskCreate(RenderTask, "Render", 512, NULL, 2, &xRenderHandle);
// Later, from another task:
xTaskNotifyGive(xRenderHandle);

A task can also get its own handle:

void SomeTask(void *pvParameters)
{
TaskHandle_t self = xTaskGetCurrentTaskHandle();
// pass 'self' somewhere so others can notify this task
...
}

MistakeConsequenceFix
Notifying before xTaskCreate completesCorrupt handle or crashCreate the task and save the handle before notifying
Using xTaskNotifyGive from an ISRKernel corruptionUse vTaskNotifyGiveFromISR in ISRs
Expecting multiple senders to work independentlyOne notification value — senders can overwrite each otherUse a queue or semaphore for multiple independent senders
Forgetting portYIELD_FROM_ISR in ISRTask wakes on next tick instead of immediatelyAlways call portYIELD_FROM_ISR(higher) after an ISR notification