4. RPMsg 基础通信示例:主核与从核的“Hello World”

这一章我们直接动手。很多朋友学 RPMsg 时,看了半天协议文档,结果连一个最简单的消息都发不通。我个人习惯是,先跑通一个“Hello World”,再回头啃理论。这样心里有底。

说白了,RPMsg 就是让 Linux 和 RTOS 互相递纸条。Linux 写个消息,通过共享内存 + 中断通知,RTOS 就能读到。反过来也一样。我们今天的目标就是:主核(Linux)发一条 “Hello from Linux”,从核(RTOS)收到后回复 “Hello from RTOS”。

核心要点: 本章演示 endpoint 的创建、消息发送、消息接收、以及 endpoint 的销毁。这是所有 RPMsg 应用的基础骨架。

4.1 整体通信架构

先看一张图,把整个流程串起来。你想想看,两个核之间没有网络,没有 socket,它们怎么通信?靠的就是 RPMsg 这个虚拟总线。

RPMsg “Hello World” 通信架构 Linux 主核 (Master) 用户态应用 (hello_world_user) RPMsg 字符设备 (/dev/rpmsg0) RPMsg 总线驱动 (virtio) 共享内存 vring 缓冲区 RTOS 从核 RPMsg 回调 Endpoint 处理 裸机/FreeRTOS 写 vring 读 vring 中断通知 (IPI / Doorbell) 回复消息路径 (RTOS -> Linux)

这张图里,共享内存是核心。Linux 把消息写到 vring 缓冲区,然后通过 IPI(核间中断)通知 RTOS。RTOS 从 vring 里把消息读出来,处理完再写回复。整个过程没有数据拷贝,全靠指针操作。

我的经验: 刚开始调试时,建议先确认共享内存的物理地址在两边是否一致。我在项目中遇到过,Linux 侧和 RTOS 侧对共享内存的映射地址不同,结果消息写进去,对方读到的全是乱码。排查了整整一天。

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,或者直接被丢弃。

我曾经踩过的坑: 有一次我改了 Linux 侧的 endpoint ID,忘了改 RTOS 侧。结果 Linux 侧一直收不到回复,因为 RTOS 侧用旧 ID 回复,消息被内核路由到了另一个 endpoint,而那个 endpoint 没人读。排查了三个小时,最后用 rpmsg_rpmsg_probe 的调试打印才发现 ID 不匹配。

4.5 编译与运行步骤

跑通这个示例,需要以下步骤。我按顺序列出来,每一步都别跳。

  1. 配置内核:确保 Linux 内核开启了 RPMsg 支持(CONFIG_RPMSG、CONFIG_RPMSG_VIRTIO)。
  2. 编译驱动:将 RPMg 字符设备驱动编译进内核或作为模块。
  3. 编译 RTOS 固件:把 RTOS 侧的代码编译成二进制,烧录到从核。
  4. 启动系统:先启动 Linux,再加载 RTOS 固件(通常通过 remoteproc 框架)。
  5. 确认设备节点:Linux 启动后,检查 /dev/rpmsg0 是否存在。
  6. 运行用户态程序:执行编译好的 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 通信链路已经通了。恭喜,你已经迈出了核间通信的第一步。

调试小技巧: 如果收不到回复,先检查中断是否到达。在 Linux 侧用 cat /proc/interrupts 查看 IPI 中断计数。如果中断计数没增加,说明 RTOS 根本没收到通知。问题大概率出在共享内存地址或中断配置上。