4、FreeRTOS SMP移植:FreeRTOS SMP架构解析、多核启动流程、任务在指定核上的亲和性设置

好,咱们进入正题。这一章聊的是FreeRTOS SMP移植,说白了就是怎么让FreeRTOS在多核芯片上跑起来。我最早接触SMP是在一个智驾域控的项目上,当时芯片是TDA4,四个Cortex-A72加上一堆R5F。嗯,那会儿FreeRTOS的SMP支持还没这么成熟,踩了不少坑。现在回头看,其实核心就三件事:架构怎么设计的、核怎么启动的、任务怎么绑核的。

4.1 FreeRTOS SMP架构解析

先说说SMP和AMP的区别。你想想看,AMP是每个核跑独立的OS,核之间通过IPC通信。SMP呢?所有核共享一个OS实例,共享内存,共享调度器。FreeRTOS的SMP实现,其实是在原版基础上加了一层“核间同步”的机制。

我个人习惯把SMP架构拆成三个层次来看:

  • 调度层:全局就绪列表,所有核共享。哪个核空闲了,就从列表里拿最高优先级的任务跑。
  • 同步层:自旋锁、临界区保护。多核同时访问内核数据结构时,必须加锁。
  • 中断层:IPI(核间中断)用于触发调度、通知事件。

核心数据结构变化不大,但多了几个关键字段。我整理了一张表,方便你对照:

数据结构 原版FreeRTOS SMP版本
TCB(任务控制块) 无核亲和性字段 添加 uxCoreAffinityMask
就绪列表 单链表 全局链表 + 每个核的本地缓存
调度器 单核触发 IPI触发其他核重新调度
临界区 关中断 关中断 + 自旋锁

这里有个关键点:自旋锁。我在项目中遇到过一个问题,两个核同时调用 vTaskDelay(),结果死锁了。为什么?因为一个核拿着锁,另一个核在等,但拿着锁的核被中断打断了,锁一直没释放。后来加了 portDISABLE_INTERRUPTS() 配合自旋锁才解决。

注意:FreeRTOS SMP的自旋锁是“忙等”的。如果临界区代码太长,性能会急剧下降。我建议临界区代码控制在10条指令以内。

4.2 多核启动流程

多核启动,说白了就是“谁先醒,谁叫醒别人”。在ARM Cortex-A系列上,典型流程是这样的:

  1. 主核(Core 0)上电后执行汇编初始化,设置栈指针、清零BSS段。
  2. 主核调用 main(),初始化硬件、创建任务、启动调度器。
  3. 主核通过 SCU(Snoop Control Unit)GIC(通用中断控制器) 发送事件,唤醒从核。
  4. 从核执行自己的启动代码,进入空闲循环,等待调度器分配任务。

代码层面,FreeRTOS SMP提供了一个 vStartScheduler() 函数。但注意,这个函数只在主核上调用一次。从核的入口函数是 vApplicationGetIdleTaskMemory() 或者你自己定义的 secondary_core_main()

我曾经在一个项目里犯过傻——从核启动后直接去访问外设寄存器,结果系统挂了。后来才发现,从核启动时MMU还没配好,访问的都是物理地址。嗯,这里要注意:从核启动后,第一件事是等待主核把内存映射配好

启动流程伪代码:

// 主核
void main(void) {
    hardware_init();        // 初始化时钟、DDR、MMU
    create_tasks();         // 创建应用任务
    vTaskStartScheduler();  // 启动调度器,永不返回
}

// 从核入口(汇编或C)
void secondary_core_entry(void) {
    wait_for_main_core_ready();  // 等待主核发信号
    portENABLE_INTERRUPTS();     // 开中断
    while(1) {
        // 调度器会自动分配任务
        __WFI();  // 等待中断
    }
}

4.3 任务在指定核上的亲和性设置

任务亲和性,就是让某个任务只跑在指定的核上。为什么要这么做?我举几个实际场景:

  • 中断密集型任务:比如CAN接收,绑在Core 1上,避免影响Core 0上的控制算法。
  • 缓存敏感型任务:比如图像处理,绑在某个核上,利用L1/L2缓存的热数据。
  • 隔离关键任务:比如安全监控,独占一个核,防止被其他任务干扰。

FreeRTOS SMP里,亲和性是通过 uxCoreAffinityMask 实现的。这是一个位掩码,每一位代表一个核。比如双核芯片,Core 0是bit 0,Core 1是bit 1。

设置亲和性的API有两个:

  • vTaskCoreAffinitySet(TaskHandle_t xTask, UBaseType_t uxCoreAffinityMask):设置任务可以跑在哪些核上。
  • vTaskCoreAffinityGet(TaskHandle_t xTask, UBaseType_t *puxCoreAffinityMask):查询当前亲和性设置。

实战示例:

TaskHandle_t xSafetyTaskHandle;

void create_safety_task(void) {
    // 创建任务,优先级最高
    xTaskCreate(vSafetyTask, "Safety", 1024, NULL, 5, &xSafetyTaskHandle);
    
    // 只允许在Core 0上运行
    vTaskCoreAffinitySet(xSafetyTaskHandle, (1 << 0));
}

void vSafetyTask(void *pvParameters) {
    for(;;) {
        // 安全监控逻辑
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

这里有个坑,我踩过不止一次:如果所有任务都绑在同一个核上,其他核就闲着。你想想看,那还不如用单核呢。所以亲和性设置要讲究平衡。我个人习惯是:

  • Core 0:跑控制类、中断处理类任务。
  • Core 1:跑数据类、计算密集型任务。
  • Core 2:跑通信类、网络协议栈。
  • Core 3:预留,跑安全监控或故障处理。
避坑指南:我曾经把一个高优先级任务绑在Core 1上,但Core 1上还有个中断服务程序,优先级更高。结果那个任务一直被中断抢占,几乎没机会运行。后来我把中断绑到了Core 2上,问题才解决。记住:亲和性设置要考虑中断分布

最后说一句,FreeRTOS SMP的亲和性设置是“软亲和”,不是硬绑定。什么意思?就是调度器会尽量把任务安排在指定核上,但如果那个核忙不过来,而其他核空闲,调度器可能会“偷跑”。如果你需要硬绑定,得自己改调度器代码,或者用 taskYIELD() 强制切换。

嗯,这一章的内容差不多就这些。总结一下:SMP架构的核心是共享调度器加自旋锁;多核启动要处理好主从核的同步;亲和性设置要结合实际负载,别让核闲着也别让核累死。下一章咱们聊具体移植步骤,到时候拿一个实际芯片做例子,手把手带你过一遍。