RPMsg协议原理深度解析
各位好,欢迎来到RPMsg协议栈调试课程的第二讲。今天我们要啃的这块硬骨头,是RPMsg协议的核心原理。说实话,我早年刚接触这个协议时,也被它绕得够呛——virtio、remoteproc、rpmsg,这三者到底什么关系?数据是怎么从A核跑到B核的?
别急,我们一层层剥开它。你只要搞懂了今天的内容,后面调试性能问题、定位故障,就有了底气。
一、RPMsg协议栈全景图
先看整体。RPMsg不是凭空冒出来的协议,它建立在两个底层框架之上:virtio 提供传输通道,remoteproc 管理远程处理器的生命周期。RPMsg本身,说白了就是在这两层之间加了一个消息总线。
核心关系链:
remoteproc(启动/加载固件)→ virtio(共享内存+通知机制)→ RPMsg(消息封装+端点路由)
我画了一张图,把这三层的关系和关键数据结构标了出来。你看一眼,心里就有谱了。
这张图我建议你保存下来。每次调试时,先定位问题出在哪一层,再深入细节,效率会高很多。
二、核心数据结构:rpmsg_hdr
所有RPMsg消息,在共享内存里传输时,都带着一个头部——struct rpmsg_hdr。这个结构体只有16字节,但每个字段都至关重要。
struct rpmsg_hdr {
__u32 src; // 源端点地址
__u32 dst; // 目标端点地址
__u32 len; // 有效载荷长度(不包含头部)
__u16 flags; // 标志位(保留/校验等)
__u16 reserved; // 保留对齐
// 紧接着是 payload 数据
} __packed;
我的经验:调试时第一个要检查的就是 len 字段。我曾经遇到一个坑:发送端填的 len 是 1024,但实际 payload 只有 200 字节,接收端直接读越界了。这种问题用 printk 打出来一眼就能发现。
这里有个关键点:端点地址(src/dst)不是物理地址,也不是CPU核心号,而是RPMsg总线上的逻辑地址。每个端点(endpoint)在创建时会被分配一个唯一的32位地址。发送方填src为自己的地址,dst填接收方的地址。
| 字段 | 大小 | 说明 | 调试关注点 |
|---|---|---|---|
| src | 4字节 | 源端点地址 | 检查是否与注册的端点匹配 |
| dst | 4字节 | 目标端点地址 | 接收端路由的依据 |
| len | 4字节 | payload长度 | 最容易出错的字段,注意字节序 |
| flags | 2字节 | 控制标志 | 目前多用bit0表示是否需要ACK |
| reserved | 2字节 | 对齐填充 | 必须为0,否则兼容性会出问题 |
注意字节序! 如果Master核是x86(小端),Remote核是ARM(也是小端),一般没问题。但如果你遇到PowerPC或某些DSP(大端),rpmsg_hdr 的所有多字节字段都需要做字节序转换。我见过一个团队因为这个排查了整整两天。
三、工作流程:从virtio到rpmsg总线
好,现在我们来看一条消息从发送到接收的完整路径。我把它拆成6个步骤,你跟着走一遍就明白了。
- 端点发送:应用层调用
rpmsg_send(),数据进入rpmsg核心层。核心层封装rpmsg_hdr,填入src/dst/len。 - 入队vring:封装好的消息被放入virtio的TX vring。这里用的是
virtqueue_add_outbuf(),把buffer描述符链到vring_desc表里。 - kick通知:调用
virtqueue_kick(),写Remote核的Doorbell寄存器(通常是写一个共享内存地址或触发硬件中断)。 - Remote核接收:Remote核收到中断,在中断处理函数中调用
virtqueue_get_buf()从RX vring取出buffer。 - 解析头部:Remote核的rpmsg驱动解析
rpmsg_hdr,根据dst字段找到对应的端点回调函数。 - 回调分发:调用端点的回调函数,把payload传给上层应用。
你看,整个过程没有数据拷贝——Master核把数据写到共享内存,Remote核直接读同一块内存。这就是RPMsg高性能的关键。
性能关键路径:
整个链路中,最耗时的操作是第3步的kick和第4步的中断响应。如果你在做性能调优,重点关注这两个地方的延迟。
四、RPMsg与virtio、remoteproc的关系
这三者的关系,我习惯用一个比喻来理解:
- remoteproc 是「物业」——负责把Remote核这个「房间」打扫干净(加载固件、分配内存),然后告诉你房间准备好了。
- virtio 是「管道」——在Master和Remote之间铺设了一条共享内存的管道,两端各有一个vring作为收发缓冲区。
- RPMsg 是「邮递员」——它不管管道怎么铺设,只管把信封装好(rpmsg_hdr),通过管道投递到正确的收件人(端点)。
从代码层面看,它们的初始化顺序是固定的:
// 伪代码:初始化流程
1. rproc_boot() // remoteproc启动Remote核
2. → rproc_parse_resource_table() // 解析资源表,找到virtio设备描述
3. → rproc_virtio_probe() // 注册virtio设备
4. → rpmsg_probe() // RPMsg总线探测,创建ns_service端点
5. → rpmsg_register_device() // 注册rpmsg总线,等待通道创建
这里有个细节:RPMsg总线是在virtio设备探测成功后自动注册的。所以如果你发现 /sys/bus/rpmsg 下面没有设备,先检查virtio层有没有正常工作。
| 层次 | 职责 | 调试切入点 |
|---|---|---|
| remoteproc | 固件加载、内存分配、生命周期 | 检查 /sys/kernel/debug/remoteproc/ 下的状态文件 |
| virtio | 共享内存管理、vring操作、kick通知 | 检查vring地址是否正确,可用 trace_virtio_queue() |
| rpmsg | 消息封装、端点路由、通道管理 | 检查端点地址是否冲突,用 rpmsg_chrdev 测试收发 |
避坑指南:我曾经遇到一个问题:Remote核的固件里virtio设备描述符的vring地址,和Master核remoteproc解析出来的地址不一致。原因是资源表中的地址是Remote核视角的物理地址,而Master核需要做一次地址转换。如果你发现kick之后Remote核没反应,先查这个地址对不对。
五、端点与通道:RPMsg的路由机制
RPMsg的消息路由,靠的是端点(endpoint)和通道(channel)这两个概念。
通道代表一个逻辑连接,比如「Master核的音频服务 ↔ Remote核的音频处理」。每个通道有唯一的名称(如"rpmsg-audio")。
端点是通道两端的通信端口。每个端点有一个32位地址。Master核的音频端点地址是0x1,Remote核的音频端点地址是0x2。
当Master核发送消息时,rpmsg_hdr.src=0x1, dst=0x2。Remote核收到后,根据dst=0x2找到对应的端点,调用它的回调函数。
端点的创建有两种方式:
- 静态创建:在资源表中预定义,固件加载时自动注册。
- 动态创建:通过NS(Name Service)消息协商。Master核发送一个NS公告,Remote核收到后创建对应的端点和通道。
动态通道创建的NS消息格式:
NS消息本身也是一个RPMsg消息,它的 rpmsg_hdr 中 dst=0(特殊地址,表示NS服务端点),payload里包含通道名称和标志位。
嗯,到这里,RPMsg协议的核心原理你应该已经清楚了。从rpmsg_hdr这个16字节的头部,到virtio的vring传输,再到remoteproc的底层支撑,整个链路其实并不复杂。复杂的是调试时怎么定位问题——这个我们后面几章会专门讲。
记住一句话:RPMsg的本质,就是在共享内存上做的一次零拷贝消息投递。 搞懂了这个,你就抓住了它的灵魂。