FreeRTOS Series Article 27 – Analysis of FreeRTOS System Delays

Follow and star our public account, to access exciting content

FreeRTOS Series Article 27 - Analysis of FreeRTOS System Delays

ID: Technology Makes Dreams Greater

Author: Li Xiaoyao

FreeRTOS provides two system delay functions: the relative delay function vTaskDelay() and the absolute delay function vTaskDelayUntil().

Relative delay means that each delay starts from the execution of the task function vTaskDelay() and ends after the specified delay time;

Absolute delay means that the task calls the vTaskDelayUntil() function at specified intervals, in other words: the task executes at a fixed frequency.

In the article “FreeRTOS Series Article 11 – FreeRTOS Task Control”, the prototypes and usage of these two API functions have been introduced. This article will analyze the implementation principles of these two functions.

1. Relative Delay Function vTaskDelay()

Consider the following task, where Task A calls the relative delay function vTaskDelay() to enter a blocked state after executing its main code.

In addition to Task A, there are other tasks in the system, but Task A has the highest priority.

void vTaskA( void * pvParameters )  
{  
    /* Block for 500ms. Note: The macro pdMS_TO_TICKS is used to convert milliseconds to ticks. This macro is available in FreeRTOS V8.1.0 and later. If using an earlier version, you can use 500 / portTICK_RATE_MS */  
    const portTickType xDelay = pdMS_TO_TICKS(500);  
    
    for( ;; )  
    {  
        //  ...  
        //  This is the main task code  
        //  ...  
        
        /* Call the system delay function, block for 500ms */  
        vTaskDelay( xDelay );  
    }  
}  

For such a task, the execution process is shown in Figure 1-1.

When Task A gains CPU control, it first executes the main code of Task A, then calls the system delay function vTaskDelay() to enter a blocked state.

After Task A enters the blocked state, other tasks can execute.

The FreeRTOS kernel periodically checks whether Task A’s block has reached the limit; if the blocking time is reached, Task A is set to the ready state.

Since Task A has the highest priority, it will preempt the CPU and execute its main code again, continuously looping.

As shown in Figure 1-1, the delay for Task A is always counted from the moment the delay function vTaskDelay() is called, so it is called a relative delay function.

From Figure 1-1, it can also be seen that if an interrupt occurs during the execution of Task A, the execution period of Task A will be extended. Therefore, using the relative delay function vTaskDelay() cannot execute Task A periodically.

FreeRTOS Series Article 27 - Analysis of FreeRTOS System Delays
Figure 1-1: Execution Diagram of the Relative Delay Function
/** Let's take a look at the source code. */
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
 
    /* If the delay time is 0, the current task will not be added to the delay list */
    if( xTicksToDelay > ( TickType_t ) 0U )
    {
        vTaskSuspendAll();
        {
            /* Remove the current task from the ready list, calculate the wake-up time based on the current system tick counter value, and then add the task to the delay list */
            prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
        }
        xAlreadyYielded = xTaskResumeAll();
    }
 
    /* Force a context switch */
    if( xAlreadyYielded == pdFALSE )
    {
        portYIELD_WITHIN_API();
    }
}

If the delay is greater than 0, the current task will be removed from the ready list and added to the delay list.

This process is completed by calling the function prvAddCurrentTaskToDelayedList().

As mentioned in previous articles, many local static variables are defined in tasks.c, one of which is the variable xTickCount defined as follows:

static volatile TickType_t xTickCount = ( TickType_t ) 0U;

This variable is used to count the number of system tick interrupts; it is cleared when the scheduler starts and incremented by 1 each time a system tick interrupt occurs.

The relative delay function will use this variable; xTickCount represents the current number of system tick interrupts, and this value plus the specified delay time (in ticks) xTicksToDelay will be the time to wake the task next. xTickCount + xTicksToDelay will be recorded in the task TCB and linked to the delay list with the task.

We know that the variable xTickCount is of type TickType_t, and it can overflow.

In a 32-bit architecture, when xTicksToDelay reaches 4294967295 and is incremented, it will overflow and become 0.

To solve the overflow problem of xTickCount, FreeRTOS uses two delay lists: xDelayedTaskList1 and xDelayedTaskList2, and uses two pointer variables pxDelayedTaskList and pxOverflowDelayedTaskList to point to the above delay lists (the delay list pointer is set when creating the task).

“By the way”, the above two delay list pointer variables and the two delay list variables are defined as static local variables in tasks.c.

If the kernel determines that xTickCount + xTicksToDelay overflows, the current task will be linked to the list pointed to by the pointer pxOverflowDelayedTaskList; otherwise, it will be linked to the list pointed to by the pointer pxDelayedTaskList.

Each time the system tick interrupt occurs, the interrupt service function checks these two delay lists to see if the delayed tasks are due. If the time is up, the task will be removed from the delay list and re-added to the ready list. If the priority of the newly added task in the ready list is higher than the current task, a context switch will be triggered.

2. Absolute Delay Function vTaskDelayUntil()

Consider the following task, where Task B first calls the absolute delay function vTaskDelayUntil() to enter a blocked state, and after the blocking time is reached, it executes the main code of the task.

In addition to Task B, there are other tasks in the system, but Task B has the highest priority.

void vTaskB( void * pvParameters )  
{  
    static portTickType xLastWakeTime;  
    const portTickType xFrequency = pdMS_TO_TICKS(500);  
    
    // Initialize variable xLastWakeTime with the current time, note this is different from the vTaskDelay() function  
    xLastWakeTime = xTaskGetTickCount();  
    
    for( ;; )  
    {  
        /* Call the system delay function, periodically block for 500ms */        
        vTaskDelayUntil( &xLastWakeTime,xFrequency );  
    
         //  ...  
         //  This is the main task code, executed periodically. Note this is also different from the vTaskDelay() function  
         //  ...  
    }  
}  

For such a task, the execution process is shown in Figure 2-1.

When Task B gains CPU control, it first calls the system delay function vTaskDelayUntil() to put the task into a blocked state.

After Task B enters the blocked state, other tasks can execute.

The FreeRTOS kernel periodically checks whether Task B’s block has reached the limit; if the blocking time is reached, Task B is set to the ready state.

Since Task B has the highest priority, it will preempt the CPU and execute its main code next.

After the main code of the task is executed, it will continue to call the system delay function vTaskDelayUntil() to put the task into a blocked state, repeating this process.

As shown in Figure 2-1, starting from the call to the function vTaskDelayUntil(), the main code of Task B will be executed once every fixed period, even if an interrupt occurs during the execution of Task B, it will not affect this periodicity; it will only shorten the execution time of other tasks!

Therefore, this function is called an absolute delay function, which can be used for the periodic execution of Task B’s main code.

FreeRTOS Series Article 27 - Analysis of FreeRTOS System Delays
Figure 2-1: Execution Diagram of the Absolute Delay Function

How does the function vTaskDelayUntil() achieve periodicity? Let’s take a look at the source code.

void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
 
    vTaskSuspendAll();
    {
        /* Save the system tick interrupt count */
        const TickType_t xConstTickCount = xTickCount;
 
        /* Calculate the next wake-up time for the task (in terms of system tick count) */
        xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
        
        /* If *pxPreviousWakeTime saves the last wake-up time, if the last wake-up time is greater than the current time, it indicates that the tick counter has overflowed */
        if( xConstTickCount < *pxPreviousWakeTime )
        {
            /* Only when the periodic delay time is greater than the execution time of the task's main code, will the task be linked to the delay list. */
            if( ( xTimeToWake < *pxPreviousWakeTime ) &amp;&amp; ( xTimeToWake > xConstTickCount ) )
            {
                xShouldDelay = pdTRUE;
            }
        }
        else
        {
            /* Also ensure that the periodic delay time is greater than the execution time of the task's main code */
            if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
            {
                xShouldDelay = pdTRUE;
            }
        }
 
        /* Update the wake-up time, preparing for the next call to this function. */
        *pxPreviousWakeTime = xTimeToWake;
 
        if( xShouldDelay != pdFALSE )
        {
            /* Add this task to the delay list, note that the blocking time is not referenced from the current time, hence the current system tick count value is subtracted */
            prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
        }
    }
    xAlreadyYielded = xTaskResumeAll();
 
    /* Force a context switch */
    if( xAlreadyYielded == pdFALSE )
    {
        portYIELD_WITHIN_API();
    }
}

Unlike the relative delay function vTaskDelay, this function adds a parameter pxPreviousWakeTime to point to a variable that saves the last time the task was unblocked.

This variable must be set to the current system tick count when the task starts (as seen in the example of Task B above), and thereafter the function vTaskDelayUntil() automatically updates this variable internally.

Since the variable xTickCount may overflow, the program must check for various overflow conditions and ensure that the delay period is not less than the execution time of the task’s main code.

This is easy to understand; it is impossible to execute a task that takes 20 milliseconds to complete every 5 milliseconds.

If we represent the range of the variable xTickCount on the x-axis, the left end of the x-axis is 0, and the right end is the maximum value that the variable xTickCount can represent.

In the three cases shown in Figure 2-2, the task can be added to the delay list. In Figure 2-2, *pxPreviousWakeTime and xTimeToWake represent the periodic delay time of the task, while *pxPreviousWakeTime and xConstTickCount represent the execution time of Task B’s main code.

In Figure 2-2, the first case handles the overflow of the system tick count (xConstTickCount) and the wake-up time count (xTimeToWake); the second case handles the overflow of the wake-up time count (xTimeToWake); the third case handles the normal non-overflow situation. From the figure, it can be seen that whether it is an overflow or not, it is required that the current task’s main code must be executed before the next task is woken up.

In Figure 2-2, this is represented by the variable xTimeToWake always being greater than the variable xConstTickCount (each overflow is equivalent to adding the maximum value once).

FreeRTOS Series Article 27 - Analysis of FreeRTOS System Delays
Figure 2-2: Three Cases for Adding Tasks to the Delay List

Once the calculated wake-up time is valid, the current task is added to the delay list, which also has two.

Each time the system tick interrupt occurs, the interrupt service function checks these two delay lists to see if the delayed tasks are due. If the time is up, the task will be removed from the delay list and re-added to the ready list. If the priority of the newly added task in the ready list is higher than the current task, a context switch will be triggered.

3. Summary

In the above examples, the tasks calling the system delay are all of the highest priority, which is done intentionally for analysis purposes; in practice, tasks may not necessarily be set to the highest priority.

For relative delays, if the task is not the highest priority, the execution period of the task becomes less predictable. This is not a big issue, as we would not use it for precise delays;

For the absolute delay function, if the task is not the highest priority, it can still periodically unblock the task, but the unblocked task may not necessarily gain CPU control, so the main code of the task will not always execute at precise periodic intervals.

If you want to execute a task with precise periodicity, you can use the system tick hook function vApplicationTickHook(), which is called in the system tick interrupt service function, so the code in this function must be concise.

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧
Follow my public account and reply "Join Group" to join the technical exchange group according to the rules.

Welcome to follow my video account:

FreeRTOS Series Article 27 - Analysis of FreeRTOS System Delays

Click "Read the original text" for more shares, and feel free to share, bookmark, like, and view.

Leave a Comment