3. RPMsg架构与组件
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; // 记得置空,防止悬空指针
销毁时会发生什么?
- 框架会通知对端:这个端点没了
- 对端的
unbind_cb会被调用 - 本端的资源被释放
曾经踩过的坑: 我在销毁端点后,忘了置空指针。结果另一个线程还在用这个指针发数据,直接崩溃。还有一次,销毁时回调函数正在执行,造成了 use-after-free。后来我加了引用计数和销毁锁才彻底解决。
3.6 知识体系总览
下面这张图把本章的核心逻辑串起来了:
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,确认地址对不对得上。