3. 异构核间通信机制
在 i.MX8M、i.MX8QXP 这类异构 SoC 上,Cortex-A 核运行 Linux,Cortex-M 核运行 RTOS 或裸机程序,两者之间没有共享的缓存一致性域,也没有统一的地址空间视图。核间通信(IPC)要解决的核心问题不是“怎么发一个字节”,而是“如何在两个独立运行的软件栈之间,建立一条有边界、有通知、有协议约束的数据通道”。本节围绕 RPMsg、Messaging Unit(MU)、共享内存、Mailbox 与中断触发方式,说明它们各自处在通信链路的哪一层,以及如何组合成一条可用的通信协议。
一、先分清“搬运”和“通知”两件事
异构核间通信可以拆成两个正交的动作:数据搬运和事件通知。数据搬运通常由共享内存(Shared Memory / DRAM 或 OCRAM 中的一段保留区域)承担,因为它是双方都能访问的物理介质;事件通知则由 MU、Mailbox 或硬件中断控制器承担,用来告诉对端“数据已经写好了”或“缓冲区已经空了”。把这两件事混在一起,是初学者最常见的错误——例如把数据直接写进 MU 的寄存器,结果发现只能传 4 个 32 位字,根本承载不了图像帧或控制报文。
因此,一条典型的通信链路是:发送方把数据写入共享内存的环形缓冲区 → 更新写指针 → 通过 MU 或 Mailbox 触发对端中断 → 接收方在中断服务程序中读取指针、搬运数据、更新读指针 → 回送确认或触发反向中断。共享内存负责“放”,MU/Mailbox 负责“喊”,中断负责“被听见”。
二、RPMsg:把共享内存包装成“消息总线”
RPMsg(Remote Processor Messaging)是 Linux 内核中成熟的异构通信框架,它建立在 virtio 之上,用 vring(描述符环)管理共享内存中的缓冲区。每个方向一个 vring:A→M 的 vring0 和 M→A 的 vring1。发送方把数据放入空闲缓冲区,填写描述符,更新 avail 索引,然后通过 MU 触发对端中断;接收方在中断中读取 used 索引,取出描述符对应的缓冲区,处理完后归还。
RPMsg 的关键抽象是“端点(endpoint)”。每个端点有一个地址(如 0x400)和一个名字(如 rpmsg-openamp-demo-channel),双方通过名字匹配建立通道。Linux 侧通常用 rpmsg_char 或 rpmsg_tty 暴露给用户空间,M 核侧用 RPMsg-Lite 的 rpmsg_queue_recv() 接收。RPMsg 适合传输长度不固定、频率中等的控制报文,单条消息建议不超过 512 字节,避免占用过多 vring 缓冲区。
三、MU 与 Mailbox:通知通道的两种形态
Messaging Unit 是 i.MX 系列上的专用硬件,提供 4 个发送寄存器(TR0–TR3)和 4 个接收寄存器(RR0–RR3),每个 32 位。它既能传短消息(直接写 TR,对端读 RR),也能作为 doorbell 触发中断。MU 的中断可以配置为“收到消息触发”或“寄存器空触发”,前者用于通知对端有新数据,后者用于通知对端可以继续发送。
Mailbox 是 Linux 内核提供的通用框架,i.MX 的 MU 驱动注册为 mailbox controller。A 核通过 mbox_send_message() 发送一个带通道号的通知,M 核在中断中读取 MU 状态寄存器判断来源。相比直接操作 MU 寄存器,Mailbox 框架屏蔽了不同 SoC 的差异,但代价是增加了抽象层,延迟略高。对于 1 kHz 以下的控制环,这个开销可以接受;对于 10 kHz 以上的硬实时场景,建议 M 核侧直接操作 MU 寄存器。
四、中断触发方式与实时性权衡
中断触发方式直接决定通信延迟和 CPU 占用。常见有三种:
- 边沿触发 + 中断聚合:MU 收到消息时产生一次边沿中断,适合低频控制报文。若短时间内多条消息到达,可配置 MU 的
GIER寄存器做中断合并,减少中断次数。 - 轮询 + 中断兜底:M 核在实时任务中周期性轮询 vring 的 used 索引,只有长时间无数据时才依赖中断唤醒。这种方式延迟确定,适合 1 kHz 以上的控制环。
- Doorbell 中断 + 忙等确认:A 核写 TR 后触发中断,M 核在 ISR 中读取数据并立即回写 RR 作为 ACK,A 核在发送后短暂忙等 RR 置位。适合对往返延迟敏感但数据量小的场景。
fsl,mu 节点的 interrupts 属性与 M 核的向量表一致。五、通信协议设计:从“能通”到“可靠”
有了 RPMsg 和 MU,通信链路已经能跑通,但要让它在实际系统中可靠工作,还需要一层协议设计。建议在 payload 头部固定 8 字节头:
struct ipc_msg_hdr {
uint16_t magic; // 0x4950 ("IP")
uint8_t version; // 协议版本,如 0x01
uint8_t type; // 0x01=控制, 0x02=数据, 0x03=心跳
uint16_t seq; // 序列号,用于丢包检测
uint16_t len; // payload 长度
uint32_t crc32; // 可选,对 payload 校验
} __packed;
发送方按 magic → version → type → seq → len → payload 的顺序填充,接收方先校验 magic 和 version,再按 len 读取 payload,最后用 seq 判断是否丢包。若连续两个 seq 不连续,说明中间有消息丢失,应触发重传或告警。对于控制类报文,建议在应用层实现“请求-应答”模型:A 核发送 type=0x01 的请求,M 核在 1 ms 内回送同 seq 的应答,超时则重发,最多重试 3 次。
共享内存的布局也需要在链接脚本和 DTS 中显式保留。例如在 i.MX8M 上,可以把 0x8FF00000–0x8FFFFFFF 这 1 MB 保留给 RPMsg,A 核侧在 DTS 中标记 no-map,M 核侧在链接脚本中把 .resource_table 和 vring 放到该区域。双方必须使用同一份地址映射,否则会出现“写进去了但读出来是旧值”的问题。
/proc/iomem 中该区域未被 System RAM 占用。六、选型建议
| 场景 | 推荐机制 | 理由 |
|---|---|---|
| 控制报文,≤512 B,1 kHz | RPMsg + MU 中断 | 框架成熟,端点管理方便 |
| 短命令,≤16 B,事件驱动 | MU TR/RR 直传 | 延迟最低,无需共享内存 |
| 大块数据,如图像帧 | 共享内存 + MU doorbell | 避免拷贝,带宽高 |
| 多核多通道 | Mailbox + RPMsg | 通道抽象清晰,便于扩展 |
实际项目中,往往是“共享内存 + MU 中断 + RPMsg 协议头”三者组合:共享内存提供带宽,MU 提供低延迟通知,RPMsg 提供端点与缓冲区管理,自定义头部提供可靠性与类型区分。理解每一层负责什么,才能在调试时快速定位问题——是数据没写进共享内存,还是 MU 中断没触发,还是协议头校验失败。