ESP32-S3 Floating-Point Use Can Change a Task’s Core Affinity
An ESP32-S3 worker created without a fixed core can stop being migratable after it first uses the hardware floating-point unit. Adding a calibration or signal-processing path can therefore change the scheduler’s eligible-core map, not just the duration of one calculation. A product may begin balancing work across two cores and later concentrate particular workers on one.
This article targets ESP-IDF v5.2.5 on ESP32-S3, using the default IDF FreeRTOS kernel with both cores enabled. Specifically, CONFIG_FREERTOS_SMP and CONFIG_FREERTOS_UNICORE are disabled. The first setting’s name is easy to misread: in this release it selects the experimental alternative Amazon SMP kernel, not whether the default IDF kernel supports two cores. The versioned Kconfig distinguishes these implementations. No scheduling or numerical tests were run for this article.

Trace the affinity mutation itself
The IDF FreeRTOS guide explains lazy FPU context management and automatic pinning. Its purpose is to keep a task’s floating-point context associated with the core that owns it. This is different from merely paying an extra context-save cost.
The implementation makes the scheduling consequence concrete. In the Xtensa port assembly, _frxt_coproc_exc_hook obtains the current core number. In a multicore build, with that core’s scheduler running and outside interrupt nesting, it locates the current task’s control block and writes the core number into TCB.xCoreID.
There is no “only if currently unpinned” branch around that write: a task already pinned to its executing core receives the same value. The important change occurs when an initially unpinned task reaches this path. Returning from the math function does not reverse the assignment.
The kernel’s affinity eligibility check accepts a task for a core when its stored core ID matches or its affinity is tskNO_AFFINITY. Replacing that unrestricted value with one core number changes which processor may select it. Raising task priority does not make the other core eligible again.
The trigger is executed hardware use, not a type declaration
The Xtensa exception implementation calls the RTOS hook on the coprocessor-disabled exception path. Review the final linked instructions and actual call flow when deciding whether a feature reaches that path.
A float variable’s declaration, a copied data structure or inclusion of a math header is not sufficient evidence. An expression can be constant-folded, removed or implemented differently under a changed build. Conversely, a helper several calls below an apparently integer-oriented task can execute hardware floating-point instructions. Inspect the feature path, not just the task entry function.
Double-precision arithmetic is not hardware-accelerated on this target. Do not assume that a double-only operation exercises the same mechanism, and do not replace float with double merely to avoid pinning: software arithmetic changes execution cost and numerical behavior. A call graph that mixes types still needs instruction-level review.
Keep the experiment in task context. This ESP32-S3 baseline is not an invitation to use floating point inside an ISR. The versioned configuration’s optional level-1 ISR feature is restricted to original ESP32, not a universal switch for every ESP32-family device.
Diagnose a phase change in scheduling
Consider two hypothetical background workers created with tskNO_AFFINITY. Initially they process integer-only messages. Later, each receives its first calibration request while executing on Core 0. If those paths reach the pinning mechanism, both workers become restricted to Core 0 even after the requests finish.
A subsequent burst can then leave one worker waiting while Core 1 is available. That observation need not imply a broken scheduler: the runnable tasks may no longer have permission to use the free core. The example describes a possible sequence, not a measured load imbalance or a guarantee about which core wins the first dispatch.
Record affinity before the optional feature, immediately after it, and during later ordinary work. Distinguish affinity from a momentary execution sample. The IDF additions API provides xTaskGetCoreID() for the task’s configured affinity in this kernel; xPortGetCoreID() identifies the executing core. Repeatedly observing one core before the feature does not establish that the task was already pinned.
Use preallocated integer records and inspect them after the interval. Avoid adding a floating-point diagnostic helper before the operation whose first hardware use you are trying to observe. A pair of separate readbacks is not an atomic trace of the exact first instruction.
Make the placement policy deliberate
If a worker needs predictable placement, select its core at creation and budget the complete workload on that core. This does not make the workload faster; it removes an implicit runtime choice from the product’s placement plan.
If the design needs migratable front-end work, consider a bounded handoff to a deliberately placed numerical worker. That is an architectural option requiring queue-capacity and latency tests, not an automatic fix. Do not force a task’s internal affinity field back to unrestricted after FPU use; doing so would bypass the context-management assumption being reviewed.
For release qualification, retain the chip, SDK, selected kernel, compiler options and linked disassembly. Exercise the optional path for the first time under differing two-core workloads, then repeat it after pinning has occurred. Compare task affinity, per-core load, completion latency and numerical correctness. Include a feature-disabled control and verify that its binary truly avoids the relevant hardware instructions.
Repeat this review after changes to calibration, formatting, libraries or compiler settings. The acceptance question is whether the actual post-feature affinity map still meets the product’s throughput and latency requirements. Source inspection establishes the mechanism; compilation, instruction traces, scheduling measurements and target numerical tests remain unperformed here.