ChebbiOS

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).
Note: There is also 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.

  1. CPU Interrupts current task.
  2. Kernel increments xTickCount.
  3. Kernel checks if any blocked tasks (e.g., vTaskDelay(10)) need to wake up.
  4. 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.

// Scenario
Block A
USED
Block B
// If we free the middle block...
One Giant Free Block (A + Middle + B)

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