4. 异构通信硬件基础

在 i.MX 系列 SoC 中,Cortex-A 核与 Cortex-M 核运行着不同的操作系统与实时任务,二者之间的通信并非简单的函数调用,而是跨越总线、时钟域和中断控制器的硬件协作。理解这条链路上的每一层硬件机制,是后续编写稳定 RPMsg 应用、排查通信超时和丢包问题的前提。本节从物理介质出发,逐层向上梳理共享内存、Mailbox、MU、RPMsg 以及中断路由与时钟同步之间的关系。

一、共享内存:通信的物理载体

异构核之间最基础的通信资源是一块双方都能访问的 DDR 或 OCRAM 区域。以 i.MX8M Mini 为例,典型做法是在 DDR 中预留一段物理地址连续的区域,例如 0x4800_0000 起始、长度 0x0040_0000(4 MB),通过设备树 reserved-memory 节点声明,使 Linux 内核在启动时不会将其纳入通用内存管理。Cortex-M 侧则通过链接脚本把该区域映射到自己的地址空间。

共享内存本身只是一块“哑”内存,它不产生任何通知。因此必须约定一套数据结构来描述“谁在写、写到哪、是否写完”。RPMsg 使用的 virtio 环形缓冲区(vring)正是建立在这块内存之上的描述符环、可用环和已用环。关键点在于:共享内存解决的是“数据放在哪里”,而通知机制解决的是“对方怎么知道数据已经到了”。二者缺一不可。

二、Mailbox 与 MU:跨核通知的硬件通道

Mailbox 是 i.MX 上用于核间中断的通用外设抽象,它提供若干通道,每个通道可配置为向指定核发送中断。而 MU(Messaging Unit)是 i.MX7ULP、i.MX8 系列上更专用的异构通信单元,它同时具备两重能力:一是中断通知(通过 TCR/TSR 等寄存器触发对方核的中断),二是小数据传递(通过 TR/RR 四个 32 位收发寄存器直接搬运最多 16 字节的短消息)。

可以把 MU 理解为“带数据寄存器的门铃”。当 A 核要向 M 核发送一个 vring 索引或事件编号时,只需写入 MU 的 TR 寄存器并置位状态,M 核的中断服务程序读取 RR 寄存器即可获得该值。相比纯 Mailbox,MU 省去了一次共享内存读写,延迟更低。下图展示了从共享内存到 RPMsg 应用层的完整信号链与中断路由关系。

i.MX 异构通信硬件分层与中断路由 Cortex-A(Linux) 应用层 / rpmsg_char open / read / write rpmsg_virtio 驱动 vring 管理 / 缓冲分配 imx-mailbox / MU 驱动 mbox_send_message() GIC 中断控制器 SPI 中断号路由 时钟:CCM 提供 MU 时钟 Cortex-M(RTOS / Bare-metal) 应用任务 rpmsg_recv_callback rpmsg_lite 协议栈 vring 收发 / 队列 MU 中断服务程序 MU_IRQHandler() NVIC 中断控制器 MU IRQ 优先级配置 时钟:MU 根时钟分频 共享内存(DDR / OCRAM) vring 描述符环 + 数据缓冲 物理地址:0x4800_0000 大小:4 MB(示例) MU / Mailbox 硬件 TR/RR 寄存器(≤16 B) TCR/TSR 中断触发 通道 0:A→M,通道 1:M→A 读写 读写 写 TR 触发 IRQ 通知已写入 回写 RR 数据流:共享内存搬运有效载荷 | 控制流:MU/Mailbox 传递事件与索引

三、RPMsg:建立在 virtio 之上的消息抽象

RPMsg(Remote Processor Messaging)是 Linux 与 RTOS 之间事实上的标准通信协议。它并不直接操作 MU 寄存器,而是把共享内存中的 vring 封装成“端点(endpoint)”和“通道(channel)”。每个端点有一个本地地址,通信双方通过 rpmsg_create_ept() 或设备树中的 rpmsg 节点建立绑定。发送一条消息的完整路径是:应用调用 rpmsg_send() → 协议栈把数据放入 vring 的空闲描述符 → 更新 avail 环 → 通过 MU 发送一个通知 → 对端 MU 中断触发 → 对端协议栈扫描 used 环 → 回调应用。

这里有一个容易混淆的点:RPMsg 的消息长度上限由 vring 缓冲区大小决定,通常为 512 字节,而 MU 的 TR/RR 寄存器只有 16 字节。因此 MU 传递的从来不是消息正文,而是“有消息到达”这个事件本身,或者最多是一个描述符索引。理解这一点,就能明白为什么 RPMsg 的吞吐量主要取决于共享内存带宽和中断频率,而不是 MU 的寄存器宽度。

四、中断路由:从 MU 到 NVIC 与 GIC

MU 产生的中断需要被正确路由到目标核。在 Cortex-M 侧,MU 中断线连接到 NVIC 的某个 IRQ 编号,通过 NVIC_SetPriority() 和 NVIC_EnableIRQ() 使能。在 Cortex-A 侧,MU 中断作为 SPI 接入 GIC,Linux 通过设备树 interrupts 属性声明,并由 request_irq() 注册处理函数。

实际工程中常见的故障是“消息发出但对方无响应”,排查顺序应为:先确认 MU 的 TCR 是否置位、对端 TSR 是否观察到;再确认 NVIC/GIC 中该中断是否使能且优先级未被屏蔽;最后检查共享内存区域是否被 Linux 内存管理误用。这三步覆盖了通知链路上最典型的断点。

五、时钟同步:不可忽视的隐性依赖

MU 和 Mailbox 都是总线上的外设,其寄存器访问依赖时钟。如果 Cortex-M 侧的 MU 根时钟被关闭或分频不当,寄存器写入将无效,中断也不会产生。在 i.MX8M Mini 上,MU 时钟通常来源于 CCM 的 MU 根时钟,需要在 M 核启动代码中通过 CLOCK_EnableClock(kCLOCK_Mu) 使能。此外,若双方使用共享内存中的时间戳做超时判断,必须保证两侧对时间的理解一致——A 核的 ktime_get() 与 M 核的 SysTick 计数并不天然对齐,建议在协议层使用“序号 + 超时重传”而非绝对时间戳。

调试建议:在 M 核的 MU 中断服务程序入口翻转一个 GPIO,用示波器观察中断是否真正到达。这是区分“MU 未触发”与“协议栈未处理”最快的方法。