3. RPMsg 基础API:端点创建、发送与接收

RPMsg 的 API 其实不多,但每个都挺关键。我刚开始接触时,觉得不就是发消息嘛,结果在端点生命周期上栽过跟头。今天咱们就把 rpmsg_create_ept、rpmsg_send、rpmsg_recv 这三个核心 API 掰开揉碎讲清楚。

3.1 端点(Endpoint)是什么?

先想一个问题:两个核之间通信,数据到了对方那边,怎么知道该交给谁?

RPMsg 用「端点」来解决这个问题。每个端点有一个唯一的地址,类似 IP 通信里的端口号。发送方指定目标地址,接收方注册对应地址的端点,数据就能准确送达。

核心概念: 端点 = 地址 + 回调函数 + 上下文指针。地址决定数据去哪,回调函数决定数据怎么处理。

我个人习惯把端点想象成「信箱」。你注册一个端点,就是在信箱上贴了地址标签。别人往这个地址发信,邮递员(RPMsg 框架)就把信投进你的信箱,然后你开箱处理。

3.2 rpmsg_create_ept:创建端点

这是使用 RPMsg 的第一步。函数原型长这样:

struct rpmsg_endpoint *rpmsg_create_ept(
    struct rpmsg_device *rdev,
    const char *name,
    uint32_t src,
    uint32_t dst,
    rpmsg_ept_cb cb,
    rpmsg_ns_unbind_cb unbind_cb,
    void *priv
);

参数看着多,其实就三组:

参数 含义 我的建议
rdev RPMsg 设备实例 从 rpmsg_probe 里拿,别自己造
name 端点名称(用于 NS 公告) 起个有意义的名字,调试时方便
src 源地址(本端地址) 传 RPMSG_ADDR_ANY 让框架分配
dst 目标地址(对端地址) 已知就填,未知填 0
cb 收到消息的回调函数 必须实现,否则收不到数据
unbind_cb 对端删除端点时的回调 用于资源清理,建议实现
priv 用户私有数据指针 传结构体指针,回调里用

小技巧: src 填 RPMSG_ADDR_ANY 时,框架会自动分配一个可用地址。我习惯这么做,省得地址冲突。

回调函数的签名是:

static int my_ept_cb(struct rpmsg_endpoint *ept,
                     void *data, size_t len,
                     uint32_t src, void *priv)
{
    // data 是收到的数据
    // len 是数据长度
    // src 是发送方地址
    // priv 是创建端点时传的私有数据
    return 0;
}

嗯,这里要注意:回调函数是在中断上下文还是任务上下文,取决于底层实现。我在 FreeRTOS + OpenAMP 上遇到过回调里调了阻塞函数导致死锁,后来统一在回调里只做数据拷贝,用消息队列通知任务线程处理。

3.3 rpmsg_send:发送数据

发送 API 有几种变体,我列个表对比:

函数 特点 适用场景
rpmsg_send 基础发送,数据拷贝到内部缓冲区 大多数场景
rpmsg_sendto 指定目标地址发送 多端点通信
rpmsg_trysend 非阻塞发送,失败立即返回 实时性要求高的场景
rpmsg_send_offchannel 指定源和目标地址 底层控制,一般不用

最常用的是 rpmsg_send:

int ret = rpmsg_send(ept, data, len);
if (ret < 0) {
    // 发送失败,处理错误
}

曾经踩过的坑: 我刚开始以为 rpmsg_send 会等对方确认才返回。其实它只是把数据放进共享内存的环形缓冲区就返回了。如果缓冲区满了,它会阻塞等待。所以发送端要考虑流控,别一股脑塞爆了。

为什么会阻塞?因为 RPMsg 底层用 vring(虚拟环形缓冲区)传输数据。vring 满了,发送就得等消费端取走数据。我记得有一次调试,A 核疯狂发,B 核处理慢,结果 A 核任务被挂起,整个系统卡住。后来加了发送队列和超时机制才解决。

3.4 rpmsg_recv:接收数据

严格来说,RPMsg 没有独立的 rpmsg_recv 函数。接收是通过回调机制完成的。数据到达时,框架自动调用你注册的回调函数。

但有些封装层提供了 rpmsg_recv 接口,本质上是阻塞等待回调触发。比如:

// 伪代码,某些封装提供
int rpmsg_recv(struct rpmsg_endpoint *ept,
               void *buf, size_t *len,
               int timeout_ms);

我个人不太喜欢这种阻塞接收方式。回调模型更自然,也更高效。你想想看,数据来了才处理,不用轮询,CPU 利用率更高。

推荐模式: 回调 + 消息队列。回调里把数据入队,任务线程出队处理。这样既不会阻塞中断,又能保证数据处理的有序性。

3.5 端点生命周期管理

端点的生命周期就四个阶段:创建 → 绑定 → 使用 → 销毁。但这里有几个容易翻车的地方。

3.5.1 创建与绑定

rpmsg_create_ept 返回的端点指针,在成功创建后就已经和 RPMsg 设备绑定了。不需要额外调用绑定函数。但要注意:

  • 如果 dst 填了具体地址,框架会尝试和对端建立连接
  • 如果 dst 填 0,需要后续通过 NS 服务动态发现对端

3.5.2 使用中的注意事项

端点创建后,随时可能收到数据。所以回调函数必须线程安全。我曾经在回调里直接操作了全局链表,结果另一个线程也在操作,数据全乱了。

建议: 回调里只做三件事——拷贝数据、发信号、更新原子变量。其他事情交给任务线程。

3.5.3 销毁端点

用 rpmsg_destroy_ept 销毁:

rpmsg_destroy_ept(ept);
ept = NULL; // 记得置空,防止悬空指针

销毁时会发生什么?

  1. 框架会通知对端:这个端点没了
  2. 对端的 unbind_cb 会被调用
  3. 本端的资源被释放

曾经踩过的坑: 我在销毁端点后,忘了置空指针。结果另一个线程还在用这个指针发数据,直接崩溃。还有一次,销毁时回调函数正在执行,造成了 use-after-free。后来我加了引用计数和销毁锁才彻底解决。

3.6 知识体系总览

下面这张图把本章的核心逻辑串起来了:

RPMsg 端点生命周期与 API 关系图 RPMsg 端点生命周期与 API 关系 阶段1:创建 rpmsg_create_ept 阶段2:绑定 自动绑定到 rdev 阶段3:通信 rpmsg_send / 回调 阶段4:销毁 rpmsg_destroy_ept 数据到达 触发回调函数 💡 关键要点 • 创建时指定 src/dst 地址,回调函数必须实现 • 发送使用 rpmsg_send 系列函数,注意阻塞与非阻塞选择 • 销毁后必须置空指针,防止悬空引用 端点生命周期:创建 → 绑定 → 通信 → 销毁,回调贯穿整个通信阶段 NS 公告 动态地址发现

3.7 完整示例:一个简单的 ping-pong

最后给个完整示例,把今天讲的 API 串起来:

// 端点回调
static int ping_cb(struct rpmsg_endpoint *ept,
                   void *data, size_t len,
                   uint32_t src, void *priv)
{
    char *msg = (char *)data;
    printf("收到: %s\n", msg);
    
    // 回复 pong
    rpmsg_send(ept, "pong", 5);
    return 0;
}

// 创建端点
struct rpmsg_endpoint *ept;
ept = rpmsg_create_ept(rdev, "ping-ep",
                       RPMSG_ADDR_ANY, RPMSG_ADDR_ANY,
                       ping_cb, NULL, NULL);
if (!ept) {
    printf("创建端点失败\n");
    return -1;
}

// 发送 ping
rpmsg_send(ept, "ping", 5);

// 销毁端点(不再使用时)
rpmsg_destroy_ept(ept);
ept = NULL;

这个例子虽然简单,但涵盖了创建、回调、发送、销毁四个关键操作。你可以在自己的开发板上跑一下,看看两个核之间能不能成功对话。

调试小技巧: 如果收不到数据,先检查端点地址是否匹配。我经常在回调里打印 src 和 dst,确认地址对不对得上。