RPMsg协议基础:协议栈架构与核心机制
RPMsg这个协议,说白了就是让异构处理器之间能像聊天一样收发消息。比如一个Cortex-A核和一个Cortex-M核,它们怎么通信?RPMsg就是干这个的。我最早接触它是在一个车载项目里,A核跑Linux,M核跑裸机,两边要交换传感器数据。当时踩了不少坑,今天我把这些经验揉碎了讲给你。
RPMsg协议栈架构
RPMsg的协议栈分三层,每层各司其职。我习惯把它想象成一个快递系统:上层是寄件人,中间是分拣中心,底层是运输车队。
| 层级 | 角色 | 典型实现 |
|---|---|---|
| 应用层 | 用户态或内核态驱动 | rpmsg_char、rpmsg_tty |
| 传输层 | 消息路由与端点管理 | virtio-rpmsg 总线 |
| 物理层 | 共享内存与中断机制 | virtqueue、vring |
应用层只管发数据,传什么、发给谁,它不关心底层怎么搬运。传输层负责查路由表,找到目标端点。物理层最苦最累,它要操作共享内存、触发硬件中断。嗯,这里要注意:物理层的实现直接决定了性能上限。
关键认知:RPMsg不是重新发明轮子,它是在virtio框架上封装了一层消息语义。你理解了virtio,RPMsg就懂了一半。
virtio框架:RPMsg的骨架
virtio最初是为虚拟化设计的,让虚拟机里的驱动能直接跟宿主机硬件对话。RPMsg借用了这套机制,只不过把"虚拟机-宿主机"换成了"A核-M核"。
核心数据结构就两个:virtqueue和vring。virtqueue是队列的抽象,vring是队列在共享内存里的具体布局。我举个例子:
// virtqueue 结构体(简化版)
struct virtqueue {
struct vring vring; // 环形缓冲区
void *priv; // 私有数据
bool (*callback)(struct virtqueue *vq); // 回调函数
};
// vring 布局
struct vring {
unsigned int num; // 描述符数量
struct vring_desc *desc; // 描述符数组
struct vring_avail *avail; // 可用环
struct vring_used *used; // 已用环
};
为什么用环形缓冲区?因为它是无锁的。生产者写avail,消费者读used,两边各写各的,不需要互斥锁。我在一个项目中测试过,这种设计在单生产者单消费者场景下,延迟能控制在微秒级。
我的习惯:调试virtqueue时,先检查vring的num值。如果num是0或者明显不对,那共享内存的地址映射肯定出问题了。我曾经因为这个bug查了整整两天。
共享内存管理:数据搬运的战场
共享内存是RPMsg的命脉。两边处理器都能访问同一块物理内存,但怎么划分、怎么同步,这里头门道很多。
典型的共享内存布局是这样的:
- 控制区域:存放vring的基地址、大小、中断号等元数据。通常只有几十字节。
- 数据区域:vring描述符指向的实际数据缓冲区。大小取决于消息负载。
- 对齐要求:ARM架构下,共享内存必须64字节对齐,否则cache一致性会出问题。
我记得第一次调共享内存时,A核写的数据M核死活读不到。后来发现是cache没刷。A核写完后数据还在cache里,M核直接读物理内存,当然读不到。解决方案是加一条dmb指令或者用dma_alloc_coherent分配内存。
避坑指南:我曾经在Zynq平台上遇到过共享内存地址重叠的问题。设备树里配的内存区域跟别的驱动冲突了,结果一跑就死机。建议你在设备树里给共享内存单独留一段地址,并且加上no-map属性,防止Linux内核把它映射成页表。
端点通信模型:消息的收发逻辑
RPMsg的端点(endpoint)类似于网络编程里的socket。每个端点有一个唯一的地址,消息通过地址路由到正确的接收者。
通信流程大致分四步:
- 创建端点:调用
rpmsg_create_ept,传入回调函数和私有数据。 - 发送消息:调用
rpmsg_send,指定目标地址和数据指针。 - 接收消息:底层virtqueue收到数据后,触发回调,把消息递到端点。
- 销毁端点:通信结束后,调用
rpmsg_destroy_ept释放资源。
你想想看,这个模型跟UDP是不是很像?无连接、不可靠(但底层有重传机制)。实际上RPMsg确实借鉴了socket的设计思路,只不过把网络栈换成了共享内存。
// 创建端点的典型代码
struct rpmsg_endpoint *ept;
ept = rpmsg_create_ept(rpdev, my_cb, NULL, MY_ADDR);
if (!ept) {
pr_err("Failed to create endpoint\n");
return -ENOMEM;
}
// 发送消息
int ret = rpmsg_send(ept, data, len);
if (ret) {
pr_err("Send failed: %d\n", ret);
}
核心要点:端点的地址分配要避免冲突。我习惯用RPMSG_ADDR_ANY让内核自动分配,然后通过name service广播出去。两边都注册name service回调,就能动态发现对方。
RPMsg协议栈全景图
下面这张图把整个协议栈的层次和交互画出来了。你盯着看一分钟,应该能理清脉络。
从图里能看出来,RPMsg的层次非常清晰。每一层只跟相邻层打交道,耦合度很低。这意味着你可以把物理层的virtqueue换成Mailbox,上层代码几乎不用改。我就在一个项目里这么干过,把virtio后端换成了Mailbox驱动,上层rpmsg_char照跑不误。
小结
RPMsg协议栈的核心就三样:virtio框架提供队列机制,共享内存提供数据通道,端点模型提供通信语义。你把这三点吃透了,后面调适配的时候心里就有底了。
嗯,我记得第一次调通RPMsg通信时,A核发了个"Hello"给M核,M核回了个"World"。虽然就两个词,但那一刻的感觉,比写通一个复杂的算法还爽。希望你在学习过程中也能体会到这种成就感。