Task notifications are a lightweight inter-task communication (IPC) and synchronization mechanism provided by FreeRTOS. Each task has a built-in “notification value” that can be sent directly to a specified task, avoiding the use of heavier objects such as queues, semaphores, and event groups. The characteristics include no involvement of object allocation, deallocation, and intermediate data structures, directly manipulating fields within the task control block (TCB), with each task having only one pending notification, which can only be waited on by the receiving task itself.1. Kernel Data Structure ImplementationAll states of task notifications are directly embedded in the task’sTask Control Block (TCB).
typedef struct tskTaskControlBlock { // ... other TCB fields, such as stack pointer, state, priority, etc. ... /* Core fields of the task notification mechanism */ volatile uint32_t ulNotifiedValue; // Notification "value" volatile uint8_t ucNotifyState; // Notification "state"} tskTCB;
1. Notification Value (<span>ulNotifiedValue</span>)
Function: This is the “payload” of the notification. When sending a notification, you can update this value. There are several ways to update:
Overwrite: The new value directly overwrites the old value.
Increment: The new value is added to the old value (very suitable for counting semaphore mode).
Bit Set: The new value is ORed with the old value (very suitable for event group mode).
No Operation: The current value remains unchanged (suitable for lightweight binary semaphores).
2. Notification State (<span>ucNotifyState</span>)
Function: This is the state machine of the notification mechanism, which is core to implementing wait/notify semantics. It typically has three states:
<span>taskNOT_WAITING_NOTIFICATION</span>:
-
Meaning: The task is not waiting for a notification. That is, the task has not called
<span>xTaskNotifyWait()</span>or<span>ulTaskNotifyTake()</span>and has not entered a blocked state. -
Behavior: If a notification is sent at this time, the notification value will be updated, and the state may change to
<span>taskNOTIFICATION_RECEIVED</span>, but the task will not be unblocked by the notification (since it was not blocked to begin with).
<span>taskWAITING_NOTIFICATION</span>:
Meaning: The task is waiting for a notification. That is, the task has already called the API to wait for a notification and has entered a blocked state due to not receiving a notification.
Behavior: If a notification is sent at this time, the kernel will immediately update the notification value, change the state to <span>taskNOTIFICATION_RECEIVED</span>, and then move the task from the blocked state back to the ready state, which may also trigger a task switch.
<span>taskNOTIFICATION_RECEIVED</span>:
-
Meaning: The task has received a notification but has not yet “taken” it.
-
Behavior: This state is mainly used to prevent race conditions. For example, if a task checks the state while a notification arrives in an interrupt. It ensures that the notification is not lost.
2. Core WorkflowScenario 1: Task Waiting for Notification (Receiver)
The task calls <span>xTaskNotifyWait()</span> or <span>ulTaskNotifyTake()</span>.
-
API Call: The task (receiving task) executes
<span>xTaskNotifyWait(0, 0, NULL, portMAX_DELAY);</span>. -
Enter Critical Section: The API will first disable interrupts or the scheduler for atomic operations.
-
Check State:
-
Case A (Notification Pending): If
<span>ucNotifyState == taskNOTIFICATION_RECEIVED</span>, it means that a notification was sent before this API was called. The API will immediately “consume” this notification (read<span>ulNotifiedValue</span>and decide whether to clear some or all bits based on parameters), then reset the state to<span>taskNOT_WAITING_NOTIFICATION</span>, exit the critical section, and immediately return (success), without blocking. -
Case B (No Notification Pending): If
<span>ucNotifyState == taskNOT_WAITING_NOTIFICATION</span>, it means there are no pending notifications.
(No Notification Pending) Prepare to Block:
-
Set
<span>ucNotifyState</span>to<span>taskWAITING_NOTIFICATION</span>. -
Remove the task from the ready list.
-
If a timeout is specified, the task will also be added to the delay list.
Exit Critical Section and Switch Tasks: The kernel performs a context switch, and the CPU starts running the highest priority ready task. The current task enters a blocked state here.

Scenario 2: Sending Notification (Sender)
An interrupt service routine (ISR) or another task calls <span>xTaskNotify()</span> / <span>xTaskNotifyGive()</span>.
-
API Call: The sender executes
<span>xTaskNotify( xTaskHandle, ulValue, eAction );</span>. -
Enter Critical Section: Similarly, first enter the critical section to protect access to the TCB.
-
Atomic Operation:
Update the target task’s <span>ulNotifiedValue</span><span> based on the </span><code><span>eAction</span><code><span> parameter (such as </span><code><span>eSetValueWithOverwrite</span>, <span>eIncrement</span>, <span>eSetBits</span>).
-
Check State and Decide:
Check the target task’s <span>ucNotifyState</span>.
Case 1 (Task is Waiting):
If <span>ucNotifyState == taskWAITING_NOTIFICATION</span>.
Set ucNotifyState to taskNOTIFICATION_RECEIVED. Move the task from the blocked list back to the ready list. This is a key step in unblocking. If the unblocked task’s priority is higher than the currently running task, set a context switch flag (pdTRUE).
Case 2 (Task is Not Waiting):
If <span>ucNotifyState == taskNOT_WAITING_NOTIFICATION</span>.
Set ucNotifyState to taskNOTIFICATION_RECEIVED. Do not change the task’s state list (the task remains in the ready list running or waiting for scheduling). Usually, there is no need to set the context switch flag (unless it is a preemptive switch).
Case 3 (Task has Notification Not Taken):
If <span>ucNotifyState == taskNOTIFICATION_RECEIVED</span>.
Process <span>ulNotifiedValue</span><span> based on </span><code><span>eAction</span><span> (e.g., increment, overwrite, etc.).</span>
The state remains unchanged. This means that if the previous value has not been read, it may be overwritten or modified by the new value.
Exit Critical Section: Exit the critical section. If a context switch flag was set in step 4, and this was sent in an interrupt (<span>xTaskNotifyFromISR</span>), a context switch request will be made before exiting the interrupt (<span>portYIELD_FROM_ISR()</span>).