Master The Pico WiFi: Installing FreeRTOS
Written by Harry Fairhead and Mike James   
Monday, 01 September 2025
Article Index
Master The Pico WiFi: Installing FreeRTOS
Scheduling and Tasks
The Standard Tasks

Scheduling and Tasks

FreeRTOS works in terms of tasks. A task is a function that can be run as if it was a “main” program in its own right. That is, a task is like a function call, but it doesn’t block its creator until it has finished. Tasks never return and are generally written as infinite loops. Tasks can be destroyed via FreeRTOS. Creating a FreeRTOS program is all about creating and managing tasks.

The basic FreeRTOS call to create a task is:

xTaskCreate(pTaskFunction, pName, StackDepth,
pParameters, Priority, pTaskHandle)

Its parameters are:

  • pTaskFunction Function to run as the task

  • pName Name used to identify the task to the programmer

  • StackDepth Stack size in bytes

  • pParameters Pointer to parameters to be passed to the task

  • Priority Scheduling priority of the task

  • pTaskHandle Pointer to a task handle for managing the task

The function that implements a task looks like an interrupt handler, for example:

void TaskFunction(void *arg)

The stack size should be set to be large enough to store all of the local variables that are created by the task or by any functions it calls. You can find out how close you are to running out of stack memory using uxTaskGetStackHighWaterMark, which reports the smallest free stack space since the task started running.

Memory Management

The memory needed for the task is allocated by FreeRTOS. If you want to control this then you can use xTaskCreateStatic() and supply pointers to memory to be used by FreeRTOS. In most situations you don’t need to do this.

FreeRTOS maintains a heap to allocate memory dynamically and you have to select what degree of memory management you want to use. At the time of writing it supports five levels of heap management:

heap 1  The very simplest, does not permit memory to be freed

heap 2  Permits memory to be freed, but does not coalesce adjacent free blocks

heap 3  Simply wraps the standard malloc() and free() for thread safety

heap 4  Coalesces adjacent free blocks to avoid fragmentation and includes absolute address placement option

heap 5  As per heap 4 with the ability to span the heap across multiple non-adjacent memory areas

Heap 1 can be avoided by simply using static task creation and not using the heap at all. Heap 2 is more or less superseded by heap 4 which does everything it does plus rearranging memory to produce larger contiguous free areas. Heap 5 is more sophisticated but it is slower and uses more program memory. Overall, heap 4 is the best compromise and it is used by all of the examples in this and subsequent chapters. Heap 4 is selected within the CmakeLists.txt file:

FreeRTOS-Kernel-Heap4

Core Affinity

By default FreeRTOS uses both cores. The Kernel and a timer task run on core 0 and core 1 is free for you to use. A task that you create will run on either core and can even swap which core it is running on. In the jargon, the task is said to have no core affinity.

To assign a task to a particular core we can use:

void vTaskCoreAffinitySet( 
const TaskHandle_t pTaskFunction, UBaseType_t uxCoreAffinityMask )

where the uxCoreAffinityMask has a bit for each core. For example, to set a task to use only core 1:

uxCoreAffinityMask = 1 << 1;  
vTaskCoreAffinitySet( pTaskFunctionpTaskFunction,
                                 uxCoreAffinityMask );

You can set more than one core in the mask and then FreeRTOS will use any of the assigned cores to run the task as and when they become available. There is also a vTaskCoreAffinityGet function. For these functions to work, configUSE_CORE_AFFINITY has to be set to 1 in the FreeRTOSConfig.h file, which it is by default in the supplied file.

Notice that you can start tasks in the main program before starting the scheduler and you can start new tasks from within running tasks. In this case there is no need to start or stop the scheduler as the task will be started or queued for starting when the task that created it is finished.

masterPicoE2180

The Task Queue

Not all of the tasks you create can be running at any given time. If you only have two cores, like the Pico, then at most two tasks can be running.

A task can be in one of four states: Running, Ready (to run), Blocked or Suspended. The difference between Blocked and Suspended is that a task that is Blocked is waiting on something that the system can supply, such as the time being up for a task that has called vTaskDelay. The system can change the status of a task from Blocked to Running on its own. A task can change its state to Suspended by calling vTaskDelay and also becomes Suspended by another task calling vTaskSuspend in which case it can only be returned to the Ready state by another task calling vTaskResume.

On a single-core machine there can be only one Running task rather than two on a dual-core machine. Tasks are stored in a list which the scheduler has access to. The system is configured so that every portTICK_PERIOD_MS milliseconds, 1ms by default, there is a timer interrupt that runs the scheduler. This causes the currently running task to change state to Ready and the scheduler examines the list of Ready task and runs the one with the highest priority. If there are multiple tasks with the same priority then they each get their turn to run in a round-robin fashion.

This is a very simple scheduler, but there are a few things to notice. The first is that a task doesn’t have any choice about giving up control if the system selects another task to run. That is, FreeRTOS is a priority-based preemptive scheduler. Also notice that if there are tasks that are ready to run with a higher priority, then lower-priority tasks don’t get a look in.

You can protect a task against preemption using:

  • void vTaskPreemptionDisable(const TaskHandle_t
                                         pTaskFunction)
  • void vTaskPreemptionEnable(const TaskHandle_t
                                         pTaskFunction)

For these to be available, configUSE_TASK_PREEMPTION_DISABLE must be defined as 1, which it is by default.

So how do lower-priority tasks ever get to run? The answer is that tasks, irrespective of priority, are not always in a Ready state. If a task is waiting for input, then it will be Blocked and hence not ready to run. If a task has suspended itself using a vTaskDelay(t / portTICK_PERIOD_MS) then it is not ready to run until the time is up. If it has been suspended by itself or another task then it will not be ready to run until another task causes it to resume. At the time of writing the tick period for the Pico is 1ms.

For all these reasons, it may well be that there are no tasks of a given priority in the Ready state. In this case the scheduler looks for the highest-priority task that is ready to run.

To summarize:

  • Every task has a fixed priority assigned when it is created.

  • The scheduler gets to run whenever the current task leaves the running state, either because it is suspended or is blocked. If this doesn’t happen for portTICK_PERIOD_MS milliseconds, 1ms in the case of the Pico, then the running task is interrupted and the scheduler runs.

  • When the scheduler runs, it first examines all tasks suspended for a time and if that time is up they are marked as Ready.

  • The scheduler then looks for the task in the Ready state with the highest priority. If there is more than one then the tasks are run in turn, i.e. in round-robin fashion.

This is a very simple scheduling algorithm and has the advantage that you can mostly work out what is going to happen. However, the picture is slightly complicated by the fact that tasks can have core affinities. If two high-priority tasks both want to run on the same core, then one of them will run and the other will have to wait while a lower-priority task runs on the other core. Similarly, round-robin selection among tasks of equal priority also has to take account of the tasks’ core affinities.



Last Updated ( Monday, 01 September 2025 )