FreeRTOS Task Switching, State Transitions, and Interrupt Handling on STM32
The Role of FreeRTOS




On an STM32 running FreeRTOS, the flow from reset to task startup and multitasking operation is as follows:

- Reset → ② → ③: Start the first task by calling
vTaskStartScheduler(), which triggers an SVC interrupt to launch the first task. - ④ → ③: Multitask switching occurs via the SysTick timer interrupt, which then generates a PendSV interrupt. Alternatively, a regular interrupt might occur while a task is running.
- ② → ⑤: When running bare-metal, a regular interrupt can occur.
Processor Operating Modes and Privilege Levels
ARM SP, LR, PC registers, along with all other registers and processor operating modes are discussed.
The difference between privileged and unprivileged modes lies in which registers can be accessed or modified.
For Cortex-M3/M4 cores, detailed explanation of privileged vs. unprivileged modes:

Cortex-M3 Dual Stack: MSP and PSP
Cortex-M3 uses Main Stack Pointer (MSP) and Process Stack Pointer (PSP). When does the M3 core use MSP vs. PSP?
Privileged mode can use both MSP and PSP. Unprivileged mode can use PSP; it's unclear if MSP is accessible. In exception handlers, only MSP is used.

What is the Difference Between Regular Interrupts and SVC/PendSV Interrupts?
Regular interrupts automatically push certain CPU registers onto the stack: xPSR, PC (task entry address), R14 (LR), R12, R3, R2, R1, R0 (task parameter). On return from interrupt, the CPU automatically pops these values.

When an interrupt occurs in an STM32, the processor automatically pushes the following registers onto the stack to save the current context:
- Program Counter (PC): Holds the address of the next instruction.
- xPSR (Program Status Register): Contains status flags and interrupt state.
- Link Register (LR): Holds the return address.
- R0-R3: General-purpose registers used for parameter passing and return values.
- R12: Another general-purpose register.
The stack frame structure is:
+-----------------+
| xPSR |
+-----------------+
| PC (Return Addr)|
+-----------------+
| LR |
+-----------------+
| R12 |
+-----------------+
| R3 |
+-----------------+
| R2 |
+-----------------+
| R1 |
+-----------------+
| R0 |
+-----------------+
These values are saved before the ISR starts. After the ISR completes, the processor restores them. If the ISR uses registers R4-R11, they must be manually saved and restored at the start and end of the ISR.
However, SVC and PendSV interrupts require manual saving of R4-R11 registers inside the interrupt handler.
Why Don't Regular Interrupts Need to Save R4-R11?
Regular interrupt functions typically use few local variables, so they seldom require R4-R11. In multitasking, however, more data is processed, leading to more local variables and function calls.
Thread Mode Privileged ↔ Handler Mode Privileged (bare-metal interrupts):
Only processor mode changes occur at the start and end of exception handling:

In RTOS multitasking switching:
Thread Mode Unprivileged ↔ Handler Mode Privileged (PendSV interrupt for context switching):
When CONTROL[0]=1 (unprivileged thread mode), both processor mode and privilege level change at exception entry and exit:

In RTOS development with an MMU, thread mode unprivileged ↔ thread mode privileged resembles Linux user mode to kernel mode.
Thread Mode Unprivileged ↔ Thread Mode Privileged & Handler Mode Privileged:

After chip reset, the system enters thread mode privileged. Switching betweeen thread mode privileged and handler mode occurs via exception/interrupt entry and exit. Privileged code can set CONTROL[0] to enter unprivileged mode. Handler mode is always privileged. Any exception runs at privileged level; after return, the system reverts to the privilege level that existed when the exception occurred. Handler mode is entered via exception/interrupt and exits back to thread mode. On exception return, you can set CONTROL[0] (unprivileged thread mode) or clear it (privileged thread mode) to change the returning thread mode's privilege level. Unprivileged thread mode code cannot modify CONTROL[0] to return to privileged mode; it must trigger an exception to handler mode, where the handler clears CONTROL[0], so that upon return, the thread mode becomes privileged.
During exception return, besides clearing CONTROL[0], you can also modify the LR register (cannot modify CONTROL[1] in an interrupt handler) so that the returned thread mode is privileged and uses PSP.

xTaskCreate()

- prvInitialiseNewTask()

- prvAddNewTaskToReadyList()
- Increments global task counter
uxCurrentNumberOfTasks. - If
pxCurrentTCBis NULL, point it to the newly created task. - If it's the first task, call
prvInitialiseTaskLists()to initialize task-related lists (currently only ready list).
- Increments global task counter
/* Initialize task-related lists */
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for ( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
}
}
vListInitialise() sets pxList->xListEnd.xItemValue = portMAX_DELAY.

Lists and List Items
The list header xLIST is a global variable (e.g., ready list pxReadyTasksLists). List items are xLIST_ITEM members of TCB_t allocated in heap.
When static void prvAddNewTaskToReadyList( TCB_t * pxNewTCB ) calls listINSERT_END( pxList, pxNewListItem ), it inserts the xLIST_ITEM from the heap-allocated TCB_t into the xLIST (ready list).

xListEnd.xItemValueis initialized during list initialization:void vListInitialise( List_t * const pxList ).ListItem2.xItemValueis set in the delay functionprvAddCurrentTaskToDelayedList()vialistSET_LIST_ITEM_VALUE.
vListInsertEnd() insertion:
pxIndexpoints to the list header. The list is a circular doubly-linked list; inserting at the end places the new item afterpxIndex.

vListInsert() insertion:
- Sorts by
xItemValueand inserts accordingly.

For further analysis, refer to: "From Single-Chip to Operating System ⑤—FreeRTOS List & List Item Source Code Interpretation"
Priority Bitmap uxTopReadyPriority
Supports multiple priorities.
FreeRTOS Task Scheduler
1. Starting the Task Scheduler Initialization and Launching the First Task
For detailed code analysis of each function, refer to: "09_FreeRTOS Task Scheduler"
[FreeRTOS] 2. SVC System Call
vTaskStartScheduler()
This function internally implements the following steps:
- Create idle task.
- If software timers are enabled, create timer task.
- Disable interrupts to prevent interference before or during scheduler start; interrupts will be re-enabled when the first task runs.
- Initialize global variables and set scheduler running flag.
- Initialize the time base timer for task runtime statistics.
- Call
xPortStartScheduler().
xPortStartScheduler()
This function handles hardware-specific configuraton for starting the scheduler and launching the first task.
Internal implementation:
- Check interrupt configuration in
FreeRTOSConfig.h.- Set PendSV and SysTick interrupt priorities to the lowest.
- Call
vPortSetupTimerInterrupt()to configure SysTick.- Initialize critical section nesting counter to 0.
- Call
prvEnableVFP()to enable FPU (M3 lacks FPU), and thenprvStartFirstTask()to launch the first task.
void prvStartFirstTask()
This function launches the first task.
Used for ...