· Esp32  · 6 min read

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

Learn how to use FreeRTOS task notifications to implement lightweight and efficient inter-task communication on the ESP-WROVER-KIT. This article includes in-depth explanations, practical code examples, and best practices for real-time applications.

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:

FunctionPurpose
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.

  • freertos
  • esp32
  • esp-wrover-kit
  • rtos
  • embedded-systems
  • iot

Related articles

View All Articles »

Why OTA for ESP32 Is Still Too Hard — and What I'm Building to Fix It

Over-the-air (OTA) firmware updates are critical for modern embedded devices, but implementing a secure, automated OTA system on the ESP32 is still more complex than it should be. In this post, I break down the current state of OTA for ESP32, common pain points with existing solutions, and why I’m building an open-source, developer-friendly OTA update system with CI/CD support, firmware signing, and a lightweight OTA agent.

Task Synchronization with FreeRTOS Event Groups on ESP32 (ESP-WROVER-KIT Example)

FreeRTOS event groups provide an efficient way to synchronize tasks based on multiple conditions using binary flags. In this post, we explore how to use event groups on the ESP-WROVER-KIT with ESP-IDF. Through a practical example involving sensor and network initialization, we demonstrate how event bits can signal system readiness to a processing task. This approach enables clean, scalable task coordination in real-time embedded applications using the ESP32.

Using FreeRTOS Mutex with ESP-WROVER-KIT for Resource Protection

FreeRTOS mutexes are essential for managing shared resources in embedded systems, ensuring that only one task accesses a resource at a time. In this post, we explore how mutexes work in FreeRTOS and demonstrate their use on the ESP-WROVER-KIT by protecting UART output shared between two tasks. This practical example shows how to prevent race conditions and output conflicts, while highlighting the importance of mutual exclusion in real-time applications on the ESP32 platform.

Understanding FreeRTOS Semaphores with ESP-WROVER-KIT

FreeRTOS semaphores are powerful synchronization tools that help manage task coordination and shared resource access in real-time embedded systems. In this post, we explore how binary semaphores work in FreeRTOS and demonstrate their use with the ESP-WROVER-KIT by creating a simple application that synchronizes sensor data collection and UART transmission. Whether you're dealing with task-to-task signaling or interrupt-driven events, semaphores are essential for building stable and responsive applications on the ESP32 platform.