4. RPMsg通信流程
理论说再多,不如跑通一个程序来得实在。这一章我们就动手写第一个RPMsg程序——Linux端发消息,RTOS端收消息。说白了,就是让两个核打个招呼。
我刚开始接触RPMsg时,也踩过不少坑。记得第一次调通时,看到RTOS终端打印出"Hello from Linux",心里还是挺激动的。好,咱们开始。
4.1 整体架构与数据流
先看看这个Hello World程序是怎么工作的。整体流程其实很简单:
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为例,通常需要:
- 配置OpenAMP库的编译选项
- 将上面的接收代码加入工程
- 编译生成RTOS固件
- 将固件烧录到目标板
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的"敲门砖"。调通了它,你就掌握了核间通信的基本流程。后面章节我们会在这个基础上,加入更复杂的交互逻辑和性能优化。