1. 课程导入
各位同学,咱们今天开篇就来聊聊RPMsg协议栈的完整数据通路。说实话,我见过不少工程师,调了半年RPMsg,遇到问题还是两眼一抹黑——根本原因就是没把整条链路看清楚。
RPMsg,全称Remote Processor Messaging,说白了就是让主CPU和协处理器之间能互相发消息。你想想看,一个系统里既有Cortex-A跑Linux,又有Cortex-M跑RTOS,它们怎么通信?RPMsg就是答案。
核心认知:RPMsg不是单一协议,而是一整套分层架构。从最底层的硬件Mailbox寄存器,到顶层的Linux字符设备接口,中间跨越了物理层、传输层、协议层和应用层。
1.1 整体架构分层
我个人习惯把RPMsg协议栈分成四层。每一层都有明确的职责,也都有各自的坑。咱们一层层剥开来看。
| 层级 | 名称 | 核心组件 | 典型问题 |
|---|---|---|---|
| L4 | 应用层 | /dev/rpmsgX, rpmsg_chardev | read/write阻塞、缓冲区大小不匹配 |
| L3 | 协议层 | rpmsg_proto, virtio_rpmsg_bus | 端点地址冲突、命名服务注册失败 |
| L2 | 传输层 | virtio_ring, vring | 描述符耗尽、内存屏障缺失 |
| L1 | 物理层 | Mailbox, shared memory, IPC | 中断丢失、共享内存对齐问题 |
嗯,这里要注意:很多工程师只盯着L4的应用层调,出了问题就怀疑是驱动bug。其实我踩过最大的坑,往往在L1和L2——硬件中断没配好,或者vring描述符用完了,应用层怎么调都没用。
1.2 硬件Mailbox:一切通信的起点
咱们先从最底层说起。Mailbox是什么?说白了就是一组寄存器和中断逻辑。主CPU往某个寄存器写数据,协处理器那边就会收到一个中断。反过来也一样。
我在项目中遇到过一种情况:两个核之间通信偶尔超时,查了三天,最后发现是Mailbox的中断优先级配得太低,被其他高频率中断淹没了。你想想看,硬件层面的问题,软件怎么调都白搭。
实战经验:调试Mailbox时,我建议你先用示波器抓一下中断引脚。如果中断信号正常,再往软件层查;如果中断根本没出来,直接查硬件配置和时钟。
1.3 共享内存与vring:数据传输的通道
Mailbox只负责通知,真正的数据传输靠共享内存。RPMsg用的是virtio框架,核心数据结构是vring——一个环形缓冲区。
vring分两部分:
- 描述符表(descriptor table):记录每个buffer的地址、长度和标志位
- 可用环(avail ring)和已用环(used ring):生产者消费者模型
主CPU往avail ring里放描述符,然后敲Mailbox通知协处理器。协处理器从avail ring取走描述符,处理完数据,把描述符放到used ring里,再敲Mailbox通知主CPU。
这个模型看着简单,但实际调起来坑不少。我记得有一次,协处理器那边一直收不到数据,查到最后发现是avail ring的索引更新没有加内存屏障。ARM架构下,CPU和DMA之间的内存序问题,稍不注意就出鬼。
避坑指南:我曾经因为共享内存的cache一致性没处理好,导致数据错乱。如果你用的是不带硬件cache coherency的芯片,记得在访问共享内存前后加dmb/dsb指令,或者把共享内存区域配置为non-cacheable。
1.4 virtio_rpmsg_bus:协议层的核心
再往上走,就到了virtio_rpmsg_bus。这一层负责把vring里的原始数据包解析成RPMsg消息。每个RPMsg消息有一个固定头部,包含源地址、目的地址、长度和保留字段。
struct rpmsg_hdr {
__le32 src; // 源端点地址
__le32 dst; // 目的端点地址
__le32 len; // 数据长度
__le16 flags; // 标志位
__le16 reserved;// 保留
// 后面跟着实际数据
} __packed;
这里有个关键点:端点地址(endpoint address)。每个RPMsg通道由一对端点地址标识。Linux侧和协处理器侧各自维护一张端点地址表。发送消息时,必须知道对方的端点地址。
那问题来了:一开始双方都不知道对方的端点地址怎么办?RPMsg的解决方案是命名服务(name service)。协处理器启动后,会通过一个预定义的端点(通常是0x35)广播自己的服务名称。Linux侧收到广播后,动态分配一个端点地址,建立通道。
关键机制:命名服务广播使用的是RPMSG_NS_ADDR(0x35)这个固定端点。如果这个端点被占用或者被屏蔽,整个服务发现流程就会失败。我调试过的一个项目,就是有人在应用层不小心占用了0x35,导致协处理器一直注册不上。
1.5 完整数据通路:一次消息发送的全过程
咱们把整个流程串起来,看看一次消息从应用层写到协处理器收到,到底经历了什么。
- 应用层调用write():用户程序往/dev/rpmsgX写数据
- rpmsg_chardev:把用户数据拷贝到内核缓冲区,封装成rpmsg_hdr
- virtio_rpmsg_bus:查找目标端点地址,把消息放入发送队列
- vring:分配描述符,把数据buffer地址写入avail ring
- Mailbox:写触发寄存器,产生硬件中断通知协处理器
- 协处理器侧:中断服务程序读取avail ring,取出描述符
- 协处理器侧:根据描述符地址从共享内存读取数据
- 协处理器侧:处理完数据后,把描述符放入used ring
- Mailbox:协处理器写回中断,通知主CPU
- Linux侧:中断处理函数唤醒等待队列,应用层read()返回
你看,一次简单的消息收发,背后是整整10个步骤。任何一个环节出问题,都会导致通信失败。
1.6 整体数据流图
下面这张图把整个数据通路画出来了。我建议你把它存下来,后面每讲一个故障案例,都回来对照这张图,看看问题出在哪一层。
1.7 各层之间的接口契约
层与层之间不是随便粘在一起的,它们有明确的接口契约。理解这些契约,对故障定位至关重要。
| 接口 | 生产者 | 消费者 | 数据格式 | 同步方式 |
|---|---|---|---|---|
| L4→L3 | rpmsg_chardev | virtio_rpmsg_bus | rpmsg_hdr + payload | 等待队列 |
| L3→L2 | virtio_rpmsg_bus | virtio_ring | scatterlist数组 | spinlock |
| L2→L1 | vring_kick() | Mailbox驱动 | 寄存器写操作 | 硬件中断 |
嗯,这里有个细节:L3到L2的接口用的是scatterlist,不是连续内存块。这意味着你的数据可能被拆成多个不连续的片段。如果协处理器那边不支持scatter-gather DMA,你就得在驱动里做数据拼接——这个坑我踩过不止一次。
调试技巧:当消息收发异常时,我习惯在每一层的接口处加trace点。比如在vring_kick()之前打印avail ring的索引值,在Mailbox中断处理函数里打印中断状态寄存器。这样能快速定位问题出在哪一层。
1.8 小结
这一章咱们把RPMsg协议栈的四层架构和完整数据通路过了一遍。从硬件Mailbox到Linux应用层,每一层都有它的职责,也都有它的坑。后面的课程里,我们会针对每一层展开讲故障定位和性能调优。
记住一句话:RPMsg的问题,90%出在边界上——层与层的接口、核与核的共享内存、中断与轮询的切换。把这些边界搞清楚,你就已经赢了一半。