Hands-on

Here I listed several practices based on FreeRTOS 202212.01

Creating tasks

Output:

Code

void HelloTask1(s) {
	while (1) {
		printf("Task 1 is running!\n");
		vTaskDelay(500);
	}
}

void HelloTask2(s) {
	while (1) {
		printf("Task 2 is running!\n");
		vTaskDelay(500);
	}
}

int main(void) 
{
	prvInitialiseHeap();
	vTraceEnable(TRC_START);

	xTaskHandle HT = NULL;
	xTaskCreate(HelloTask1, "HelloTask 1", 1000, NULL, 3, &HT);

	xTaskCreate(HelloTask2, "HelloTask 2", 100, NULL, 1, &HT);

	vTaskStartScheduler();
	for (;;);
	return 0;
}

Task Parameters

The code uses the pvParameters parameter to the xTaskCreate() function is used to pass the text string into the task.

Output

Code

void vTaskFunction(void* pvParameters) {
	char* pcTaskName;
	/* The string to print out is passed in via the parameter. Cast this to a
	character pointer. */
	pcTaskName = (char*)pvParameters;
	while (1) {
		printf("%s\n", pcTaskName);
		vTaskDelay(500);
	}
}

static const char* pcTextForTask1 = "Task 1 is running!";
static const char* pcTextForTask2 = "Task 2 is running!";

int main(void) 
{
	prvInitialiseHeap();
	vTraceEnable(TRC_START);

	xTaskHandle HT = NULL;
	xTaskCreate(vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, &HT);

	xTaskCreate(vTaskFunction, "Task 2", 100, (void*)pcTextForTask2, 1, &HT);

	vTaskStartScheduler();
	for (;;);
	return 0;
}

Task Priority

void vTaskFunction(void* pvParameters) {
	char* pcTaskName;
	/* The string to print out is passed in via the parameter. Cast this to a
	character pointer. */
	pcTaskName = (char*)pvParameters;
	while (1) {
		printf("%s\n", pcTaskName);
		vTaskDelay(500);
	}
}

static const char* pcTextForTask1 = "Task 1 is running!";
static const char* pcTextForTask2 = "Task 2 is running!";

int main(void) 
{
	prvInitialiseHeap();
	vTraceEnable(TRC_START);

	xTaskHandle HT = NULL;
	xTaskCreate(vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, &HT);

	xTaskCreate(vTaskFunction, "Task 2", 100, (void*)pcTextForTask2, 2, &HT);

	vTaskStartScheduler();
	for (;;);
	return 0;
}
Blocked State to Create Delay

vTaskDelay() places the calling task into the Blocked state for a fixed number of tick interrupts. The task does not use any processing time while it is in the Blocked state, so the task only uses processing time when there is actually work to be done

  • xTicksToDelay -- The number of tick interrupts that the calling task will remain in the Blocked state before being transitioned back into the Ready state.

The macro pdMS_TO_TICKS() can be used to convert a time specified in milliseconds into a time specified in ticks. For example, calling vTaskDelay( pdMS_TO_TICKS( 100 ) ) will result in the calling task remaining in the Blocked state for 100 milliseconds.

Output

void vTaskFunction(void* pvParameters) {
	char* pcTaskName;
	const TickType_t xDelay250ms = pdMS_TO_TICKS(250);
	/* The string to print out is passed in via the parameter. Cast this to a
	character pointer. */
	pcTaskName = (char*)pvParameters;
	while (1) {
		printf("%s\n", pcTaskName);
		vTaskDelay(xDelay250ms);
	}
}
vTaskDelayUntil()

The two tasks created in the last practice are periodic tasks, but using vTaskDelay() does not guarantee that the frequency at which they run is fixed, as the time at which the tasks leave the Blocked state is relative to when they call vTaskDelay(). Converting the tasks to use vTaskDelayUntil() instead of vTaskDelay() solves this potential problem.

The vTaskDelayUntil() API Function

vTaskDelayUntil() is similar to vTaskDelay(). As just demonstrated, the vTaskDelay() parameter specifies the number of tick interrupts that should occur between a task calling vTaskDelay(), and the same task once again transitioning out of the Blocked state. The length of time the task remains in the blocked state is specified by the vTaskDelay() parameter, but the time at which the task leaves the blocked state is relative to the time at which vTaskDelay() was called.

The parameters to vTaskDelayUntil() specify, instead, the exact tick count value at which the calling task should be moved from the Blocked state into the Ready state. vTaskDelayUntil() is the API function that should be used when a fixed execution period is required (where you want your task to execute periodically with a fixed frequency), as the time at which the calling task is unblocked is absolute, rather than relative to when the function was called (as is the case with vTaskDelay()).

  • pxPreviousWakeTime - This parameter is named on the assumption that vTaskDelayUntil() is being used to implement a task that executes periodically and with a fixed frequency. In this case, pxPreviousWakeTime holds the time at which the task last left the Blocked state (was ‘woken’ up). This time is used as a reference point to calculate the time at which the task should next leave the Blocked state.

  • xTimeIncrement - This parameter is also named on the assumption that vTaskDelayUntil() is being used to implement a task that executes periodically and with a fixed frequency—the frequency being set by the xTimeIncrement value.

Code

void vTaskFunction(void* pvParameters) {
	char* pcTaskName;
	const TickType_t xDelay250ms = pdMS_TO_TICKS(250);

	TickType_t xLastWakeTime;
	/* The string to print out is passed in via the parameter. Cast this to a
	character pointer. */
	pcTaskName = (char*)pvParameters;
	xLastWakeTime = xTaskGetTickCount();

	while (1) {
		printf("%s\n", pcTaskName);
		vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(250));
	}
}

Combing Blocking and Non-blocking Tasks

Previous codes have examined the behavior of both polling and blocking tasks in isolation. This part re-enforces the stated expected system behavior by demonstrating an execution sequence when the two schemes are combined, as follows.

  • Two tasks are created at priority 1. These do nothing other than continuously print out a string. These tasks never make any API function calls that could cause them to enter the Blocked state, so are always in either the Ready or the Running state. Tasks of this nature are called ‘continuous processing’ tasks, as they always have work to do (albeit rather trivial work, in this case).

  • A third task is then created at priority 2, so above the priority of the other two tasks. The third task also just prints out a string, but this time periodically, so uses the vTaskDelayUntil() API function to place itself into the Blocked state between each print iteration.

Code

void vContinuousProcessingTask(void* pvParameters) {
    char* pcTaskName;
    /* The string to print out is passed in via the parameter. Cast this to a
    character pointer. */
    pcTaskName = (char*)pvParameters;

    while (1) {
        printf("%s\n", pcTaskName);
    }
}

void vPeriodicTask(void* pvParameters) {
    char* pcTaskName;
    const TickType_t xDelay3ms = pdMS_TO_TICKS(3);

    TickType_t xLastWakeTime;
    /* The string to print out is passed in via the parameter. Cast this to a
    character pointer. */
    pcTaskName = (char*)pvParameters;
    xLastWakeTime = xTaskGetTickCount();

    while (1) {
        printf("Periodic task is running\n");
        vTaskDelayUntil(&xLastWakeTime, xDelay3ms);
    }
}


static const char* pcTextForTask1 = "Continous Task 1 is running!";
static const char* pcTextForTask2 = "Continous Task 2 is running!";

int main(void) 
{
	prvInitialiseHeap();
	vTraceEnable(TRC_START);

	xTaskCreate(vContinuousProcessingTask, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL);

	xTaskCreate(vContinuousProcessingTask, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL);

    xTaskCreate(vPeriodicTask, "Task 3", 1000, NULL, 2, NULL);

	vTaskStartScheduler();
	for (;;);
	return 0;
}

Changing Task Priorities

This code demonstrates this by using the vTaskPrioritySet() API function to change the priority of two tasks relative to each other. The code creates two tasks at two different priorities. Neither task makes any API function calls that could cause it to enter the Blocked state, so both are always in either the Ready state or the Running state. Therefore, the task with the highest relative priority will always be the task selected by the scheduler to be in the Running state.

The code behaves as follows:

  • Task 1 is created with the highest priority, so is guaranteed to run first. Task 1 prints out a couple of strings before raising the priority of Task 2 to above its own priority.

  • Task 2 starts to run (enters the Running state) as soon as it has the highest relative priority. Only one task can be in the Running state at any one time, so when Task 2 is in the Running state, Task 1 is in the Ready state.

  • Task 2 prints out a message before setting its own priority back down to below that of Task 1.

  • Task 2 setting its priority back down means Task 1 is once again the highest priority task, so Task 1 re-enters the Running state, forcing Task 2 back into the Ready state.

Output:

Code

/* Used to hold the handle of Task2. */
TaskHandle_t xTask2Handle;

void vTask1(void* pvParameters)
{
    unsigned portBASE_TYPE uxPriority;

    /* This task will always run before Task2 as it has the higher priority.
    Neither Task1 nor Task2 ever block so both will always be in either the
    Running or the Ready state.

    Query the priority at which this task is running - passing in NULL means
    "return our own priority". */
    uxPriority = uxTaskPriorityGet(NULL);

    for (;; )
    {
        /* Print out the name of this task. */
        printf("Task1 is running\r\n");

        /* Setting the Task2 priority above the Task1 priority will cause
        Task2 to immediately start running (as then Task2 will have the higher
        priority of the    two created tasks). */
        printf("About to raise the Task2 priority\r\n");
        vTaskPrioritySet(xTask2Handle, (uxPriority + 1));

        /* Task1 will only run when it has a priority higher than Task2.
        Therefore, for this task to reach this point Task2 must already have
        executed and set its priority back down to 0. */
    }
}

void vTask2(void* pvParameters)
{
    unsigned portBASE_TYPE uxPriority;

    /* Task1 will always run before this task as Task1 has the higher priority.
    Neither Task1 nor Task2 ever block so will always be in either the
    Running or the Ready state.

    Query the priority at which this task is running - passing in NULL means
    "return our own priority". */
    uxPriority = uxTaskPriorityGet(NULL);

    for (;; )
    {
        /* For this task to reach this point Task1 must have already run and
        set the priority of this task higher than its own.

        Print out the name of this task. */
        printf("Task2 is running\r\n");

        /* Set our priority back down to its original value.  Passing in NULL
        as the task handle means "change our own priority".  Setting the
        priority below that of Task1 will cause Task1 to immediately start
        running again. */
        printf("About to lower the Task2 priority\r\n");
        vTaskPrioritySet(NULL, (uxPriority - 2));
    }
}

int main(void) 
{
	prvInitialiseHeap();
	vTraceEnable(TRC_START);
    /* Create the first task at priority 2.  This time the task parameter is
    not used and is set to NULL.  The task handle is also not used so likewise
    is also set to NULL. */
    xTaskCreate(vTask1, "Task 1", 200, NULL, 2, NULL);

    /* The task is created at priority 2 ^. */

    /* Create the second task at priority 1 - which is lower than the priority
    given to Task1.  Again the task parameter is not used so is set to NULL -
    BUT this time we want to obtain a handle to the task so pass in the address
    of the xTask2Handle variable. */
    xTaskCreate(vTask2, "Task 2", 200, NULL, 1, &xTask2Handle);

	vTaskStartScheduler();
	for (;;);
	return 0;
}

Deleting Tasks

The code behaves as follows:

  • Task 1 is created by main() with priority 1. When it runs, it creates Task 2 at priority 2. Task 2 is now the highest priority task, so it starts to execute immediately. The source for main() is shown, and the source for Task 1.

  • Task 2 does nothing other than delete itself. It could delete itself by passing NULL to vTaskDelete() but instead, for demonstration purposes, it uses its own task handle. The source for Task 2.

  • When Task 2 has been deleted, Task 1 is again the highest priority task, so continues executing—at which point it calls vTaskDelay() to block for a short period.

  • The Idle task executes while Task 1 is in the blocked state and frees the memory that was allocated to the now deleted Task 2.

  • When Task 1 leaves the blocked state it again becomes the highest priority Ready state task and so pre-empts the Idle task. When it enters the Running state it creates Task 2 again, and so it goes on.

Output:

Code

void vTask1(void* pvParameters);
void vTask2(void* pvParameters);

/* Used to hold the handle of Task2. */
TaskHandle_t xTask2Handle;

void vTask1(void* pvParameters)
{
    const TickType_t xDelay100ms = 100 / portTICK_PERIOD_MS;

    for (;; )
    {
        /* Print out the name of this task. */
        printf("Task1 is running\r\n");

        /* Create task 2 at a higher priority.  Again the task parameter is not
        used so is set to NULL - BUT this time we want to obtain a handle to the
        task so pass in the address of the xTask2Handle variable. */
        xTaskCreate(vTask2, "Task 2", 200, NULL, 2, &xTask2Handle);
        /* The task handle is the last parameter ^^^^^^^^^^^^^ */

     /* Task2 has/had the higher priority, so for Task1 to reach here Task2
     must have already executed and deleted itself.  Delay for 100
     milliseconds. */
        vTaskDelay(xDelay100ms);
    }
}

void vTask2(void* pvParameters)
{
    /* Task2 does nothing but delete itself.  To do this it could call vTaskDelete()
    using a NULL parameter, but instead and purely for demonstration purposes it
    instead calls vTaskDelete() with its own task handle. */
    printf("Task2 is running and about to delete itself\r\n");
    vTaskDelete(xTask2Handle);
}

int main(void) 
{
	prvInitialiseHeap();
	vTraceEnable(TRC_START);
    /* Create the first task at priority 1.  This time the task parameter is
    not used and is set to NULL.  The task handle is also not used so likewise
    is also set to NULL. */
    xTaskCreate(vTask1, "Task 1", 200, NULL, 1, NULL);

	vTaskStartScheduler();
	for (;;);
	return 0;
}
Creating a Queue

This code demonstrates a queue being created, data being sent to the queue from multiple tasks, and data being received from the queue. The queue is created to hold data items of type int32_t. The tasks that send to the queue do not specify a block time, whereas the task that receives from the queue does. The priority of the tasks that send to the queue are lower than the priority of the task that receives from the queue. This means the queue should never contain more than one item because, as soon as data is sent to the queue the receiving task will unblock, pre-empt the sending task, and remove the data—leaving the queue empty once again.

Output:

Code

#include "queue.h"

/* The tasks to be created.  Two instances are created of the sender task while
only a single instance is created of the receiver task. */
static void vSenderTask(void* pvParameters);
static void vReceiverTask(void* pvParameters);

/* Declare a variable of type QueueHandle_t.  This is used to store the queue
that is accessed by all three tasks. */
QueueHandle_t xQueue;


static void vSenderTask(void* pvParameters)
{
    long lValueToSend;
    portBASE_TYPE xStatus;

    /* Two instances are created of this task so the value that is sent to the
    queue is passed in via the task parameter rather than be hard coded.  This way
    each instance can use a different value.  Cast the parameter to the required
    type. */
    lValueToSend = (long)pvParameters;

    /* As per most tasks, this task is implemented within an infinite loop. */
    for (;; )
    {
        /* The first parameter is the queue to which data is being sent.  The
        queue was created before the scheduler was started, so before this task
        started to execute.

        The second parameter is the address of the data to be sent.

        The third parameter is the Block time � the time the task should be kept
        in the Blocked state to wait for space to become available on the queue
        should the queue already be full.  In this case we don�t specify a block
        time because there should always be space in the queue. */
        xStatus = xQueueSendToBack(xQueue, &lValueToSend, 0);

        if (xStatus != pdPASS)
        {
            /* We could not write to the queue because it was full � this must
            be an error as the queue should never contain more than one item! */
            printf("Could not send to the queue.\r\n");
        }

        /* Allow the other sender task to execute. */
        taskYIELD();
    }
}

static void vReceiverTask(void* pvParameters)
{
    /* Declare the variable that will hold the values received from the queue. */
    long lReceivedValue;
    portBASE_TYPE xStatus;
    const TickType_t xTicksToWait = 100 / portTICK_PERIOD_MS;

    /* This task is also defined within an infinite loop. */
    for (;; )
    {
        /* As this task unblocks immediately that data is written to the queue this
        call should always find the queue empty. */
        if (uxQueueMessagesWaiting(xQueue) != 0)
        {
            printf("Queue should have been empty!\r\n");
        }

        /* The first parameter is the queue from which data is to be received.  The
        queue is created before the scheduler is started, and therefore before this
        task runs for the first time.

        The second parameter is the buffer into which the received data will be
        placed.  In this case the buffer is simply the address of a variable that
        has the required size to hold the received data.

        the last parameter is the block time � the maximum amount of time that the
        task should remain in the Blocked state to wait for data to be available should
        the queue already be empty. */
        xStatus = xQueueReceive(xQueue, &lReceivedValue, xTicksToWait);

        if (xStatus == pdPASS)
        {
            /* Data was successfully received from the queue, print out the received
            value. */
            printf("Received = %d\r\n", lReceivedValue);
        }
        else
        {
            /* We did not receive anything from the queue even after waiting for 100ms.
            This must be an error as the sending tasks are free running and will be
            continuously writing to the queue. */
            printf("Could not receive from the queue.\r\n");
        }
    }
}

int main(void) 
{
	prvInitialiseHeap();
	vTraceEnable(TRC_START);
    /* Create the first task at priority 1.  This time the task parameter is
    not used and is set to NULL.  The task handle is also not used so likewise
    is also set to NULL. */
    xQueue = xQueueCreate(5, sizeof(long));

    if (xQueue != NULL)
    {
        /* Create two instances of the task that will write to the queue.  The
        parameter is used to pass the value that the task should write to the queue,
        so one task will continuously write 100 to the queue while the other task
        will continuously write 200 to the queue.  Both tasks are created at
        priority 1. */
        xTaskCreate(vSenderTask, "Sender1", 200, (void*)100, 1, NULL);
        xTaskCreate(vSenderTask, "Sender2", 200, (void*)200, 1, NULL);

        /* Create the task that will read from the queue.  The task is created with
        priority 2, so above the priority of the sender tasks. */
        xTaskCreate(vReceiverTask, "Receiver", 200, NULL, 2, NULL);

        /* Start the scheduler so the created tasks start executing. */
        vTaskStartScheduler();
    }
    else
    {
        /* The queue could not be created. */
    }

	for (;;);
	return 0;
}

Block when Sending to a Queue

This example is similar to the previous one, but the task priorities are reversed, so the receiving task has a lower priority than the sending tasks. Also, the queue is used to pass structures, rather than integers.

In the previous code, the receiving task has the highest priority, so the queue never contains more than one item. This results from the receiving task pre-empting the sending tasks as soon as data is placed into the queue. In this part, the sending tasks have the higher priority, so the queue will normally be full. This is because, as soon as the receiving task removes an item from the queue, it is pre-empted by one of the sending tasks which then immediately refills the queue. The sending task then re-enters the Blocked state to wait for space to become available on the queue again.

Output:

Code

#include "queue.h"

#define mainSENDER_1    1
#define mainSENDER_2    2

/* The tasks to be created.  Two instances are created of the sender task while
only a single instance is created of the receiver task. */
static void vSenderTask(void* pvParameters);
static void vReceiverTask(void* pvParameters);

/* Declare a variable of type QueueHandle_t.  This is used to store the queue
that is accessed by all three tasks. */
QueueHandle_t xQueue;


/* Define the structure type that will be passed on the queue. */
typedef struct
{
    unsigned char ucValue;
    unsigned char ucSource;
} xData;


/* Declare two variables of type xData that will be passed on the queue. */
static const xData xStructsToSend[2] =
{
  { 100, mainSENDER_1 }, /* Used by Sender1. */
  { 200, mainSENDER_2 }  /* Used by Sender2. */
};


static void vSenderTask(void* pvParameters)
{
    portBASE_TYPE xStatus;
    const TickType_t xTicksToWait = 100 / portTICK_PERIOD_MS;

    /* As per most tasks, this task is implemented within an infinite loop. */
    for (;; )
    {
        /* The first parameter is the queue to which data is being sent.  The
        queue was created before the scheduler was started, so before this task
        started to execute.

        The second parameter is the address of the structure being sent.  The
        address is passed in as the task parameter.

        The third parameter is the Block time - the time the task should be kept
        in the Blocked state to wait for space to become available on the queue
        should the queue already be full.  A block time is specified as the queue
        will become full.  Items will only be removed from the queue when both
        sending tasks are in the Blocked state.. */
        xStatus = xQueueSendToBack(xQueue, pvParameters, xTicksToWait);

        if (xStatus != pdPASS)
        {
            /* We could not write to the queue because it was full - this must
            be an error as the receiving task should make space in the queue
            as soon as both sending tasks are in the Blocked state. */
            printf("Could not send to the queue.\r\n");
        }

        /* Allow the other sender task to execute. */
        taskYIELD();
    }
}

static void vReceiverTask(void* pvParameters)
{
    /* Declare the structure that will hold the values received from the queue. */
    xData xReceivedStructure;
    portBASE_TYPE xStatus;

    /* This task is also defined within an infinite loop. */
    for (;; )
    {
        /* As this task only runs when the sending tasks are in the Blocked state,
        and the sending tasks only block when the queue is full, this task should
        always find the queue to be full.  3 is the queue length. */
        if (uxQueueMessagesWaiting(xQueue) != 3)
        {
            printf("Queue should have been full!\r\n");
        }

        /* The first parameter is the queue from which data is to be received.  The
        queue is created before the scheduler is started, and therefore before this
        task runs for the first time.

        The second parameter is the buffer into which the received data will be
        placed.  In this case the buffer is simply the address of a variable that
        has the required size to hold the received structure.

        The last parameter is the block time - the maximum amount of time that the
        task should remain in the Blocked state to wait for data to be available
        should the queue already be empty.  A block time is not necessary as this
        task will only run when the queue is full so data will always be available. */
        xStatus = xQueueReceive(xQueue, &xReceivedStructure, 0);

        if (xStatus == pdPASS)
        {
            /* Data was successfully received from the queue, print out the received
            value and the source of the value. */
            if (xReceivedStructure.ucSource == mainSENDER_1)
            {
                printf("From Sender 1 = %d\r\n", xReceivedStructure.ucValue);
            }
            else
            {
                printf("From Sender 2 = %d\r\n", xReceivedStructure.ucValue);
            }
        }
        else
        {
            /* We did not receive anything from the queue.  This must be an error
            as this task should only run when the queue is full. */
            printf("Could not receive from the queue.\r\n");
        }
    }
}

int main(void) 
{
	prvInitialiseHeap();
	vTraceEnable(TRC_START);
    /* The queue is created to hold a maximum of 3 structures of type xData. */
    xQueue = xQueueCreate(3, sizeof(xData));

    if (xQueue != NULL)
    {
        /* Create two instances of the task that will write to the queue.  The
    parameter is used to pass the structure that the task should write to the
    queue, so one task will continuously send xStructsToSend[ 0 ] to the queue
    while the other task will continuously send xStructsToSend[ 1 ].  Both
    tasks are created at priority 2 which is above the priority of the receiver. */
        xTaskCreate(vSenderTask, "Sender1", 200, (void*)&(xStructsToSend[0]), 2, NULL);
        xTaskCreate(vSenderTask, "Sender2", 200, (void*)&(xStructsToSend[1]), 2, NULL);

        /* Create the task that will read from the queue.  The task is created with
        priority 1, so below the priority of the sender tasks. */
        xTaskCreate(vReceiverTask, "Receiver", 200, NULL, 1, NULL);

        /* Start the scheduler so the created tasks start executing. */
        vTaskStartScheduler();
    }
    else
    {
        /* The queue could not be created. */
    }

	for (;;);
	return 0;
}

Last updated