Home FreeRTOS Anatomy of a Simple FreeRTOS Project: Beginner’s Guide

Anatomy of a Simple FreeRTOS Project: Beginner’s Guide

by shedboy71

A simple FreeRTOS project is easier to understand once you stop thinking of it as “just normal embedded code plus a few extra functions” and start seeing it as a system with clear layers. There is still startup code, hardware initialization, and application logic, but there is now also a kernel that creates tasks, schedules them, manages timing, and optionally provides queues, semaphores, and software timers.

FreeRTOS’s own beginner documentation describes the core flow very plainly: the application starts like a non-RTOS program until vTaskStartScheduler() is called, and only then does the scheduler take over task execution.

That one idea explains most of the anatomy. Before the scheduler starts, your code is still in ordinary startup territory. After the scheduler starts, the structure changes. Tasks, the idle task, and optionally the timer service task become part of the running system, and scheduling rules now decide what executes when. The idle task is created automatically when the scheduler starts, and the timer daemon task is also created if software timers are enabled.

This tutorial breaks a simple FreeRTOS project into its main parts so you can see what each part does and how they fit together.

The big picture

A simple FreeRTOS project usually has these main pieces:

The startup and hardware initialization layer.

The FreeRTOS kernel files and configuration.

The application code that creates tasks and other kernel objects.

The scheduler start point.

The task functions that do the real work.

Sometimes queues, semaphores, or software timers if the project is slightly more advanced.

At its smallest, a FreeRTOS project can be just one task and a scheduler. But even that tiny system still has a recognizable structure. It is not just a while(1) loop anymore. It is an application that hands control over to the RTOS kernel after setup.

The startup phase

A FreeRTOS project still begins like an ordinary embedded project. The processor resets, the startup code runs, memory sections are initialized, and eventually main() is reached. FreeRTOS does not replace this part. It sits above it.

That means your startup files, linker script, clock initialization, GPIO setup, UART setup, and board-specific initialization are still important. If the board clock is wrong, the tick timing may be wrong. If the interrupt setup is broken, the scheduler may not behave properly. If the heap or memory layout is incorrect, task creation may fail before the project even starts.

This is why beginners sometimes get confused. They think “I am writing a FreeRTOS project,” but the project is still also a normal embedded project underneath. FreeRTOS does not remove the need for correct low-level setup.

The kernel files

A simple FreeRTOS project includes the kernel itself. That usually means the core kernel source files plus the correct port files for the processor architecture and compiler. FreeRTOS also requires a configuration header called FreeRTOSConfig.h, which controls many core behaviors such as tick rate, maximum priorities, hooks, memory allocation options, and whether certain API features are enabled.

This is one of the most important files in the whole project. A simple FreeRTOS application may only have one or two user tasks, but FreeRTOSConfig.h still defines the environment those tasks run in.

For example, it affects things like:

The scheduler tick frequency.

Whether preemption is enabled.

Whether software timers are enabled.

How much heap is available.

Whether stack overflow hooks or malloc failure hooks are used.

This is why two projects with very similar application code can behave differently if their configuration files differ.

The memory management choice

A simple FreeRTOS project also has to choose how kernel-related dynamic memory is handled. FreeRTOS provides several heap management schemes, and its documentation describes these as different memory management options with different trade-offs. Task creation with xTaskCreate() uses RAM from the FreeRTOS heap, while static APIs such as xTaskCreateStatic() avoid that automatic allocation path.

That matters even in a beginner project. If you create tasks dynamically, their control blocks and stacks need memory from the FreeRTOS heap. If there is not enough heap available, task creation can fail, and if there is not enough memory even for the idle task, vTaskStartScheduler() can return instead of successfully starting the application.

So memory is part of the anatomy, not an afterthought. Even a tiny two-task demo depends on having the heap configured sensibly.

The main() function in a simple FreeRTOS project

In a simple FreeRTOS project, main() usually looks different from a bare-metal superloop design. Instead of performing all application work forever, it typically does setup, creates tasks, and then starts the scheduler.

A minimal pattern looks like this:

int main(void)
{
    hardware_init();

    xTaskCreate(Task1, "Task1", 256, NULL, 1, NULL);
    xTaskCreate(Task2, "Task2", 256, NULL, 1, NULL);

    vTaskStartScheduler();

    for (;;)
    {
    }
}

This small example reflects the standard project structure very well.

First, hardware is initialized.

Next, tasks are created.

Then vTaskStartScheduler() is called.

The infinite loop after that is usually only there as a fallback. In a correctly running system, execution should not normally continue past vTaskStartScheduler(), because once the scheduler starts, it takes control of task execution. FreeRTOS documents that after vTaskStartScheduler() the RTOS kernel controls which tasks execute and when.

This is one of the defining anatomical changes from bare-metal design. main() becomes a launch point rather than the long-term home of application logic.

The tasks

Tasks are the most visible part of a FreeRTOS project. Each task is a function, but not a normal one-shot function. A task is expected to run in a controlled infinite loop or otherwise remain alive for the scheduler to manage.

A typical task looks like this:

void LedTask(void *pvParameters)
{
    for (;;)
    {
        toggle_led();
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

This function is not called directly like an ordinary helper function. It is registered with the kernel, and the scheduler decides when it runs.

That difference matters a lot. A beginner may look at a task and think it is “just another function.” It is not. It is a schedulable execution context with its own stack, state, and priority.

Every task in the system has anatomy of its own:

A function body.

A stack.

A priority.

A task control block maintained by the kernel.

Possibly a handle if the application needs to refer to it later.

Once you understand that, a FreeRTOS project starts to look less like one program flow and more like several managed flows.

Task priorities

A simple FreeRTOS project often introduces priorities very early. Each task is created with a priority, and that priority influences which ready task gets CPU time. This is one of the biggest structural differences from a single-loop bare-metal application.

Even if a project has only two tasks, their priorities shape behavior. If both are ready and one has a higher priority, the higher-priority task will run first. If they share a priority, time slicing and scheduler behavior become relevant depending on the configuration.

This means that project anatomy is not only about files and functions. It is also about execution relationships. In FreeRTOS, how the pieces interact at runtime is part of the design.

The scheduler

The scheduler is the heart of the project. It is the component that turns a set of task functions into a multitasking system.

In a simple project, the scheduler decides things like:

Which ready task runs now.

When a delayed task becomes ready again.

When the idle task runs.

How time slicing works between equal-priority tasks.

How blocking operations affect execution flow.

FreeRTOS explicitly documents that vTaskStartScheduler() starts the scheduler and that after this call the kernel controls execution. It also notes that the idle task is created automatically when the scheduler starts.

That is why the scheduler is such a critical dividing line in project anatomy. Before it starts, you are still in plain initialization code. After it starts, the FreeRTOS world is active.

The tick interrupt

A simple FreeRTOS project also depends on the system tick. The tick interrupt is one of the main timing foundations of the kernel. It allows delayed tasks to wake up, time slicing to happen where configured, and time-based kernel services to operate.

You do not always see this directly in the application source, but it is part of the project’s anatomy. If the tick is misconfigured, task timing becomes wrong. If the port layer or interrupt priorities are incorrect, the scheduler may behave unpredictably.

This is why the port layer matters. A FreeRTOS project is not portable in the sense of being hardware-independent without any adjustment. It relies on the correct architecture-specific port.

The idle task

One detail beginners often miss is that the idle task is part of every normal FreeRTOS system. FreeRTOS documentation states that the idle task is created automatically when the scheduler starts.

The idle task is not something you usually create yourself, but it is an important structural piece. It runs when no other task is ready. In simple projects, this can be easy to overlook, but it matters for understanding scheduler behavior. If all your application tasks are blocked or delayed, the idle task runs.

This also explains why a project must have enough memory not just for your tasks, but for system tasks too.

Optional software timer support

A very simple FreeRTOS project may not use software timers, but once they are enabled they add another structural element. The FreeRTOS documentation notes that the timer daemon task is created when needed if software timers are used.

This means software timers are not just lightweight callback features floating independently. They depend on a service task in the background. That is useful to understand because timer callbacks do not execute in the same way as interrupts or ordinary direct function calls.

In anatomy terms, software timers add another subsystem inside the project.

Queues and communication

Many FreeRTOS tutorials quickly move beyond “two blinking tasks” into task communication. This is where the project anatomy starts to become richer.

A queue is not just a helper object. It is part of the structure that connects tasks. A sensor task may place data into a queue, and a display or logging task may remove that data from the queue. Once that happens, the project is no longer just a collection of parallel tasks. It becomes a coordinated system.

A small example might look like this:

QueueHandle_t sensorQueue;

void SensorTask(void *pvParameters)
{
    int value;

    for (;;)
    {
        value = read_sensor();
        xQueueSend(sensorQueue, &value, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void PrintTask(void *pvParameters)
{
    int received;

    for (;;)
    {
        if (xQueueReceive(sensorQueue, &received, portMAX_DELAY) == pdPASS)
        {
            print_value(received);
        }
    }
}

Now the anatomy includes not just tasks and scheduler behavior, but also a communication path between tasks.

Hooks and fault handling

A simple FreeRTOS project may also include hook functions depending on configuration. These can include things like malloc failure handling or stack overflow handling. This is important because real projects need a way to respond when something goes wrong.

FreeRTOS troubleshooting guidance highlights malloc failure and insufficient heap as common reasons a project may fail to start or behave unexpectedly.

This means defensive structure is part of project anatomy too. A simple project is not complete if it has tasks but no way to expose stack or memory failures during development.

A realistic simple FreeRTOS file structure

A beginner-friendly simple project often ends up with a structure roughly like this:

main.c for board setup, task creation, and scheduler start.

FreeRTOSConfig.h for kernel configuration.

One or more application source files for tasks.

The FreeRTOS kernel source files.

The architecture-specific port files.

A heap implementation file if dynamic allocation is used.

Even if the project is tiny, these files reflect a very important split between responsibilities:

Board and hardware initialization.

Kernel configuration.

Kernel implementation.

Application logic.

That separation is a big part of what makes RTOS-based projects easier to scale than giant bare-metal superloops.

How a simple FreeRTOS project differs from a bare-metal one

The clearest contrast is this:

A bare-metal project usually has one main control loop and interrupts.

A simple FreeRTOS project has startup code, task creation, scheduler startup, and multiple schedulable execution contexts.

In a bare-metal design, the main loop is the center.

In a FreeRTOS design, the scheduler is the center.

That is the biggest mental shift. The project anatomy changes because the system is no longer organized around one forever loop. It is organized around tasks and kernel services.

Common beginner misunderstandings

A common misunderstanding is thinking that main() keeps running normally after the scheduler starts. In a typical project, it does not. The scheduler takes over execution control.

Another is assuming task creation is free. It is not. Task creation consumes RAM for stacks and control structures, and dynamic creation depends on the FreeRTOS heap.

Another is assuming a simple RTOS project no longer needs normal embedded setup. It still does. Clocks, interrupts, board initialization, and memory layout all still matter.

Another is forgetting that system-created tasks such as the idle task also need resources.

These misunderstandings usually disappear once you see the project as a set of layers rather than a single code file.

A practical way to think about it

A good way to understand the anatomy is to imagine a simple FreeRTOS project as having three phases.

First, the normal embedded startup phase. This gets the chip and runtime ready.

Second, the application setup phase inside main(). This creates tasks, queues, and any other kernel objects.

Third, the scheduler-driven runtime phase. This is where tasks, delays, priorities, queues, and the idle task define actual behavior.

If you keep those three phases separate in your mind, FreeRTOS projects become much easier to reason about.

Conclusion

A simple FreeRTOS project is built from familiar embedded pieces, but arranged around a different execution model. Startup code and hardware initialization still come first. Then main() creates tasks and other kernel objects. Then vTaskStartScheduler() hands control to the kernel, which manages task execution, timing, and system behavior from that point onward.

The key structural pieces are the kernel files, FreeRTOSConfig.h, memory management choice, task definitions, scheduler start, and any communication objects such as queues. The idle task is automatically part of the system, and software timers add a timer service task when enabled.

Share

You may also like