freertos-task-notifications-notificaciones

Task Notifications in FreeRTOS

  • 5 min

A Task Notification is a lightweight notification channel directly associated with a task, designed to synchronize or send simple values without creating extra objects.

We’ve already used queues to pass data. FreeRTOS also offers semaphores and event groups to synchronize tasks, but they are all independent objects.

They also have a “price”: they are independent objects that must be created, consume Heap RAM, and require more management from the Kernel.

What if I just want to notify a task that “something happened”? Or send it a number? Do I really need to create a full queue for that?

The answer is NO. FreeRTOS has a tool designed precisely for this case: Task Notifications.

What are task notifications?

Unlike queues or semaphores, which are external objects to tasks (a queue exists even if no one uses it), a Notification belongs directly to a Task.

Each task has in its TCB one or more 32-bit notification values, depending on configTASK_NOTIFICATION_ARRAY_ENTRIES. The traditional configuration uses a single entry.

This means that:

  1. They don’t need to be created: There is no xNotificationCreate. They are already there.
  2. No separate object is needed: The state is part of the TCB, although it naturally occupies space within it.
  3. They have little overhead: In many cases, they require less RAM and processing than an equivalent queue or semaphore.

Comparison with a queue or a semaphore

FeatureQueue / SemaphoreTask Notification
SpeedDepends on the object and contentTypically lower overhead
RAM UsageObject and, in a queue, bufferState integrated into TCB
RecipientsMultipleOnly one
CapacityConfigurable FIFO queueOne value, counter, or mask per entry
Ease of UseStandard APISpecific API

How do they work?

The concept is to send an event directly to a specific task, knowing its Handle.

When we send a notification to a task, we can manipulate those internal 32 bits in several ways, allowing Notifications to act as:

  • Binary Semaphore: “Wake up, something happened.”
  • Counting Semaphore: “5 things happened.”
  • Mailbox: “Take this 32-bit value.”
  • Event Group: “Event A (bit 0) and Event C (bit 2) occurred.”

As a semaphore (Give / Take)

The most common use is as a “tap” to wake up a task. It is ideal for synchronizing a task with an Interrupt (ISR) or with another task.

We use the simplified macros: xTaskNotifyGive (give) and ulTaskNotifyTake (take).

Example: Task waiting for a button

// Global variable for the Handle of the task to be woken up
TaskHandle_t hLEDTask = NULL;

void LEDTask(void *pvParameters) {
    for(;;) {
        // 1. Wait for notification.
        // pdTRUE = clear counter to 0 on exit.
        // portMAX_DELAY = Infinite wait
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        
        // If we reach here, someone has notified us
        Serial.println("Received! Blinking LED.");
        digitalWrite(LED_BUILTIN, HIGH);
        vTaskDelay(pdMS_TO_TICKS(200));
        digitalWrite(LED_BUILTIN, LOW);
    }
}

void ButtonTask(void *pvParameters) {
    pinMode(0, INPUT_PULLUP); // ESP32 BOOT button
    
    for(;;) {
        if(digitalRead(0) == LOW) {
            // Simple debounce
            vTaskDelay(pdMS_TO_TICKS(50));
            
            if(digitalRead(0) == LOW) {
                Serial.println("Button pressed. Sending notification...");
                
                // 2. Send notification DIRECTLY to the task
                xTaskNotifyGive(hLEDTask);
                
                // Wait for release
                while(digitalRead(0) == LOW) {
                    vTaskDelay(pdMS_TO_TICKS(10));
                }
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

void setup() {
    Serial.begin(115200);
    pinMode(LED_BUILTIN, OUTPUT);

    // Create the task and save its Handle (CRITICAL)
    xTaskCreate(LEDTask, "LED", 2048, NULL, 1, &hLEDTask);
    xTaskCreate(ButtonTask, "Btn", 2048, NULL, 1, NULL);
}

void loop() {}
Copied!

Notice the simplicity. We didn’t create any semaphore object in setup(). We use the task’s Handle to “tap it on the shoulder.”

Sending values with xTaskNotify

We can also use those 32 bits to send data. For example, sending a command or a status.

The generic function is:

BaseType_t xTaskNotify( 
    TaskHandle_t xTaskToNotify, // Who to notify
    uint32_t ulValue,           // The value (data or bit mask)
    eNotifyAction eAction       // WHAT to do with that value
);
Copied!

The eAction parameter is the key. It defines how we modify the 32 bits of the destination task:

  • eSetBits: Performs a logical OR (useful for flags).
  • eIncrement: Increments the counter (like xTaskNotifyGive).
  • eSetValueWithOverwrite: Overwrites the previous value with the new one.
  • eSetValueWithoutOverwrite: Only writes if the previous value has already been read.

Example: Sending commands

TaskHandle_t hMotor = NULL;

// Define commands
#define CMD_STOP     0x00
#define CMD_FORWARD  0x01
#define CMD_REVERSE  0x02

void MotorTask(void *param) {
    uint32_t receivedCommand;
    
    for(;;) {
        // Wait indefinitely for a notification
        // Save the received value in 'receivedCommand'
        xTaskNotifyWait(
            0x00,             // Bits to clear on entry (none)
            0xFFFFFFFF,       // Bits to clear on exit (all -> reset)
            &receivedCommand, // Where to save the value
            portMAX_DELAY     // Timeout
        );
        
        switch(receivedCommand) {
            case CMD_FORWARD: Serial.println("Motor: FORWARD"); break;
            case CMD_REVERSE: Serial.println("Motor: REVERSE"); break;
            case CMD_STOP: Serial.println("Motor: STOP"); break;
        }
    }
}

void setup() {
    Serial.begin(115200);
    xTaskCreate(MotorTask, "Motor", 2048, NULL, 1, &hMotor);
    
    vTaskDelay(pdMS_TO_TICKS(1000));
    
    // Send commands by overwriting the value
    xTaskNotify(hMotor, CMD_FORWARD, eSetValueWithOverwrite);
    vTaskDelay(pdMS_TO_TICKS(2000));
    xTaskNotify(hMotor, CMD_STOP, eSetValueWithOverwrite);
}

void loop() {}
Copied!

When not to use them

Although they are wonderful, Task Notifications cannot replace Queues and Semaphores in 100% of cases. They have important limitations:

  1. Each notification belongs to a task: There can be multiple senders, but only its owner task waits on that entry. If you need to wake up multiple tasks with the same bits, consider an Event Group.
  2. They don’t store history: If you send 5 consecutive values (eSetValueWithOverwrite) before the receiving task reads them, it will only read the last one. The previous 4 are lost. If you need a buffer (FIFO) where nothing is lost, use a Queue.
  3. Each entry only contains 32 bits: You cannot send a complete structure. Sending a pointer is possible, but it reintroduces the problem of ownership and buffer lifetime.