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() 配合自旋锁才解决。
4.2 多核启动流程
多核启动,说白了就是“谁先醒,谁叫醒别人”。在ARM Cortex-A系列上,典型流程是这样的:
- 主核(Core 0)上电后执行汇编初始化,设置栈指针、清零BSS段。
- 主核调用
main(),初始化硬件、创建任务、启动调度器。 - 主核通过 SCU(Snoop Control Unit) 或 GIC(通用中断控制器) 发送事件,唤醒从核。
- 从核执行自己的启动代码,进入空闲循环,等待调度器分配任务。
代码层面,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:预留,跑安全监控或故障处理。
最后说一句,FreeRTOS SMP的亲和性设置是“软亲和”,不是硬绑定。什么意思?就是调度器会尽量把任务安排在指定核上,但如果那个核忙不过来,而其他核空闲,调度器可能会“偷跑”。如果你需要硬绑定,得自己改调度器代码,或者用 taskYIELD() 强制切换。
嗯,这一章的内容差不多就这些。总结一下:SMP架构的核心是共享调度器加自旋锁;多核启动要处理好主从核的同步;亲和性设置要结合实际负载,别让核闲着也别让核累死。下一章咱们聊具体移植步骤,到时候拿一个实际芯片做例子,手把手带你过一遍。