Each task is a small program on its own right. It has an entry point, will normally run forever within an infinite loop. Task are implemented as C functions. the only about them its their prototype which must return a void and take a void. FreeRTOS tasks must not be allowed to return from their implementing function in any way. They must not contain a return statement. If no longer required, it should be explicitly deleted, using a FreeRTOS API Function.

void vTaskFunction(void *pvParmeters)
{
	/*init here*/
	for(;;)
    {
		/*your code here*/
    }
}

An application can consist of many tasks. If the microcontroller running the application only contains a single core, then only one task can actually be executing at any given time. This implies that a task can exist in of two states, "Running and Not-running"

When a task is in the running state is actually executing its code

Task are created using the FreeRTOS API function.

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskcode,    /*Function top work as a task*/
    const char * const pcName,    /*name in a string representation*/
    uint16_t usStackDepth,        /*stack size in words (4 bytes)*/
    void * pvParameters,          /*pointer to paramters to pass*/
    UBaseType_t uxPriority,       /*task priority*/
    TaskHandle_t * pxCreatedTask  /*task handler*/
);

The following code shows two simple tasks created almost equal, from two different functions, same priority, no parameters and no handlers

...

/*Create first task*/
xTaskCreate( vTask1, "task1", 240, NULL, 1, NULL );
/*Create second task*/
xTaskCreate( vTask2, "task2", 240, NULL, 1, NULL );

...

void vTask1( void *pvParameters )
{
    for( ; ; )
    {
        for( uint32_t i = 0 ; i < 100000u ; i++ );
        SEGGER_SYSVIEW_PrintfHost( "Soy Tarea 1" );
    }
}

void vTask2( void *pvParameters )
{
    for( ; ; )
    {
        for( uint32_t i = 0 ; i < 100000u ; i++ );
        SEGGER_SYSVIEW_PrintfHost( "Soy Tarea 2" );
    }
}

From the previous program we can visualize the execution with the following graphic where is appreciated how each task runs for a fixed period of time

Context

As a task executes it utilizes the processor/microcontroller registers and accesses RAM and ROM just as any other program. These resources together (the processor registers, stack, etc.) comprise the task execution context.

A task is a sequential piece of code - it doesn’t know when it is going to get suspended or resumed by the kernel and doesn’t even know when this has happened. While the task is suspended other tasks will execute and may modify the processor register values. Upon resumption the task will not know that the processor registers have been altered.

CPU register represent the context and must be store in RAM memory

The operating system kernel is responsible for ensuring that upon resumption a task has a context identical to that immediately prior to its suspension and does so by saving the context of a task as it is suspended. When the task is resumed its saved context is restored by the operating system kernel prior to its execution.

Each task executes for a “time slice”; it enters the running state at the start of the time slice and exits the running state at the end of the time slice.To be able to select the next task to run, the scheduler itself must execute at the end of each time slice. The “tick interrupt”, is used for this purpose.

Execution pattern with tick visualization

Task Priorities

The uxPriority parameter of the xTaskCreate() function assigns an initial priority to the task being created. Low priority values denote low priority tasks. Priority 0 is the lowest possible value. and the maximum is configMAX_PRIORITIES - 1. The scheduler will always ensure that the highest priority task that is able to run is the Running state.

In your project FreeRTOSConfig.h file you will find the following define.

#define configMAX_PRIORITIES            ( 5UL )

Change this value to set the maximum priority level for your application. The higher the value the more RAM the kernel will consume, make sure you only set up to the value your application really needs.

To make our tasks actually useful we need to make them, event driven, and that means tasks only has work to perform after the occurrence of the event that triggers it. Using event driven tasks allows create a lot of different priority tasks without the highest priority task starving processing time of the lower priority one.

The block state

In reality the tasks “not running” state can expanded to another three different states

  • Blocked State.- A task that is waiting for an event to occurred.
  • Suspend State.- Task in the suspend state are not available to the scheduler.
  • Ready State.- Tasks that are in the not-running state but are not Blocked or Suspended are said to be in the ready state.

The tasks created in the last day have been periodic. The delay has been generated very crudely using a null loop. While executing the null loop the task remained in the ready state, starving the other task of any processing time. While polling the task does not really have any work to do, but it still uses maximum processing time and so wastes processor cycles. To avoid this situation we can use the vTaskDelay. this function place the calling task into the blocked state for a fixed number of "tick" interrupts.

/* in FreeRTOSConfig.h */
#define INCLUDE_vTaskDelay				1

/* this enable the freertos fcuntion */
void vTaskDelay( TickType_t xTicksToDelay );

The next program sets the same two tasks but this time each of them send itself to the block state using the vTaskDelay function

...
xTaskCreate( vTask1, "task1", 240, NULL, 1, NULL);
xTaskCreate( vTask2, "task2", 240, NULL, 2, NULL);

...
void vTask1( void *pvParameters )
{
    for( ; ; )
    {
        SEGGER_SYSVIEW_PrintfHost( "Hola retardo1" );
        vTaskDelay( 2000 );
    }
}

void vTask2(void *pvParameters)
{
    for( ; ; )
    {
        SEGGER_SYSVIEW_PrintfHost( "Hola retardo2" );
        vTaskDelay( 2000 );
    }
}

Selecting Task Priorities

Rate Monotonic Scheduling is a common priority assigned technique that dictates a unique priority be assigned to each task in accordance with the task periodic execution time.

"The highest priority is assigned to the task that has the highest frequency of periodic execution. And the lowest priority is assigned to the task with the lowest frequency of periodic execution."
  • Each task is assigned a priority.
  • Each task can exist in one of the several states.
  • Only one task can exist in the Running state at any one time.
  • The scheduler will always select the highest priority Ready state task to enter the Running state.

Task Management

Once a task has been created, its possible to control and modify its behavior. (priority, running state, deleted, etc...). You have to enable each task management function with the following includes in your FreeRTOSConfig.h file

...
#define INCLUDE_vTaskPrioritySet                    0
#define INCLUDE_uxTaskPriorityGet                   0
#define INCLUDE_vTaskDelete                         0
#define INCLUDE_vTaskCleanUpResources               0
#define INCLUDE_vTaskSuspend                        0
#define INCLUDE_vTaskDelayUntil                     0
#define INCLUDE_vTaskDelay                          1
#define INCLUDE_uxTaskGetStackHighWaterMark         0
#define INCLUDE_eTaskGetState                       0
...

Delay until

vTaskDelay() will cause a task to block for the specified number of ticks from the time vTaskDelay() is called. It is therefore difficult to use vTaskDelay() by itself to generate a fixed execution frequency as the time between a task unblocking following a call to vTaskDelay() and the next calling vTaskDelay() may not be fixed. The vTaskDelayUntil() specifies the absolute ( exact ) time at which it wishes to unblock.

void vPeriodic( void * pvParameters )
{
    TickType_t xLastWakeTime = xTaskGetTickCount();
    
    for( ;; )
    {
        SEGGER_SYSVIEW_PrintfHost( "Periodic function" );
        vTaskDelayUntil( &xLastWakeTime, 100 );
    }
}

The Idle Task

The task created in example #1 spend most of their time in the blocked state. In this state, they are not able to run and can not be selected by the scheduler. The processor always need something to execute, so there must be at least one task that can enter the Running state. to ensure this is the case, an idle task is automatically created by the scheduler when vTaskStartScheduler() is called.

It is possible to add application specific functionality directly into the idle task through the use of an idle hook function. Common use for the idle task hook include:

  • Low priority execution processing
  • Measure the amount of processing time
  • Placing the processor into low power mode
//set to 1 in FreeRTOSConfig.h
#define configUSE_IDLE_HOOK		1

//Global variable
unsigned long ulCycle = 0ul;

//you can define in your application
void vApplicationIdleHook( void )
{
	ulCycle++;
}

Exercises

  1. Write a task that rotates a turned-off LED on port C at a speed that is perceptible to the human eye and a second task the blink the Nucleo on board led every 500ms.
  2. Write a program that turns on an LED when a button is pressed and turns it off when the button is released. ( The LED will only turn on when the button is pressed ), use a task to read the button a a second task to control the led state, a global variable shall be used to communicate both tasks
  3. Write a program that rotates an LED on port C, but this time with three speeds and three buttons. Each button will activate a different speed. again one task will control the buttons an second task the leds and a global variable to communicate both tasks
  4. Modify exercise 2 so that pressing the button once turns on the LED, and pressing it again turns it off.
  5. Repeat exercise 3, but this time using only one button and four speeds. Each time the button is pressed, the speed will increase, and when it reaches the last speed, it will start over.
  6. Modify the previous program using two buttons. Pressing one button will rotate the LEDs from left to right, and pressing the other button will rotate them from right to left