4. RPMsg通信流程

理论说再多,不如跑通一个程序来得实在。这一章我们就动手写第一个RPMsg程序——Linux端发消息,RTOS端收消息。说白了,就是让两个核打个招呼。

我刚开始接触RPMsg时,也踩过不少坑。记得第一次调通时,看到RTOS终端打印出"Hello from Linux",心里还是挺激动的。好,咱们开始。

4.1 整体架构与数据流

先看看这个Hello World程序是怎么工作的。整体流程其实很简单:

Hello World RPMsg 数据流 Linux 端 发送 "Hello World" RPMsg 通道 共享内存 + 中断 RTOS 端 接收并打印消息 操作步骤: 1. Linux 端打开 RPMsg 设备节点 /dev/rpmsg0 2. Linux 端写入字符串 "Hello World" 3. RPMsg 驱动通过共享内存传递数据,触发中断通知 RTOS 4. RTOS 端回调函数收到数据,通过串口打印出来

4.2 环境准备

动手之前,先确认你的环境准备好了。我列个清单,你对照着检查一下:

项目 要求 说明
硬件平台 支持AMP架构的SoC 例如Zynq、i.MX8、STM32MP1等
Linux内核 4.19+,开启RPMsg 需要CONFIG_RPMSG=y
RTOS 支持RPMsg的版本 FreeRTOS、OpenAMP等
交叉编译工具链 ARM/Linux工具链 用于编译Linux端程序
串口终端 至少两个串口 一个给Linux,一个给RTOS

小提示:如果你用的是QEMU模拟环境,也能跑通这个例子。我个人习惯先在模拟器上调通逻辑,再上真板子,这样能省不少调试时间。

4.3 Linux端:发送程序

Linux端的程序其实很简单。RPMsg在Linux中被抽象为一个字符设备,你只需要打开设备节点,然后像写普通文件一样写数据就行。

来看代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

#define RPMSG_DEVICE   "/dev/rpmsg0"
#define MSG_SIZE       256

int main(int argc, char *argv[])
{
    int fd;
    char buf[MSG_SIZE];
    int ret;

    // 1. 打开RPMsg设备节点
    fd = open(RPMSG_DEVICE, O_RDWR);
    if (fd < 0) {
        perror("open rpmsg device failed");
        return -1;
    }

    // 2. 准备要发送的消息
    snprintf(buf, sizeof(buf), "Hello World from Linux!");

    // 3. 写入数据,触发RPMsg传输
    ret = write(fd, buf, strlen(buf) + 1);
    if (ret < 0) {
        perror("write to rpmsg failed");
        close(fd);
        return -1;
    }

    printf("Sent %d bytes: %s\n", ret, buf);

    // 4. 关闭设备
    close(fd);
    return 0;
}

这段代码做了四件事:打开设备、准备数据、写入数据、关闭设备。嗯,就这么简单。

关键点:write()函数返回的是实际写入的字节数。如果返回-1,说明RPMsg通道没准备好,或者RTOS端还没初始化完成。我在项目中遇到过这种情况,后来加了重试机制才解决。

4.4 RTOS端:接收程序

RTOS端的代码稍微复杂一点。因为RPMsg在RTOS中通常以回调函数的方式工作——当Linux端发来数据时,RPMsg框架会调用你注册的回调函数。

以FreeRTOS + OpenAMP为例:

#include <openamp/open_amp.h>
#include <metal/sys.h>
#include <metal/device.h>

#define RPMSG_SERVICE_NAME    "rpmsg-openamp-demo-channel"

static struct rpmsg_endpoint lept;
static int data_received = 0;

// RPMsg回调函数:当收到数据时被调用
static int rpmsg_endpoint_cb(struct rpmsg_endpoint *ept,
                             void *data, size_t len,
                             uint32_t src, void *priv)
{
    char *msg = (char *)data;

    // 确保字符串以'\0'结尾
    msg[len - 1] = '\0';

    // 通过串口打印收到的消息
    printf("Received: %s\n", msg);
    printf("Length: %d bytes\n", len);

    data_received = 1;
    return 0;
}

void rpmsg_receiver_task(void *arg)
{
    struct rpmsg_device *rpdev;
    int ret;

    // 1. 初始化RPMsg设备
    rpdev = platform_create_rpmsg_vdev(0, 0, 0);
    if (!rpdev) {
        printf("Failed to create rpmsg vdev\n");
        return;
    }

    // 2. 创建端点并注册回调
    ret = rpmsg_create_ept(&lept, rpdev,
                           RPMSG_SERVICE_NAME,
                           0, RPMSG_ADDR_ANY,
                           rpmsg_endpoint_cb, NULL);
    if (ret) {
        printf("Failed to create endpoint\n");
        return;
    }

    printf("Waiting for messages...\n");

    // 3. 等待数据到达
    while (!data_received) {
        // 让出CPU,等待中断
        taskYIELD();
    }

    // 4. 清理资源
    rpmsg_destroy_ept(&lept);
    platform_release_rpmsg_vdev(rpdev);
}

注意:回调函数是在中断上下文或高优先级任务中执行的。不要在回调里做耗时操作,比如打印大量日志。我曾经因为回调里加了调试打印,导致系统卡死,排查了半天才发现是打印占用了太多时间。

4.5 编译与运行

代码写好了,怎么编译和运行呢?我分两步说。

4.5.1 编译Linux端程序

用交叉编译工具链直接编译就行:

arm-linux-gnueabihf-gcc -o rpmsg_send rpmsg_send.c

编译好后,把可执行文件拷贝到目标板的Linux文件系统中。

4.5.2 编译RTOS端程序

RTOS端的编译要看你的具体环境。以OpenAMP为例,通常需要:

  1. 配置OpenAMP库的编译选项
  2. 将上面的接收代码加入工程
  3. 编译生成RTOS固件
  4. 将固件烧录到目标板

4.5.3 运行步骤

步骤 操作 预期结果
1 启动目标板,RTOS先运行 RTOS终端打印 "Waiting for messages..."
2 Linux系统启动完成 检查 /dev/rpmsg0 是否存在
3 运行 ./rpmsg_send Linux终端打印 "Sent X bytes: Hello World from Linux!"
4 查看RTOS终端 打印 "Received: Hello World from Linux!"

调试技巧:如果RTOS端没收到消息,先检查设备节点是否存在。用 ls -l /dev/rpmsg* 看看。如果节点不存在,可能是内核模块没加载,执行 modprobe rpmsg_user_dev_driver 试试。

4.6 常见问题与避坑

跑通这个Hello World,你可能会遇到几个问题。我把常见的列出来:

  • 设备节点不存在:检查内核配置,确认CONFIG_RPMSG和CONFIG_RPMSG_USER_DEV_DRIVER已开启。
  • write返回-1:RTOS端还没初始化完成,或者通道名称不匹配。检查两端使用的service name是否一致。
  • RTOS端收到乱码:检查共享内存地址对齐,RPMsg要求数据按4字节对齐。
  • 系统卡死:回调函数里做了阻塞操作。记住,回调里不要调用printf、malloc等可能阻塞的函数。

我曾经踩过的坑:有一次RTOS端一直收不到消息,查了两天。最后发现是Linux端的RPMsg驱动加载顺序不对——先加载了驱动,RTOS才初始化,导致通道建立失败。正确的做法是先让RTOS初始化完成,再加载Linux驱动。

4.7 小结

这一章我们跑通了第一个RPMsg程序。你想想看,整个过程其实就三步:Linux端写数据、RPMsg通道传数据、RTOS端收数据。代码量不大,但背后的机制值得好好理解。

我个人觉得,这个Hello World程序就像RPMsg的"敲门砖"。调通了它,你就掌握了核间通信的基本流程。后面章节我们会在这个基础上,加入更复杂的交互逻辑和性能优化。