Continuing from the previous article, we have already learned about the porting and startup process of FreeRTOS. Today, we will continue to study the task management part of FreeRTOS, starting with understanding what a task is.
1. Task
1.1 Introduction to Tasks
(1) In bare-metal systems, we generally use a front-and-back system for development. If you are not clear about what a front-and-back system is, let me explain: a front-and-back system refers to having an infinite loop in the main function, where the CPU executes program content sequentially, often in conjunction with several interrupts. I believe you have used this method before.

(2) In FreeRTOS, the overall functionality is divided into several independent infinite loop functions based on their functions, and each of these infinite loop functions is called a task.
1.2 Task SchedulingSince our CPU is a single-core processor, at any given moment, only one task can run. This is similar to our brain, which can only do one thing at a time. So who decides what to do? In the previous example, it is, of course, the brain that decides. In FreeRTOS, there is also a brain, which is the scheduler, and it decides which task to run. The scheduler quickly starts and stops each task, and due to the rapid switching, we perceive that multiple tasks are running simultaneously.
FreeRTOS is a semi-real-time operating system that supports a preemptive scheduling mechanism, so it always executes the highest priority task. High-priority tasks can interrupt tasks that are currently executing at lower priorities, while low-priority tasks can only be scheduled after high-priority tasks are blocked or completed. This is similar to the emperor’s harem, where everyone from the queen to the concubines can use the emperor, but according to the rules, no one can monopolize the emperor; they must be able to release the emperor actively, and the rank of the concubines corresponds to the priority of the tasks.
1.3 Task States (Ready / Running / Blocked / Suspended)
Task states are typically divided into the following four types:
Ready (ready): In the system, there is a ready list that stores all tasks in the ready state. Tasks in the ready state are capable of execution and are just waiting for the scheduler to schedule them. Newly created tasks are in the ready state.
Running (running): This state indicates that the task is currently executing, and at this time, the task occupies the CPU. The FreeRTOS scheduler always selects the highest priority task from the ready state to run. When a task starts running, its state changes to ready.
Blocked (blocked): When a running task becomes blocked (due to delay, waiting for a semaphore, reading/writing a queue, or waiting for an event), it will be removed from the ready list, and its state changes from running to blocked.
Suspended (suspended): A task in the suspended state is invisible to the scheduler. The only way to put a task into the suspended state is to call the vTaskSuspend() function; the only way to resume it is to call the vTaskResume() or vTaskResumeFromISR() functions.
The difference between suspended and blocked states:
A task in the suspended state does not participate in task scheduling and can only participate in scheduling again after being resumed. In contrast, a task in the blocked state requires the system to determine whether the blocking time has elapsed or whether the awaited semaphore or event has occurred.

The conversion relationships between them are shown in the figure below:
(1): Create task → Ready (ready): After the task is created, it enters the ready state, indicating that the task is ready to run and is just waiting for the scheduler to schedule it.
(2): Ready → Running (running): When a task switch occurs, the highest priority task in the ready list is executed, thus entering the running state.
(3): Running → Ready: When a higher priority task is created or resumed, a task scheduling occurs, at which point the highest priority task in the ready list becomes the running task, and the previously running task changes from running to ready, remaining in the ready list, waiting for the highest priority task to finish running before continuing to run the original task (this can be seen as the CPU usage being preempted by a higher priority task).
(4): Running → Blocked (blocked): When a running task becomes blocked (due to delay, waiting for a semaphore), it will be removed from the ready list, and its state changes from running to blocked, followed by a task switch to run the current highest priority task in the ready list.
(5): Blocked → Ready: When a blocked task is resumed (task resumed, delay time expired, semaphore timeout, or signal received), the resumed task will be added to the ready list, changing its state from blocked to ready; if the resumed task’s priority is higher than the currently running task’s priority, a task switch will occur, changing the task’s state from ready to running.
(6) (7) (8): Ready, Blocked, Running → Suspended (suspended): A task can be suspended by calling the vTaskSuspend() API function, which can suspend tasks in any state. A suspended task does not get CPU usage and will not participate in scheduling unless it is resumed.
(9): Suspended → Ready: The only way to resume a suspended task is to call the vTaskResume() or vTaskResumeFromISR() API function. If the resumed task’s priority is higher than the currently running task’s priority, a task switch will occur, changing the task’s state from ready to running.
1.4 Idle Task
The idle task is the lowest priority task created when the scheduler starts, primarily responsible for cleaning up system memory. If there are no other tasks to run, the CPU will run the idle task.
void vTaskStartScheduler( void ){BaseType_t xReturn;/* Add the idle task at the lowest priority. *//* The Idle task is being created using dynamically allocated RAM. */ xReturn = xTaskCreate( prvIdleTask, configIDLE_TASK_NAME, configMINIMAL_STACK_SIZE, ( void * ) NULL, portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */ &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */}
The above code creates the idle task when starting task scheduling.
2. Creating Two Tasks
2.1 Defining Dynamic Memory Heap
Using dynamic memory, i.e., heap, occupies the RAM space of the microcontroller. FreeRTOS defines a large array in SRAM as the heap memory for dynamic memory allocation functions. When used for the first time, the system initializes the defined heap memory, and the related configuration is in the previously mentioned FreeRTOSConfig.h file.
// Total heap size for the system#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) (1)static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; (2)(1) The size of the heap memory is defined by the user in FreeRTOSConfig.h as configTOTAL_HEAP_SIZE. The macro configSUPPORT_DYNAMIC_ALLOCATION must be enabled when using the FreeRTOS operating system. (2) A static array ucHeap is defined in the internal SRAM, with the size determined by the macro configTOTAL_HEAP_SIZE, currently set to 17KB.
2.2 Defining Task Functions
Two task functions are created to make the LED blink at frequencies of 500ms and 1000ms, respectively. The specific implementation is shown in the code listing:
static void LED1_Task(void* parameter) { while (1) { LED1_ON; vTaskDelay(500); /* Delay for 500 ticks */ LED1_OFF; vTaskDelay(500); /* Delay for 500 ticks */ } } static void LED2_Task(void* parameter) { while (1) { (1) LED2_ON; vTaskDelay(1000); /* Delay for 1000 ticks */ (2) LED2_OFF; vTaskDelay(1000); /* Delay for 1000 ticks */ }}
(1): Tasks must be in an infinite loop.
(2): Each task must have an action to actively release the CPU (by calling a relative or absolute delay, etc.). When the program calls the vTaskDelay() function, the current task enters a blocked state, and the scheduler will execute the highest priority task in the ready list, thus achieving multitasking.
2.3 Defining Task Control Block Pointers
The Task Control Block (TCB) is allocated memory space when the task is created. The task creation function returns a pointer to the task control block, so a task control block pointer (task handle) must be defined in advance for the task stack. The task handle is like the “ID card” of the task, and many subsequent operations are based on the task handle. The data type of the task handle is TaskHandle_t, defined in task.h, which is essentially a null pointer. The specific implementation is shown in the code listing:
/* Task handle */ typedef void * TaskHandle_t; // Define task handle: static TaskHandle_t LED1_Task_Handle = NULL; /* LED1 task handle */ static TaskHandle_t LED2_Task_Handle = NULL; /* LED2 task handle */
2.4 Dynamically Creating Tasks with xTaskCreate()
When using dynamic memory, the xTaskCreate() function is used to create a task.
/* Create LED1_Task task */ xReturn = xTaskCreate((TaskFunction_t )LED1_Task, /* Task entry function */ (1) (const char* )"LED1_Task", /* Task name */ (2) (uint16_t )128, /* Task stack size */ (3) (void* )NULL, /* Task entry function parameter */ (4) (UBaseType_t )1, /* Task priority */ (5) (TaskHandle_t* )&LED1_Task_Handle); /* Task control block pointer (6)
(1): The task entry function is the name of the task function.
(2): The task name is a string, with a maximum length defined by the configMAX_TASK_NAME_LEN macro in FreeRTOSConfig.h. Excess characters will be automatically truncated. It is best for the task name to match the task function entry name for easier debugging.
(3): The task stack size is measured in words. In a 32-bit processor, one word equals 4 bytes, so the task size is 128*4 = 512 bytes. You can query the task stack usage through corresponding interface functions to determine the required stack size for the task. (This knowledge point is often asked in interviews!!!!!)
(4): The task entry function parameter can be set to 0 or NULL if not used.
(5): The task priority. The priority range is determined by the configMAX_PRIORITIES macro in FreeRTOSConfig.h: in FreeRTOS, the higher the number, the higher the priority. 0 represents the lowest priority, generally assigned to the idle task.
(6): The task control block pointer. When using dynamic memory, the task creation function xTaskCreate() will return a pointer to the task control block, which is a block of memory dynamically allocated inside the xTaskCreate() function.
Once the task is created, it is in the ready state, and tasks in the ready state can participate in the operating system’s scheduling.
2.5 Statically Creating Tasks with xTaskCreateStatic()
Static task creation uses the xTaskCreateStatic() interface function, which requires specifying stack space, meaning you need to allocate an array yourself.
TaskHandle_t LED1_Task_Handle;#define LED2_Task_STACK_SIZE 128 StackType_t LED2TaskStack[LED2_Task_STACK_SIZE]; /* Create LED2_Task task */ xReturn = xTaskCreate((TaskFunction_t )LED2_Task, /* Task entry function */ (1) (const char* )"LED2_Task", /* Task name */ (2) (uint16_t )128, /* Task stack size */ (3) (void* )NULL, /* Task entry function parameter */ (4) (UBaseType_t )2, /* Task priority */ (5)(StackType_t *)LED2TaskStack, /* Task static stack address */ (6) (TaskHandle_t* )&LED2_Task_Handle); /* Task control block pointer (7)
(1): The task entry function is the name of the task function.
(2): The task name is a string, with a maximum length defined by the configMAX_TASK_NAME_LEN macro in FreeRTOSConfig.h. Excess characters will be automatically truncated. It is best for the task name to match the task function entry name for easier debugging.
(3): The task stack size is measured in words. In a 32-bit processor, one word equals 4 bytes, so the task size is 128*4 = 512 bytes. You can query the task stack usage through corresponding interface functions to determine the required stack size for the task. (This knowledge point is often asked in interviews!!!!!)
(4): The task entry function parameter can be set to 0 or NULL if not used.
(5): The task priority. The priority range is determined by the configMAX_PRIORITIES macro in FreeRTOSConfig.h: in FreeRTOS, the higher the number, the higher the priority. 0 represents the lowest priority, generally assigned to the idle task.
(6): The task stack is generally an array. The array type must be StackType_t.
(7): The task control block pointer. Once the task is created, it is in the ready state, and tasks in the ready state can participate in the operating system’s scheduling.

2.6 Starting Tasks with vTaskStartScheduler()
After creating tasks, we need to start the scheduler because creating tasks only adds them to the system; they are not actually scheduled yet, and the idle task has not been implemented, nor has the timer task. These are all implemented in the scheduler startup function vTaskStartScheduler().
/* Step 3: Start task scheduling */ if (pdPASS == xReturn) vTaskStartScheduler(); /* Start tasks, begin scheduling */ else return -1; while (1); /* Normally, this will not be executed */
The task scheduler only starts once and will not execute again. From this point on, task management is handled by FreeRTOS, and we truly enter a real-time operating system.
3. Common Task Functions
3.1 Suspending Tasks with vTaskSuspend()
Tasks can be suspended by calling the vTaskSuspend() function, which can suspend tasks in any state. A suspended task does not get CPU usage and will not participate in scheduling; it is invisible to the scheduler unless it is resumed.
Note: A task can call the vTaskSuspend() function to suspend itself, but doing so will involve a task context switch. To suspend itself, simply pass NULL for the xTaskToSuspend parameter.
Any task can be suspended regardless of its state, as long as the vTaskSuspend() function is called.
Code example:
vTaskSuspend(LED_Task_Handle); /* Suspend LED task */
3.2 Resuming Tasks with vTaskResume()
Resuming a task means bringing a suspended task back into the ready state. The resumed task retains its state information from before suspension and continues running based on its state at the time of suspension.
If the resumed task is the highest priority task among all ready tasks, the system will perform a task context switch. Task context switching refers to task scheduling.
vTaskResume(LED_Task_Handle); /* Resume LED task! */
3.3 Deleting Tasks with vTaskDelete()
vTaskDelete() is used to delete a task.
(1) When one task deletes another task, the parameter is the task handle returned when the task was created, which is the “ID card” we mentioned earlier;
(2) If deleting itself, the parameter should be NULL.
Code example:
vTaskDelete(DeleteHandle);
The deleted task will be removed from all ready, blocked, and suspended tasks.
3.4 Blocking Delay Functions
3.4.1 Relative Delay Function vTaskDelay()
Blocking delay means that after a task calls this delay function, it will be stripped of CPU usage and enter a blocked state until the delay ends, at which point the task can regain CPU usage and continue running. During the time the task is blocked, the CPU can execute other tasks. If other tasks are also in a delay state, the CPU will run the idle task. The duration of the delay is determined by the parameter xTicksToDelay, measured in system tick periods. For example, if the system clock tick period is 1ms, calling vTaskDelay(1) will result in a delay of 1ms.
vTaskDelay() is a relative delay, meaning the specified delay time is calculated from the end of the vTaskDelay() call. For example, vTaskDelay(100) means that after the vTaskDelay() call ends, the task enters a blocked state, and after 100 system clock tick periods, the task is unblocked. Therefore, vTaskDelay() is not suitable for periodic tasks. Additionally, other tasks and interrupt activities can also affect the call to vTaskDelay() (for example, if a higher priority task preempts the current task before the call), thus affecting the next execution time of the task. Task delays are particularly common in practice because they require pausing a task to yield the CPU, and after the delay ends, the task continues running. If there are no blocks in the task, tasks with lower priority than that task will not be able to use the CPU and will not run. This description is something to pay attention to during development.
Code example:
static void LED1_Task(void* parameter) { while (1) { LED1_ON; vTaskDelay(500); /* Delay for 500 ticks */ LED1_OFF; vTaskDelay(500); /* Delay for 500 ticks */ } }
3.4.2 Absolute Delay Function vTaskDelayUntil()
In FreeRTOS, in addition to the relative delay function, there is also the absolute delay function vTaskDelayUntil(). This absolute delay is commonly used for tasks that need to run at a fixed frequency, unaffected by external factors. The time interval from the last run of a task to the next run is absolute, not relative.
Code example:
void vTask( void * pvParameters ) { /* Variable to save the last time. The system automatically updates after calling */ static portTickType PreviousWakeTime; /* Set delay time, converting to tick count */ const portTickType TimeIncrement = pdMS_TO_TICKS(5); /* Get current system time */ PreviousWakeTime = xTaskGetTickCount(); while (1) { /* Call absolute delay function, task time interval is 5 ticks */ vTaskDelayUntil( &PreviousWakeTime, TimeIncrement ); // Other task functions }}
Note: When using, the delay time must be converted to system ticks, and the delay function should be called before the task body.
The task will first call vTaskDelayUntil() to enter the blocked state, and when the time is up, it will be unblocked and execute the body code. After the task body code is executed, it will call vTaskDelayUntil() again to enter the blocked state, and this cycle will continue.
The difference: Simply put, relative delay is calculated from the start of the delay, while absolute delay is calculated from the start of task execution. Even if the task is interrupted during execution, it will not affect the task’s running period; it will only shorten the blocking time, and the task will still be awakened at the scheduled time.

Follow me for more content in the future.

