3. RPMsg协议原理与消息流

RPMsg(Remote Processor Messaging)是 OpenAMP 框架中运行在共享内存之上的消息传递协议。它并不负责搬运数据本身,而是定义了一套“谁发给谁、发到哪个端点、消息有多长、如何被接收方确认”的约定。理解 RPMsg,关键要抓住四个概念:端点(Endpoint)、通道(Channel)、地址模型(Address Model)和消息头(Header)。把这四者串起来,就能看清一条消息从 Cortex-A 侧用户程序到 Cortex-M 侧固件、再返回的完整生命周期。

一、端点与通道:通信的两级抽象

RPMsg 的通信对象是端点。一个端点可以理解为一个“消息邮箱”,它由本地地址(local address)唯一标识,并绑定一个回调函数。当消息到达该端点时,RPMsg 核心层调用回调,把数据交给上层应用。端点不是全局唯一的,它只在某个 rpmsg 设备(即一条 vring 通道)内部有效。

多个端点通过通道组织起来。通道用一个字符串名字标识,例如 "rpmsg-openamp-demo-channel"。名字的作用是让通信双方能够“对上号”:A 侧创建一个名为 rpmsg-tty 的端点,M 侧也创建同名端点,双方即可互相收发。通道名是逻辑约定,真正的寻址仍依赖端点地址。一个通道下可以有多个端点,但同一时刻同一名字通常只对应一个服务实例。

二、地址模型:源地址与目的地址

RPMsg 消息头中携带两个 32 位地址字段:src(源端点地址)和 dst(目的端点地址)。地址由本地 RPMsg 核心在创建端点时分配,典型规则是:地址 0 保留给“名称服务”(Name Service),地址 1~1023 用于动态端点。名称服务是一个特殊端点,它负责把“通道名”解析成“端点地址”。

当 A 侧想与 M 侧通信时,A 侧先向名称服务端点(地址 0)发送一条 RPMSG_NS_ANNOUNCE 消息,消息体中包含通道名和 A 侧端点地址。M 侧名称服务收到后,查找本地是否有同名通道;若有,则创建对应端点,并把 A 侧地址与本地端点绑定。此后双方直接用 src/dst 地址收发,不再经过名称服务。这就是 RPMsg 的“先解析、后直连”地址模型。

RPMsg 消息流:名称服务解析 + 端点直连 Cortex-A 侧 (Linux) 用户程序 / rpmsg_char open("/dev/rpmsg_ctrl0") 端点 A (local addr = 0x400) 回调 rpmsg_recv_cb() 名称服务端点 (addr = 0) 发送 NS_ANNOUNCE 共享内存 vring TX vring / RX vring 消息头 + 数据 Cortex-M 侧 (RTOS/裸机) 应用任务 / 服务线程 rpmsg_create_ept() 端点 B (local addr = 0x401) 回调 rpmsg_recv_cb() 名称服务端点 (addr = 0) 解析通道名 → 创建端点 共享内存 vring RX vring / TX vring 消息头 + 数据 ① NS_ANNOUNCE (通道名 + 端点地址) ② NS 回复 / 端点绑定完成 ③ 数据消息 src=0x400 dst=0x401 ④ 回复消息 src=0x401 dst=0x400 消息头 (16 字节) src(4) | dst(4) | len(4) | flags(4)

三、消息头格式:16 字节的约定

RPMsg 消息头固定 16 字节,定义在 struct rpmsg_hdr 中,字段顺序如下:

struct rpmsg_hdr {
    uint32_t src;    // 源端点地址
    uint32_t dst;    // 目的端点地址
    uint32_t len;    // 有效数据长度(不含头部)
    uint16_t flags;  // 标志位,如 RPMSG_HDR_FLAG_NS
    uint16_t reserved;
};

其中 flags 字段用于区分普通数据消息和名称服务消息。当 flags 置位 RPMSG_HDR_FLAG_NS 时,表示这是一条名称服务通告,接收方应交给名称服务端点处理,而不是普通应用端点。数据区紧跟在头部之后,长度由 len 指定。由于 vring 缓冲区大小有限(常见 512 字节或 4096 字节),单条 RPMsg 消息的有效载荷通常不超过 496 字节(512 减去 16 字节头)。

四、消息传递流程与生命周期

一条 RPMsg 消息的完整生命周期可分为五个阶段:

  1. 创建端点:应用调用 rpmsg_create_ept(),RPMsg 核心分配本地地址,注册回调,并把端点挂到对应 rpmsg 设备上。
  2. 名称服务解析:若双方尚未建立通道,先通过地址 0 交换 NS_ANNOUNCE,完成通道名到端点地址的映射。
  3. 发送消息:调用 rpmsg_send(),核心层填充 src/dst/len/flags,把消息头和数据写入 TX vring 的空闲缓冲区,然后触发 IPI(处理器间中断)通知对端。
  4. 接收与回调:对端收到 IPI 后,从 RX vring 取出缓冲区,解析消息头,根据 dst 找到对应端点,调用其回调函数,把数据交给应用。
  5. 释放缓冲区:应用处理完毕后,调用 rpmsg_release_rx_buffer() 或由核心层自动回收,把缓冲区归还给 vring,供后续消息复用。

这里有一个容易忽略的细节:RPMsg 的发送是异步的。调用 rpmsg_send() 返回成功,只表示消息已写入 vring 并触发中断,并不代表对端已经处理。如果应用需要确认,必须在协议层自行设计应答消息,例如在消息体中携带序列号,对端回复相同序列号表示已处理。

五、一个可参考的收发示例

假设 A 侧要发送字符串 "hello" 给 M 侧,端点地址分别为 0x400 和 0x401。发送端构造的消息缓冲区内容如下(十六进制示意):

偏移 0x00: 00 04 00 00   // src = 0x400
偏移 0x04: 01 04 00 00   // dst = 0x401
偏移 0x08: 05 00 00 00   // len = 5
偏移 0x0C: 00 00 00 00   // flags = 0, reserved = 0
偏移 0x10: 68 65 6C 6C 6F  // "hello"

接收端解析时,先读 dst 确认是发给自己的,再读 len 确定数据长度,最后从偏移 0x10 开始取数据。若 flags 为 RPMSG_HDR_FLAG_NS,则跳过应用回调,直接交给名称服务处理。

端点地址是本地概念,A 侧的 0x400 和 M 侧的 0x400 并不冲突,它们分属不同的地址空间。真正跨核传递的是“源地址 + 目的地址”这一对数值,接收方用 dst 匹配本地端点,用 src 记录对端地址以便回复。
RPMsg 不保证消息顺序之外的任何语义。如果一条消息超过 vring 缓冲区大小,必须由应用层分片,RPMsg 本身不做重组。此外,端点销毁时要确保没有未处理的消息,否则回调可能访问已释放的资源。