Efficient Inter-Task Communication with FreeRTOS Task Notifications on ESP-WROVER-KIT

In modern embedded systems, using a real-time operating system (RTOS) like FreeRTOS is common practice for managing multiple concurrent tasks. While semaphores, queues, and event groups are frequently used for task synchronization and communication, FreeRTOS task notifications offer an ultra-lightweight and efficient alternative that is often overlooked.

In this article, we will explore what task notifications are, why they matter, and how to use them effectively on the ESP-WROVER-KIT, which features the popular ESP32 chip. We’ll write and explain an example program that shows how task notifications can be used to coordinate two tasks: one for gathering sensor data and another for processing it.

By the end of this post, you will have a solid understanding of task notifications, their advantages, and how to apply them in real-world embedded systems to reduce latency and save resources.


Why use task notifications?

FreeRTOS provides several primitives for inter-task communication and synchronization:

  • Queues for passing data between tasks or between interrupts and tasks
  • Semaphores for signaling and resource protection
  • Event groups for managing multiple events as bit flags

While all of these are powerful, they do come with some overhead: they require extra memory to maintain their internal data structures, and extra CPU cycles to manage them.

Task notifications address these issues by being:

  • Extremely lightweight, using only a 32-bit value stored in each task’s control block
  • Fast to operate (almost immediate signaling)
  • Suitable for signaling events or passing simple data like counters or flags
  • Usable directly from interrupt service routines (ISRs)

This makes task notifications ideal for situations where:

  • A task just needs to be notified about a single event
  • An ISR needs to wake a task quickly
  • Memory and CPU usage must be kept to a minimum

How task notifications work

Every FreeRTOS task has an internal 32-bit notification value. FreeRTOS provides APIs to manipulate this value, which can be used as:

  • A binary semaphore
  • A counting semaphore
  • An event flag register (using its bits)
  • A direct-to-task message passing mechanism

Key API functions include:

Function Purpose
xTaskNotifyGive() Notify a task, incrementing its notification value (ideal for counting).
ulTaskNotifyTake() Wait for a notification, then clear or decrement the value.
xTaskNotify() Set the notification value, set bits, or overwrite it, depending on options.
xTaskNotifyWait() Wait until specific bits are set in the notification value.

This allows you to implement signaling between tasks (or between ISRs and tasks) with minimal overhead.


Example project overview

In our example, we will create two FreeRTOS tasks:

  • sensor_task: simulates reading sensor data every second
  • processing_task: waits to be notified, then processes the latest sensor data

When sensor_task reads a new value, it will call xTaskNotifyGive() to wake processing_task. This pattern is common in embedded systems where one task collects data and another task processes or logs it.

Task notification flow


Creating the project

First, set up a new ESP-IDF project:

idf.py create-project freertos_task_notify_example
cd freertos_task_notify_example

Open main/main.c and replace its content with the following code.

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_random.h"

#define TAG "TASK_NOTIFY_EXAMPLE"

TaskHandle_t sensor_task_handle = NULL;
TaskHandle_t processing_task_handle = NULL;

// Shared sensor value
volatile int sensor_value = 0;

// Task that simulates reading sensor data
void sensor_task(void *pvParameters) {
    while (1) {
        sensor_value = esp_random() % 100;  // Generate random value between 0-99
        ESP_LOGI(TAG, "Sensor task: Read new value: %d", sensor_value);

        // Notify the processing task
        xTaskNotifyGive(processing_task_handle);

        vTaskDelay(pdMS_TO_TICKS(1000)); // Wait 1 second
    }
}

// Task that waits for notifications and processes data
void processing_task(void *pvParameters) {
    while (1) {
        // Wait indefinitely for notification
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        ESP_LOGI(TAG, "Processing task: Processing value: %d", sensor_value);
    }
}

void app_main() {
    xTaskCreate(sensor_task, "Sensor Task", 2048, NULL, 5, &sensor_task_handle);
    xTaskCreate(processing_task, "Processing Task", 2048, NULL, 5, &processing_task_handle);
}

How this code works

  • sensor_task simulates a sensor reading every second and notifies processing_task with xTaskNotifyGive().
  • processing_task blocks on ulTaskNotifyTake() until it receives a notification, then prints the value.
  • The notification acts like a binary semaphore: each call to xTaskNotifyGive() increments the notification count, and each call to ulTaskNotifyTake() decrements it.

This keeps the design lightweight and avoids using queues or extra synchronization objects.


Building and flashing the project

Connect your ESP-WROVER-KIT and run:

idf.py build
idf.py -p /dev/ttyUSB0 flash monitor

Replace /dev/ttyUSB0 with your device’s port. You should see logs similar to:

I (1234) TASK_NOTIFY_EXAMPLE: Sensor task: Read new value: 42
I (1235) TASK_NOTIFY_EXAMPLE: Processing task: Processing value: 42

Every second, a new random value is generated and processed.


Alternative: Passing data with xTaskNotify

The previous example just signals the task to process a shared variable. But you can also send data directly in the notification value:

Sender task:

uint32_t value_to_send = sensor_value;
xTaskNotify(processing_task_handle, value_to_send, eSetValueWithOverwrite);

Receiver task:

uint32_t received_value;
if (xTaskNotifyWait(0x00, 0x00, &received_value, portMAX_DELAY) == pdTRUE) {
    ESP_LOGI(TAG, "Processing task: Received value: %d", received_value);
}

Here, the sender directly sends the value. The receiver reads it from received_value.


Using bits as event flags

Task notifications can also act like a lightweight version of event groups by treating the 32-bit notification value as individual flags.

For example:

Sender task:

xTaskNotify(processing_task_handle, BIT0, eSetBits); // Set bit 0

Receiver task:

uint32_t notification_value;
if (xTaskNotifyWait(0x00, 0x00, &notification_value, portMAX_DELAY) == pdTRUE) {
    if (notification_value & BIT0) {
        ESP_LOGI(TAG, "Processing task: Bit 0 set");
    }
}

You can set or clear specific bits, enabling multiple events to be represented in one notification value.


Best practices

  • Each task has only one notification value; use it carefully.
  • Use task notifications mainly for single events or small data, not large data transfers.
  • Protect shared data with critical sections or mutexes if accessed by multiple tasks.
  • Avoid overwhelming the receiver: if the sender notifies faster than the receiver consumes, notifications may get lost if you use overwrite modes.
  • For large data or complex communication, use FreeRTOS queues.

Debugging tips

  • Use esp_log_level_set() to change log verbosity.
  • Use uxTaskGetStackHighWaterMark() to check for stack overflows.
  • Monitor task states using the vTaskList() API or ESP-IDF monitor.

Practical use cases

FreeRTOS task notifications are well-suited for:

  • Waking tasks when ADC conversions complete (signaled by ISR)
  • Handling simple button presses or sensor events
  • Controlling motor tasks or actuator tasks based on real-time input
  • Lightweight signaling in power-constrained IoT applications

Summary

FreeRTOS task notifications provide a powerful yet lightweight tool for inter-task and ISR-to-task communication. They:

  • Use very little memory and CPU
  • Can act as binary or counting semaphores
  • Can send 32-bit values or act as event flags
  • Have low latency, ideal for real-time systems

By applying task notifications thoughtfully, you can make your ESP-WROVER-KIT applications more efficient and responsive.


Conclusion

Task notifications are often underutilized in FreeRTOS projects, yet they offer the simplest and fastest way to synchronize tasks or respond to interrupts. With the ESP-WROVER-KIT and ESP-IDF, it is straightforward to implement and test these patterns.