Static FreeRTOS Tasks: Lifetime Rules for TCBs, Stacks and Reused Storage

Static allocation changes ownership, not lifetime

A task created successfully with xTaskCreateStatic() can still fail after its creator returns or after a later restart. The kernel did not allocate the task’s storage, but it continues to use the task control block and stack. The application must keep both valid for the entire kernel-visible lifetime. This guide targets upstream FreeRTOS Kernel V11.1.0 and makes single-core assumptions where deletion sequencing is discussed. SMP and vendor-specific deletion hooks need a separate review.

The pinned task API takes a StaticTask_t object and a StackType_t array. Stack depth is a count of stack elements, not an arbitrary byte count. A useful review artifact is an ownership table with four columns: storage address, allocator or declarer, active task generation, and condition permitting reuse. A linker-allocated object is easy to locate, but that alone does not tell you whether the kernel still references it.

Reserved TCB and stack progress through active use and pending cleanup before safe reuse; a permanent worker avoids this cycle.
Original conceptual diagram. Arrows show ownership or lifecycle dependencies, not measured timing.

Give the stack, TCB and arguments compatible lifetimes

File-scope or static-duration backing objects are a straightforward choice for tasks that exist for the life of the product. An automatically allocated array inside an initialization function is unsafe if the function returns while the task still exists. The same issue applies to the argument pointer passed at creation. The kernel transfers a pointer value; it does not make an independent lifetime guarantee for the configuration structure behind that pointer.

An application-managed memory pool can also provide static-task backing storage, provided alignment, size and lifetime match the chosen port. Record which pool slot is occupied and do not let its free list treat task exit as immediate reclamation. Keep stack storage separate from a payload arena that another subsystem may compact, clear, or reuse. Turning on configSUPPORT_STATIC_ALLOCATION does not make unrelated library allocations disappear, so audit those separately rather than labeling the whole firmware allocation-free.

Separate logical completion from kernel cleanup

A worker announcing “job complete” is an application event. A worker calling vTaskDelete(NULL) is a kernel lifecycle operation. Those events do not prove that every internal reference to the TCB has gone away. In V11.1.0, a running or yielding task is placed on a termination list for later idle-task cleanup. The fact that application-supplied stack and TCB storage are not returned to a heap does not remove that list operation. See the version-pinned deletion implementation.

Therefore a completion notification sent immediately before self-deletion is not permission to overwrite the static TCB. A supervisor can wake first and create a replacement into the same address while cleanup still refers to the previous instance. A delay of one tick is also not a lifecycle proof: priorities, scheduler suspension, and other work can prevent the idle task from reaching the cleanup point during that interval.

Prefer reuse of the worker over reuse of its TCB

For repeated jobs, create the worker once and let it block on a command queue between jobs. Reset the worker’s application state through a defined command, not by clearing its stack. This avoids coupling product restart logic to internal deletion timing. It also provides a place to close files, stop DMA, release mutexes, and return outstanding payloads before accepting the next job.

If deletion is genuinely necessary, use a supervisor-controlled protocol. The worker stops admitting new work, releases resources, acknowledges a safe boundary, and parks in a documented wait. On a reviewed single-core port, the supervisor can delete that non-running task and verify the immediate cleanup path in the selected implementation before reclaiming backing storage. Do not generalize that reasoning to a task running on another core. If no supported completion boundary can be established, keep the storage reserved instead of guessing.

Invalidate handles and external references together

A TaskHandle_t identifies an instance, not an eternal service name. After deletion, clear or replace every published handle under the same synchronization used by its consumers. A delayed callback holding the old handle is a separate hazard even if the TCB was safely reclaimed. Reusing the same address can make this bug difficult to recognize because the stale handle may now refer to a different generation.

Give long-lived service requests an application generation number when callbacks can outlive restarts. A callback should validate its generation before issuing work, and shutdown should stop its producer before the instance is retired. Generation checks supplement synchronization; they do not make dereferencing freed memory safe. Application resources such as DMA descriptors, sockets, and library state need their own close or reclaim sequence because task deletion does not automatically unwind C cleanup logic.

Test the lifetime boundary deliberately

First inspect the linker map and creation sites. Verify stack element counts, alignment, argument lifetime, and whether two task instances can receive the same backing objects. Keep an explicit registry in a development build so a duplicate allocation fails immediately rather than corrupting an unrelated task later. This is a proposed diagnostic, not a kernel facility supplied by FreeRTOS.

Next exercise repeated start, stop, and restart while delaying the idle task in a controlled test. Trigger shutdown during a blocked operation and with a callback queued. Log application generation, handle, storage slot, safe-point acknowledgement, and cleanup decision. The acceptance condition is absence of any overlap between an old instance’s kernel-visible lifetime and a replacement’s initialization. Merely surviving a large loop count is weaker evidence than checking this ordering directly.

Release checklist and limits

  • Every TCB, stack and argument has a documented owner and a sufficient lifetime.
  • Task completion, task deletion and backing-storage reuse are distinct states.
  • Self-deletion never triggers immediate reuse based only on a notification or fixed delay.
  • Handles, callbacks and application-owned resources participate in shutdown.
  • The port’s actual deletion path, including SMP and runtime hooks, is reviewed before reclamation.
  • Validation reports describe the exact build and observed ordering, without inferring a universal restart guarantee.

The examples here are lifecycle designs and source-level checks. No target firmware was compiled or repeatedly restarted for this article.

Similar Posts