FreeRTOS Source Code Analysis – Observing How the OS Starts Running from the main Function
FreeRTOS is a lightweight real-time operating system kernel suitable for microcontrollers and embedded systems. Its open-source, lightweight, and portable characteristics make it a popular and widely used RTOS in embedded systems.To further learn the underlying principles of RTOS, this series will analyze the implementation of FreeRTOS from the source code perspective based on the STM32 HAL library.
1. FreeRTOS Source File Structure
FreeRTOS-Kernel
├─ CMSIS_RTOS_V2/
│ ├─ cmsis_os.h
│ ├─ cmsis_os2.c
│ ├─ cmsis_os2.h
│ ├─ freertos_mpool.h
│ └─ freertos_os2.h
|
├─ include/
│ ├─ atomic.h
│ ├─ croutine.h → Coroutines (deprecated, still compatible)
│ ├─ deprecated_definitions.h
│ ├─ event_groups.h → Event flag groups
│ ├─ FreeRTOS.h → Must include "main entry"
│ ├─ FreeRTOSConfig_template.h
│ ├─ list.h
│ ├─ message_buffer.h → Message buffer (V10.0+)
│ ├─ mpu_prototypes.h
│ ├─ mpu_wrappers.h
│ ├─ portable.h
│ ├─ projdefs.h
│ ├─ queue.h → Queue / Semaphore / Mutex API
│ ├─ semphr.h → Semaphore/Mutex macro wrappers
│ ├─ stack_macros.h
│ ├─ stackMacros.h → Stack overflow detection helper macros
│ ├─ stream_buffer.h → Stream buffer (V10.0+)
│ ├─ task.h → Task management API
│ └─ timers.h → Software timers
|
├─ portable/ ← "Porting layer" - code related to compiler/hardware
│ ├─ MemMang/ ← Heap management porting points (only "entry" for heap_1~5)
│ │ ├─ heap_1.c
│ │ ├─ heap_2.c
│ │ ├─ heap_3.c
│ │ ├─ heap_4.c
│ │ └─ heap_5.c
│ └─ RVDS/
│ └─ ARM_CM4F/
│ ├─ port.c
│ └─ portmacro.h
|
├─ croutine.c ← Coroutines (legacy)
├─ event_groups.c ← Event flag groups
├─ list.c ← Doubly linked list (basis for ready list, delay list, suspended list)
├─ queue.c ← Unified implementation of queue / semaphore / mutex
├─ stream_buffer.c ← Shared implementation of stream / message buffer
├─ tasks.c ← Core scheduler implementation (task creation, switching, ready list, delay list)
├─ timers.c ← Software timer Daemon task
|
└─ LICENSE.md
CMSIS_RTOS_V2
CMSIS (Cortex Microcontroller Software Interface Standard – Real-Time Operating System) is a unified RTOS interface standard defined by ARM for Cortex-M.Common APIs for FreeRTOS are encapsulated in cmsis_os2.c, allowing users to not worry about which OS is specifically used at the lower level.
portable
Porting adaptation, files related to hardware/compiler
- • MemMangDifferent heap memory management implementations
- • RVDS/ARM_Cm4F/port.cInterrupt switching, scheduler startup, PendSV context switching, SysTick heartbeat, critical section nesting, miscellaneous barriers.
Kernel
Tasks, timers, queues, event groups, etc.
2. Analyzing from the main Function
int main(void)
{
...
/* Init scheduler */
osKernelInitialize(); /* Call init function for freertos objects (in cmsis_os2.c) */
MX_FREERTOS_Init();
/* Start scheduler */
osKernelStart();
/* We should never get here as control is now taken by the scheduler */
while (1)
{
}
}
osKernelInitialize
<span>osKernelInitialize</span> sets the kernel state to <span>Ready</span>
osStatus_t osKernelInitialize (void) {
osStatus_t stat;
if (IS_IRQ()) {
stat = osErrorISR;
}
else {
if (KernelState == osKernelInactive) {
...
KernelState = osKernelReady;
stat = osOK;
} else {
stat = osError;
}
}
return (stat);
}
MX_FREERTOS_Init
A default task is created in <span>MX_FREERTOS_Init</span>, and a dedicated article will analyze task creation later, so we will skip it for now.
void MX_FREERTOS_Init(void) {
/* Create the thread(s) */
/* creation of defaultTask */
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
}
osKernelStart
<span>osKernelStart</span> is the main function of focus in this article, as the kernel starts scheduling from this function
osStatus_t osKernelStart (void) {
osStatus_t stat;
if (IS_IRQ()) {
stat = osErrorISR;
}
else {
if (KernelState == osKernelReady) {
/* Ensure SVC priority is at the reset value */
SVC_Setup();
/* Change state to enable IRQ masking check */
KernelState = osKernelRunning;
/* Start the kernel scheduler */
vTaskStartScheduler();
stat = osOK;
} else {
stat = osError;
}
}
return (stat);
}
<span>osKernelStart()</span> is the only entry point for “starting the scheduler” in the CMSIS-RTOS2 encapsulation of <span>FreeRTOS</span> (in cmsis_os2.c). It hands over control of the <span>CPU</span> to <span>FreeRTOS</span>, and from then on, tasks begin to compete for execution, while the <span>main()</span> thread becomes the <span>idle</span> context.
1. Check if currently in interrupt state
if (IS_IRQ()) {
stat = osErrorISR;
- •
<span>CMSIS</span>stipulates: the scheduler cannot be started in<span>IRQ</span>context. - •
<span>IS_IRQ()</span>typically reads __get_IPSR(), where a non-zero value indicates an exception/interrupt is occurring.
2. State machine check
if (KernelState == osKernelReady)
- • Kernel state machine:
- •
<span>osKernelInactive</span>→<span>osKernelReady</span>(after<span>osKernelInitialize()</span>) - •
<span>osKernelReady</span>→<span>osKernelRunning</span>(set by this function) - • Only ready state is allowed to enter running state, repeated calls return
<span>osError</span>.
3. Ensure SVC priority is at the lowest
__STATIC_INLINE void SVC_Setup (void) {
#if (__ARM_ARCH_7A__ == 0U)
/* Service Call interrupt might be configured before kernel start */
/* and when its priority is lower or equal to BASEPRI, svc instruction */
/* causes a Hard Fault. */
NVIC_SetPriority (SVCall_IRQ_NBR, 0U);
#endif
}
- • Set
<span>NVIC_SVC_PRIORITY</span>to the lowest priority (highest value). - • Prevent user misconfiguration from causing SVC exceptions to be preempted, leading to context switch crashes.
- • Effective for Cortex-M3/4/7/33 cores with SVC; empty function for M0/M0+/M23 without SVC.
4. Switch global state
KernelState = osKernelRunning;
After this, macros like <span>IS_IRQ_MASKING()</span><span> will </span><strong><span>prohibit object creation outside critical sections, ensuring API timing safety</span></strong><span>.</span>
5. Start FreeRTOS scheduler
Call <span>vTaskStartScheduler</span> to start the scheduler
3. How the Scheduler is Started
vTaskStartScheduler
void vTaskStartScheduler( void )
{
StaticTask_t *pxIdleTaskTCBBuffer = NULL;
StackType_t *pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
/* The Idle task is created using user provided RAM - obtain the
address of the RAM then create the idle task. */
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
configIDLE_TASK_NAME,
ulIdleTaskStackSize,
( void * ) NULL, /*lint !e961. The cast is not redundant for all compilers. */
portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
...
xTimerCreateTimerTask();
...
/* Interrupts are turned off here, to ensure a tick does not occur
before or during the call to xPortStartScheduler(). The stacks of
the created tasks contain a status word with interrupts switched on
so interrupts will automatically get re-enabled when the first task
starts to run. */
portDISABLE_INTERRUPTS();
/* Setting up the timer tick is hardware specific and thus in the
portable interface. */
xPortStartScheduler();
}
<span>vTaskStartScheduler</span> mainly accomplishes the following tasks:
- • Create
<span>idle</span>task (static or dynamic),<span>#define configMINIMAL_STACK_SIZE ((uint16_t)128)</span> - • Create
<span>Timer</span>daemon task (if<span>configUSE_TIMERS</span><span> is enabled), further analysis in the Timer section.</span> - • Disable interrupts
- • Call
<span>xPortStartScheduler()</span><span> to start the scheduler</span>
xPortStartScheduler
BaseType_t xPortStartScheduler( void )
{
...
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* Start the timer that generates the tick ISR. Interrupts are disabled
here already. */
vPortSetupTimerInterrupt();
...
/* Ensure the VFP is enabled - it should be anyway. */
prvEnableVFP();
/* Start the first task. */
prvStartFirstTask();
}
<span>xPortStartScheduler</span> mainly accomplishes the following tasks:
- • Set
<span>PendSV/Systick</span>priority - • Set
<span>PendSV</span>and<span>SysTick</span><span> to the lowest priority (</span><code><span>configKERNEL_INTERRUPT_PRIORITY</span><span>)</span> - • Load
<span>SysTick->LOAD</span><span> with </span><code><span>configCPU_CLOCK_HZ / configTICK_RATE_HZ</span><span> and start counting</span> - • Enable
<span>FPU</span><span> (if for </span><code><span>CM4F/CM7</span><span> and </span><code><span>configENABLE_FPU</span><span> is 1)</span> - • Call
<span>prvStartFirstTask</span><span>, starting the first task</span>
prvStartFirstTask
__asm void prvStartFirstTask( void )
{
PRESERVE8
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
/* Set the msp back to the start of the stack. */
msr msp, r0
/* Clear 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. */
mov r0, #0
msr control, r0
/* Globally enable interrupts. */
cpsie i
cpsie f
dsb
isb
/* Call SVC to start the first task. */
svc 0
nop
nop
}
<span>prvStartFirstTask</span> is written in assembly language and will trigger the first context switch via <span>SVC 0</span><span> - hardware jumps to the context of the first task:</span>
1. Find the “birth address” of the main stack<span>MSP</span>
ldr r0, =0xE000ED08 ; r0 = &SCB->VTOR
ldr r0, [r0] ; r0 = VTOR value (vector table base address)
ldr r0, [r0] ; r0 = vector table item 0 = main stack top (_estack)
In <span>Cortex-M</span><span>, the vector table offset register </span><code><span>VTOR</span><span> contains the "starting address of the vector table", and the first word of the table is the initial value of </span><code><span>MSP</span><span> after reset. This step retrieves the "stack top written in Flash during the Boot phase" to ensure that the subsequent </span><code><span>msp</span> points to a valid and 8-byte aligned <span>RAM</span><span> top.</span>
2. Reset MSP to factory settings
msr msp, r0 ; Write back to MSP officially
Before this, <span>FreeRTOS</span><span> may have used </span><code><span>MSP</span><span> as a temporary stack or even run C code; now the old stack is completely discarded to prevent "upper layer" residual data from contaminating the first task. After this, </span><code><span>MSP</span><code><span> will only be used for exception frames, while </span><code><span>PSP</span><span> will run task code.</span>
3. Clear FPU active flag
mov r0, #0
msr control, r0 ; CONTROL.FPCA = 0
<span>CONTROL[2] (FPCA) = 1</span> indicates that the hardware will automatically push floating-point registers <span>S0-S15&FPSCR</span><span> on exception entry, occupying 68 bytes. If the </span><code><span>bootloader</span><span> or early C code used the </span><code><span>FPU</span><span>, and this is not cleared, then upon entering </span><code><span>SVC</span><span>, 68 bytes of garbage will be pushed, wasting </span><code><span>RAM</span><span> and disrupting alignment. After clearing, if the first task uses </span><code><span>FPU</span><span>, it will be re-enabled in </span><code><span>vPortEnableVFP()</span><span>, implementing "lazy saving".</span>
4. Globally enable interrupts
cpsie i ; Enable IRQ (= __enable_irq())
cpsie f ; Enable FIQ (in M series, FIQ is the global interrupt switch)
dsb
isb ; Pipeline and bus barrier, ensuring the previous "enable" takes effect immediately
<span>vTaskStartScheduler()</span><code><span> calls </span><code><span>portDISABLE_INTERRUPTS()</span><span> to disable global interrupts; here is the formal "release". If not opened, after </span><code><span>SVC</span><span>, both </span><code><span>PendSV</span><span> and </span><code><span>SysTick</span><span> will not respond, causing the scheduler to deadlock.</span>
5. Trigger the first context switch
svc 0 ; Trigger SVC exception
nop
nop
<span>SVC 0</span> jumps to <span>vPortSVCHandler()</span><span>, and the following two nops are just placeholders that will never execute</span>
We will analyze up to this point for now; the next step is the formal jump to the first task to start running. In the next article, we will continue to analyze how tasks are created and how multiple tasks switch between each other.