1. RPMsg 概述:协议背景、通信模型与系统角色

各位同学,咱们今天正式开讲 RPMsg。说实话,我第一次接触这个协议的时候,也被它绕得有点晕。但搞明白了之后你会发现,它其实就是一套「让两个处理器能互相递纸条」的标准流程。

先说说背景。为什么会有 RPMsg?

现在的嵌入式系统越来越复杂。一个芯片里可能集成了 Cortex-A 跑 Linux,Cortex-M 跑 RTOS,甚至还有 DSP 或 GPU。这些核心各司其职,但数据得互通啊。比如 A 核采集了摄像头数据,要传给 M 核做实时控制。怎么传?

早期做法很粗暴:直接读写共享内存,再用 GPIO 中断通知对方。但这样问题很多——没有标准协议,没有流控,没有通道隔离,调试起来简直噩梦。我早期一个项目就吃过这个亏,两个核抢同一块内存,数据错乱,查了三天才发现是同步没做好。

RPMsg 就是来解决这个问题的。

1.1 核间通信模型

先看一张整体架构图,我习惯把 RPMsg 的通信模型画成三层:

RPMsg 核间通信三层模型 RPMsg 核间通信三层模型 应用层 用户自定义消息 / 服务发现 / 数据封装 例:摄像头数据流、控制指令、日志上报 传输层(RPMsg 协议) 通道管理 / 消息分帧 / 流控 / 端点寻址 核心:struct rpmsg_endpoint, rpmsg_send(), rpmsg_recv() 物理层(VirtIO / 共享内存 + 中断) VirtIO 环形缓冲区 / 共享内存区域 / 硬件中断 例:vring, vring_desc, vring_avail, vring_used 数据流向:应用层 → RPMsg 协议层 → VirtIO 物理层 → 共享内存 → 对方核 每个层只关心自己的职责,上层不关心底层用共享内存还是 Mailbox

说白了,RPMsg 就是中间那层传输协议。它不关心底层是共享内存还是硬件 Mailbox,也不关心上层传的是视频流还是控制指令。它只负责一件事:把 A 核的数据可靠地送到 B 核。

我个人习惯把这种模型叫做「三明治结构」——上下两层可以随便换,中间这层保持稳定。你在项目里换硬件平台,只要 VirtIO 层适配好了,上层应用代码基本不用动。

1.2 RPMsg 在 Linux 中的角色

在 Linux 侧,RPMsg 是以 remoteproc/rpmsg 框架的形式存在的。它不是一个独立的驱动,而是和远程处理器管理框架绑在一起的。

组件 角色 我遇到过的坑
remoteproc 负责加载固件、启动/停止远程核 固件路径配错,M 核起不来
rpmsg_bus 注册 rpmsg 设备,管理通道和端点 通道名称冲突导致 probe 失败
virtio_rpmsg 实现 VirtIO 传输层,操作 vring vring 地址对齐不对,数据错位
rpmsg_char 提供用户态 /dev/rpmsgX 接口 多进程同时读写,没加锁

Linux 这边的设计思路是「把远程核当作一个外设」。你想想看,在 Linux 里操作一个 I2C 设备,和操作一个 RPMsg 通道,流程其实很像:打开设备、读写数据、关闭设备。只不过 RPMsg 的数据要跨过核间边界。

关键点:Linux 侧的 RPMsg 驱动是运行在 内核态 的。用户态程序通过字符设备接口访问,但真正的消息收发是在内核里完成的。这意味着——如果你在用户态做性能测试,每次读写都有一次系统调用开销。我在做高吞吐优化时,最后不得不写了一个内核模块来绕过这个瓶颈。

1.3 RPMsg 在 RTOS 中的角色

RTOS 这边就简单多了。没有复杂的进程管理,没有虚拟内存,一切都是裸奔状态。RPMsg 在 RTOS 里通常就是一个 库 + 一个任务。

我记得第一次在 FreeRTOS 上移植 RPMsg,最让我头疼的是内存分配。Linux 那边可以随便 kmalloc,RTOS 这边得自己管理共享内存池。稍不注意就会踩到对齐问题。

RTOS 组件 说明
rpmsg_lite NXP 提供的轻量级 RPMsg 实现,无操作系统依赖
共享内存管理 通常是一段固定的物理地址,双核都能访问
中断处理 收到新消息时触发中断,通知 RTOS 任务去取
端点回调 注册回调函数,消息到来时自动调用

我的建议:在 RTOS 侧,尽量把 RPMsg 接收处理放在一个独立的高优先级任务里。别在中断上下文直接处理消息——中断里只做数据搬运,消息解析交给任务。否则一旦消息量上来,中断延迟会拖垮整个系统。

1.4 双核通信的典型数据流

咱们用一个实际场景来串一下:Linux 上的一个应用想给 RTOS 发一条「启动电机」的指令。

  1. 应用层:用户程序调用 write(/dev/rpmsg0, "start_motor", 12)
  2. rpmsg_char:将数据从用户态拷贝到内核缓冲区
  3. virtio_rpmsg:把数据打包成 rpmsg_hdr 格式,写入 vring
  4. 硬件触发:写 Doorbell 寄存器,通知 M 核
  5. M 核中断:RTOS 收到中断,从 vring 中取出数据
  6. rpmsg_lite:解析头部,找到对应的端点回调
  7. 应用回调:执行 "start_motor" 指令

整个过程,从 Linux write() 到 RTOS 回调执行,延迟通常在 几十微秒 级别。如果优化得好(比如用共享内存轮询代替中断),可以压到 个位数微秒。

注意:我曾经在一个项目里发现,Linux 侧用 printf 调试 RPMsg 时,打印本身会引入毫秒级延迟,导致 M 核那边超时复位。调试 RPMsg 时,千万别在数据路径上放 printk——用 tracepoint 或者专用日志缓冲区。

1.5 为什么选择 RPMsg?

市面上核间通信的方案不少,Mailbox、共享内存轮询、自定义协议……为什么 RPMsg 成了主流?

  • 标准化:Linux 内核原生支持,不用自己造轮子
  • 通道隔离:不同应用走不同 endpoint,互不干扰
  • 流控机制:基于 vring 的缓冲区管理,不会丢消息
  • 跨平台:同一个协议,A 核跑 Linux、M 核跑 RTOS,两边代码风格统一

说白了,RPMsg 就是核间通信界的「TCP/IP」——它不一定是最快的,但它是大家都能接受的通用标准。你在项目里用 RPMsg,至少不用担心换芯片平台后通信代码要重写。