Task notification can also be used as a binary semaphores, through the direct task notification. This technique allows that a task or an ISR notify to another task directly, without the use of a traditional semaphore. Task notifications are more efficient in terms of execution time and saving memory space compared with the binary semaphores, this is because the direct task notification doesn't need to create an object of semaphore on the Heap of freeRTOS.

Although both semaphores and task notifications work in similar ways, they are suited for different situations. For example, semaphores are ideal for scenarios where multiple tasks might need to wait for the same event, such as when a shared resource becomes available. On the other hand, task notifications are best used when you need to notify a specific task about an event.

To use task notifications as binary semaphores, you'll need to use additional functions. Let me explain the API functions you should use.

Let’s start with the functions that send notifications to other tasks to unblock their execution.

BaseType_t xTaskNotifyGive( 
  TaskHandle_t xTaskToNotify                /* handle of the RTOS task being notified */
);

void vTaskNotifyGiveFromISR( 
  TaskHandle_t xTaskToNotify,               /* handle of the RTOS task being notified */
  BaseType_t *pxHigherPriorityTaskWoken     /* must be initialised to 0, if sending the notification caused a task to unblock parameter
                                              change to pdTRUE, the value of parameter is used to change the context ON ISR */
);

Next, the function to receive the notification.

uint32_t ulTaskNotifyTake( 
  BaseType_t xClearCountOnExit,             /* If parameter is set to pdFALSE the value of task notification will decrement usually used
                                               as a conter semaphore, otherwise if parameter is pdTRUE the task notification value will be 
                                               restored to 0 value each execution, usually used as a binary semaphore */
  TickType_t xTicksToWait                   /* maximum time to wait in the Blocked state for a notification to be received if a notification 
                                               is not already pending */
);

Code Example:

#include "bsp.h"

void Task1( void *pvParameters );    /* Funtion of task to be triggered */

TaskHandle_t TaskH1 = NULL;          /* Declare the Handler Task to send notifications */

int main( void )
{
    HAL_Init();

    SEGGER_SYSVIEW_Conf();
    SEGGER_SYSVIEW_Start();

    HAL_NVIC_SetPriority( EXTI4_15_IRQn, 2, 0  );                       /* Set the priority of external interrupt */
    HAL_NVIC_EnableIRQ( EXTI4_15_IRQn );                                /* Enable External interrupt */

    xTaskCreate( Task1, "TriggeredTask", 240, NULL, 2, &TaskH1 );       /* Register Task to be triggered */

    vTaskStartScheduler();                                              /* Run Kernel */

    return 0;
}

void Task1( void *pvParameters )
{
    UNUSED(pvParameters);

    for(;;)
    {
        /* Take the notification, pdTrue to binary semaphore, pdFlase to decrement and use as a counter semaphore */
        ulTaskNotifyTake( pdTRUE, portMAX_DELAY );  /* If notification is received unblocks the task and execute */   
    
        HAL_GPIO_TogglePin( GPIOC, GPIO_PIN_0 );    /* change the state of an LED every time notification is received */
    }
}

void HAL_GPIO_EXTI_Rising_Callback( uint16_t GPIO_Pin )                     /* Callback of interrupt */
{
    SEGGER_SYSVIEW_RecordEnterISR();                                        /* Record enter of IRS on SytemView timeline */
    BaseType_t xTaskWoken = pdFALSE;

    vTaskNotifyGiveFromISR( TaskH1, &xTaskWoken );                          /* Notify the task transmission is complete */
    
    SEGGER_SYSVIEW_RecordExitISR();                                         /* Record exit of IRS on SytemView timeline */
    portEND_SWITCHING_ISR( xTaskWoken );                                    /* Change context */
}

Here is an example of using task notifications as binary semaphores. In this application, an event is sent when an interrupt occurs. This event needs to be processed by the registered task. When a button is pressed, an interrupt is generated, and the ISR callback sends the notification. This works similarly to the xSemaphoreGiveFromISR() function used to release a semaphore.

Next, the task that was in a blocked state is unblocked because it receives the event, much like how xSemaphoreTake() works with semaphores. Once the task receives the notification, it processes the event. In this case, the event causes an LED to change its state.

SystemView Output:

The terminal is not used for this example, but you can observe how the application works when an ISR occurs in the table above.

TimeLine:

Document

A button connected to generate an interruption is used as a trigger event using a task. Observe that the ISR is generated when the button is pressed and the task run immediately after the ISR callback, changing the LED state.

Before the next interrupt, the task enters a blocked state, which means the LED maintains its current state.
The image shows another interrupt being generated. As a result, the task executes again and changes the LED state once more.