One of the core tasks of an operating system is task scheduling. Tasks have different names in different operating systems, such as task, thread, or process, with processes typically associated with independent address spaces. The goals of scheduling are to ensure fairness, real-time performance (for real-time systems), maximize CPU utilization, and meet task priority requirements. The implementation of the scheduler relies on both hardware (such as timers and interrupt controllers) and operating system policies (such as preemptive scheduling).Task scheduling in FreeRTOS within the STM32F4 environment can be simply described by the three interrupts and three linked lists, which are clearly indicated by their names.
First, let’s talk about the startup of task scheduling, which begins with a call to osKernelStart. The function vTaskStartScheduler is an API provided by FreeRTOS to start the scheduler. In this function, xTaskCreateStatic is called to create an idle task, ensuring that there is at least one task in the system. Then, xPortStartScheduler is called to enter some hardware-related settings, which for STM32F4 includes setting up interrupts, timers, floating-point registers, etc. Below is the function description:
/* * Setup the hardware ready for the scheduler to take control. This generally * sets up a tick interrupt and sets timers for the correct tick frequency. */BaseType_t xPortStartScheduler( void ) PRIVILEGED_FUNCTION;
Finally, it enters the prvPortStartFirstTask function, where task scheduling truly begins, and the entire system is taken over by FreeRTOS. Below is the order of calls:
Next, let’s take a look at the prvPortStartFirstTask function, which is a pure assembly function. Its main purpose is to set up the main stack, enable interrupts, and call SVC 0. SVC 0 is a Supervisor Call (SVC) instruction used to trigger an SVC exception, thus entering privileged mode (Handler Mode) and executing the SVC interrupt service routine (SVC_Handler).
static void prvPortStartFirstTask( void ){ /* Start the first task. This also clears the bit that indicates the FPU is in use in case the FPU was used before the scheduler was started - which would otherwise result in the unnecessary leaving of space in the SVC stack for lazy saving of FPU registers. */ __asm volatile( " ldr r0, =0xE000ED08 \n" /* Use the NVIC offset register to locate the stack. */ " ldr r0, [r0] \n" " ldr r0, [r0] \n" " msr msp, r0 \n" /* Set the msp back to the start of the stack. */ " mov r0, #0 \n" /* Clear the bit that indicates the FPU is in use, see comment above. */ " msr control, r0 \n" " cpsie i \n" /* Globally enable interrupts. */ " cpsie f \n" " dsb \n" " isb \n" " svc 0 \n" /* System call to start first task. */ " nop \n" );}
Below is the SVC Handler function:
void vPortSVCHandler( void ){ __asm volatile ( " ldr r3, pxCurrentTCBConst2 \n" /* Restore the context. */ " ldr r1, [r3] \n" /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */ " ldr r0, [r1] \n" /* The first item in pxCurrentTCB is the task top of stack. */ " ldmia r0!, {r4-r11, r14} \n" /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */ " msr psp, r0 \n" /* Restore the task stack pointer. */ " isb \n" " mov r0, #0 \n" " msr basepri, r0 \n" " bx r14 \n" " \n" " .align 4 \n" "pxCurrentTCBConst2: .word pxCurrentTCB \n" );}
Again, this is assembly code, but from the comments, we can see that its main function is to retrieve the stack saved in pxCurrentTCB, and then switch to the task saved in pxCurrentTCB using bx r14. So how is pxCurrentTCB obtained?We need to look at the task creation function xTaskCreateStatic, where the first parameter is the pointer to the task’s entry function.
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */ const uint32_t ulStackDepth, void * const pvParameters, UBaseType_t uxPriority, StackType_t * const puxStackBuffer, StaticTask_t * const pxTaskBuffer )
After this function executes, it will call prvInitialiseNewTask to initialize the newly created task, and the sub-function pxPortInitialiseStack initializes the stack in the TCB. Below is the function definition:
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. */ /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts, and to ensure alignment. */ pxTopOfStack--; *pxTopOfStack = portINITIAL_XPSR; /* xPSR */ pxTopOfStack--; *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */ pxTopOfStack--; *pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; /* LR */ /* Save code space by skipping register initialisation. */ pxTopOfStack -= 5; /* R12, R3, R2 and R1. */ *pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */ /* A save method is being used that requires each task to maintain its own exec return value. */ pxTopOfStack--; *pxTopOfStack = portINITIAL_EXC_RETURN; pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */ return pxTopOfStack;}
In the function xTaskCreateStatic, the last call to prvAddNewTaskToReadyList adds the TCB to the Ready List. If the task’s priority is higher than the tasks currently in the list, pxCurrentTCB is set to this task, which is the first task executed mentioned in vPortSVCHandler. Combining pxPortInitialiseStack and vPortSVCHandler, the former generates a task’s stack, while the latter restores the stack, where bx r14 is crucial.
In <span>vPortSVCHandler</span>, the evolution of the stack pointer is as follows:
ldr r0, [r1] ; R0 = pxCurrentTCB->pxTopOfStack(task stack top)ldmia r0!, {r4-r11, r14} ; Restore R4-R11 and LR, R0 updates to R0 + 10*4 = R0 + 40
msr psp, r0 ; PSP = R0 (points to the remaining unpopped exception frame)
At this point, the PSP points to the location: storing the remaining exception frame (<span>xPSR</span>, <span>PC</span>, <span>LR</span>, <span>R12</span>, <span>R3-R0</span><span>). </span>
The location of <span>pvParameters</span>: during the initialization of the FreeRTOS task stack, <span>pvParameters</span> is the task’s parameter, typically stored in the stack at <span>R0</span> (because the task’s entry function is <span>void TaskFunction(void *pvParameters)</span>, and the parameter is passed through <span>R0</span>).
The hardware automatic operation during <span>BX LR</span>:
When executing <span>BX R14</span> (R14(<span>LR) = 0xFFFFFFFD</span>), the hardware will:
-
Check
<span>EXC_RETURN</span>:
-
Use PSP as the stack pointer.
-
Return to Thread Mode.
-
Find that
<span>LR = 0xFFFFFFFD</span>, and decide:
Pop the exception frame from the stack pointed to by PSP:
-
The remaining data in the stack is popped in the following order (addresses from low to high):
Offset Register Description +0 R0 Task’s first parameter ( <span>pvParameters</span>)+4 R1 – +8 R2 – +12 R3 – +16 R12 – +20 LR Task’s return address (usually unused) +24 PC Task’s entry function address (pxCode) +28 xPSR Program status register -
Hardware automatically adjusts PSP: for each popped register,
<span>PSP</span>increments by<span>4</span>bytes, ultimately<span>PSP</span>points to the current top of the task stack.
Jump to task code:
-
The popped
<span>PC</span>value (task entry function address) is loaded into the program counter, and the MCU begins executing the task code. -
<span>R0</span>‘s value (i.e.,<span>pvParameters</span>) will be passed as the first parameter to the entry function.
At this point, the task defined in FreeRTOS begins execution, and the world enters an infinite loop state. Next, we will explore task switching, which relies on SysTick and PendSV interrupts. To be continued.