5.5.10 · D5Embedded Systems & Real-Time Software

Question bank — FreeRTOS — task creation, priorities, xTaskCreate

2,401 words11 min readBack to topic

Anchor the picture first — the four task states

Before the traps, pin the mental model. A task is always in exactly one of four states, and every trap on this page is really a question about which state a task is in and why.

The next figure shows when memory from vTaskDelete(NULL) is actually reclaimed — it is not freed the instant you call it.


Local glossary — every symbol used on this page

Because the parent note assumed these, here they are in plain words, so nothing below is unexplained.


Two ways to create — dynamic vs static

The parent note only showed xTaskCreate, which carves the stack and TCB out of the heap. There is a second door.


True or false — justify

A task in the Blocked state still consumes a slice of CPU each tick.
False. Blocked means it is removed from the ready list until its event (delay, queue, semaphore) fires — it uses zero CPU, which is the entire reason blocking exists.
Setting a task's priority to configMAX_PRIORITIES gives it the highest possible urgency.
False. Valid priorities are 0 .. configMAX_PRIORITIES - 1, so the top is configMAX_PRIORITIES - 1. Passing the max value itself is an off-by-one bug that may be silently clamped.
Two tasks at equal priority will run in strict alternation forever.
Only roughly true — and only if configUSE_TIME_SLICING = 1. They then round-robin one tick each, but a task that blocks early hands its remaining slice to the other, so it is not clockwork alternation.
With configUSE_TIME_SLICING = 0, two equal-priority CPU-bound tasks still take turns each tick.
False. With slicing off, the running task keeps the CPU until it blocks or explicitly yields (taskYIELD). A peer at the same priority can then starve indefinitely — the complementary edge case people forget.
usStackDepth = 256 reserves 256 bytes of stack.
False. Stack depth is in words, so on a 32-bit Cortex-M that is bytes. Reading it as bytes under-provisions the stack by a factor of the word size.
If xTaskCreate returns pdPASS, the task has already run at least once.
False. Creation and first execution are deliberately separate steps: pdPASS only means the TCB and stack were allocated and the task was placed on the Ready list. The kernel separates allocation from the first context switch because the running context (or the not-yet-started scheduler) may be higher priority, so the new task must wait to be picked — allocation success says nothing about being scheduled.
The idle task always runs at tskIDLE_PRIORITY = 0 and can never be starved.
False on the second half. The idle task is priority 0, but a non-blocking higher-priority task starves it — breaking power-saving hooks, watchdog feeds, and cleanup of deleted tasks.
Calling vTaskStartScheduler() returns once all tasks finish.
False. Tasks are infinite loops that never finish, so vTaskStartScheduler() normally never returns; it only returns if there is not enough heap to create the idle/timer task.
A higher-priority task will always preempt a lower one the instant it becomes Ready.
True under the default preemptive scheduler — but only if preemption is enabled (configUSE_PREEMPTION = 1) and the CPU is not inside a critical section (interrupts masked). Inside taskENTER_CRITICAL()/taskEXIT_CRITICAL() the switch is deferred until the critical section ends, even at prio difference.

Spot the error

void vTask(void *p){ doWork(); }   // what breaks?

::: The task function returns after doWork(). A returning task falls off the end into undefined code and corrupts the scheduler. Wrap the body in for(;;) or end with vTaskDelete(NULL).

xTaskCreate(vHigh, "hi", 128, NULL, 1, NULL);
xTaskCreate(vLow,  "lo", 128, NULL, 3, NULL);  // vLow does heavy math, never blocks

::: The names lie: vLow was given priority 3, higher than vHigh at 1. Since bigger = more urgent and vLow never blocks, it starves vHigh completely. The intended urgency is inverted.

void vCritical(void *p){ for(;;){ readSensor(); } }  // top priority, must be responsive

::: No blocking call, so this top-priority loop never yields — every lower task, including idle, starves. Responsiveness comes from blocking until the next event, not from spinning. Add vTaskDelay or block on a queue/semaphore.

void isr_handler(void){ xTaskCreate(vLog, "log", 128, NULL, 2, NULL); }

::: xTaskCreate is not ISR-safe and must not be called from an interrupt. Pre-create the task at startup and, from the ISR, use a ...FromISR API to signal it — see ISR-safe APIs (FromISR) and deferred interrupt handling.

xTaskCreate(vBlink, "L", 128, NULL, 1, NULL);
vTaskDelete(h);   // h is uninitialised — we passed NULL for the handle

::: The handle was never captured (pxCreatedTask = NULL), so h is garbage. To delete or suspend a task later you must pass &h and store the real handle.

xTaskCreate(vTask, "T", 4096, NULL, 1, &h);  // "only 4 KB, plenty of heap"

::: 4096 is in words, so on Cortex-M this reserves per task — four times the assumed size. On a small MCU this may return errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY.

taskENTER_CRITICAL();
vTaskDelay(pdMS_TO_TICKS(10));   // "just a short pause inside the critical section"
taskEXIT_CRITICAL();

::: Blocking calls like vTaskDelay are illegal inside a critical section — you cannot yield the CPU while interrupts are masked. This deadlocks or asserts. Do timing outside the critical region.


Why questions

Why does FreeRTOS express stack size in words instead of bytes?
A "word" is the CPU's natural register width, and every push/pop moves register-sized units. Counting in words matches how the stack actually grows and stays portable across 8/16/32-bit cores.
Why is "bigger number = more urgent" a trap even for experienced programmers?
Many systems (Linux nice, several classic RTOSes) treat smaller numbers as more important, so muscle memory inverts the meaning. FreeRTOS simply runs the numerically largest ready priority.
Why must a real-time high-priority task spend most of its life Blocked?
A preemptive scheduler gives the CPU to the highest Ready task; if it never blocks it is always Ready and monopolises the core, starving everyone. Blocking until the next event is what makes room for other work while keeping response deterministic.
Why store the TaskHandle_t output even if you don't need it right now?
The handle is the only way to later suspend, resume, delete, or change the priority of that task. Without it those control APIs have nothing to target.
Why does pdMS_TO_TICKS(500) exist instead of just writing the tick count?
FreeRTOS measures time in ticks whose rate (configTICK_RATE_HZ) varies per project. The macro converts milliseconds to ticks — — so the code reads in real time and stays portable when the tick rate changes.
Why does a critical section defer preemption even when configUSE_PREEMPTION = 1?
taskENTER_CRITICAL() masks interrupts (up to a threshold), and preemption is driven by the tick interrupt and other IRQs. With those masked, the scheduler literally cannot fire until you exit — so critical sections must be kept as short as possible.
Why does each task need its own stack rather than sharing one?
Each task can be preempted mid-function, so its local variables and return addresses must be preserved independently. A shared stack would let one task's frames clobber another's on every context switch — see Context Switching and the TCB.
Why can equal-priority tasks fairly share the CPU but unequal ones cannot?
The scheduler only time-slices among tasks at the same, highest ready priority (and only when configUSE_TIME_SLICING = 1). A lower-priority task is simply never chosen while any higher one is Ready, so "fairness" only exists within a priority level.

Edge cases

What happens if every task blocks simultaneously (nothing is Ready)?
The scheduler runs the idle task at priority 0, which may enter a low-power sleep and feed the watchdog until some event unblocks a real task.
How does configUSE_TICKLESS_IDLE = 1 change when the idle task and low-power sleep behave?
Instead of waking on every periodic tick, the kernel stops the tick timer when it predicts a long idle, sleeps deeply, and on wake corrects the tick count for the elapsed time. So idle no longer runs on a fixed cadence — it runs, sleeps for a computed span, and the tick "jumps" forward. Great for battery life, but timing sources must survive sleep.
What if you create a task before calling vTaskStartScheduler()?
That is the normal pattern — the task becomes Ready but does not run until the scheduler starts. All setup tasks created in main() begin executing only after vTaskStartScheduler().
What if two tasks at different priorities both become Ready at the exact same tick?
No contest — the scheduler always picks the higher-priority one to Run; the lower stays Ready. "Same tick" only causes sharing when priorities are equal.
What happens if usStackDepth is set too small for the task's actual usage?
The task overflows its stack, silently corrupting adjacent memory (often the TCB or another task). Enable configCHECK_FOR_STACK_OVERFLOW to catch it via a hook instead of a mystery crash.
What does vTaskDelete(NULL) do, and when is its memory actually freed?
A task passes NULL to delete itself. Its TCB/stack are not freed immediately — a running task cannot free its own stack — so the kernel just marks it and the idle task reclaims that memory on its next turn (see the timeline figure). Another reason idle must not starve.
Can a task change its own priority at runtime, and what's the risk?
Yes, via vTaskPrioritySet. The risk is starving others by raising itself, or creating priority-inversion scenarios if it interacts with shared resources — see Priority Inversion and Priority Inheritance.
If you built with configSUPPORT_DYNAMIC_ALLOCATION = 0, what breaks?
Plain xTaskCreate won't compile/link — there is no heap to carve from. You must use xTaskCreateStatic with your own stack and TCB storage (configSUPPORT_STATIC_ALLOCATION = 1).

Recall One-line self-test

Bigger number = more urgent, stack is in words, tasks never return, high-priority tasks must block, and xTaskCreate is not ISR-safe. If you can justify all five, you own this topic.