Tasks are the smallest units of execution competing for system resources. Multiple tasks with the same priority can share the same priority level. In FreeRTOS, if configUSE_TIME_SLICING
is defined as 1, then multiple ready tasks with the same priority will share the processor in a time-slicing manner, where one time slice equals one tick.
Task Stack
The task stack is a predefined global array, with a default size of 128 words, which is 512 bytes, and is also the recommended minimum task stack size in FreeRTOS.
Each task has a task control block, and the data structure of the task control block is as follows:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* Stack top */
ListItem_t xStateListItem; /* Task node */
StackType_t *pxStack; /* Task stack starting address */
/* Task name, in string form */
char pcTaskName[ configMAX_TASK_NAME_LEN ];
TickType_t xTicksToDelay;
UBaseType_t uxPriority;
} tskTCB;
-
pxTopOfStack
— Stack top pointer -
xStateListItem
— Task node, which is a linked list node that connects the task control block to various linked lists -
pxStack
— Task stack starting address
In FreeRTOS, the addresses of the LR and FP registers can be determined through the task stack top pointer (pxTopOfStack
) and the task stack starting address (pxStack
). What? You ask what the LR and FP pointers are used for?
Then let me ask you, when your board runs and needs to perform a task switch, if there is no LR register to save the current stack top pointer’s location, how can the task scheduler determine where to start running from the current task? How can it determine the value of the program counter (PC) pointer? Answer me!
Also, when your poorly written code causes the board to crash and you need to backtrack through the stack to find the crash call stack, without the FP pointer to manage the stack frame structure during function calls, how can you quickly locate the crash? Answer me! Look in my eyes! Tell me why? Why baby why?
The values of the registers during task switching are assigned through the pxPortInitialiseStack
interface. Below is the register assignment scene during task switching for ARM_CM3 architecture chips (PS: The FP pointer is R11; for questions about registers, you can refer to my previous ARM series, where register knowledge is discussed):
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
TaskFunction_t pxCode,
void * pvParameters )
{
/* Simulate the stack frame as it would be created by a context switch
* interrupt. */
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
Now back to the main topic, generally, we configure FreeRTOS with a full-level decreasing stack, that is, portSTACK_GROWTH < 0
, meaning the stack grows from high addresses to low addresses.
Moreover, the stack top pointer needs to be aligned to 8 bytes, as follows:
pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );
Note: Here we are performing 8-byte alignment. Why 8-byte alignment? Shouldn’t the 32-bit bus width be 4 bytes? Actually, it is to be compatible with floating-point operations, which operate on 64 bits.
Once the task is created, it is added to the ready list! The index of the ready list corresponds to the task’s priority.
Scheduler
The scheduler implements task switching by finding the highest priority task from the ready list and executing that task. The xPortStartScheduler
configures the pendSV
and systick
interrupt priorities to the lowest, starting the first task. Both pendSV
and systick
are involved in system scheduling.
Critical Section
A critical section is a segment of code that cannot be interrupted during execution: the most common case is operations on global variables!
System scheduling and external interrupts can cause critical sections to be interrupted. In FreeRTOS, system scheduling will ultimately generate a PENDSV interrupt, and task switching is implemented in the interrupt handler, so system calls can also be seen as an interrupt. Therefore, FreeRTOS’s protection of critical sections ultimately returns to controlling the enabling and disabling of interrupts!

That is, enter the critical section by disabling interrupts; exit the critical section by enabling interrupts!
Idle Task
The idle task has the lowest priority and is at the beginning of the ready list. We generally perform some system memory cleanup work in the idle task function! The idle task is created in the scheduler startup function.
Task Switching
Calling taskYIELD()
will generate a PendSV interrupt, and in the PendSV
interrupt service function, the context switch function vTaskSwitchContext()
will be called. The purpose of this function is to find the highest priority ready task and then update pxCurrentTCB
. The minimum time unit in the operating system is the interrupt cycle of systick
, which we call a tick; the function of the systick
interrupt service function is to update the system time base, which is implemented by incrementing xTickCount
by 1 each time, and then scanning all tasks’ xTickToDelay
. If it is not 0, it will decrement xTickToDelay
by 1; then call portYIELD()
to perform task switching.
Multi-Priority
When creating tasks, the TCB of the task is inserted into different positions in the ready list based on the task’s priority. Tasks with the same priority are inserted into the same linked list in the ready list. During task scheduling, the global TCB pointer pxCurrentTCB
needs to point to the TCB of the highest priority ready task.
To find the TCB of the highest priority in the ready list, there are generally two methods:
- 1: A general array traversal method from back to front;
- 2: The CORTEX-M optimized method, using the CLZ instruction to count leading zeros.

Generally, tasks with shorter processing times should have higher priority settings. Some interviewers may ask you what considerations you have when defining task priorities.
Time Slicing
This means that under the same priority, there can be multiple tasks, and each task takes turns to share the same CPU time, which we call a time slice. In FreeRTOS, one time slice is one tick, but in RT-THREAD, one time slice can consist of multiple ticks.