Home FreeRTOS Creating Your First Task on ARM with FreeRTOS

Creating Your First Task on ARM with FreeRTOS

by shedboy71

Creating your first task is the moment a FreeRTOS project starts to feel different from bare-metal programming. In a bare-metal design, main() often contains the main loop and directly controls what happens forever. In a FreeRTOS project, main() usually becomes the place where hardware is initialized, tasks are created, and the scheduler is started.

After that, the kernel decides which task runs and when. FreeRTOS documents this flow clearly: tasks are created with xTaskCreate() or xTaskCreateStatic(), and once vTaskStartScheduler() is called, the scheduler takes control of execution.

That shift is important because a task is not just an ordinary function. A task is a schedulable unit of execution with its own stack, its own priority, and its own lifetime inside the RTOS. When you create your first task on an ARM microcontroller, you are not merely calling a function later. You are registering a task with the kernel so the scheduler can manage it.

This tutorial explains the structure of a first task, how to create it, what each parameter means, what main() usually looks like in a beginner project, and what common mistakes to avoid on ARM boards running FreeRTOS.

What a task really is

A task in FreeRTOS is a function that the kernel schedules. It is written in C like an ordinary function, but it is not used like a one-time helper function. Instead, it is expected to remain alive for as long as the application needs it, usually by running inside an infinite loop.

A basic task function has this shape:

void MyTask(void *pvParameters)
{
    for (;;)
    {
        /* Task work goes here */
    }
}

This pattern exists because the scheduler may switch in and out of the task many times. The task should not simply run once and return. If it returns unexpectedly, that is usually a bug in a beginner project.

FreeRTOS also specifies the standard task function form: a task function returns void and accepts a single void * parameter. That parameter can be used to pass information into the task at creation time, although many first examples simply use NULL.

The simplest mental model

A very simple way to think about your first FreeRTOS task is this:

You write a task function.

You ask FreeRTOS to create that task.

You start the scheduler.

The scheduler then runs the task according to its priority and state.

That is the core flow. Everything else builds on top of it.

The basic API: xTaskCreate

The most common beginner API for creating a task is xTaskCreate(). FreeRTOS documents this as the dynamic task-creation function. When you use it, the memory needed for the task control structures and task stack is allocated from the FreeRTOS heap.

A typical call looks like this:

xTaskCreate(
    MyTask,         /* Task function */
    "MyTask",       /* Task name */
    256,            /* Stack depth */
    NULL,           /* Task parameter */
    1,              /* Priority */
    NULL            /* Optional task handle */
);

This is the call that most beginners see first, and it is worth understanding every argument clearly.

Breaking down the parameters

Task function

The first argument is the function that implements the task:

MyTask

This tells the kernel which function should be run when the task is scheduled.

Task name

The second argument is a text name for the task:

"MyTask"

This is mainly useful for debugging and trace output. It does not control behavior, but it makes the system easier to inspect.

Stack depth

The third argument is the task stack size. This is one of the most misunderstood parts for beginners.

256

This value is not a general “memory amount” in bytes in the way many people first assume. It is the task stack depth measured in stack units used by the port type. In practice, beginners should treat it as a stack-sizing parameter that may need tuning based on what the task actually does. If the task uses lots of local variables, formatting functions, or nested calls, it may need more stack. FreeRTOS’s task creation documentation makes clear that each task needs its own stack, and this parameter controls that allocation.

Task parameter

The fourth argument is a pointer passed into the task function:

NULL

If you do not need to pass data into the task, NULL is fine. Later, you may use this to pass configuration structures, strings, device handles, or other context data.

Priority

The fifth argument is the task priority:

1

Higher numeric values mean higher priorities. For a first task, a small value such as 1 is common. Priorities matter because when multiple tasks are ready, the scheduler chooses the highest-priority ready task to run.

Task handle

The final argument is an optional handle:

NULL

If you do not need to refer to the task later, NULL is fine. If you want to suspend, resume, notify, or otherwise manage the task later, you can pass the address of a TaskHandle_t variable instead. FreeRTOS documents task handles as the reference type used to identify tasks.

Your first complete task example

Here is a very simple beginner example for an ARM board using FreeRTOS. The task toggles an LED and delays between changes.

#include "FreeRTOS.h"
#include "task.h"

void LedTask(void *pvParameters)
{
    (void) pvParameters;

    for (;;)
    {
        toggle_led();
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

int main(void)
{
    hardware_init();

    xTaskCreate(
        LedTask,
        "LED",
        256,
        NULL,
        1,
        NULL
    );

    vTaskStartScheduler();

    for (;;)
    {
    }
}

This example shows the standard anatomy of a first FreeRTOS application.

hardware_init() represents your board setup, such as clocks, GPIO, and UART if needed.

xTaskCreate() creates the LED task.

vTaskStartScheduler() starts the RTOS.

The infinite loop after that is only a fallback. In a correctly running system, execution should not normally continue there because the scheduler should now be running the tasks. FreeRTOS explicitly documents that after vTaskStartScheduler() the RTOS kernel has control over which tasks execute and when. It also notes that the idle task is created automatically when the scheduler starts.

Why the task uses vTaskDelay

A first task should usually not sit in a tight infinite loop doing work constantly. That can waste CPU time and starve lower-priority tasks.

This is why vTaskDelay() is so useful in simple examples:

vTaskDelay(pdMS_TO_TICKS(500));

This blocks the task for a period of time, allowing other ready tasks to run. It also makes the task behavior predictable and easier to understand.

The pdMS_TO_TICKS() macro converts milliseconds into scheduler ticks, which is the correct way to express time delays in a portable FreeRTOS style.

What happens when the scheduler starts

Before vTaskStartScheduler() is called, your code is still running in ordinary startup mode. After it is called, the kernel begins scheduling tasks.

That means your first created task does not begin running at the instant xTaskCreate() is called. It becomes part of the set of tasks that the scheduler may run once the scheduler is active.

This is an important beginner detail. Creating a task and starting the scheduler are separate steps.

A first example with two tasks

Once one task works, the next natural step is to create two tasks.

#include "FreeRTOS.h"
#include "task.h"

void LedTask(void *pvParameters)
{
    (void) pvParameters;

    for (;;)
    {
        toggle_led();
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void PrintTask(void *pvParameters)
{
    (void) pvParameters;

    for (;;)
    {
        serial_print("FreeRTOS is running\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void)
{
    hardware_init();

    xTaskCreate(LedTask, "LED", 256, NULL, 1, NULL);
    xTaskCreate(PrintTask, "PRINT", 256, NULL, 1, NULL);

    vTaskStartScheduler();

    for (;;)
    {
    }
}

This example is useful because it shows that multiple tasks can coexist, each with its own loop and timing.

If both tasks have the same priority and both spend much of their time delayed, they can share the CPU quite naturally.

Passing data into your first task

Although many beginner examples use NULL, FreeRTOS lets you pass a pointer into the task when it is created.

For example:

#include "FreeRTOS.h"
#include "task.h"

static const char *taskMessage = "Task started\n";

void PrintTask(void *pvParameters)
{
    const char *message = (const char *) pvParameters;

    for (;;)
    {
        serial_print(message);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void)
{
    hardware_init();

    xTaskCreate(PrintTask, "PRINT", 256, (void *)taskMessage, 1, NULL);

    vTaskStartScheduler();

    for (;;)
    {
    }
}

This is a good first example of task parameters because it introduces the idea that tasks can be configured when they are created.

A common beginner safety rule is to pass pointers to data that will remain valid for the life of the task. Passing the address of a temporary local variable from main() can lead to trouble if the data does not remain valid.

Dynamic creation versus static creation

Your first task is often created with xTaskCreate(), which uses dynamic memory from the FreeRTOS heap. FreeRTOS also provides xTaskCreateStatic(), which allows the application to provide the memory for the task stack and task control block directly. This requires static allocation support to be enabled in FreeRTOSConfig.h.

For a first tutorial, xTaskCreate() is usually simpler because it involves fewer pieces. But it is important to know that static creation exists, especially for systems where memory allocation policy matters.

A minimal xTaskCreateStatic example

Here is the general idea:

#include "FreeRTOS.h"
#include "task.h"

static StackType_t ledTaskStack[256];
static StaticTask_t ledTaskBuffer;

void LedTask(void *pvParameters)
{
    (void) pvParameters;

    for (;;)
    {
        toggle_led();
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

int main(void)
{
    hardware_init();

    xTaskCreateStatic(
        LedTask,
        "LED",
        256,
        NULL,
        1,
        ledTaskStack,
        &ledTaskBuffer
    );

    vTaskStartScheduler();

    for (;;)
    {
    }
}

This version is slightly more advanced, but it helps show that FreeRTOS can create tasks either with kernel-managed heap allocation or with memory supplied by the application.

What must exist before your first task works

A beginner often focuses only on the task function, but several pieces must already be correct before the task actually runs properly.

Your startup code must work.

Your ARM port files must match the processor and compiler.

Your FreeRTOSConfig.h must be present and valid.

Your heap configuration must be suitable if using xTaskCreate().

Your board clock and basic hardware initialization must be correct.

If any of these are broken, task creation or scheduler startup may fail even if the task code itself looks fine. FreeRTOS troubleshooting guidance specifically notes that failures around scheduler startup are often related to insufficient heap or, on ARM Cortex-M, incorrect interrupt priority configuration. ([freertos.org][7])

Why stack size matters so much

One of the most common problems when creating a first task is choosing an unrealistic stack size.

A task that only toggles an LED may need very little stack.

A task that uses formatted printing, large local buffers, or nested function calls may need much more.

If the stack is too small, the system may behave unpredictably or trigger stack overflow handling if that feature is enabled. So even in a first simple project, stack size is not a decorative parameter. It is part of making the task stable.

A good beginner habit is to start with a conservative value, keep task code simple, and later monitor stack usage if the project grows.

Why your first task should usually block

A very common bad beginner task looks like this:

void BadTask(void *pvParameters)
{
    for (;;)
    {
        toggle_led();
    }
}

This task never blocks, never delays, and never waits. If it has a high enough priority, it can consume CPU constantly.

A better first task is one that performs a small action and then blocks for a while:

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

This pattern teaches the right habit early: tasks should often wait when they have nothing useful to do.

A good first ARM board example

On an ARM board such as an STM32 Nucleo, LPCXpresso board, or similar Cortex-M development board, a strong first task example is usually one of these:

  • A blinking LED task.
  • A serial print task.
  • A button-reading task that posts an event.
  • A queue-based sender task later on.

These are good first tasks because they are easy to observe and easy to debug. If the LED blinks or the serial message appears, you know the task is running.

Common beginner mistakes

One common mistake is forgetting that the task function must match the expected FreeRTOS signature.

Another is forgetting to call vTaskStartScheduler(). The task may be created successfully, but it will not run until the scheduler starts.

Another is using too little heap memory when relying on xTaskCreate(). Since dynamic task creation allocates from the FreeRTOS heap, insufficient heap can cause task creation or scheduler startup problems.

Another is giving the task too small a stack.

Another is creating a task that never blocks, which can make the system appear stuck or unfair.

Another is assuming code after vTaskStartScheduler() will run normally. In a functioning system, it usually should not.

Another is misconfiguring interrupt priorities on ARM Cortex-M systems. FreeRTOS explicitly warns that interrupt priority setup on Cortex-M is a frequent source of problems.

A useful beginner checklist

Before running your first task, check these points:

  • The task function has the correct form.
  • The task is created successfully.
  • The scheduler is started.
  • The stack size is reasonable.
  • The task blocks or delays rather than spinning forever.
  • The heap configuration supports task creation.
  • The board port and interrupt settings are correct for your ARM target.

This checklist solves many first-project problems.

A simple mental model

A helpful way to think about it is this:

  • main() prepares the system.
  • xTaskCreate() registers work with the kernel.
  • vTaskStartScheduler() turns the kernel on.
  • The task function becomes one execution context managed by the RTOS.

Once that model is clear, FreeRTOS stops feeling mysterious.

Conclusion

Creating your first task on ARM with FreeRTOS is really about understanding the shift from ordinary embedded flow to scheduler-driven execution. You define a task function, create it with xTaskCreate() or xTaskCreateStatic(), and then call vTaskStartScheduler() so the kernel can begin running tasks. The task itself is not just a helper function. It is a managed execution context with its own stack, priority, and lifetime inside the RTOS.

The most important beginner lessons are straightforward. Keep the task function simple. Use a sensible stack size. Let the task block when idle. Understand what each xTaskCreate() parameter means. Make sure the heap, config, and ARM port are set up correctly. Once that first task runs, the rest of FreeRTOS becomes much easier to build on.

Share

You may also like