This is the fourth post in my FreeRTOS series. I have introduced FreeRTOS and how to set it up on previous three parts, so now it is time to actually start exploring the FreeRTOS task(s). To recap here is a quote from part 1:
Applications are typically divided into tasks, where each is responsible for a specific function.
What are tasks really?
So let us start digging deeper this time, and expand what tasks really are.
A task is an independent function that FreeRTOS schedules and executes in its context. There are no dependencies to any other tasks or the scheduler for that matter, as functions do not know, or care, what runs them. Part 1 also described that a task can be either in “Running” or “Not Running” states. This is because only one task can be running at one time (FreeRTOS is generally run on single core targets) and it is up to the scheduler to decide which it is. Thus it is possible that the scheduler repeatedly starts and stops a task. 1
Here are the API functions for creating a task:
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE uxStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask
);
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer );
During this blog post I will refer back to these. The main thing to know now is that xTaskCreate allocates memory dynamically while xTaskCreateStatic uses static allocation from RAM that the user provides via puxStackBuffer and pxTaskBuffer parameters. Both of them also give out a handle, either by out parameter (xTaskCreate), or as return value (xTaskCreateStatic). This handle can later be used to modify the behavior of the task.
States
Now it is time to ditch the “Running” or “Not Running” dichotomy this series has been using so far and start using correct terms. A task can be in one of four mutually exclusive states depending on the state of execution: Running, Ready, Blocked, or Suspended. As running has already been explained:
When a task is “Running”, the processor is executing the code inside the task.
Let us focus more on the other three states, starting from Blocked, moving to Suspended and ending with Ready.
When a task is Blocked it is waiting for an event due to an API function call. In FreeRTOS there are two types of events, external (or synchronization) and temporal. External events come from queues, semaphores, event groups, notifications etc., while temporal events move tasks to Blocked state until time limit has been reached. Call to vTaskDelay (a blocking API function) is completely temporal as the task is Blocked until timeout, while call to xQueueReceive (another type of a blocking API function) can be both as Blocked state can expire due to the item arriving in the queue OR timeout has been reached (if parameter other than portMAX_DELAY was used when calling xQueueReceive).
Tasks that are Blocked do not use CPU time and cannot directly go to Running state. Same can be said about tasks in Suspended state, which is entered when a call to vTaskSuspend is made. A task can suspend itself, causing it to go to Suspended state, but there is no way for the task to move out of this state on its own as the call to vTaskResume must be done from an external source, such as another task.
Finally the Ready state. A task in this state is eligible for execution. For the task to return from Blocked or Suspended state to Running state it must first be in Ready state so that the scheduler can select it for execution. What determines when the selection happens depends on two things: priorities and scheduling. 2 3
Timeouts and portMAX_DELAY
Blocking API functions like vTaskDelay and xQueueReceive take their timeout in ticks, not milliseconds. One tick is one period of the FreeRTOS system clock, set via configTICK_RATE_HZ in FreeRTOSConfig.h, so the real-time duration of a tick depends on configuration. To pass a millisecond value, use the pdMS_TO_TICKS macro:
// Block for up to 100 ms
xQueueReceive(queue_handle, &item, pdMS_TO_TICKS(100));
The special value portMAX_DELAY means “wait indefinitely”. A task blocked with portMAX_DELAY resumes only when the event it is waiting on actually happens, with no timeout. For this to actually block forever rather than for a very long but finite tick count, INCLUDE_vTaskSuspend must be set to 1 in FreeRTOSConfig.h. When enabled, portMAX_DELAY is treated as an infinite wait internally rather than as the maximum representable tick count.
State transition table
| From | To | Cause |
|---|---|---|
| Ready | Suspended | vTaskSuspend() called |
| Suspended | Ready | vTaskResume() called |
| Ready | Running | Scheduler selects the task |
| Running | Ready | Preempted / time slice ends |
| Running | Blocked | Blocking API function called |
| Blocked | Ready | Event occurs |
Priorities
It is now known that tasks are waiting in “Ready” state to be picked up by the scheduler so that they can start “Running”. Once again, let us start with a quote from part 1 before diving deeper:
…tasks with higher priority take precedence over lower priority ones…
When a task is created with either of these functions it is given a priority as a uxPriority parameter. The highest priority a task can have is one less than configMAX_PRIORITIES (defined in FreeRTOSConfig.h), since priorities are zero-indexed, like arrays. The lowest priority is zero.
For the sake of example, if configMAX_PRIORITIES is defined as 7, the valid priority range is 0 (lowest) to 6 (highest). When looking at the source code from FreeRTOS’s tasks.c file, the reason for zero-index becomes even more obvious as configMAX_PRIORITIES is used to create an array:
/* Lists for ready and blocked tasks. --------------------
* xDelayedTaskList1 and xDelayedTaskList2 could be moved to function scope but
* doing so breaks some kernel aware debuggers and debuggers that rely on removing
* the static qualifier. */
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; /**< Prioritised ready tasks. */
When using the generic non architecture-optimized scheduler there is no upper limit to configMAX_PRIORITIES, however the more priorities there are, the more memory is required, since the value determines, for example, the size of the list above. There can also be an increase in the maximum length of time a lower priority task could take to execute.
The architecture-optimized scheduler takes a different trade-off. Enabled with configUSE_PORT_OPTIMISED_TASK_SELECTION on supported ports (such as ARM Cortex-M3 and above), it tracks the ready priorities in a single 32-bit word and uses the CLZ (count leading zeros) instruction to pick the highest-priority ready task in constant time. That bitmap width caps configMAX_PRIORITIES at 32, regardless of how much memory is available. The cap is worth knowing about for the bare-metal Cortex-M7 setup this series builds on, since the optimized path is the one most readers will end up using. 4
Thus, instead of having a different priority value for each individual task, it is possible to share a priority, as once again, there is no limit on tasks having the same priority.
One last thing before moving on to scheduling: it is also possible to change a task’s priority after it has been created:
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );
This function is available after INCLUDE_vTaskPrioritySet has been defined in FreeRTOSConfig.h. If the uxNewPriority is higher than the priority of the task that made the function call to vTaskPrioritySet, then the task whose priority was promoted will start executing instantly. 5 6
Scheduling
Since the highest priority task is always selected to run first, one might wonder what happens when two or more tasks with the same high priority are both in “Ready” state. Well, the answer is again that the scheduler decides. To be more precise, there is a scheduling algorithm that determines the selection process and the execution time given to each of the high priority tasks. This is known as scheduling behavior and let us go through key concepts:
- Time slicing lets the scheduler switch between equal-priority tasks on each tick interrupt.
- One tick equals one time slice.
- Preemptive scheduling makes the highest-priority ready task always run, interrupting any lower-priority task immediately, even mid-time-slice.
- Fixed priority means that the scheduler never permanently changes a task’s priority.
- Context switching is switching from one task to another:
- The scheduler performs this at different stages of execution depending on configuration.
- Tasks perform them when context switch triggers are hit:
- calling a blocking API function, such as
vTaskDelay - suspending with
vTaskSuspend() - explicitly calling
taskYIELD()
- calling a blocking API function, such as
The “context” being switched is the snapshot of what the CPU was doing when a task last ran: its CPU registers, stack pointer, and program counter. When the scheduler swaps a task out, the register values are stored in the task’s own memory, and the stack pointer is recorded in the task’s Task Control Block. Swapping back in is the reverse and execution resumes exactly where it left off. The previous Cortex-M7 without hardware series covered how a stack frame and program counter drive a single thread of execution. The model here is the same, there are just multiple stacks now (one per task) and the scheduler decides which one is active. 7
On a single-core microcontroller, FreeRTOS uses fixed-priority scheduling by default and depending on the configuration it is either preemptive or non-preemptive, with possibility of time slicing. The algorithm can be configured with configUSE_PREEMPTION and configUSE_TIME_SLICING via FreeRTOSConfig.h.
This leaves the following configuration possibilities, note however that on cooperative scheduling the time slicing does not modify the behavior in any way, so there are actually three different algorithms. 8 9
configUSE_PREEMPTION | configUSE_TIME_SLICING | Scheduling behavior |
|---|---|---|
| 1 | 1 | Preemptive scheduling with time slicing |
| 1 | 0 | Preemptive scheduling without time slicing |
| 0 | 1 | Cooperative scheduling (time slicing has no effect) |
| 0 | 0 | Cooperative scheduling |
Preemptive scheduling with time slicing
Gives the most responsive system, as the scheduler always runs the highest-priority task that is able to run, and when other task(s) in “Ready” state share the same priority, it switches between them on every tick interrupt. A task with even higher priority becoming “Ready” will immediately take precedence over (preempt) whatever is currently running, even mid-time-slice.
Preemptive scheduling without time slicing
Same preempting as previously, so a task that has the highest priority will immediately take precedence, but tasks with the same priority will not be switched between ticks. Useful on power-sensitive or low-overhead systems, as the scheduler does less context switching. When designing an application with this configuration, equal-priority tasks must cooperate to share the processing time with context switch triggers.
Cooperative scheduling
Gives the most predictable system, as the scheduler never forcibly halts the execution of a running task and context switch triggers are used to switch between tasks. Interrupt Service Routines (ISRs) can still explicitly request a switch.
Memory
Here are a few paragraphs about a task’s memory, followed by a literal word of warning.
When a task is being created it requires two memory blocks, one for the Task Control Block (TCB) that contains the stored stack pointer and one for the stack. The stack is used for all the local objects such as variables and arrays that are created during the task’s lifetime. When using xTaskCreate the memory blocks for the TCB and stack are allocated from the heap, while calling xTaskCreateStatic requires two extra parameters pxTaskBuffer (area for TCB) and puxStackBuffer (area for stack), both of which are blocks of memory pre-allocated by the user.
So which function should be chosen? xTaskCreate is fine when heap usage is acceptable. For avoiding the heap completely, xTaskCreateStatic is the right choice, giving deterministic memory layout where the linker map shows exactly where each task’s memory lives. Do note that which of these is even available depends on FreeRTOSConfig.h: configSUPPORT_DYNAMIC_ALLOCATION (defaults to 1) enables xTaskCreate, and configSUPPORT_STATIC_ALLOCATION must be explicitly set to 1 to enable xTaskCreateStatic. Both can be enabled at the same time.
Both xTaskCreate (via uxStackDepth) and xTaskCreateStatic (via ulStackDepth) have a parameter that defines the size of the stack for a task. What now follows is that warning: depth values are words instead of bytes. The size of a depth word is defined by configSTACK_DEPTH_TYPE in FreeRTOSConfig.h and it defaults to StackType_t which is port specific, so on GCC Cortex-M7 port it is uint32_t which is 32-bits = 4 bytes. Therefore, depending on the configuration / port the stack depth of 512 could be 2048 bytes (when configSTACK_DEPTH_TYPE is 32-bits) or 1024 bytes (with 16-bits). 10 11
Stack overflow
Since the stack depth is pre-determined by the stack depth parameter, and no extra stack space is allocated during runtime even when using the non-static xTaskCreate version, stack overflows are a real and ever-present risk. The overflow happens when stack usage exceeds the depth, typically due to large local arrays or a deep function call nesting. It causes application instability and undefined behavior that is usually very time-consuming to debug.
Some processors offer hardware-level protection against stack corruption via an MPU or similar mechanism, generating a fault when a task accesses memory outside its stack. Independently of that, FreeRTOS provides a software-level “Stack Overflow Detection” that can be enabled in FreeRTOSConfig.h with configCHECK_FOR_STACK_OVERFLOW. Detection has more choices than just on/off, so accepted values are:
- 0, disables the detection completely
- 1, for checking that the stack pointer is still within the task’s allocated stack range on context switch.
- 2, for checking that the last 16 bytes of the stack still contain the original fill pattern on context switch.
- 3, for hardware-assisted ISR stack overflow detection that triggers an assert on overflow.
- only available for certain ports
When configCHECK_FOR_STACK_OVERFLOW is non-zero, the following callback function must be defined (values 1 and 2 call it; value 3 asserts directly):
void vApplicationStackOverflowHook( TaskHandle_t xTask,
char *pcTaskName )
{
// log the error
// halt or reset the system
}
FreeRTOS calls it upon stack overflow, you decide what happens next. 12
The Idle task
FreeRTOS always needs at least one task that can run, even when every task the application created is Blocked or Suspended. To guarantee this the kernel creates its own task, the Idle task, automatically when the scheduler starts. It is not something the application developer writes, just a simple loop with nothing moving it to “Blocked” state. Besides keeping a runnable task available at all times, it also reclaims the memory of tasks that have deleted themselves, which is covered later when we look at vTaskDelete. While always having a runnable task might sound like it prevents other tasks from being run, it is not the case, since the Idle task uses tskIDLE_PRIORITY which is always zero.
The Idle task is also where the Idle hook lives. When configUSE_IDLE_HOOK is set to 1 in FreeRTOSConfig.h, the kernel calls vApplicationIdleHook on each pass of the Idle loop, which is a handy place for low-priority background work or for putting the processor into a low-power sleep. The one rule is that the hook must never block, since blocking the Idle task could leave the kernel with no runnable task at all.
If you create tasks at tskIDLE_PRIORITY, they sit at the same level as the Idle task and the scheduler splits time between them like any other equal-priority task. Do note that this means the Idle task takes a slice it could otherwise hand to your own priority-zero work. The configIDLE_SHOULD_YIELD setting in FreeRTOSConfig.h covers exactly this, by letting the Idle task yield to other tasks at its priority instead of running out its slice. The behavior ties back to the scheduling rules from the Scheduling section earlier. 13 14
Writing a task
So far I have explained how setting the task priority via the uxPriority parameter determines when a task gets picked up by the scheduler, and how setting the stack depth using uxStackDepth or ulStackDepth determines the amount of memory available for the task. Now it is time to move on to the remaining parameters. While pxTaskCode, pvTaskCode and pvParameters will be fully covered in the anatomy of a task function, let us focus now on pcName.
Naming a task
As the name and the parameter type of const char * const might suggest, pcName is a human-readable name for the task. The name is not used by FreeRTOS in any way so it is only for debugging purposes, so that the user does not need to identify a task by its handle. As such, the name can be anything and the only limiting factor is configMAX_TASK_NAME_LEN defined in FreeRTOSConfig.h. It sets the maximum length including the NULL terminator, causing longer names to be truncated. 10
Anatomy of a task function
The FreeRTOS Kernel Book defines a task function as a
…small program in its own right. It has an entry point, will normally run forever in an infinite loop, and does not exit.
A task function must match the TaskFunction_t signature, which returns void and takes a single void *:
typedef void (* TaskFunction_t)( void * );
So the following compiles fine and can be passed as the pxTaskCode or pvTaskCode argument on task creation:
void my_function(void*){}
The catch is that the compiler only enforces the signature. It cannot check that an infinite loop, such as a for(;;), is actually inside the function, so my_function is accepted even though it returns the moment the scheduler runs it. That return is the real problem. On many ports (the ARM Cortex-M ones included) the task’s stack is set up so the link register points at prvTaskExitError(). If a task function ever returns at runtime, execution lands there, and with configASSERT() defined it fires an assert before disabling interrupts and spinning forever. 15 16
Let’s have a proper example next and dissect each part of it so we really understand the anatomy:
void task_function(void *pvParameters)
{
const int max_error_count = *(int *) pvParameters;
int error_count = 0;
for(;;)
{
if(WaitForEvent(EventObject,TimeOut) == pdPASS)
{
error_count = 0;
// Handle event here
}
else
{
// No event arrived, something is likely wrong
if(++error_count >= max_error_count)
{
// Too many failures, leave the loop for a clean exit
break;
}
}
}
// Loop has been left, delete this task so it never returns
vTaskDelete(NULL);
}
pvParameters
Since this is not a basic tutorial on programming, I will skip the void task_function part and focus instead on the only parameter that a task function can have, used to pass user-defined data to a task upon creation. The name of the parameter can be anything as long as the type is a void pointer. The name pvParameters is chosen here as it matches the name used in the public APIs for task creation. Here FreeRTOS follows the standard C practice of letting users pass arbitrary data that is cast back to the appropriate type within the function.
max_error_count and error_count
Here we see the cast back, with the value assigned to a local variable. In this example max_error_count is the maximum number of failed waits the task will tolerate before giving up, while error_count keeps the running tally. It is declared const since the threshold does not change after creation. Local variables can be declared inside a task function as normal, and they are stored in the task’s stack memory. So, for example, a char array of 1024 bytes will overflow a task that only has 512 bytes of stack available.
for(;;)
The reason for the infinite loop inside a task function is that the kernel expects task functions to never return. The loop itself is not a hard requirement, but more of a should. If there is no loop the task is not a “small program” but simply a function. Think of it as the difference between this function performs X versus this task’s responsibility is to gather data from a sensor, calculate something from it, and pass it on. The alternative to looping forever is a run-once task that does its work and then deletes itself, which is covered next.
WaitForEvent(EventObject,TimeOut) == pdPASS
The generic WaitForEvent is a stand-in for any of the communication and synchronization primitives the kernel offers, such as xQueueReceive or xSemaphoreTake. Again, while it is not strictly necessary to have an event driven task, it is highly recommended. A high priority task that never goes to “Blocked” state could cause lower priority tasks to never be run. The if/else then decides what to do with each wake-up: on success the event is handled, the counter is reset, and the wait starts over, while anything else is treated as a failure that increments error_count. Once enough failures have piled up to reach max_error_count, the break leaves the loop.
vTaskDelete(NULL);
A task must never return or exit from its function. When the task is no longer needed it should delete itself instead, by calling vTaskDelete(NULL) where the NULL argument means “delete the calling task”. This is available only when INCLUDE_vTaskDelete is set to 1 in FreeRTOSConfig.h.
In the example above this is exactly what the break sets up. Once error_count reaches max_error_count the task leaves the loop, and instead of falling off the end of the function (which would trigger the trap covered earlier) it calls vTaskDelete(NULL) to terminate cleanly. A task that keeps receiving its events never hits the limit, so the call is never reached and the task runs forever as intended.
One thing to note about deletion is memory. When a dynamically created task deletes itself, the kernel cannot free the stack right away as the task is still running on it, so the Idle task reclaims the TCB and stack later. Memory provided through xTaskCreateStatic is never freed by the kernel, so cleaning it up is left to the user. 17
Code example
To demonstrate FreeRTOS tasks I have created a tag:
# Clone the tag
git clone \
--depth 1 \
--branch blog-freertos-task --single-branch \
https://gitlab.com/sorhanp/cortex-m7-qemu.git \
cortex-m7-qemu-freertos-task
# Go to folder
cd cortex-m7-qemu-freertos-task
# Configure, build and run
cmake --preset arm-none-eabi-debug
cmake --build --preset arm-gcc-debug-build --target run-qemu
The example builds on the bare-metal Cortex-M7 project from earlier in the series. main creates a single Main task and hands control to the scheduler:
xTaskCreate(MainTask, "Main", 512, nullptr, 1, &mainTaskHandle);
vTaskStartScheduler();
MainTask then creates two worker tasks, an LED task and a Status task, both at priority 2, one step above MainTask itself. Each worker follows the same pattern from the anatomy section: it loops a fixed number of times, prints a line, and waits with vTaskDelay(pdMS_TO_TICKS(...)) so it spends most of its life Blocked rather than hogging the CPU. Their per-task settings (name, delay, loop count) arrive through pvParameters as a TaskConfig_t struct, which is the cast-back-the-void-pointer trick covered earlier.
This is also where the handle pays off. Each xTaskCreate is given a TaskHandle_t out parameter (&ledTaskHandle and so on), and that handle is the only way to refer to a task from the outside. Once the workers report they are done, MainTask uses their handles to clean them up:
vTaskDelete(ledTaskHandle);
This is the counterpart to the vTaskDelete(NULL) self-delete from before, here one task deletes another by handle. The workers themselves never return either. When finished they call vTaskSuspend(NULL) and wait to be deleted, which keeps them clear of the “falling off the end” trap while leaving the actual deletion to MainTask.
Expected output:
Hello from Cortex-M7 main!
Main task: started
LED task: toggled LED, iteration 1/5
Status task: running, iteration 1/3
Main task: waiting for worker tasks
LED task: toggled LED, iteration 2/5
Status task: running, iteration 2/3
LED task: toggled LED, iteration 3/5
LED task: toggled LED, iteration 4/5
Status task: running, iteration 3/3
LED task: toggled LED, iteration 5/5
Main task: waiting for worker tasks
LED task: finished
Status task: finished
Main task: both worker tasks have finished
Main task: deleting LED task
Main task: deleting status task
Main task: exiting cleanly
Next
Next up I will start covering the synchronization primitives, the mechanisms tasks use to communicate and coordinate. Rather than one giant post or a thin post per primitive, I will group them by how they relate to each other:
- Queues first, on their own. They are the foundation the others are built on, so they earn a dedicated post, which will be Part 5.
- Semaphores and mutexes next, since they share the same take/give API and let me dig into mutex priority inheritance, in Part 6.
- Event groups and task notifications after that, pairing “wait on several things at once” with the lightweight direct signal, in Part 7.
Stream buffers and message buffers will slot in somewhere after those.
-sorhanp
References
FreeRTOS Documentation - Tasks and Co-routines: https://freertos.org/Documentation/02-Kernel/02-Kernel-features/01-Tasks-and-co-routines/00-Tasks-and-co-routines ↩︎
FreeRTOS Documentation - Task states: https://freertos.org/Documentation/02-Kernel/02-Kernel-features/01-Tasks-and-co-routines/02-Task-states ↩︎
FreeRTOS Kernel Book - Expanding the Not Running State: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch04.md#47-expanding-the-not-running-state ↩︎
FreeRTOS Documentation - Customization: https://freertos.org/Documentation/02-Kernel/03-Supported-devices/02-Customization#configuse_port_optimised_task_selection ↩︎
FreeRTOS Documentation - Task Priorities: https://freertos.org/Documentation/02-Kernel/02-Kernel-features/01-Tasks-and-co-routines/03-Task-priorities ↩︎
FreeRTOS Kernel Book - Task Priorities: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch04.md#45-task-priorities ↩︎
GitHub - FreeRTOS-Kernel/task.c: https://github.com/FreeRTOS/FreeRTOS-Kernel/blob/main/tasks.c ↩︎
FreeRTOS Documentation - scheduling: https://freertos.org/Documentation/02-Kernel/02-Kernel-features/01-Tasks-and-co-routines/04-Task-scheduling ↩︎
FreeRTOS Kernel Book - Scheduling Algorithms: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch04.md#412-scheduling-algorithms ↩︎
FreeRTOS Kernel Book - Task Creation: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch04.md#44-task-creation ↩︎ ↩︎
FreeRTOS Kernel Book - Heap Memory Management: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch03.md#3-heap-memory-management ↩︎
FreeRTOS Documentation - stack usage and stack overflow checking: https://freertos.org/Documentation/02-Kernel/02-Kernel-features/09-Memory-management/02-Stack-usage-and-stack-overflow-checking ↩︎
FreeRTOS Documentation - idle task: https://freertos.org/Documentation/02-Kernel/02-Kernel-features/01-Tasks-and-co-routines/15-Idle-task ↩︎
FreeRTOS Kernel Book - The Idle Task and the Idle Task Hook: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch04.md#48-the-idle-task-and-the-idle-task-hook ↩︎
FreeRTOS Documentation - Implementing a Task: https://freertos.org/Documentation/02-Kernel/02-Kernel-features/01-Tasks-and-co-routines/05-Implementing-a-task ↩︎
FreeRTOS Kernel Book - Task Functions: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch04.md#42-task-functions ↩︎
FreeRTOS Documentation - vTaskDelete: https://freertos.org/Documentation/02-Kernel/04-API-references/01-Task-creation/03-vTaskDelete ↩︎