Consumer-grade, industrial-grade, and automotive-grade multi-core MCUs have gradually become popular from high-end applications, especially in the automotive sector where some high-end specifications have reached six cores. The operating systems for automotive multi-core MCUs are generally adapted by basic software manufacturers, with FreeRTOS being less common. However, automotive MCUs still hold significance for applications that do not require AUTOSAR specifications or for early performance validation and evaluation of MCUs. Recently, there has been a project requiring the evaluation of Qichip Micro’s FC7300F8MDT, which features three Cortex-M7 application cores (including two pairs of lock-step cores) with a maximum frequency of 300 MHz and parallel processing capabilities, along with 8MB Flash and 1.1MB RAM. The FreeRTOS SMP (Symmetric Multi-Processing) version is lightweight enough to be an ideal choice for evaluating complex applications of multi-core MCUs and collaborative task scheduling solutions. For more information on FreeRTOS SMP, you can visit the official website at https://www.freertos.org/zh-cn-cmn-s/Documentation/02-Kernel/02-Kernel-features/13-Symmetric-multiprocessing-introduction; I will not elaborate here. Since the FreeRTOS official library has a rich adaptation for different types of single-core systems, common core porting can generally ignore the specific details of how the OS implements scheduling and context switching. The adaptation for different types of homogeneous multi-core systems currently only includes:
- XCORE AI
- Raspberry Pi Pico
The former is an Intel core with Espressif’s Wi-Fi MCU, while the latter is the Raspberry Pi’s RP2040 Cortex-M0+ dual-core. Currently, there is no ready-made multi-core adaptation for Cortex-M7 in the official library. I referenced the Raspberry Pi and the single-core adaptation of CM7 for the multi-core interface. The entire porting adaptation process is not as simple as copying a model; here I would like to share some insights and experiences from the porting process. 1> SMP can be constructed even without support for atomic instructions. A multi-core spinlock is one of the most important mutual exclusion mechanisms. FreeRTOS SMP requires the implementation of a recursive self-locking method, based on which task locks and interrupt locks (which can be nested) are implemented. For MCUs that support atomic instructions, the exclusive feature of atomic instruction bus reads can be used to implement software spinlocks. However, considering CPU performance, the 7300 has cut support for atomic instructions, so a mutex application using the Mailbox peripheral is adopted to implement the spinlock.
// Mutex lock low-level implementation
static inline Std_ReturnType vportspinlock_acquire(volatile uint32 *pSpinLock,uint8_t channel){
Std_ReturnType eStatus = E_NOT_OK;
if (0u == *pSpinLock)
{ /* get sema lock */
if (0u != MB_HWA_LockChannel(channel))
{ if (0u == *pSpinLock)
{ *pSpinLock = 1u;
MCAL_DATA_MEMORY_BARRIER();
eStatus = E_OK;
}
/* release channel */
MB_HWA_ReleaseChannel(channel);
}
}
return eStatus;
}
static inline Std_ReturnType vportspinlock_lock(uint32 *pLock,uint8_t channel){
Std_ReturnType eSucc = E_OK;
/* PRQA S 2905 ++ Constant: Positive integer value truncated by cast to a smaller unsigned type. REASON: The actual timeout period may vary depending on the MCU frequency. */
uint32 u32Timeout = OS_SPINLOCK_TRY_MAX_CNT;
/* PRQA S 2905 -- */
while ((0U < u32Timeout) && (E_OK != vportspinlock_acquire(pLock,channel)))
{
u32Timeout--;
}
if (0U == u32Timeout)
{
eSucc = E_NOT_OK;
}
return eSucc;
}

2>FreeRTOS SMP only supports the static creation of tasks and cannot recreate tasks within tasks.
FreeRTOS SMP usesa global unified ready queue, which is shared by all cores. New tasks must be added to the queue through<span><span>prvAddNewTaskToReadyList()</span></span>. If other cores are making scheduling decisions at this time (such as<span><span>vTaskSwitchContext()</span></span>), it may lead to data corruption in the queue (such as linked list breakage or priority disorder) due tolack of atomic protection, which may result in task loss, scheduler crashes, or deadlocks.
3>In FreeRTOS SMP, the main core core0 is responsible for updating the global time base and notifying other cores to switch tasks as needed through inter-core communication.
The complete process of task switching is as follows:
- Core0 detects the need for a task switch (such as a high-priority task becoming ready).
- Core0 sets the xYieldPendings flag for the target core.
- Core0 sends an IPI to the target core (Core1 or Core2) through prvYieldCore().
- The target core receives the IPI interrupt and enters IPI_IRQHandler.
- The target core triggers the PendSV interrupt.
- The task context switch is completed in the PendSV handler.
Figure 3: Multi-core task switching flowchart
4> Considering the specific core exclusivity of certain peripherals and execution efficiency, all tasks are not migrated across cores but are bound to run on specific cores.
#define CORE0 (1<<0)
#define CORE1 (1<<1)
#define CORE2 (1<<2)
xTaskCreateAffinitySet( Task100msEntry_c2,"Task_100ms_c2",configMINIMAL_STACK_SIZE,
mainREG_TEST_TASK_1_PARAMETER,5,CORE2,
&c2_task100ms_handle ); // Bound to run on CORE2
5, Atomic operations are implemented through critical sections. Refer to the OS header file atomic.h
static portFORCE_INLINE uint32_t Atomic_Add_u32( uint32_t volatile * pulAddend,
uint32_t ulCount ){
uint32_t ulCurrent;
ATOMIC_ENTER_CRITICAL();
{
ulCurrent = *pulAddend;
*pulAddend += ulCount;
}
ATOMIC_EXIT_CRITICAL();
return ulCurrent;
}
6, Implementation of the task switching interrupt function. Some code examples are as follows:
uint8_t coreid = Cpm_HWA_GetCoreId(); // Get current core ID
__asm volatile (
" mrs r0, psp \n" // Load current task's stack pointer
" isb \n"
" mov r9, %[core_id] \n" // Store core ID in R9 (needs protection)
/* Multi-core critical modification 1: Index TCB array by core ID */
" ldr r3, =pxCurrentTCBs \n" // Load base address of TCB array
" lsl r1, r9, #2 \n" // Core ID ×4 (32-bit pointer)
" ldr r2, [r3, r1] \n" // Load current core's TCB address[6](@ref)
" stmdb sp!, {r0, r3, r9} \n" // Temporarily save registers
" mov r0, %[max_pri] \n"
" mov r0, r9 \n"
" bl vTaskSwitchContext \n"
: [core_id] "r" (coreid), // Input core ID
)//
DEMO Example
static void prvSetupHardware( void ){
if (0U == GET_CPU_ID())
{
Bsp_Mcu_Init();
Bsp_Port_Init();
Bsp_Mb_Init();
}
else if (1U == GET_CPU_ID())
{
Bsp_Mcu_Init();
Bsp_Mb_Init();
}
else if (2U == GET_CPU_ID())
{
Bsp_Mcu_Init();
Bsp_Mb_Init();
}
else
{
}}
int main( void ){
/* Configure the hardware ready to run the demo. */
prvSetupHardware();
if (0U == GET_CPU_ID())
{
xTaskCreateAffinitySet( prvRegTestTaskEntry1,"Task_100ms",configMINIMAL_STACK_SIZE,
mainREG_TEST_TASK_1_PARAMETER,5,CORE0,&c0_task100ms_handle);
xTaskCreateAffinitySet( prvRegTestTaskEntry2,"Task_100ms",configMINIMAL_STACK_SIZE,
mainREG_TEST_TASK_1_PARAMETER,5,CORE1,
&c1_task100ms_handle );
xTaskCreateAffinitySet( Task100msEntry_c2,"Task_100ms_c2",configMINIMAL_STACK_SIZE,
mainREG_TEST_TASK_1_PARAMETER,5,CORE2,
&c2_task100ms_handle );
}
vTaskStartScheduler();
for( ;; );
return 0;
}