1. Introduction
FreeRTOS has become the preferred choice for embedded real-time systems due to its small footprint, portability, and flexible customization capabilities. Before delving into the source code, let us review the core requirements of real-time operating systems (RTOS) in the embedded field:
- Determinism: Predictable task response delays.
- Scalability: Support for memory usage ranging from a few KB to several tens of KB.
- Low Overhead: Efficient context switching, scheduling, and synchronization.
This article will combine the implementation of FreeRTOS with comparisons to other common RTOS (such as ThreadX and Zephyr) and general-purpose operating systems (such as Linux-CFS), helping readers build a more complete understanding of scheduling and context switching.
2. Task Creation and Resource Initialization
2.1 FreeRTOS Task Control Block (TCB) Structure
The <span>tskTCB</span> in FreeRTOS is the core data structure for tasks:
typedef struct tskTaskControlBlock{ volatile StackType_t *pxTopOfStack; /* Stack top pointer */ ListItem_t xStateListItem; /* State list node */ UBaseType_t uxPriority; /* Static priority */ UBaseType_t uxBasePriority; /* For priority inheritance */ /* Other members: task name, event list, TLS, etc. */} tskTCB;
Comparison:
- ThreadX uses a bitmap + ready list dual mechanism;
- Zephyr uses a “ready queue group” plus a bitmap, allowing constant time to locate the highest priority.
- Linux-CFS is not suitable for hard real-time scenarios, as it uses a red-black tree (
<span>rb-tree</span>) to store ready processes, dynamically adjusting based on runtime share.
2.2 Stack Frame Initialization
FreeRTOS pre-loads the “exception stack frame” onto the task stack in <span>prvInitialiseTaskStack()</span>:
StackType_t *prvInitialiseTaskStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters ){ /* xPSR, PC, LR, R12, R3, R2, R1, R0 */ *--pxTopOfStack = portINITIAL_XPSR; *--pxTopOfStack = ( StackType_t ) pxCode; /* PC */ *--pxTopOfStack = ( StackType_t ) prvTaskExitError; /* ... */ *--pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */ /* Reserve R4–R11 */ pxTopOfStack -= 8; return pxTopOfStack;}
Comparison:
- µC/OS-II also constructs the exception stack frame in the stack;
- Zephyr implements this through assembly macros, using architecture-specific interrupt entry points.
3. Scheduler and Ready List Management
3.1 Organization of Ready Tasks
FreeRTOS maintains a doubly linked list for each priority in <span>pxReadyTasksLists[]</span>, and uses a bitmap <span>uxReadyPriorities</span> to record non-empty list bits:
static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];static UBaseType_t uxReadyPriorities;
- Insertion:
<span>vListInsertEnd()</span>adds to the end of the corresponding priority list. - Selection:
<span>taskRECORD_READY_PRIORITY()</span>and bit operations quickly update; - Finding the Highest Priority:
<span>portGET_HIGHEST_PRIORITY()</span>uses CLZ/BSF instructions to obtain the highest set bit in O(1).
Comparison:
- ThreadX directly uses a 32 or 64-bit ready bitmap, combined with
<span>__CLZ()</span>to achieve O(1).- Linux-CFS uses a red-black tree (O(log n)) or completely fair queue, suitable for a large number of processes.
3.2 Priority Inheritance and Mutexes
FreeRTOS’s mutex <span>Mutex</span> supports priority inheritance:
- If a low-priority task holds the lock, a high-priority task is blocked, and the holding task is temporarily elevated to the blocked task’s priority.
- After releasing the lock, the original priority is restored.
/* In vTaskPriorityInherit(): */if( pxMutexHolder->uxPriority < uxMutexWaitingPriority ){ /* Temporarily elevate */ vTaskPrioritySet( pxMutexHolder, uxMutexWaitingPriority );}
Comparison:
- Zephyr also supports priority inheritance and priority ceiling;
- Linux RT-PREEMPT has added a mutex inheritance mechanism in the kernel, with higher complexity.
4. Clock Ticks and Triggering Context Switches
4.1 SysTick Driver
On the Cortex-M platform, FreeRTOS uses SysTick by default to generate fixed ticks, with the interrupt handling as follows:
void xPortSysTickHandler( void ){ if( xTaskIncrementTick() != pdFALSE ) { /* If a higher priority task is ready, trigger PendSV */ portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; }}
<span>xTaskIncrementTick()</span>: Updates the global tick<span>xTickCount</span>, handling delayed tasks and waking software timers.- Optional Tickless: In
<span>configUSE_TICKLESS_IDLE=1</span>mode, SysTick can be dynamically disabled to further reduce power consumption.
Comparison:
- FreeRTOS Tickless vs Zephyr Tickless: Both calculate the next wake-up time using RTC or low-power timers.
- Linux has recently promoted fully dynamic ticks (
<span>NO_HZ_FULL</span>), but due to high complexity, it is rarely used in embedded systems.
5. PendSV Interrupt: Core Context Switching
5.1 Assembly Implementation
PendSV is the lowest priority exception used for executing context switches:
PendsvHandler: MRS R0, PSP STMDB R0!, {R4–R11} ; Save caller registers LDR R1, =pxCurrentTCB STR R0, [R1] ; Update current task stack top BL vTaskSwitchContext ; Select the next TCB from the ready list LDR R1, =pxCurrentTCB LDR R0, [R1] ; New task stack top LDMIA R0!, {R4–R11} ; Restore registers MSR PSP, R0 BX LR
5.2 Switching Overhead and Measurement
- Number of Registers: Cortex-M saves R4–R11, totaling 8 registers.
- Context Overhead: Approximately 40–50 CPU cycles, depending on compiler optimization and memory access latency.
- Comparison: General-purpose OS (like Linux) typically needs to save more registers, MMU context, page tables, TLB, etc., with overhead in the hundreds of cycles.
6. Comparison with Other Operating Systems
| Feature | FreeRTOS | ThreadX | Zephyr | Linux-CFS |
|---|---|---|---|---|
| Ready Organization | Priority list + bitmap O(1) | Bitmap O(1) + list | Ready queue group + bitmap O(1) | rb-tree O(log n) |
| Context Switching | PendSV O(1) | SVC + PendSV | IRQ + Switch | Soft interrupt + Trap, high overhead |
| Priority Inheritance | Supported | Supported | Supported | Supported (RT-PREEMPT patch) |
| Tickless | Optional support | Optional | Optional | Partial support, complex |
| Memory Usage | ≈ 8–15 KB | ≈ 4–12 KB | ≈ 18–40 KB | >1 MB |
7. Flowchart
flowchart LR subgraph Task Creation A[xTaskCreate] --> B[Allocate TCB & Stack] B --> C[Initialize Stack Frame] C --> D[Insert into Ready List] end subgraph Scheduling and Switching E[SysTick Interrupt] --> F[xTaskIncrementTick] F -->|Higher priority ready| G[Trigger PendSV] G --> H[PendSV Handler] H --> I[vTaskSwitchContext] I --> J[Restore New Task Context] end
8. In-Depth Thoughts
- Scalability: FreeRTOS allows developers to add custom statistics and tracking through hook functions and configuration macros.
- Multi-architecture Support: Supports not only Cortex-M but also ARM7/9, RISC-V, MIPS, PowerPC, etc. Porting requires only providing the corresponding ISR and context switching assembly.
- Real-time Performance Evaluation: Can measure context switching and interrupt latency using hardware timers to ensure worst-case response time (WCRT) is met.
9. Conclusion
This article not only analyzes the task creation, ready management, tick triggering, and context switching of FreeRTOS from the source code perspective but also helps readers understand the trade-offs in performance and functionality through comparisons with other RTOS and general-purpose operating systems. For further reading, you can delve into the following source files:
<span>tasks.c</span>,<span>list.c</span>,<span>queue.c</span>: Core task and queue management<span>port.c</span>,<span>portasm.s</span>: Porting layer context switching details<span>timers.c</span>: Software timer implementation
I hope this article helps you quickly get started with the FreeRTOS kernel and lays a solid foundation for real-time systems.