FreeRTOS: Under The Hood
Dissecting the world's most popular RTOS kernel.
FreeRTOS is deceptively simple. It's just a C library that provides a scheduler and some IPC primitives. But how does it actually turn a single-core CPU into a multitasking powerhouse?
1. The Core Files
The kernel itself is tiny. You only need 3 source files to get started:
-
tasks.c: The heavy lifter. Implements the scheduler, TCBs (Task Control Blocks), and Time Slicing. -
queue.c: The backbone of IPC. Almost everything in FreeRTOS (Semaphores, Mutexes, Message Buffers) is actually just a Queue wrapper. The queue handles data copying and thread blocking. -
list.c: A doubly-linked list implementation used internally by the kernel to track tasks (Ready List, Blocked List, Suspended List).
port.c, which
contains the assembly magic specific to your processor (ARM,
RISC-V, etc.) to handle context switching.
2. The Scheduler
FreeRTOS uses a Fixed-Priority Preemptive scheduler. This means:
- The Highest Priority ready task ALWAYS runs.
- If multiple tasks have the same priority, they Round Robin (share time slices).
- A low priority task will never run if a high priority task is busy spinning (the Starvation problem).
The Tick Interrupt
The heartbeat of the OS. On ARM Cortex-M, this is usually the SysTick timer firing every 1ms.
- CPU Interrupts current task.
- Kernel increments
xTickCount. -
Kernel checks if any blocked tasks (e.g.,
vTaskDelay(10)) need to wake up. - Kernel checks if a context switch is needed.
3. Memory Management (Heap_4.c)
Embedded systems are RAM-constrained. FreeRTOS offers 5 heap schemes, but heap_4.c is the standard choice.
Coalescence
The killer feature of heap_4.c is that it merges
adjacent free blocks.
Without coalescence, your heap would fragment until `pvPortMalloc` fails, even if total free ram is sufficient.
Context Switching (ARM Cortex-M)
The context switch is the "magic trick". It saves the CPU registers (R0-R15) of Task A onto Task A's stack, loads Task B's registers from Task B's stack, and resumes execution.
// PendSV Handler (Simplified Assembly)
mrs r0, psp // Get current task stack pointer
stmdb r0!, {r4-r11} // Save lingering registers
ldr r3, =pxCurrentTCB
str r0, [r3] // Save stack pointer to TCB
// -- Switch Tasks --
ldr r3, =pxCurrentTCB
ldr r1, [r3] // Load new Task TCB
ldr r0, [r1] // Load new Stack Pointer
ldmia r0!, {r4-r11} // Restore registers
msr psp, r0
bx lr // Return to new Task! Connect & Discuss
Have questions about systems engineering, or found a bug in the code? Reach out!
Feedback
This blog is a static site, but I'd love to hear your thoughts. You can discuss this post by sending me an email or reaching out on social media.
Send Feedback