ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FreeRTOS多核SMP架构深度解析——从调度模型到双核IPC实战

FreeRTOS多核SMP架构深度解析——从调度模型到双核IPC实战 TL;DRFreeRTOS v11正式合并SMP支持——单一内核实例管理所有核、全局共享调度对象、但刻意不做负载均衡用确定性换取吞吐。本文拆解其调度模型、三层同步机制与移植层接口并给出配套的双核IPC实战设计当硬件异构核、无cache一致性走不了SMP时如何用邮箱中断共享内存无锁环形缓冲搭建AMP通信面——控制面与数据面分离的完整方案附代码骨架与cache维护细节。目录为什么嵌入式需要多核RTOSSMP核心设计数据结构变了什么调度模型Run-to-Completion不做负载均衡同步机制三层保护体系Tick分布与中断处理移植层Port新增接口SMP vs AMP架构分水岭判定双核IPC实战邮箱中断共享内存已知坑点清单总结参考一、为什么嵌入式需要多核RTOS1.1 硬件现状多核已无处不在过去五年嵌入式SoC的多核化速度超出预期芯片核配置典型产品场景ESP32-P4双核RISC-V 400MHzAIoT网关、屏驱设备RP2040/RP2350双核Cortex-M0/M33通用MCU、外设密集型i.MX RT1170M7M4 异构电机控制HMIWiFi/BT Combo SoC协议核应用核TWS耳机、智能音箱N22D25类异构双核RISC-V DSP端侧AI、音频处理单核频率墙已到48MHz→64MHz容易300MHz→600MHz意味着功耗和成本翻倍。多核用两个中等频率的核替代一个高频核同时天然隔离了实时任务与非实时任务。1.2 软件的尴尬单核内核管不了双核v10及以前的FreeRTOS内核里全局只有唯一一个pxCurrentTCB指针——当前运行任务在单核世界是唯一定义的。双核芯片上直接跑两份单核内核AMP任务无法自然共享队列、信号量、事件组全部割裂跨核通信要自己造轮子。业界等一个官方答案等了六年。2024年FreeRTOS v11.0正式合并SMP分支源自Xilinx的维护工作嵌入式世界才有了一个内核管所有核的生产级方案。1.3 两个核心问题本文回答两个递进的问题SMP版FreeRTOS内部怎么设计的——调度、同步、中断、移植层四个维度什么时候SMP走不通、双核IPC该怎么设计——异构无cache一致性的硬件上AMP通信面的完整工程方案二、SMP核心设计数据结构变了什么理解SMP版最快的方式数据结构基本没变变的是保护方式。2.1 从单指针到指针数组/* 单核≤v10全局唯一当前任务 */PRIVILEGED_DATA TCB_t*volatilepxCurrentTCBNULL;/* SMPv11每核一个当前任务 */PRIVILEGED_DATA TCB_t*volatilepxCurrentTCBs[configNUMBER_OF_CORES];这一个改动是所有变化的源头。内核里大量pxCurrentTCB的使用点现在都要回答哪个核的当前任务——v11通过portGET_CORE_ID()在每个API入口处获取当前核号。2.2 关键数据结构对照表对象单核≤v10SMPv11当前任务pxCurrentTCB单指针pxCurrentTCBs[N]指针数组就绪链表全局一套pxReadyTasksLists[]仍是全局一套共享非每核复制Idle任务一个每核一个Idle 一个Passive Idle调度触发portYIELD()本核切换新增portYIELD_CORE(x)定向触发临界区本核关中断本核关中断 自旋锁挂起调度器挂起本核调度挂起所有核的调度注意就绪链表的设计选择全局共享而非每核独立。这意味着任何核都能看到全部就绪任务任务与核的绑定是调度时的选择而非数据结构的隔离——这是SMP语义任务可在任意允许的核上运行的基础。2.3 任务创建affinity从出生开始/* v11新增API创建时直接指定核亲和性 */TaskHandle_t xTaskHandleNULL;xTaskCreateAffinitySet(vAudioTask,/* 任务函数 */Audio,/* 名字 */512,/* 栈深度 */NULL,/* 参数 */configMAX_PRIORITIES-1,/* 高优先级 */(10),/* ★affinity掩码只允许跑在核0 */xTaskHandle);/* 运行时调整 */vTaskCoreAffinitySet(xTaskHandle,tskNO_AFFINITY);/* 放开任意核 */vTaskCoreAffinityGet(xTaskHandle);/* 查询当前掩码 *//* v11.1新增限制任务在指定核上的时间片防抢占配对 */vTaskCoreAffinitySetBeforeSleep(xTaskHandle,0);/* 睡眠时不占核 */掩码语义位i置1表示允许在核i运行。tskNO_AFFINITY0xFFFFFFFF…为默认值任意核可跑。三、调度模型Run-to-Completion不做负载均衡这是v11 SMP最核心的设计决策也是与Linux SMP的根本分歧点。3.1 调度规则一句话版本每个核从全局就绪队列中挑选允许跑在自己身上的最高优先级任务。没有工作窃取work stealing没有负载追踪没有迁移启发式。核0闲着、核1上有个可迁移的低优先级任务在忙——不迁移。核0空转。3.2 为什么不做均衡是正确设计负载均衡的成本在嵌入式实时场景被急剧放大cache局部性破坏任务在核间漂移L1 cache全部失效WCET最坏执行时间抖动数倍调度延迟不可分析迁移决策本身需要时间且使任务启动时刻变成概率分布——硬实时认证IEC 61508类直接没得谈收益错位Linux均衡是为吞吐服务CPU利用率嵌入式要的是抖动下界电机控制周期、音频编解码deadline一个数字对比电机控制任务要求每1ms±5µs准时执行。负载均衡下这个±5µs是守不住的固定affinity绑核抖动可压到±1µs内。确定性优先于吞吐是实时系统与通用OS的分水岭。3.3 代价与应对affinity规划成为设计活动不做均衡的直接后果affinity规划错误 核饥饿。若三个高优先级任务都绑在核0核1永远闲置核0上低优先级任务全部饿死。工程实践守则1. 实时关键任务控制环、音频pipeline→ 独占绑定专用核 2. 非实时任务日志、OTA、网络协议→ tskNO_AFFINITY浮动填缝 3. 中断密集型任务 → 绑到处理该中断的核减少IPI 4. 共享cache/TCM的核如双核紧耦合→ 考虑按数据流分核而非按负载规划好的affinity约等于单核时代的优先级规划——它是设计出来的不是运行时优化出来的。四、同步机制三层保护体系多核并发的本质难题两个核同时执行内核代码。v11用三层机制解决每层适用不同场景。4.1 第一层临界区 关中断 自旋锁/* v11的taskENTER_CRITICAL()展开后的语义 */taskENTER_CRITICAL();/* ① 本核关中断防本核ISR抢占打脸 ② 获取自旋锁防其他核同时进入 - 若锁被他核持有自旋等待忙等不睡眠 ③ 之后的代码在整个系统层面独占执行 */ulSharedCounter1;taskEXIT_CRITICAL();/* ④ 释放自旋锁 恢复本核中断 */关键细节自旋期间本核中断是关闭的。因此自旋等待的时间上界 持锁方临界区长度而持锁方不会被自己的中断打断——这保证了自旋不会无限期。代价是临界区必须极短内核内部都是几十条指令的链表操作。v11把原来单核版的大临界区拆分成了多把细粒度自旋锁就绪链表一把、延时链表一把、事件等待一把……缩小争用域减少自旋概率。这是从单核思维到多核思维最典型的代码演化。4.2 第二层挂起调度器 全核挂起/* 单核习惯挂起期间我只是暂时不让出CPU */vTaskSuspendAll();/* 长一点的互斥操作允许中断 */xTaskResumeAll();/* SMP语义变化SuspendAll挂起的是【所有核】的调度器 *//* 每个核各自进入挂起计数任何核都不发生任务切换 *//* 直到所有核都ResumeAll后调度才恢复 */从单核代码迁移时这是最容易踩的语义变化——原来挂起调度器影响我自己现在影响全系统。若某核忘配对的ResumeAll全系统调度冻结。4.3 第三层IPI核间中断——跨核联动灵魂/* 场景核0上的任务向满队列发送数据 核1上的接收者正在阻塞等待 */xQueueSend(xQueue,item,0);/* 内核发现队列已有等待者且它阻塞在核1 *//* → 需要立刻唤醒核1的接收者 *//* → 但核1正跑着别的任务它自己不知道该切换了 */portYIELD_CORE(1);/* ★向核1发核间中断 *//* 核1的IPI handler → 触发本核调度 → 接收任务被调度运行 */IPI的硬件实现因架构而异架构IPI机制发送动作RISC-VCLINT的MSIP寄存器往目标核MSIP写1Cortex-MNVIC软件中断每个NVIC带目标核号设置目标核NVIC pending位ESP-IDFSW interrupts esp_crosscore_int写共享寄存器触发没有IPI跨核的事件通知只能靠轮询——SMP的实时性就名存实亡。做port时IPI是第一个要验证的功能。4.4 自旋锁的经典陷阱跨核优先级反转核0低优先级任务 L 持有自旋锁 S正在临界区内 核1高优先级任务 H 尝试获取 S → 自旋等待 分析 - L不会被核0上的其他任务抢占吗会被——但若被抢 L迟迟不释放SH在核1上无限自旋关中断状态 - 核1上的所有中断都被H的自旋关掉了 → 核1中断全丢 v11的防线是【约定】而非硬件 ★自旋锁临界区内禁止调用任何可能阻塞的API★ 不许send带超时、不许take信号量、不许vTaskDelay 规则靠纪律保证硬件不帮你兜底。五、Tick分布与中断处理5.1 单Tick核模型v11默认只有一个核处理tickconfigTICK_CORE配置通常核0tick中断仅核0接收 │ ├─ 遍历延时链表唤醒到期任务 ├─ 检查每个核是否需要切换更高优先级任务 │ ├─ 核0需要 → 本核直接调度 │ └─ 核1需要 → portYIELD_CORE(1) 发IPI └─ 时间片轮转记账同优先级任务为什么不让每个核都有自己的tick——多tick意味着延时链表的多核并发访问激增且tick漂移让时间语义复杂化。单tick核用一次IPI的代价换全局时间一致性划算。5.2 ISR的FromISR规则任何核的ISR都可以安全调用FromISR API/* 核1上的GPIO中断唤醒核0上的等待任务 */voidvGPIO_IRQHandler(void)/* 跑在核1 */{BaseType_t xHigherPriorityWokenpdFALSE;xSemaphoreGiveFromISR(xSem,xHigherPriorityWoken);/* 内核内部找到等待者所在核核0*//* 自动调用 portYIELD_CORE(0) —— 跨核唤醒对用户透明 */portYIELD_FROM_ISR(xHigherPriorityWoken);/* 本核退出处理 */}用户不需要关心等待者在哪个核——内核自动路由IPI。这是SMP版本把复杂度吃进内核的典型设计。5.3 Tickless IdleSMP下的复杂度暴涨单核tickless所有任务睡眠→停tick→进低功耗→定时器唤醒。双核SMP下核的睡眠状态组合变成2^核数种核0深睡核1浅睡谁保持time base唤醒时如何重同步tickv11.1才逐步完善多核ticklessconfigTICK_CORE与空闲协调。低功耗产品选型时这是必查项。六、移植层Port新增接口给新芯片做SMP port相比单核port新增的契约接口语义实现要点portYIELD_CORE( xCoreID )向指定核发IPIRISC-VCLINT MSIP写1要求极低延迟portGET_CORE_ID()当前核号高频调用每个内核API入口必须O(1)——读CSR或内存变量portSET_INTERRUPT_MASK_FROM_ISR()ISR内原子关中断现在要考虑多核并发非仅本核中断portMEMORY_BARRIER()内存屏障RISC-V弱内存序下必需见下文每核栈初始化每个核的调度栈独立pxPortInitialiseStack按核分配RISC-V的特殊课题内存屏障RISC-V是弱内存序模型RVWMO——一个核上的写操作另一个核看到的顺序可能与执行顺序不同/* 经典丢失更新问题 *//* 核0 */shared_data42;/* ① 写数据 */mailbox-flag1;/* ② 发标志 *//* 核1 */if(mailbox-flag1)/* ③ 读到flag1 */use(shared_data);/* ④ 却可能读到旧值*//* RVWMO下①②可能被重排为②①或④提前执行 *//* 修复fence隔离 *//* 核0 */shared_data42;__asmvolatile(fence rw, rw);/* 写写屏障 */mailbox-flag1;/* 核1 */if(mailbox-flag1){__asmvolatile(fence rw, rw);/* 读写屏障 */use(shared_data);/* 保证读到42 */}Cortex-M是强内存序TSO单核习惯迁到RISC-V时fence缺失是偶发诡异bug的头号来源——测试千百次正常量产万台出三台。硬件前提cache一致性官方SMP port假设硬件cache一致性ACE/CHI总线或共享L2且核间可见。无硬件一致性的系统很多MCU的双核是各自L1无一致性协议做不了标准SMP——这正是下一节AMP的存在理由。七、SMP vs AMP架构分水岭判定7.1 两张架构图SMPFreeRTOS v11多核 AMP两个独立内核实例 ┌─────────┬─────────┐ ┌─────────┐ ┌─────────┐ │ 核0 │ 核1 │ │ 核A │ │ 核B │ │ Audio │ App │ │ 协议栈 │ │ 音频DSP │ ├─────────┴─────────┤ ├─────────┤ ├─────────┤ │ 一个调度器 │ │RTOS实例1 │ │RTOS实例2 │ │ 全局任务/队列 │ ├─────────┴──────┴─────────┤ │ 统一优先级空间 │ │ 通信层共享内存邮箱中断 │ │ 需硬件cache一致 │ │ cache需软件维护 │ └───────────────────┘ └───────────────────────────┘ 任务对所有核可见 任务归属固定跨核显式消息 一处affinity规划 两套优先级空间各自规划 单一时间基准 各核独立tick需对齐协议7.2 判定决策树你的双核硬件 │ ├─ 同构ISA 硬件cache一致性 │ │ │ ├─ 是 → SMPv11✓ 统一调度开发体验最好 │ │ 典型ESP32-P4、RP2350、双A核Linux裸机除外 │ │ │ └─ 否 → AMP ✓ 各核独立RTOS 自建IPC │ 典型异构M7M4 / N22D25、无一致性MCU、 │ 一核跑RTOS一核跑裸机DSP固件 │ └─ 混合形态一核跑Linux一核跑FreeRTOS → 也是AMP走OpenAMP/remoteproc框架本文不展开异构双核WiFi/BT Combo、音频SoC的RISC-VDSP几乎必然走AMP——两家不同厂商的核ISA都不同SMP无从谈起。八、双核IPC实战邮箱中断共享内存这是AMP的配套工程。设计目标控制面低延迟、数据面高吞吐、无锁、cache安全。8.1 总体架构控制面与数据面分离┌─────── 核A协议栈/实时任务───────┐ ┌─────── 核B音频DSP/应用───────┐ │ │ │ │ │ IPC客户端 │ │ IPC服务端 │ │ ├─ ipc_send(msg) │ │ ├─ Mailbox IRQ handler │ │ │ ├─ 写共享ring数据面 │ │ │ └─ 释放信号量唤醒任务 │ │ │ ├─ cache clean │ │ ├─ 服务任务 │ │ │ └─ 写Mailbox门铃控制面───┼──────┼→ │ ├─ 处理消息 │ │ └─ ipc_recv() │ │ │ └─ 回执同样走ring门铃 │ │ │ │ │ └─────────────────────────────────────┘ └─────────────────────────────────┘ │ ▲ └──────────► 共享内存区SRAM两核地址空间各有一份映射◄──┘ ┌─────────────────────────────┐ │ 邮箱寄存器门铃/doorbell │ ← 写入触发对方核中断 │ 环形缓冲描述符head/tail │ │ 消息槽 × N │ │ 大块数据缓冲池可选 │ └─────────────────────────────┘分工原则控制面Mailbox门铃传递有事件的信号极小数据量硬件中断直达延迟微秒级数据面共享内存ring承载实际消息与数据流无锁设计吞吐只受内存带宽限制两者严格分离的原因中断路径要短ISR里只做唤醒不搬数据数据路径要宽任务上下文里慢慢搬——混在一起则两头都做不好。8.2 数据面单生产者单消费者无锁环形缓冲SPSCSingle-Producer Single-Consumerring是无锁跨核通信的黄金结构——每个指针只有一个核有权写天然免锁/* 共享内存中的ring描述符两核各自映射同一物理地址 */typedefstruct{volatileuint32_thead;/* 只有生产者核可写 */volatileuint32_ttail;/* 只有消费者核可写 */uint32_tsize;/* 槽数量2的幂 *//* 消息槽紧随其后 */}ipc_ring_t;/* 生产者核A入队一条消息 */intipc_push(ipc_ring_t*r,constipc_msg_t*msg){uint32_theadr-head;/* 只读自己的 */uint32_tnext(head1)(r-size-1);if(nextr-tail)/* 满消费者的tail */return-1;/* 满则丢/等待绝不覆盖 */ipc_slot_write(r,head,msg);/* 写消息槽 *//* ★cache维护让核B看到新数据 */cache_clean_range(slot_addr(r,head),SLOT_SIZE);r-headnext;/* 发布只写自己的指针 */cache_clean_range(r-head,4);/* 指针本身也要clean */__asmvolatile(fence rw, rw);/* 保证数据先于指针可见 */return0;}/* 消费者核B出队 */intipc_pop(ipc_ring_t*r,ipc_msg_t*msg){uint32_ttailr-tail;if(tailr-head)/* 空生产者的head */return-1;/* ★cache维护核B的cache里可能是旧数据先无效化 */cache_invalidate_range(slot_addr(r,tail),SLOT_SIZE);ipc_slot_read(r,tail,msg);r-tail(tail1)(r-size-1);cache_invalidate_range(r-head,4);/* 下次读head拿新值 */return0;}为什么它无锁是正确的head只有核A写、tail只有核B写读写交错不会撕裂满与空用一格容量差区分判断只用对方的旧值——旧值只会导致保守判断误认为满/空不会导致数据错误。这个结构与VCD波形采集工具的ring buffer是同一个模式——单写者单读者的SPSC ring是嵌入式无锁设计的万能积木。8.3 控制面Mailbox门铃中断数据放进ring后要让对方核立刻知道——这就是门铃doorbell/* 核A发门铃RISC-V平台示例 */staticinlinevoidipc_doorbell(uint32_ttarget_core){/* 实现一CLINT MSIP若两核共享CLINT且可互写 */CLINT-MSIP[target_core]1;/* 实现二SoC自定义邮箱寄存器更常见 *//* MAILBOX-DOORBELL[ target_core ] DOORBELL_MAGIC; */}/* 核B门铃中断服务程序——极短只做唤醒 */voidvMailbox_IRQHandler(void){BaseType_t xWokenpdFALSE;MAILBOX-DOORBELL[THIS_CORE]0;/* 清门铃写1清0或W1C看硬件 *//* ISR铁律不搬数据、不处理消息只给任务发信号 */xSemaphoreGiveFromISR(xIpcSem,xWoken);portYIELD_FROM_ISR(xWoken);}/* 核B服务任务——所有消息处理在这里 */voidvIpcServerTask(void*arg){ipc_msg_tmsg;for(;;){xSemaphoreTake(xIpcSem,portMAX_DELAY);/* 平时睡眠门铃唤醒 */while(ipc_pop(g_ring,msg)0)/* 榨干ring */ipc_dispatch(msg);/* 业务处理 *//* 若ring曾满导致生产者丢帧这里可检查丢帧计数器做补救 */}}门铃可以合并核A连发三条消息只响一次铃也没关系——服务任务唤醒后会while榨干整个ring。这就是信号与数据分离的容错红利信号丢一次不影响数据完整性只是延迟一拍。8.4 cache维护的完整心智模型无硬件一致性时两核各自cache看到的内存可能不同。维护规则只有两条但必须做对顺序写共享内存的一方生产者核A ① 写数据到共享内存 ② cache CLEAN回写把核A cache行刷到真实SRAM —— 没有这步数据还在核A的cache里核B读SRAM拿到旧值 ③ 发布指针head同样clean ④ fence保证②③不被重排到①之前 读共享内存的一方消费者核B ① 读到指针变化head动了 ② cache INVALIDATE无效化丢弃核B cache里的旧数据行 —— 没有这步核B读的是自己cache里的陈旧副本 ③ 读数据 ★cache行对齐陷阱clean/invalidate的最小单位是cache行32/64B。 若一条消息横跨两个cache行且相邻行是对方核正在写的别的数据 invalidate可能误伤。对策消息槽按cache行大小对齐sizeof(msg)向上取整到32/64的倍数。8.5 与TF-M双核Mailbox的同构性这套ring门铃回执的设计与Arm PSA的TF-M双核方案参见 [[TF-M双核通信深入——Mailbox-RPC机制全链路]]是同构的不同实现设计要素TF-M MailboxNSPE↔SPE本文AMP IPC数据面共享内存队列 传输句柄SPSC环形缓冲控制面mailbox doorbell中断门铃寄存器中断请求语义RPC带completion回执消息回执可选cache维护平台相关的cache驱动钩子clean/invalidate 对齐排他保护硬件semaphore/spinlockSPSC结构天然免锁区别在语义层级TF-M mailbox承载的是RPC调用请求-响应配对本文方案是裸消息流可配回执做成RPC。设计模式可以平移语义按需裁剪——掌握了这个骨架读任何芯片SDK的双核通信代码Telink、恒玄、络达的Combo芯片固件都是同一套东西换了寄存器名。8.6 工程化补充版本握手与心跳量产产品的IPC层还缺三块拼图1. 版本握手启动时 两核固件版本必须匹配消息格式ABI。共享内存固定地址放 IPC_ABI_VERSION字段两核启动各自写入自己的版本首个门铃 交换后校验——不匹配则进入安全模式拒绝通信防OTA只升一半。 2. 心跳运行时 对方核死机检测——每100ms互发心跳消息超3次未收到判定 对核失联触发降级策略音频核失联→协议核静音等待复位。 3. 丢帧统计调试 ring满导致的丢弃必须计数并周期上报否则量产偶发数据 少了一段无从定位。这与VCD工具丢数据必须可见是同一原则。九、已知坑点清单#坑症状预防1自旋锁临界区内调用阻塞API偶发系统冻结某核中断全丢代码评审硬规则临界区内只做链表/赋值级操作2affinity全绑一个核另一核永久Idle被绑核低优先级任务饿死启动时打印每核任务分布表评审确认3RISC-V缺fence千次测试正常量产偶发数据错乱跨核数据结构访问统一走封装API内嵌屏障4cache行未对齐invalidate误伤相邻数据诡异串扰共享消息槽强制32/64B对齐静态断言5Message Buffer当多读多写用数据撕裂、丢消息FreeRTOS明确单读单写多方访问需外层互斥或改用队列6忘记clean就发门铃对核读到旧数据/空消息push流程模板化写数据→clean→更新指针→fence→门铃7ISR里处理消息门铃延迟抖动、其他中断被压ISR只给信号量处理全部在任务上下文8两核tick未对齐超时语义不一致日志时间线错乱启动握手同步起始tick或共享一个时间源十、总结SMP版FreeRTOS的数据结构近乎没变变的是保护方式——就绪链表仍全局一套临界区从关中断升级为关中断自旋锁这是理解v11源码的最短路径。不做负载均衡是刻意的实时性选择任务不迁移保住了cache局部性与可分析的调度延迟代价是affinity规划成为显式设计活动——规划错误等于核饥饿。三层同步各司其职临界区短互斥、挂起调度器长互斥、全核生效、IPI跨核联动——其中IPI是SMP的灵魂port的第一验证项。SMP有硬件门槛同构ISA硬件cache一致性缺一不可。异构双核协议核DSP、RISC-VM核走AMP两个独立RTOS实例靠IPC通信。AMP的IPC骨架是门铃SPSC ringcache维护三件套控制面与数据面分离、单写者指针免锁、clean/invalidate按序执行——这套模式从MCU双核到TF-M安全世界通信通用。
返回列表