The mechanism used to communicate (or pass data) between tasks is the use of queues. Queues behave as simple FIFO buffers, where the first data placed is the first data to be retrieved. Queues allow tasks to synchronize and serve as a secure mechanism that maintains the data integrity between tasks. These queues can be read from or written to by more than one task.

Code Example:

#include "bsp.h"

void vSenderTask( void *pvParameters );                             /* Function of sender task */
void vReceiveTask( void *pvParameters );                            /* Function of receiver task */

QueueHandle_t xQueue;                                               /* Handler of the Queue */

int main( void )
{
    HAL_Init();
    /*enable RTT and system view*/
    SEGGER_SYSVIEW_Conf( );
    SEGGER_SYSVIEW_Start( );
    
    xQueue = xQueueCreate( 5, sizeof( long ) );                     /* Creation of the Queue */

    xTaskCreate(vSenderTask, "task1", 240, (void*)100, 1, NULL);    /* Register sender task with priority 1 */
    xTaskCreate(vSenderTask, "task2", 240, (void*)200, 1, NULL);    /* Register sender task with priority 1 */
    
    xTaskCreate(vReceiveTask, "task3", 240, NULL, 2, NULL);         /* register the receiver task with highest priority */
    
    vTaskStartScheduler();                                          /* Execute the Kernel */
}

void vSenderTask( void *pvParameters )
{
    long lValueToSend;                              /* Varible to store data of parameter */
    lValueToSend = (long) pvParameters;             /* Cast the varible to receive the data to send */
    
    for(;;)
    {
        xQueueSend( xQueue, &lValueToSend, 0 );     /* Send data */
        taskYIELD();                                /* change context */
    }
}

void vReceiveTask( void *pvParameters )
{
    long lReceivedValue;                                                        /* Variable to store data received */
    const TickType_t xTicksToWait = 100 / portTICK_PERIOD_MS;                   /* Ticks to wait in ms */

    for(;;)
    {
        xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );                 /* Wait to receive data for 100ms */
        
        //SEGGER_RTT_printf(0,  "Valor recivido = %d\n", lReceivedValue );
        SEGGER_SYSVIEW_PrintfHost( "Received Value = %d\n", lReceivedValue );   /* Print the received value form Queue in Terminal */
    }
}

In the previous program, we can observe the creation of two tasks named "vTaskSender," which continuously send data to the single queue ("xQueue") we've created (if you notice, they are never suspended or blocked). Pay attention to the third parameter being zero and the queue's capacity being set to 5 elements of type "long." A third task is responsible for waiting for data to arrive in the queue with a timeout of 100ms if no data arrives. The latter situation should never occur.

💡
Before continuing: Examine the creation of the queue carefully and analyze if there's ever an instance when the queue becomes full. Review the task priorities thoroughly and consider how they impact the program's behavior.

Modify the previous program so that the tasks sending data do so every 300ms, and determine in the receiving task when a timeout occurs and when data arrives.

SystemView Output

Terminal shows that the messages sent by the 2 sender task is received by the task vReceiveTask, messages are displayed in order to the terminal.

Document

TimeLine:

Document

Timeline shows that the task1 executes first, sending the first message to the Queue, and after the receiver task executes reading the message.

After the receiver task reads the first message, the next execution is for task2 which sends the second message to the Queue. when this task ends switch the context to execute the receiver task that read the previous message sent.
After that, the tasks execute concurrently sending and receiving the messages.