跳到主要内容

RPMSG(远程处理器消息传递)

RPMSG的概念与体验​

概念与作用​

RPMSG(Remote Processor Messaging)的核心价值是让异构芯片里的多个处理器 “顺畅聊天、协同干活”,对初学者来说,不用纠结底层细节,先搞懂它能落地解决哪些实际问题。现代芯片(手机、工业控制器、车载设备)里不只有主 CPU(如 ARM/x86),还有专门的协处理器(DSP/MCU/ISP/NPU)—— 这些协处理器擅长做特定任务(比如音频处理、传感器计算、AI 推理),但没法直接和主 CPU 沟通,RPMSG 就是它们的 “对话工具”

通信场景​

openvela 支持各种各样的跨核通讯,从操作系统上来看,覆盖了从 Linux 到 openvela,openvela 到 openvela 的两种通讯模式,从底层的物理传输方式上来看,覆盖了基于 SPI/UART 等物理传输的通讯方式和基于共享内存&中断的通讯方式。

跨核通信可以分为:

① 片内异构核心间跨核通信

芯片内部存在多个异构核心,之间需要进行通讯,芯片间可以通过共享内存进行通信,这种场景下,芯片间一般会提供共享内存和中断机制。如下图所示:

② 板级芯片间跨核通信

同一个 PCB 板中,多个芯片之间需要进行通讯,芯片之间通过物理通讯总线(比如UART、SPI)相连。如下图所示:

③ 跨核间接通信

不存在直接物理连接通道的两个核心(Droid 和 Audio)需要进行跨核通讯,需要通过现有的跨核通道进行消息转发。如下图所示:

④ 虚拟机之间跨核通信

在同一个 Hypervisor 上运行的多个虚拟机之间需要进行通讯,Hypervisor 会提供共享内存和中断机制。如下图所示:

不同的跨核通讯场景,对应不同的底层物理通讯通道,在此之上的代码是没有差别的。

体验​

对于openvela,RPMSG有多种使用场景:

① Rpmsg Syslog:用于异构多核的跨核LOG输出,A核的打印信息通过B核的串口打印出来。

② Rpmsg Uart:用于异构多核的跨核 Uart 数据传输,在 AMP 系统中,有些业务需要使用虚拟串口设备和另外一个核心进行通讯。

③ Rpmsg Socket:用于异构多核的跨核 Socket 通讯,让不同核心的应用也可以通过 Socket 进行通讯。

④ Rpmsg Fs 用于异构多核的跨核文件系统访问,App 可以无感的访问远端 CPU 中的文件、目录,就像访问本核的文件系统一样。

⑤ Rpmsg Dev 用于异构多核的跨核字符设备访问,APP 可以无感的使用远端 CPU 中的字符设备,和使用本端的字符设备一样。

本节使用Rpmsg Dev,在第1个openvela开发板上操作第2个openvela开发板的LED。

硬件连接如下:

在A、B板中分别烧写如下镜像文件:

启动这2个板子后,如果不想看到太多的调试信息,可以在2个板子的串口上都执行如下命令:

mv /data/xiaozhi.sh /data/xiaozhi.sh.bak

reboot

测试RPMSG的步骤如下:

① 在A板可以看到如下文件:

nsh> ls /dev/b_gpio0

/dev/b_gpio0

nsh>

nsh> ls /dev/rpmsg

/dev/rpmsg:

100ask_b

如果没有发现“/dev/rpmsg/100ask_b”文件的话,如下解决:

. 确认A、B板子的RS485接口接好了

. 同时重启2个板子

② 在B板可以看到如下文件:

nsh> ls /dev/rpmsg

/dev/rpmsg:

100ask_a

如果没有发现“/dev/rpmsg/100ask_a”文件的话,如下解决:

a. 确认A、B板子的RS485接口接好了

b. 同时重启2个板子

③ 在A板执行如下命令控制B板的LED1:

nsh> led /dev/b_gpio0 on

nsh> led /dev/b_gpio0 off

如果没有发现这些命令卡住了的话,如下解决:

a. 确认A、B板子的RS485接口接好了

b. 同时重启2个板子

RPMSG实验的代码修改​

修改UART2的引脚​

上图中A1、B1使用的是UART2,引脚为:PE2、PE3。修改“vendor/allwinnertech/chips/r528/r528_serial.c”,如下:

修改r528_boot.c使用Rpmsg Dev​

修改“vendor/allwinnertech/chips/r528/r528_boot.c”,如下:

修改“vendor/allwinnertech/chips/r528/Make.defs”加入头文件路径:

修改rpmsgdev.c支持GPIO的ioctl​

修改“nuttx/drivers/misc/rpmsgdev.c”,如下:

修改LED测试程序​

修改“apps/examples/led/led.c”,如下:

给A板配置、编译镜像​

执行如下命令进行环境变量的设置、并选择工程:

cd ~/vela-opensource/vendor/allwinnertech/lichee/

source vela_env.sh

source envsetup.sh

lunch_nuttx

You're building on Linux

Lunch menu... pick a combo:

1. r528s3-evb4

2. r528s3-velaevb1

Which would you like?: 2

执行“m menuconfig”,加入如下配置项:

CONFIG_R528_UART2=y

CONFIG_RPMSG_LOCAL_CPUNAME=”100ask_a”

CONFIG_RPMSG=y

CONFIG_RPMSG_PORT=y

CONFIG_RPMSG_PORT_UART=y

CONFIG_DEV_RPMSG=y

#CONFIG_DEV_RPMSG_SERVER # 这个不要配置

然后执行如下命令清除、编译、打包:

m c

m

pack

把得到的映像文件烧入A板:

/home/ubuntu/vela-opensource/vendor/allwinnertech/lichee/out/r528s3/velaevb1_nand/rtos_nuttx_r528s3-velaevb1_uart0_256Mnand.img

给B板配置、编译镜像​

执行如下命令进行环境变量的设置、并选择工程:

cd ~/vela-opensource/vendor/allwinnertech/lichee/

source vela_env.sh

source envsetup.sh

lunch_nuttx

You're building on Linux

Lunch menu... pick a combo:

1. r528s3-evb4

2. r528s3-velaevb1

Which would you like?: 2

执行“m menuconfig”,加入如下配置项:

CONFIG_R528_UART2=y

CONFIG_RPMSG_LOCAL_CPUNAME=”100ask_b”

CONFIG_RPMSG=y

CONFIG_RPMSG_PORT=y

CONFIG_RPMSG_PORT_UART=y

# CONFIG_DEV_RPMSG # 这个不要配置

CONFIG_DEV_RPMSG_SERVER=y

然后执行如下命令清除、编译、打包:

m c

m

pack

RPMSG的层次​

RPMSG被分为4层:服务层、框架层、传输层、物理层,如下:

怎么理解这些层次?使用这个案例来分析:

假设A板调用“led /dev/b_gpio0 on”可以控制B板的LED1,A板调用“button /dev/b_fb”可以绘制B板的屏幕,那么必定有如下问题:

两个驱动程序产生的数据包发给B板后,B板怎样才能分辨出它们,而不是混在一起?B板分辨出各个数据包后,怎么处理?方法就是每个数据包前面加入唯一的标识符,比如:

数据包加上ID后发送,既表明了“是谁发送的”,也表明了“由谁来处理”,相当于创建了多条专用的、虚拟的通道,通道的两端被称为“endpoint”,如下图所示:

于是,上面框图可以划分出5层:

RPMSG服务层注册情景分析​

核心链表​

对于服务层、框架层、传输层,为了便于理解,我们先讲服务层,再讲传输层,最后讲框架层:从上、下两层开始阅读代码。

注意:RPMSG之间的通信,一般可以分为Client、Server两端。Client 端发送完成后,阻塞等待 Server 端的回复,Server 端收到消息后调用对应的系统 API 执行相关操作,拿到结果后回复消息,Client 端收到回复后,将获取到的结果再返回给用户。

阅读代码时,每个核的框架层“nuttx\drivers\rpmsg\rpmsg.c”有2个链表(Client、Server各有2个链表),它们是关键:

static METAL_DECLARE_LIST(g_rpmsg_cb);

static METAL_DECLARE_LIST(g_rpmsg);

g_rpmsg里存放的是“rpmsg”,对应“传输能力”,每个物理传输通道都对应一个“rpmsg”。比如示例中A板和B板使用UART进行通信,那么两个板子的g_rpmsg里都会有一个“uart rpmsg”。g_rpmsg里的链表项由底下的“传输层”提供。

g_rpmsg_cb里存放的是“服务层”提供的“rpmsg_cb”:对于Client里面存放的是当物理连接被创建、断开时的回调函数;对于Server里面存放的是Client发来创建endpoint的请求以便获得服务时的回调函数。

以Rpmsg Dev为例,Clinet、Server两端执行的代码如下:

核心在于往各自的g_rpmsg_cb里存放的处理函数,“rpmsg_cb”结构体如下:

client端服务层注册​

client端“vendor/allwinnertech/chips/r528/r528_boot.c”调用了如下代码:

rpmsgdev_register("100ask_b", "/dev/gpio0", "/dev/b_gpio0", 0);

它的注册分为2部分,如下图所示:

① 注册rpmsg callback:上图的“rpmsg_register_callback”向传输层的g_rpmsg_cb链表注册了一个“rpmsg_cb_s”对象,它表示“当物理连接被创建后,cb->device_created被调用”,“当物理连接被断开后,cb->device_destroy被调用”

② 常规的驱动注册:这只是一个空架子,cb->device_created被调用后才能跟Server通信。

Server端服务层注册​

server端“vendor/allwinnertech/chips/r528/r528_boot.c”调用了如下代码:

rpmsgdev_server_init();

这个函数在“nuttx/drivers/misc/rpmsgdev_server.c”中实现,代码如下:

它向传输层的g_rpmsg_cb链表注册了一个“rpmsg_cb_s”对象,它没有提供device_created和device_destroy,而是提供了ns_match、ns_bind。它表示“当物理连接被创建后并且client发来RPMSG_NS_CREATE请求,cb->device_created被调用”,“当物理连接被断开后,cb->device_destroy被调用”。

Client和Server的关系​

在下图中,当底层的物理连接被创建后:

① client端的rpmsgdev_device_created函数被调用:它调用rpmsg_create_ept函数来创建endpoint,实际上就是向server寻求服务

② server端收到服务请求后,调用rpmsgdev_ns_match确认能否提供支持

③ 如果能提供支持,server端调用rpmsgdev_ns_bind处理client端的“rpmsg_create_ept”请求,建立通信channel。

RPMSG传输层注册情景分析​

函数执行过程分析​

我们把A板、B板的串口连接后,数据通道的物理连接就建立了。但是,A板、B板不知道啊,它们还需要打招呼,确认对方的存在。这个过程分为6步:

① 假设A板的传输层先初始化:它通过串口发送连接请求(即发送一个字符“RPMSG_PORT_UART_CONNREQ”,0x7d)

② 但是B板的传输层尚未注册,所以A板无法获得回应,连接未建立

③ 当B板的传输层初始化时:它通过串口发送连接请求(即发送一个字符“RPMSG_PORT_UART_CONNREQ”,0x7d)

④ A板回应“RPMSG_PORT_UART_CONNACK”即0x7c,连接就建立起来了

⑤ client端(A板)调用之前服务层注册的cb->device_created

⑥ 打招呼成功后,A板、B板各自的g_rpmsg链表里才有各自的rpmsg device

client端(A板)“vendor/allwinnertech/chips/r528/r528_boot.c”调用了如下代码:

struct rpmsg_port_config_s a_cfg = {"100ask_b", 4, 4, 200, 300};

rpmsg_port_uart_initialize(&a_cfg, "/dev/uart2", "100ask_a");

sever端(B板)“vendor/allwinnertech/chips/r528/r528_boot.c”调用了如下代码:

struct rpmsg_port_config_s b_cfg = {"100ask_a", 4, 4, 200, 300};

rpmsg_port_uart_initialize(&b_cfg, "/dev/uart2", "100ask_b");

两端注册传输层的结果如下图所示:

两端调用“rpmsg_port_uart_initialize”的执行过程是一样的,功能如下:

① 创建一个rpmsg_device结构体:里面分配buffer、设置了各类函数,特别是buffer相关的函数,代码如下。

② 创建一个“NS”endpoint:Name Servie,名字服务。

③ 创建接收线程、发送线程:

④ 发送线程里,发送一个字符“RPMSG_PORT_UART_CONNREQ”,0x7d

⑤ 对端的接收线程收到“RPMSG_PORT_UART_CONNREQ”后:

⑥ 本端的接收线程收到“RPMSG_PORT_UART_CONNACK”后:

⑦ 当物理连接确认建立后,各端会把rpmsg注册进各自的g_rpmsg,所以g_rpmsg里每项对应一个物理连接,能传输数据,然后调用服务层在g_rpmsg_cb里注册的回调函数。代码如下:

UART数据传输格式​

整个 Rpmsg 消息从原始数据到实际传输出去的流程见上图,从原始数据开始,每一层都会根据自己本层的需求,填充一个数据头,类似网络协议栈栈的数据包的组成。如下图所示:

在基于UART的传输层,它除了发送数据包之外,还可以发送一些命令,如下所示:

#define RPMSG_PORT_UART_START 0x7f // 帧起始标记

#define RPMSG_PORT_UART_END 0x7e // 帧结束标记

#define RPMSG_PORT_UART_CONNREQ 0x7d // 连接请求

#define RPMSG_PORT_UART_CONNACK 0x7c // 连接确认

#define RPMSG_PORT_UART_ESCAPE 0x7b // 转义字符

#define RPMSG_PORT_UART_ESCAPE_MASK 0x20 // 转义掩码

比如建立物理通道时,一端发送字符RPMSG_PORT_UART_CONNREQ(0x7d),另一端回复RPMSG_PORT_UART_CONNACK(0x7c)。

当发送数据包时,流程如下:

① 发送 RPMSG_PORT_UART_START(0x7f) 标记帧开始

② 对数据进行转义处理:

- 如果数据字节在 RPMSG_PORT_UART_ESCAPE(0x7b)和RPMSG_PORT_UART_START(0x7f)范围内(特殊字符)

- 先发送 RPMSG_PORT_UART_ESCAPE (0x7b)

- 再发送 (原字节 XOR 0x20)

- 例如:0x7f → 0x7b 0x5f

③ 发送RPMSG_PORT_UART_END(0x7e) 标记帧结束

在传输层,没有endpoint的概念,它只负责把数据包完整地发个对方,并且让对方能识别出来。

RPMSG通道建立情景分析​

本节的核心:

① 每个endpoint有自己的回调函数,用来处理发给自己的数据。

② 每个rpmsg都有自己的NS endpoint

物理通道建立后,服务层注册的回调函数cb->device_created(A板提供了这个函数,B板没有提供这个函数)被调用。

A板(client端)的“vendor/allwinnertech/chips/r528/r528_boot.c”调用了如下代码:

rpmsgdev_register("100ask_b", "/dev/gpio0", "/dev/b_gpio0", 0);

它注册的回调函数如下:

所以,当物理通道建立后,client端注册的回调函数被调用:它创建了一个新的endpoint,名为“rpdev-/dev/gpio0”,并且向对端的“NS endpoint” 发送“RPMSG_NS_CREATE”的消息,向获得对端的服务、创建虚拟的通信通道。那么,对端的“NS endpoint”的回调函数rpmsg_port_ns_callback被调用,如下图所示:

对端的“NS endpoint”的回调函数rpmsg_port_ns_callback作用是:在g_rpmsg_cb中寻找匹配的“服务”,调用它的绑定函数。调用过程如下:

最后,调用B板(Server端)的“rpmsgdev_ns_match”函数判断是否匹配,让调用“rpmsgdev_ns_bind”来生成Server端的“rpdev-/dev/gpio0 endpoint”,并把地址返回给对方。

如此,就在两端创建都分别创建了“rpdev-/dev/gpio0 endpoint”,如下:

远程LED控制流程情景分析​

整个 Rpmsg 消息从原始数据到实际传输出去的流程见上图,从原始数据开始,每一层都会根据自己本层的需求,填充一个数据头,类似网络协议栈栈的数据包的组成。如下图所示:

本节分析这些数据。

在A板执行“led /dev/b_gpio0 on”时,涉及的系统调用如下:

以“open”函数为例讲解跨核通信过程,调用“led /dev/b_gpio0 on”时,rpmsg的调试信息如下(忽略了更多,下面仅仅是open函数对应的信息):

nsh> led /dev/b_gpio0 on

[70/01/ 2 02:01:48] [CPU1] [12] [nsh] Send Data (0x41f56090):

[70/01/ 2 02:01:48] [CPU1] [12] [nsh] 0000 00 00 00 00 00 00 34 00 01 04 00 00 00 04 00 00 ......4.........

[70/01/ 2 02:01:48] [CPU1] [12] [nsh] 0010 00 00 00 00 1c 00 00 00 01 00 00 00 fa ff ff ff ................

[70/01/ 2 02:01:48] [CPU1] [12] [nsh] 0020 88 c8 fb 41 00 00 00 00 14 c9 fb 41 00 ca fb 41 ...A.......A...A

[70/01/ 2 02:01:48] [CPU1] [12] [nsh] 0030 43 00 00 00 C...

[70/01/ 2 02:01:48] [CPU1] [12] [nsh] TX Packed Data (0x41f577a4):

[70/01/ 2 02:01:48] [CPU1] [12] [nsh] 0000 7f 00 00 00 00 00 00 34 00 01 04 00 00 00 04 00 .......4........

[70/01/ 2 02:01:48] [CPU1] [12] [nsh] 0010 00 00 00 00 00 1c 00 00 00 01 00 00 00 fa ff ff ................

[70/01/ 2 02:01:48] [CPU1] [12] [nsh] 0020 ff 88 c8 fb 41 00 00 00 00 14 c9 fb 41 00 ca fb ....A.......A...

[70/01/ 2 02:01:48] [CPU1] [12] [nsh] 0030 41 43 00 00 00 7e AC...~

[70/01/ 2 02:01:48] [CPU0] [11] [nsh] Received data: (0x41f56e7c):

[70/01/ 2 02:01:48] [CPU0] [11] [nsh] 0000 7f 00 00 00 00 00 00 34 00 00 04 00 00 01 04 00 .......4........

[70/01/ 2 02:01:48] [CPU0] [11] [nsh] 0010 00 00 00 00 00 1c 00 00 00 01 00 00 00 00 00 00 ................

[70/01/ 2 02:01:48] [CPU0] [11] [nsh] 0020 00 88 c8 fb 41 00 00 00 00 a8 92 fb 41 00 00 00 ....A.......A...

[70/01/ 2 02:01:48] [CPU0] [11] [nsh] 0030 00 43 00 00 00 7e .C...~

“/dev/b_gpio0”对应的驱动程序为“nuttx/drivers/misc/rpmsgdev.c”,APP调用open函数时导致对应的“rpmsgdev_open”函数被调用,跟远端的交互过程如下:

步骤较细,在视频里讲解。