远程处理器消息传递 (rpmsg) 框架¶
注意
本文档描述了 rpmsg 总线以及如何编写 rpmsg 驱动程序。要了解如何为新平台添加 rpmsg 支持,请查看 远程处理器框架(也在 Documentation/ 中)。
介绍¶
现代 SoC 通常在非对称多处理 (AMP) 配置中使用异构远程处理器设备,这些设备可能运行不同操作系统的实例,无论是 Linux 还是任何其他类型的实时操作系统。
例如,OMAP4 具有双核 Cortex-A9、双核 Cortex-M3 和一个 C64x+ DSP。通常,双核 Cortex-A9 在 SMP 配置中运行 Linux,而其他三个内核(两个 M3 内核和一个 DSP)在 AMP 配置中运行各自的 RTOS 实例。
通常,AMP 远程处理器采用专用的 DSP 编解码器和多媒体硬件加速器,因此通常用于从主应用程序处理器卸载 CPU 密集型多媒体任务。
这些远程处理器也可以用于控制延迟敏感的传感器、驱动随机硬件块或在主 CPU 空闲时执行后台任务。
这些远程处理器的用户可以是用户空间应用程序(例如,与远程 OMX 组件通信的多媒体框架)或内核驱动程序(控制仅远程处理器可访问的硬件、代表远程处理器保留内核控制的资源等)。
Rpmsg 是一种基于 virtio 的消息传递总线,允许内核驱动程序与系统上可用的远程处理器通信。反过来,如果需要,驱动程序可以公开适当的用户空间接口。
在编写将 rpmsg 通信公开给用户空间的驱动程序时,请记住,远程处理器可能可以直接访问系统的物理内存和其他敏感硬件资源(例如,在 OMAP4 上,远程内核和硬件加速器可能可以直接访问物理内存、gpio bank、dma 控制器、i2c 总线、gptimer、邮箱设备、hwspinlock 等)。此外,这些远程处理器可能正在运行 RTOS,其中每个任务都可以访问暴露给处理器的所有内存/设备。为了最大限度地降低恶意(或有缺陷的)用户空间代码利用远程错误并由此接管系统的风险,通常希望将用户空间限制在特定的 rpmsg 通道(参见下面的定义)上,它可以在这些通道上发送消息,并且如果可能,尽量减少它对消息内容有多少控制权。
每个 rpmsg 设备都是与远程处理器的通信通道(因此 rpmsg 设备被称为通道)。通道由文本名称标识,并具有本地(“源”)rpmsg 地址和远程(“目标”)rpmsg 地址。
当驱动程序开始在通道上侦听时,其 rx 回调会绑定到唯一的 rpmsg 本地地址(一个 32 位整数)。 这样,当入站消息到达时,rpmsg 核心会根据其目标地址将它们分派给适当的驱动程序(这是通过使用入站消息的有效负载调用驱动程序的 rx 处理程序来完成的)。
用户 API¶
int rpmsg_send(struct rpmsg_endpoint *ept, void *data, int len);
从给定的端点向远程处理器发送消息。调用者应指定端点、要发送的数据及其长度(以字节为单位)。消息将通过指定的端点的通道发送,即其源地址和目标地址字段将分别设置为端点的 src 地址及其父通道 dst 地址。
如果不存在可用的 TX 缓冲区,该函数将阻塞,直到有一个缓冲区变为可用(即,直到远程处理器使用一个 tx 缓冲区并将其放回 virtio 的已用描述符环上),或者经过 15 秒的超时时间。当后者发生时,将返回 -ERESTARTSYS。
该函数只能从进程上下文中调用(目前)。成功时返回 0,失败时返回适当的错误值。
int rpmsg_sendto(struct rpmsg_endpoint *ept, void *data, int len, u32 dst);
从给定端点向远程处理器发送消息,发送到调用者提供的目标地址。
调用者应指定端点、要发送的数据及其长度(以字节为单位)以及显式目标地址。
然后,消息将被发送到端点的通道所属的远程处理器,使用端点的 src 地址和用户提供的 dst 地址(因此通道的 dst 地址将被忽略)。
如果不存在可用的 TX 缓冲区,该函数将阻塞,直到有一个缓冲区变为可用(即,直到远程处理器使用一个 tx 缓冲区并将其放回 virtio 的已用描述符环上),或者经过 15 秒的超时时间。当后者发生时,将返回 -ERESTARTSYS。
该函数只能从进程上下文中调用(目前)。成功时返回 0,失败时返回适当的错误值。
int rpmsg_trysend(struct rpmsg_endpoint *ept, void *data, int len);
从给定端点向远程处理器发送消息。调用者应指定端点、要发送的数据及其长度(以字节为单位)。消息将通过指定的端点的通道发送,即其源地址和目标地址字段将分别设置为端点的 src 地址及其父通道 dst 地址。
如果不存在可用的 TX 缓冲区,该函数将立即返回 -ENOMEM,而不会等到有一个缓冲区变为可用。
该函数只能从进程上下文中调用(目前)。成功时返回 0,失败时返回适当的错误值。
int rpmsg_trysendto(struct rpmsg_endpoint *ept, void *data, int len, u32 dst)
从给定端点向远程处理器发送消息,发送到用户提供的目标地址。
用户应指定通道、要发送的数据及其长度(以字节为单位)以及显式目标地址。
然后,消息将被发送到通道所属的远程处理器,使用通道的 src 地址和用户提供的 dst 地址(因此通道的 dst 地址将被忽略)。
如果不存在可用的 TX 缓冲区,该函数将立即返回 -ENOMEM,而不会等到有一个缓冲区变为可用。
该函数只能从进程上下文中调用(目前)。成功时返回 0,失败时返回适当的错误值。
struct rpmsg_endpoint *rpmsg_create_ept(struct rpmsg_device *rpdev,
rpmsg_rx_cb_t cb, void *priv,
struct rpmsg_channel_info chinfo);
系统中的每个 rpmsg 地址都通过 rpmsg_endpoint 结构绑定到 rx 回调(因此,当入站消息到达时,它们由 rpmsg 总线使用适当的回调处理程序分派)。
此函数允许驱动程序创建这样一个端点,并通过该端点将回调以及一些私有数据绑定到 rpmsg 地址(无论是预先已知的地址,还是将为其动态分配的地址)。
简单的 rpmsg 驱动程序无需调用 rpmsg_create_ept,因为在它们被 rpmsg 总线探测时(使用它们在注册到 rpmsg 总线时提供的 rx 回调)已经为它们创建了一个端点。
所以对于简单的驱动程序来说,一切都应该正常工作:它们已经有一个端点,它们的 rx 回调绑定到它们的 rpmsg 地址,并且当相关的入站消息到达时(即,其 dst 地址等于其 rpmsg 通道的 src 地址的消息),驱动程序的处理程序被调用来处理它。
也就是说,更复杂的驱动程序可能确实需要分配额外的 rpmsg 地址,并将它们绑定到不同的 rx 回调。为了完成这个任务,这些驱动程序需要调用这个函数。 驱动程序应提供它们的通道(因此新的端点将绑定到它们的通道所属的同一个远程处理器)、一个 rx 回调函数、一个可选的私有数据(当调用 rx 回调时会返回),以及它们想要绑定到回调的地址。如果 addr 是 RPMSG_ADDR_ANY,则 rpmsg_create_ept 将动态地为它们分配一个可用的 rpmsg 地址(驱动程序应该有一个非常好的理由不总是在这里使用 RPMSG_ADDR_ANY)。
成功时返回指向端点的指针,出错时返回 NULL。
void rpmsg_destroy_ept(struct rpmsg_endpoint *ept);
销毁现有的 rpmsg 端点。 用户应提供指向先前使用 rpmsg_create_ept() 创建的 rpmsg 端点的指针。
int register_rpmsg_driver(struct rpmsg_driver *rpdrv);
向 rpmsg 总线注册 rpmsg 驱动程序。 用户应提供指向 rpmsg_driver 结构的指针,该结构包含驱动程序的 ->probe() 和 ->remove() 函数、rx 回调以及一个 id_table,指定该驱动程序有兴趣探测的通道的名称。
void unregister_rpmsg_driver(struct rpmsg_driver *rpdrv);
从 rpmsg 总线注销 rpmsg 驱动程序。 用户应提供指向先前注册的 rpmsg_driver 结构的指针。 成功时返回 0,失败时返回适当的错误值。
典型用法¶
以下是一个简单的 rpmsg 驱动程序,它在 probe() 上发送一条“hello!”消息,并且每当它收到一条传入消息时,它都会将其内容转储到控制台。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/rpmsg.h>
static void rpmsg_sample_cb(struct rpmsg_channel *rpdev, void *data, int len,
void *priv, u32 src)
{
print_hex_dump(KERN_INFO, "incoming message:", DUMP_PREFIX_NONE,
16, 1, data, len, true);
}
static int rpmsg_sample_probe(struct rpmsg_channel *rpdev)
{
int err;
dev_info(&rpdev->dev, "chnl: 0x%x -> 0x%x\n", rpdev->src, rpdev->dst);
/* send a message on our channel */
err = rpmsg_send(rpdev->ept, "hello!", 6);
if (err) {
pr_err("rpmsg_send failed: %d\n", err);
return err;
}
return 0;
}
static void rpmsg_sample_remove(struct rpmsg_channel *rpdev)
{
dev_info(&rpdev->dev, "rpmsg sample client driver is removed\n");
}
static struct rpmsg_device_id rpmsg_driver_sample_id_table[] = {
{ .name = "rpmsg-client-sample" },
{ },
};
MODULE_DEVICE_TABLE(rpmsg, rpmsg_driver_sample_id_table);
static struct rpmsg_driver rpmsg_sample_client = {
.drv.name = KBUILD_MODNAME,
.id_table = rpmsg_driver_sample_id_table,
.probe = rpmsg_sample_probe,
.callback = rpmsg_sample_cb,
.remove = rpmsg_sample_remove,
};
module_rpmsg_driver(rpmsg_sample_client);
注意
可以在 samples/rpmsg/ 中找到一个可以构建和加载的类似示例。
rpmsg 通道的分配¶
目前我们只支持 rpmsg 通道的动态分配。
这只有在具有 VIRTIO_RPMSG_F_NS virtio 设备功能集的远程处理器上才有可能。 此功能位意味着远程处理器支持动态名称服务公告消息。
启用此功能后,rpmsg 设备(即通道)的创建是完全动态的:远程处理器通过发送名称服务消息(其中包含远程服务的名称和 rpmsg 地址,请参阅 struct rpmsg_ns_msg)来宣布远程 rpmsg 服务存在。
然后,此消息由 rpmsg 总线处理,rpmsg 总线反过来动态地创建和注册一个 rpmsg 通道(表示远程服务)。 如果/当注册了相关的 rpmsg 驱动程序时,它将立即被总线探测,然后可以开始向远程服务发送消息。
该计划还包括通过 virtio 配置空间添加 rpmsg 通道的静态创建,但尚未实现。