4. RPMsg 基础通信示例:主核与从核的“Hello World”
这一章我们直接动手。很多朋友学 RPMsg 时,看了半天协议文档,结果连一个最简单的消息都发不通。我个人习惯是,先跑通一个“Hello World”,再回头啃理论。这样心里有底。
说白了,RPMsg 就是让 Linux 和 RTOS 互相递纸条。Linux 写个消息,通过共享内存 + 中断通知,RTOS 就能读到。反过来也一样。我们今天的目标就是:主核(Linux)发一条 “Hello from Linux”,从核(RTOS)收到后回复 “Hello from RTOS”。
4.1 整体通信架构
先看一张图,把整个流程串起来。你想想看,两个核之间没有网络,没有 socket,它们怎么通信?靠的就是 RPMsg 这个虚拟总线。
这张图里,共享内存是核心。Linux 把消息写到 vring 缓冲区,然后通过 IPI(核间中断)通知 RTOS。RTOS 从 vring 里把消息读出来,处理完再写回复。整个过程没有数据拷贝,全靠指针操作。
4.2 主核(Linux)端代码实现
Linux 侧我们通过 RPMsg 字符设备驱动来操作。用户态程序打开 /dev/rpmsg0,然后读写就行。内核里会自动帮你完成 endpoint 的创建和绑定。
先看代码,我加了一些关键注释。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define MSG_SIZE (256)
#define ENDPOINT_ID (14) // 自定义 endpoint ID
int main(int argc, char *argv[])
{
int fd;
char tx_buf[MSG_SIZE] = "Hello from Linux!";
char rx_buf[MSG_SIZE] = {0};
int ret;
// 1. 打开 RPMsg 设备
fd = open("/dev/rpmsg0", O_RDWR);
if (fd < 0) {
perror("open /dev/rpmsg0 failed");
return -1;
}
// 2. 创建 endpoint(绑定到 endpoint ID 14)
// 这里通过 ioctl 告诉内核,我要用这个 endpoint
ret = ioctl(fd, RPMSG_CREATE_EPT, ENDPOINT_ID);
if (ret < 0) {
perror("ioctl RPMSG_CREATE_EPT failed");
close(fd);
return -1;
}
printf("[Linux] Endpoint %d created.\n", ENDPOINT_ID);
// 3. 发送消息
ret = write(fd, tx_buf, strlen(tx_buf) + 1);
if (ret < 0) {
perror("write failed");
} else {
printf("[Linux] Sent: %s\n", tx_buf);
}
// 4. 等待接收回复(阻塞读)
memset(rx_buf, 0, MSG_SIZE);
ret = read(fd, rx_buf, MSG_SIZE);
if (ret > 0) {
printf("[Linux] Received: %s\n", rx_buf);
}
// 5. 销毁 endpoint
ioctl(fd, RPMSG_DESTROY_EPT, ENDPOINT_ID);
printf("[Linux] Endpoint %d destroyed.\n", ENDPOINT_ID);
close(fd);
return 0;
}
这段代码逻辑很直白:创建 endpoint → 发消息 → 收回复 → 销毁 endpoint。注意 ioctl 的两个命令 RPMSG_CREATE_EPT 和 RPMSG_DESTROY_EPT,它们是内核驱动里自定义的。不同厂商的驱动可能命令码不同,但语义一样。
RPMSG_CREATE_EPT 的 ioctl 返回值是 0,但实际 endpoint 没创建成功。原因是内核驱动里对 endpoint ID 做了范围校验,我传的 ID 超出了允许范围。建议创建后通过 /sys/kernel/debug/rpmsg/ 下的节点确认 endpoint 是否真的存在。
4.3 从核(RTOS)端代码实现
RTOS 侧没有文件系统,没有 ioctl。我们直接调用 RPMsg 库函数。以 OpenAMP 的 libmetal + rpmsg-lite 为例。
#include "rpmsg_lite.h"
#include "rpmsg_queue.h"
#include "rpmsg_endpoint.h"
#define ENDPOINT_ID (14)
static rpmsg_queue_handle_t app_queue;
static struct rpmsg_endpoint app_ept;
// 消息接收回调函数
static int app_rpmsg_cb(struct rpmsg_endpoint *ept,
void *data, size_t len,
uint32_t src, void *priv)
{
char *msg = (char *)data;
char reply[] = "Hello from RTOS!";
printf("[RTOS] Received: %s\n", msg);
// 回复消息
rpmsg_send(ept, reply, strlen(reply) + 1);
printf("[RTOS] Replied: %s\n", reply);
return RPMSG_SUCCESS;
}
void rpmsg_hello_world_task(void *arg)
{
struct rpmsg_lite_instance *rlite;
int ret;
// 1. 初始化 RPMsg 实例
rlite = rpmsg_lite_master_init(
(void *)SHM_BASE_ADDR, SHM_SIZE,
RPMSG_LITE_LINK_ID, RL_GET_FLAGS());
if (!rlite) {
printf("[RTOS] rpmsg_lite_master_init failed\n");
return;
}
// 2. 创建队列和 endpoint
app_queue = rpmsg_queue_create(rlite);
ret = rpmsg_create_ept(&app_ept, rlite,
ENDPOINT_ID,
RPMSG_ADDR_ANY,
app_rpmsg_cb,
app_queue);
if (ret != RL_SUCCESS) {
printf("[RTOS] rpmsg_create_ept failed\n");
return;
}
printf("[RTOS] Endpoint %d created.\n", ENDPOINT_ID);
// 3. 等待消息(阻塞在队列上)
while (1) {
rpmsg_queue_recv(rlite, app_queue,
&app_ept, NULL, RL_BLOCK);
}
// 4. 销毁 endpoint(实际不会执行到这里)
rpmsg_destroy_ept(&app_ept);
rpmsg_queue_destroy(app_queue);
}
RTOS 侧的核心是 回调函数。Linux 发来的消息到达时,app_rpmsg_cb 被自动调用。你在回调里处理消息,然后调用 rpmsg_send 回复。整个过程是事件驱动的。
4.4 Endpoint 的生命周期管理
Endpoint 是 RPMsg 通信的“端口”。没有 endpoint,消息就不知道发给谁。我们用一个表格总结它的生命周期。
| 阶段 | Linux 侧操作 | RTOS 侧操作 | 注意事项 |
|---|---|---|---|
| 创建 | ioctl(fd, RPMSG_CREATE_EPT, id) |
rpmsg_create_ept() |
ID 必须唯一,且两边一致 |
| 绑定 | 内核自动绑定到字符设备 | 绑定到队列和回调函数 | 绑定失败会导致消息丢失 |
| 通信 | read() / write() |
rpmsg_send() / 回调接收 |
注意缓冲区大小和字节对齐 |
| 销毁 | ioctl(fd, RPMSG_DESTROY_EPT, id) |
rpmsg_destroy_ept() |
销毁后不能再收发消息 |
这里有个容易被忽略的点:endpoint ID 在两边必须一致。Linux 侧创建 ID 14,RTOS 侧也必须创建 ID 14。否则消息会被路由到错误的 endpoint,或者直接被丢弃。
rpmsg_rpmsg_probe 的调试打印才发现 ID 不匹配。
4.5 编译与运行步骤
跑通这个示例,需要以下步骤。我按顺序列出来,每一步都别跳。
- 配置内核:确保 Linux 内核开启了 RPMsg 支持(
CONFIG_RPMSG、CONFIG_RPMSG_VIRTIO)。 - 编译驱动:将 RPMg 字符设备驱动编译进内核或作为模块。
- 编译 RTOS 固件:把 RTOS 侧的代码编译成二进制,烧录到从核。
- 启动系统:先启动 Linux,再加载 RTOS 固件(通常通过 remoteproc 框架)。
- 确认设备节点:Linux 启动后,检查
/dev/rpmsg0是否存在。 - 运行用户态程序:执行编译好的
hello_world_user。
如果一切顺利,你会看到类似下面的输出:
[Linux] Endpoint 14 created.
[Linux] Sent: Hello from Linux!
[RTOS] Received: Hello from Linux!
[RTOS] Replied: Hello from RTOS!
[Linux] Received: Hello from RTOS!
[Linux] Endpoint 14 destroyed.
看到这个输出,说明你的 RPMsg 通信链路已经通了。恭喜,你已经迈出了核间通信的第一步。
cat /proc/interrupts 查看 IPI 中断计数。如果中断计数没增加,说明 RTOS 根本没收到通知。问题大概率出在共享内存地址或中断配置上。