FreeRTOS Trace Hooks: Capture Scheduling Evidence Without Recursing into the Kernel
A FreeRTOS trace hook runs at its kernel call site, not in a separate logging task. It can execute while kernel data is mid-update, inside a critical section or during a context switch. Capture a small bounded record there and defer formatting and transport. Before interpreting the record, document precisely which transition has already occurred and which has not.
This guide pins FreeRTOS-Kernel V11.1.0, single-core scheduling, with the GCC ARM_CM4F port used for source-level interrupt-context analysis. A separate executed POSIX-host fixture verifies selected queue-hook positions before scheduler startup. It does not measure Cortex-M overhead, interrupt latency or concurrent trace-buffer correctness. SMP and vendor-modified hooks require a fresh audit.
Build a call-site contract for each hook
Start with the tagged task implementation and queue implementation. Do not infer semantics from the hook’s English name alone. Record the following for every enabled hook:
- Which API paths invoke it, including startup and error paths.
- Whether the caller is task context, interrupt context or a port-specific switch path.
- Which critical section, interrupt mask or scheduler lock is already active.
- Which object fields are stable at that point and whether values represent pre-update or post-update state.
- Which operations the collector may perform without changing or recursively observing the kernel transition.

Three source-level examples
| Hook | V11.1.0 placement | Interpretation boundary |
|---|---|---|
| traceQUEUE_SEND | Inside the task send critical section, before copying the item | Observed queue count is not a post-send count |
| traceQUEUE_RECEIVE | After copying the item, before decrementing the stored count | Payload transfer and count update have different boundaries |
| traceTASK_SWITCHED_IN | After task selection in the switch routine | Selected task has not necessarily executed its next application instruction |
The corresponding ISR queue hooks also run inside the ISR-specific protected path. Substituting an ISR API into a logger does not automatically make it safe inside another kernel operation. It can still recurse into the same tracing machinery or violate assumptions about partially updated state.
For the exact GCC ARM_CM4F port, PendSV calls vTaskSwitchContext() with BASEPRI set, then restores the selected task’s context afterward. This explains why a switch-in timestamp is a scheduler-selection observation, not a direct timestamp of application execution. The same switch-in macro also has a startup call site, so instrumentation must tolerate that lifecycle phase.
Audit the logger’s entire call graph
A hook that calls a printf-like function may indirectly allocate, take a library lock or use a driver that queues output. A hook that sends a trace record through an ordinary FreeRTOS queue can invoke queue trace hooks again. The recursion may appear only when a buffer fills or a library takes a slow path.
Adopt a conservative capture contract: no allocation, blocking, formatted output, filesystem access or kernel object operations. Copy only a fixed-size record through a mechanism whose context and nesting behavior have been independently reviewed. This is an engineering restriction for the collector, not a claim that every read-only kernel API is universally forbidden by FreeRTOS.
Include the timestamp source in that review. A timestamp helper must not hide a lock or unsupported register access. Define its width, rollover interpretation and clock domain. Capture object identifiers as opaque values; do not dereference a stored task or queue pointer later after deletion. If addresses can be reused, add lifecycle or generation information in a separate mapping.
Make overflow and nesting observable
Use a fixed storage budget and a defined full-buffer behavior. Dropping a trace record with an appropriate loss indication is preferable to waiting for a drain task that cannot run until the hook returns. Account for the synchronization of the loss indication itself; an unprotected increment shared by nested interrupt and task writers can lose updates.
Choose the collector design from the actual writer set. A single-core system can still have nested writers through interrupts. Separate context-specific buffers, an audited bounded reservation mechanism or an explicitly constrained capture context may be appropriate. A recursion guard can expose accidental re-entry, but silently suppressing records without a counter makes the trace look more complete than it is.
Executed placement check
An original host fixture enabled only send and receive hooks, then created a one-slot queue before starting the scheduler. It performed five send/receive pairs. The hook captured the queue pointer, event type and internal message count into four fixed entries, then counted overflow without calling kernel APIs.
Compiled with GCC 14.2.0 against unmodified V11.1.0 kernel sources and the official POSIX port, the fixture recorded send counts of zero and receive counts of one. Four entries were stored and six were dropped as intended. This confirms the selected hook positions and bounded behavior in a quiescent test; that simple fixture collector is not offered as a concurrent production logger.
A hypothetical scheduling investigation
Suppose a control task appears to start late after a queue send. Capture send, relevant ready-state, switch-out and switch-in events, plus a small application marker at the control task’s actual work entry. Map each event to its call-site contract before calculating intervals. A gap between switch-in and application entry can include context restoration and intervening execution; it is not automatically scheduler decision time.
Qualify with trace disabled, capture only and capture plus deferred drain. Check loss counters, nesting behavior and target service times under representative interrupt load. If enabling trace changes the failure, reduce capture cost and record that limitation. The result should be a defensible transition history, not a precise-looking timeline built from ambiguous hook boundaries.