Hello everyone, welcome to <span>LiXin Embedded</span>.
FreeRTOS V11.0 provides support for Symmetric Multi-Processing (SMP) on multicore systems. Previously, multicore development often required using various branch versions of FreeRTOS, which was quite cumbersome. Now, V11.0 integrates SMP directly into the mainline, allowing seamless switching between single-core and multicore. Today, we will discuss how to apply this in practical projects.
Single-Core vs. Multi-Core
Let’s start with some basic knowledge to clarify the difference between single-core and multi-core. Suppose you are using a dual-core Cortex-M33 like the NXP LPC55S69 or the ESP32 S3, which are typical multicore MCUs. Single-core FreeRTOS is well-known: one core runs one scheduler, task switching relies on tick interrupts, and the current task is managed by the pxCurrentTCB pointer, which is simple and direct.

The multicore SMP scenario is more complex but also very interesting. Multiple cores share memory, and the task lists (such as the ready task list) are also shared among all cores. Each core has its own idle task, which runs when there is no work to do. The scheduler works in conjunction with tick interrupts, context switch interrupts, and idle tasks. The key difference is that pxCurrentTCB is no longer a single pointer but an array of pointers, with each core corresponding to a current task.
In a multicore environment, tasks can be bound to run on specific cores (core affinity) or can be scheduled freely. To avoid simultaneous access to shared resources by multiple cores, FreeRTOS uses two spinlocks: one for tasks and one for interrupts, ensuring the safety of critical sections.
Configuring SMP from Config.h

To enable SMP, core configuration is done in FreeRTOSConfig.h, and the most important thing is to inform the system how many cores there are. For example, for a dual-core system, set it as follows:
#define configNUMBER_OF_CORES 2
This line is mandatory; the default is 1 (single-core). Next, there are several optional configurations that I commonly use:
- configRUN_MULTIPLE_PRIORITIES: Set to 0 to ensure that high-priority tasks run first, and low-priority tasks do not interfere when running on multiple cores. If set to 1, tasks of different priorities may run simultaneously on different cores, suitable for certain special scenarios.
- configUSE_CORE_AFFINITY: Set to 1 to enable core affinity, allowing tasks to be specified to run on a particular core. Set to 0, and the scheduler will allocate cores arbitrarily.
- configUSE_TASK_PREEMPTION_DISABLE: Set to 1 to allow certain tasks to disable preemption via API, suitable for tasks that require strict control.
- configUSE_PASSIVE_IDLE_HOOK: Set to 1 to add some background logic in the passive idle task, avoiding the need to create new tasks.
- configTIMER_SERVICE_TASK_CORE_AFFINITY: Set the core affinity for the timer service task, with the default tskNO_AFFINITY indicating no binding.
These configurations may seem numerous, but the core principle is simple: flexibly adjust task allocation and priority strategies according to project needs.
Core Affinity

Core affinity is one of the highlights of SMP, allowing tasks to be fixed to run on a specific core, avoiding arbitrary allocation by the scheduler. For example, if I want to create two tasks, one running on core 0 and the other on core 1, the code looks like this:
TaskHandle_t task0, task1;
xTaskCreate(myCore0Task, core0_task, 800/sizeof(StackType_t), NULL, tskIDLE_PRIORITY+1, &task0);
vTaskCoreAffinitySet(task0, 1<<0); // Bind to core 0
xTaskCreate(myCore1Task, core1_task, 800/sizeof(StackType_t), NULL, tskIDLE_PRIORITY+1, &task1);
vTaskCoreAffinitySet(task1, 1<<1); // Bind to core 1
Here, vTaskCoreAffinitySet is used to set affinity, where 1<<0 indicates core 0 and 1<<1 indicates core 1. If you want to simplify, you can directly use xTaskCreateAffinitySet to create tasks with affinity. I prefer to write them separately because this way the code can run in both single-core and multicore environments, ensuring better compatibility.
In actual projects, core affinity is particularly suitable for scenarios with clearly defined task divisions. For example, in a smart home project, core 0 runs the Wi-Fi protocol stack, while core 1 handles sensor data collection, with no interference between them, and the scheduler does not have to guess where to place tasks.
Starting the Scheduler
Starting the FreeRTOS scheduler is still the same:
vTaskStartScheduler();

In a multicore environment, core 0 (the main core) is responsible for starting the scheduler, handling tick timer interrupts, and waking up other cores (slave cores). Each core has its own idle task, with the main core running the standard idle task and the slave cores running the passive idle task. Tick interrupts and context switch interrupts are usually assigned to core 0 by default, but some hardware supports assigning them to other cores, depending on the chip manual.
