Electronic News Sharp Interpretation
Technical Dry Goods Updated Daily


Recommended by: cruelfox If you are also willing to become an EEWorld WeChat
Recommender, please participate: EEWorld WeChat is in your hands, recommend casually to earn red envelopes!
FreeRTOS tasks have the following states:
Running |
Running |
Ready |
Ready |
Blocked |
Blocked |
Suspended |
Suspended |
Except for the running state, all other states are collectively referred to as non-running states. Since FreeRTOS is designed for a single CPU system, at any moment, only one task can be in the running state, even if it seems that multiple tasks are running simultaneously—this is just the effect of multiple tasks switching continuously. When a task switches from the running state to a non-running state, the execution context—CPU registers are saved in the task’s private stack; upon returning to the running state, the saved registers are restored from the stack. This is the most fundamental function of task scheduling.
The ready state of a FreeRTOS task indicates that the task is currently not being executed but can be executed at any time. When the next task switch opportunity arises, FreeRTOS will select the task with the highest priority from the list of ready tasks and switch it to the running state.
The priority of a task is an attribute of the task, represented by an integer in FreeRTOS, with the lowest priority being 0, and the highest being the value defined by the configMAX_PRIORITIES macro minus 1. When creating a task, a priority must be specified, and it can also be changed later using the vTaskPrioritySet() function. The significance of the priority is that when a task is in the ready state, it will not get a chance to execute as long as there are tasks with higher priorities also in the ready state.
When two or more tasks with the same priority are in the ready state, they will take turns getting execution opportunities (but there is no guarantee that the execution time will be evenly distributed), and the system will not favor any one of them.
Task switching in FreeRTOS will definitely occur in the following situations:(A) A running task calls vTaskSuspend() to suspend itself. (B) A running task calls a delay function, requesting to wait for a period of time before executing again. (C) A running task needs to wait for a synchronization event that has not yet occurred.
Because the current task actively pauses execution, FreeRTOS needs to switch it to a suspended or blocked state, and then select the highest priority task from the list of ready tasks to execute. If there are no other tasks to execute, it will also execute the system’s built-in Idle task with the lowest priority. There is also a special case: (D) A running task calls taskYIELD() to actively request a task switch.
At this point, if there are tasks in the ready list with the same or higher priority, a task switch will occur; otherwise, the current task will continue execution. If configUSE_PREEMPTION=1 is configured in FreeRTOS, then preemptive task scheduling will occur. The meaning of preemption is that when a newly added task (including new tasks, tasks restored from suspended state, and tasks exiting from blocked state) has a higher priority than the currently running task, it will immediately switch to that higher priority task for execution. The previously running task is unaware of when its execution was paused, hence it is called “preempted”.
Preemptive scheduling ensures that the highest priority task is immediately allocated processor resources once its running conditions are met. However, if there is more than one highest priority task, by default, they will not preempt each other unless configUSE_TIME_SLICING=1 is also configured, allowing FreeRTOS to manage time slicing. When time slicing is enabled, ready tasks of the same priority will execute in turns, with each task executing for a fixed period of time, essentially distributing processor resources evenly.
In summary, under preemptive scheduling, the timing for task switching includes:(E) A running task is preempted by a higher priority task. (F) A running task executes to the end of a time slice and is preempted by a task of the same priority.Using an example from the manual, without preemptive scheduling, it looks like this: Task3 does not block and will only switch to the already ready Task1 and Task2 if it voluntarily relinquishes control.
Let’s analyze the implementation details, starting with a simple one: the vTaskSuspend() function, which adjusts a task’s state to suspended.
-
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
-
{
-
TCB_t *pxTCB;
-
taskENTER_CRITICAL();
-
pxTCB = prvGetTCBFromHandle( xTaskToSuspend );
-
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
-
{
-
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
-
}
-
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
-
{
-
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
-
}
-
vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) );
-
taskEXIT_CRITICAL();
-
if( xSchedulerRunning != pdFALSE )
-
{
-
taskENTER_CRITICAL();
-
prvResetNextTaskUnblockTime();
-
taskEXIT_CRITICAL();
-
}
-
if( pxTCB == pxCurrentTCB )
-
{
-
if( xSchedulerRunning != pdFALSE )
-
{
-
portYIELD_WITHIN_API();
-
}
-
else
-
{
-
if( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) == uxCurrentNumberOfTasks )
-
{
-
pxCurrentTCB = NULL;
-
}
-
else
-
{
-
vTaskSwitchContext();
-
}
-
}
-
}
-
}
This function has three main parts: the first part removes the task’s TCB structure member xStateListItem from its belonging state list, then inserts it into the suspended task list; and removes the xEventListItem member from its belonging list. The second part is to execute prvResetNextTaskUnblockTime() if the scheduler is running. The third part is to perform task scheduling: portYIELD_WITHIN_API() or vTaskSwitchContext() if the scheduler is not running to switch to a non-suspended task.
Ignoring the critical section operations taskENTER_CRITICAL() and taskEXIT_CRITICAL(), it is a bit strange that uxListRemove() only requires one parameter; this needs to be checked in list.c and list.h to understand that FreeRTOS’s list is implemented using a doubly linked list data structure: from the xStateListItem list member, the entire list can be indexed. When the task’s belonging state list is empty, taskRESET_READY_PRIORITY() needs to be executed to reset the ready state of that priority level? It is hard to understand, so we have to trace back the code… It turns out that each priority has its own ready task list, rather than all ready tasks using one list. The following lists are defined globally in tasks.c:
-
/* Lists for ready and blocked tasks. ——————–*/
-
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
-
PRIVILEGED_DATA static List_t xDelayedTaskList1;
-
PRIVILEGED_DATA static List_t xDelayedTaskList2;
-
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;
-
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;
-
PRIVILEGED_DATA static List_t xPendingReadyList;
It can be inferred that FreeRTOS tasks must be in some list (running tasks are in the ready list); when task scheduling occurs, the list needs to be adjusted.
The second step of vTastSuspend() is to reset the global variable xNextTaskUnblockTime because the suspended task may be in a blocking state waiting for a delay, and its influence needs to be excluded.
The third step is to perform task scheduling, which is implemented by the portYIELD() function through the scheduler (otherwise, the scheduler is not running… this will be analyzed later). The portYIELD() function for ARM Cortex-m3 platform is implemented as follows in portmacro.h:
-
#define portYIELD() \
-
{ \
-
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
-
__asm volatile( “dsb” ); \
-
__asm volatile( “isb” ); \
-
}
This macro sets the PendSV state bit, which will trigger a PendSV exception, and then the task switch is implemented in the PendSV ISR handler. It can be boldly speculated that FreeRTOS uses PendSV exceptions to perform task switching. Continuing to analyze this handler, it is an assembly-implemented function in port.c:
-
void xPortPendSVHandler( void )
-
{
-
__asm volatile (
-
” mrs r0, psp \n”
-
” isb \n”
-
” ldr r3, pxCurrentTCBConst \n”
-
” ldr r2, [r3] \n”
-
” stmdb r0!, {r4-r11} \n”
-
” str r0, [r2] \n”
-
” stmdb sp!, {r3, r14} \n”
-
” mov r0, %0 \n”
-
” msr basepri, r0 \n”
-
” bl vTaskSwitchContext \n”
-
” mov r0, #0 \n”
-
” msr basepri, r0 \n”
-
” ldmia sp!, {r3, r14} \n”
-
” ldr r1, [r3] \n”
-
” ldr r0, [r1] \n”
-
” ldmia r0!, {r4-r11} \n”
-
” msr psp, r0 \n”
-
” isb \n”
-
” bx r14 \n”
-
” .align 4 \n”
-
“pxCurrentTCBConst: .word pxCurrentTCB \n”
-
::”i”(configMAX_SYSCALL_INTERRUPT_PRIORITY) );
-
}
Since the PSP stack saved 8 registers before entering the interrupt: r0, r1, r2, r3, r12, LR, PC, xPSR, we can safely use r0 to r3. First, the current task’s TCB address is read into r2, the registers r4 to r11 are saved to the task’s stack, and then the new task stack top (which is the r0 register) is saved in the TCB (the first member of the TCB is the stack top). Next, the vTaskSwitchContext() function is called to switch context, and then the current task’s TCB address is read into r1, the task stack top is read from the TCB, the registers are popped from the stack, the PSP stack register is set, and finally, the exception handler exits.
Because the PSP register has been changed, the stack before entering the exception and the stack after exiting the exception belong to different tasks. For a task, calling the portYIELD() function and returning seems as if nothing has changed. If the vTaskSwitchContext() function does nothing, then the above exception handling has not performed any substantive operations and would still return the current task.
Let’s see how context switching is implemented: actually, the key operation is this:taskSELECT_HIGHEST_PRIORITY_TASK();This is also a macro, defined as follows:
-
{
-
UBaseType_t uxTopPriority = uxTopReadyPriority;
-
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )
-
{
-
–uxTopPriority;
-
}
-
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );
-
uxTopReadyPriority = uxTopPriority;
-
}
It is not difficult to understand that it starts searching from the highest priority down until it finds a ready task list that is not empty, selects a task from it, sets the current TCB address to its TCB address, and then saves the global ready state priority. Therefore, the vTaskSwitchContext() function mainly selects the task to be executed and sets the current global variable pxCurrentTCB.
The timer interrupt function is relatively simple
-
void xPortSysTickHandler( void )
-
{
-
portDISABLE_INTERRUPTS();
-
{
-
if( xTaskIncrementTick() != pdFALSE )
-
{
-
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
-
}
-
}
-
portENABLE_INTERRUPTS();
-
}
The main task is to call xTaskIncrementTick() for counting and determine whether a task switch is needed. The situations where a switch is needed include the round-robin execution of same-priority ready tasks, as well as when functions such as xTaskDelay() request a delay until expiration, and also when waiting for an event timeout. The details of the last two situations, which are in the DelayedTaskList list, I will not elaborate on here.
We sincerely invite you to recommend good articles for EEWorld WeChat, and welcome to participate in
EEWorld WeChat is in your hands, recommend casually to earn red envelopes activity
Recommended Reading
Dry Goods | FreeRTOS Study Notes — Application Scenarios
Dry Goods | FreeRTOS Study Notes — Stack (The Key to Task Switching)
Dry Goods | Analysis of the Causes and Solutions of Inductor Whining
Dry Goods | How to Draw Circuit Symbols Simply and Clearly?
Dry Goods | Discussion on Sine Oscillator Circuit (Part One)
Dry Goods | Discussion on Sine Oscillator Circuit (Part Two)
Dry Goods | Discussion on Sine Oscillator Circuit (Part Three)