Worked examples — FreeRTOS — task creation, priorities, xTaskCreate
The scenario matrix
Every situation this topic produces falls into one of these cells. Each worked example below is tagged with the cell(s) it covers, so by the end no cell is left dark. Two words in the table need pinning down first:
| # | Case class | The question it forces you to answer |
|---|---|---|
| C1 | Higher priority preempts lower | Does the urgent task really cut in mid-work? |
| C2 | Equal priority, both busy | Who runs — and for how long each? |
| C3 | High-priority task never blocks | What starves, and how visibly? |
| C4 | Stack too small (degenerate) | Does creation fail, or run then crash? |
| C5 | Heap exhausted (limiting) | What does xTaskCreate return, and what do you do? |
| C6 | Zero-argument / NULL handle |
Is it legal to pass NULL for params and handle? |
| C7 | Priority above the ceiling | What happens if uxPriority ≥ configMAX_PRIORITIES? |
| C8 | Task function returns | The forbidden move — what actually breaks? |
| C9 | Real-world word problem | Sizing three real tasks correctly end to end. |
| C10 | Exam-style twist | A subtle ordering/timing gotcha. |
We assume configTICK_RATE_HZ = 1000 (so 1 tick = 1 ms) unless a problem says otherwise.
Example 1 — Higher priority preempts lower (covers C1)

Step 1 — Find vHigh's wake period in ticks.
Why this step? The scheduler thinks in ticks, so translate the human "10 ms" using pdMS_TO_TICKS (defined above).
Step 2 — Count wakes in the 100 ms window.
Why this step? Each wake is a preemption event; counting them tells us how often vLow is interrupted.
That matches the ten red spikes in the figure.
Step 3 — Add up the CPU vHigh steals.
Why this step? To see whether vLow is merely delayed or actually broken.
Step 4 — Compute vLow's finish time.
Why this step? vLow needs 100 ms of CPU, but only gets what vHigh leaves behind.
Example 2 — Equal priority, both busy (covers C2)

Step 1 — Recall the equal-priority rule.
Why this step? With no priority difference, the scheduler can't pick a "winner", so with configUSE_TIME_SLICING = 1 it falls back to round-robin time-slicing: one tick each, then rotate.
Step 2 — Read the tick schedule off the figure. Why this step? Concreteness beats hand-waving — the six alternating slots in the figure are exactly:
| Tick | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Runs | A | B | A | B | A | B |
Step 3 — Sum each task's share. Why this step? To answer the numeric question.
Example 3 — High-priority task never blocks (covers C3)

Step 1 — Apply the "highest READY runs" rule.
Why this step? vGreedy is priority 5, always Ready (it never blocks), so it is always the numerically largest Ready priority.
Step 2 — Deduce everyone else's share.
Why this step? One core; whatever vGreedy doesn't yield, nobody else can take.
Step 3 — List the collateral damage. Why this step? Starvation isn't just "LED off" — the idle task at priority 0 never runs either.
- No idle task ⇒ no low-power sleep entry.
- No idle task ⇒ the watchdog (often fed from idle hook) times out and resets the chip.
Example 4 — Stack too small (degenerate input) (covers C4)
Step 1 — Convert the requested stack to bytes. Why this step? The declaration is in bytes; the API is in words. Same units or nothing means anything.
Step 2 — Compare against the demand. Why this step? The local buffer alone needs 600 bytes, plus room for saved registers and call frames.
Step 3 — Predict when it fails.
Why this step? This is the trap. xTaskCreate succeeds — the heap easily fits 512 bytes. The crash comes later, when the task runs and pushes past its stack top, silently corrupting the neighbouring TCB or heap.
Example 5 — Heap exhausted (limiting case) (covers C5)
Step 1 — Cost of one task in bytes. Why this step? Heap is spent in bytes; convert the word-based stack.
Step 2 — How many fit in the heap. Why this step? Integer division: you can't allocate a fractional task.
Step 3 — What the 5th call returns. Why this step? After 4 tasks, bytes are gone; bytes remain — not enough for the 2148-byte 5th.
Example 6 — Zero args and NULL handle (covers C6)
Step 1 — Interpret pvParameters = NULL.
Why this step? This argument is just a void* handed to your task. NULL is a perfectly valid pointer value; your task simply won't dereference it.
void vBlink(void*p){ /* ignore p entirely */ for(;;){ ... } }Step 2 — Interpret pxCreatedTask = NULL.
Why this step? This is the output handle. Passing NULL says "I don't need a handle." FreeRTOS checks for NULL and skips writing it — safe by design.
Step 3 — When would NULL handle bite you?
Why this step? You only need the handle to later vTaskDelete, vTaskSuspend, or notify this task. If the task lives forever and nobody controls it externally, NULL is correct and idiomatic.
Example 7 — Priority above the ceiling (covers C7)
Step 1 — State the legal range in symbols.
Why this step? Before deciding "9 is illegal", we must write down exactly which numbers are legal. configMAX_PRIORITIES counts how many priority levels exist, starting from 0 — so with 5 levels the valid numbers are 0, 1, 2, 3, 4. The top number is configMAX_PRIORITIES − 1, not configMAX_PRIORITIES itself (5 is one past the last valid slot):
Since , the requested priority is out of range.
Step 2 — What FreeRTOS does with 9.
Why this step? FreeRTOS internally clamps an over-range priority down to the highest legal value, configMAX_PRIORITIES - 1.
Step 3 — Why clamping is dangerous anyway. Why this step? Silent clamping means your "super-urgent priority 9" task quietly becomes priority 4 — possibly tying another task you thought it outranked. No error is raised.
Example 8 — A task function returns (covers C8)
Step 1 — Trace the stack when the function returns.
Why this step? A task was launched by the kernel jumping into vBad. There is no valid return address above it — the kernel didn't "call" it like a normal function.
Step 2 — Predict the outcome.
Why this step? Returning pops a garbage address off the stack and the CPU jumps there → hard fault, or a jump into a FreeRTOS trap handler prvTaskExitError() (if configured).
Step 3 — The two correct patterns. Why this step? Every task body must be one of:
- an infinite loop:
for(;;){ ... }, or - self-delete before returning:
vTaskDelete(NULL);as the last line.
Example 9 — Real-world word problem: sizing a data-logger (covers C9)
Step 1 — Assign priorities by urgency. Why this step? "Never miss a reading" is the hardest deadline ⇒ highest number. UI is cosmetic ⇒ lowest above idle.
Step 2 — Justify the fixed overhead we add to each stack. Why this step? A stack must hold more than your buffer. On Cortex-M, every task's stack also carries: the exception frame the CPU auto-pushes on a context switch (16 registers × 4 B = 64 B), the software-saved registers FreeRTOS pushes (8 registers × 4 B = 32 B), plus headroom for a nested function call or two (~100 B). Rounding that sum up for safety: This is why we add a flat 200 B per task below — it is derived, not magic.
Step 3 — Size each stack in words (buffer + 200 B overhead, then ÷4, round up). Why this step? The API wants words; we compute bytes first, convert, then round up to a comfortable power-of-two-ish value.
| Task | buffer (B) | +overhead (B) | total (B) | ÷4 → words | rounded up |
|---|---|---|---|---|---|
| sensor | 200 | 200 | 400 | 100 | 128 |
| logger | 1000 | 200 | 1200 | 300 | 384 |
| ui | 300 | 200 | 500 | 125 | 192 |
So: xTaskCreate(vSensor,"sensor",128,NULL,3,NULL), xTaskCreate(vLogger,"logger",384,NULL,2,NULL), xTaskCreate(vUi,"ui",192,NULL,1,NULL).
Step 4 — Total heap cost (stack bytes + 100 B TCB each). Why this step? Confirm it fits in 8192 bytes before flashing.
Step 5 — Compute the safety margin. Why this step? You want to know how much heap is left for queues, semaphores, and future tasks.
Step 6 — Check the sensor deadline. Why this step? The whole point is the sensor never missing. Priority 3 is the top, so the sensor preempts everything the instant its 20 ms timer fires; its scheduling jitter ≈ 0.
Example 10 — Exam twist: creation order vs run order (covers C10)
Step 1 — Separate creation from scheduling.
Why this step? xTaskCreate only registers a task as Ready; it does not run it. Nothing runs until vTaskStartScheduler(). So the two xTaskCreate lines both complete before any task body executes.
Step 2 — Apply the scheduling rule at start-up. Why this step? When scheduling begins, the rule is unchanged: highest-priority Ready task runs first.
Step 3 — Note the trap.
Why this step? Here creation order is H then L, which happens to match the run order — but that's a coincidence. If you had written the L line first, H would still run first, because 4 > 1. Creation order is irrelevant; only priority decides.
Recall One-line self-test per cell
C1 preempt outcome ::: vHigh runs 10×/100 ms; vLow finishes at 102 ms. C2 equal-priority split ::: 50/50 round-robin, 3 ticks each in 6. C3 greedy top task ::: vBlink gets 0%; idle+watchdog also starve. C4 tiny stack ::: creation returns pdPASS, then runtime overflow (512 B < 600 B). C5 heap limit ::: 4 tasks succeed, 5th returns errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY. C6 NULL params+handle ::: fully legal and idiomatic. C7 over-range priority ::: clamped to configMAX_PRIORITIES-1 (9→4) silently. C8 task returns ::: crash/trap — wrap in for(;;) or vTaskDelete(NULL). C9 logger sizing ::: 3116 bytes heap used, 5076 free, priorities 3/2/1. C10 order vs priority ::: highest priority runs first, creation order irrelevant.
See also: FreeRTOS — vTaskDelay vs vTaskDelayUntil · Round-robin vs Preemptive Scheduling · Heap memory management (heap_1..heap_5) in FreeRTOS · Context Switching and the TCB · ISR-safe APIs (FromISR) and deferred interrupt handling · Priority Inversion and Priority Inheritance