
FreeRTOS SMP extends the single-core scheduler to multiple identical cores sharing memory. The kernel runs as one logical instance: a single shared array of ready lists (pxReadyTasksLists) is used across all cores. Cores concurrently access these lists to find the highest-priority task whose affinity mask allows it to run on that core. This works well for throughput-oriented workloads but introduces non-determinism for hard real-time tasks if tasks are allowed to float between cores.
The SMP scheduler relies on a few key configuration constants:
configNUMBER_OF_CORES — number of cores (1 = single core, >1 = SMP)configUSE_CORE_AFFINITY — enables vTaskCoreAffinitySet()/Get()Note that task affinity defaults to tskNO_AFFINITY (all cores) when created.
On a dual-core RP2040 (Cortex-M0+) or dual-core ESP32 (Xtensa LX6), the same FreeRTOS image runs on both cores. Core 0 starts first, initializes the kernel, creates tasks, and then calls vTaskStartScheduler(). The port-specific startup code within the scheduler automatically releases Core 1 from reset and starts scheduling on it—you do not call vTaskStartScheduler() on Core 1 manually. Note that FreeRTOS SMP requires identical cores (Symmetric Multiprocessing) and cannot be used on asymmetric chips like the STM32H7 (Cortex-M7 + M4), which require an AMP (Asymmetric Multiprocessing) approach.
/* Set affinity mask: bit 0 = core 0, bit 1 = core 1, etc. */void vTaskCoreAffinitySet(TaskHandle_t xTask, UBaseType_t uxAffinityMask);/* Get current affinity mask */UBaseType_t uxTaskCoreAffinityGet(TaskHandle_t xTask);/* Example: pin a task to core 1 only */vTaskCoreAffinitySet(xHandle, (1UL << 1));/* Example: allow task to run on cores 0 and 2 (3-core system) */vTaskCoreAffinitySet(xHandle, (1UL << 0) | (1UL << 2));
+-----------------------------------------------------------+| SHARED READY LISTS (pxReadyTasksLists) || || Priority 5: [Task A (Core 0/1)] [Task F (Core 0 Only)] || Priority 4: [Task D (Core 0/1)] [Task E (Core 0/1)] |+-----------------------------------------------------------+^ ^| |+-------+--------+ +-------+--------+| CORE 0 | | CORE 1 || Checks highest | | Checks highest || priority task | | priority task || matching its | | matching its || core mask (0) | | core mask (1) |+----------------+ +----------------+
When a core becomes idle or a scheduling event occurs, the core checks the shared pxReadyTasksLists array. It scans from the highest priority downwards, looking for a ready task whose affinity mask has the bit for this core set. The list itself is protected by spinlocks since multiple cores may attempt to access it simultaneously.
Note: The RP2040 (Cortex-M0+) has no CPU data caches, so cache maintenance cost is zero on that platform. The table below represents a cached Cortex-M33 system (e.g., RP2350).
| Phase | Cycles | Time (µs) |
|---|---|---|
| IPI send + ack | ~800 | 4.0 |
| Source context save | ~400 | 2.0 |
| Destination context restore | ~450 | 2.25 |
| Cache maintenance (if needed) | ~1200 | 6.0 |
| Scheduler decision | ~200 | 1.0 |
| Total (with cache maint.) | ~3050 | ~15.25 |
| Total (no cache, e.g., M0+) | ~1850 | ~9.25 |
configUSE_TRACE_FACILITY and use the traceTASK_SWITCHED_IN hook to detect core changes (compare the current core ID against the task’s previous core). There is no built-in taskMIGRATE event, but tools like Percepio Tracealyzer can reconstruct migration timelines from switch-in events. Frequent migrations indicate poor affinity choices.TaskHandle_t xMotorTask;void vMotorControlTask(void *pvParameters) {TickType_t xLastWakeTime = xTaskGetTickCount();for (;;) {/* Read core-local ADC on core 1 */uint16_t adc_val = adc_read_core1(ADC_CH_CURRENT);/* Compute PWM update - cache-hot loop */motor_update_pwm(adc_val);vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(1));}}void main(void) {xTaskCreate(vMotorControlTask, "Motor", 1024, NULL, 5, &xMotorTask);/* Pin to core 1 (bit 1) */vTaskCoreAffinitySet(xMotorTask, (1UL << 1));/* Core 0 runs comms, logging, UI */vTaskStartScheduler();}
| Pitfall | Symptom | Fix |
|---|---|---|
| Pinning too many tasks | Core 0 overloaded, core 1 idle | Reduce pinning; use unpinned for background work |
| Ignoring cache coherency | Stale data on cores with non-coherent L1 caches | Ensure the FreeRTOS port implements cache maintenance macros |
| Assuming FIFO migration order | Priority inversion on migration | Migrations respect priority; high-priority tasks migrate first |
| Not testing on target | Works on QEMU, fails on silicon | Always validate on hardware with cache enabled |
FreeRTOS SMP brings multi-core support to the familiar FreeRTOS API. The key abstraction is task affinity—a bitmask controlling which cores a task may run on. Unpinned tasks migrate automatically for load balancing; pinned tasks stay put for determinism and cache locality. Migration overhead is ~5-15µs on Cortex-M, acceptable for most control loops but problematic for sub-microsecond deadlines. Profile before pinning, pin with purpose, and trace migrations to verify behavior.
tasks.c — vTaskCoreAffinitySet, prvSelectHighestPriorityTask SMP implementationQuick Links
Legal Stuff


