FreeRTOS on Raspberry Pi Pico: Apply Core Affinity Before the First Sensitive Instruction

A task's final affinity mask does not prove it never ran elsewhere during startup. Creating a task and pinning it afterward leaves a different initialization question from creating it with the required placement already established.

This distinction matters for Pico SDK integrations that associate asynchronous work with a particular core. The discussion uses the SDK 2.3.1 release note and FreeRTOS V11.3.1 API declarations. It describes a review method, not a claim that a particular application has reproduced the reported race.

Affinity diagram identifies the first core-sensitive instruction, selects creation-time placement and separately tests shared-resource ownership.
Original conceptual engineering diagram. Proposed review method, not a measured result.

Start with the SDK's documented startup fix

The Pico SDK 2.3.1 release note reports a change to FreeRTOS SMP asynchronous-context initialization: the worker is pinned to the caller's core at creation rather than shortly afterward, preventing an initial run on the wrong core.

That is a concrete reason to inspect initialization timing, rather than treating affinity solely as a steady-state performance option. A later observation that the worker is on the desired core does not establish where its first work occurred.

For a hypothetical custom peripheral worker, identify the first operation whose correctness depends on core placement. It might occur in initialization before the main loop begins. The placement requirement must hold before that operation, not merely before the first periodic sample collected by a debugger.

Distinguish creation-time placement from later changes

The tagged FreeRTOS task interface provides affinity-aware dynamic and static task-creation APIs when their configuration conditions are met. It also provides a later affinity setter. Affinity is expressed as a bitmask of permitted cores, rather than a bare core number.

These are different tools. A post-creation setter can be appropriate when changing an existing task's eligible cores, but it does not by itself describe the task's earlier execution. Review the full sequence that makes a task runnable and allows it to reach core-sensitive code.

Check the actual kernel and port version before adopting an API name from an example. Dynamic allocation, static allocation, multicore configuration and affinity support all affect which declarations are available. A working patch in one build is not a universal replacement recipe.

Review shared state separately from placement

Pinning a worker does not give it exclusive ownership of every peripheral or shared object it uses. Other tasks or interrupt handlers may still access the same resources. Keep the synchronization and ownership review even when placement is fixed.

Conversely, not every task needs a fixed core. State the correctness requirement first. Pinning all tasks as a general debugging measure can change scheduling enough to hide a race without explaining it.

In the hypothetical peripheral case, document both rules: the worker's first sensitive operation runs on the required core, and competing access follows the agreed synchronization protocol. Testing only one rule leaves the other unproven.

Observe the first execution, not only the settled system

Use minimally intrusive instrumentation at the task's initial entry and at the first core-sensitive operation. Record the executing core and the relevant initialization milestone in a preallocated observation buffer suitable for the context. Avoid relying solely on late serial output, which can reorder observations or change scheduling.

Exercise startup under conditions that make both cores active. Repeat with the actual task priorities and initialization order used by the product. These are proposed test conditions; no number of successful runs is supplied here as a reliability claim.

When evaluating an SDK upgrade, confirm that the intended release and integration code reached the build, then repeat the targeted startup test. Do not infer the fix from a version string embedded by unrelated application code.

The useful result is an ordering guarantee backed by the selected creation path: placement is established before the worker can perform the operation that depends on it. A final affinity readback remains useful, but it answers a later question.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *