远程处理器消息(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 banks、dma 控制器、i2c 总线、gptimer、邮箱设备、hwspinlocks 等)。此外,这些远程处理器可能正在运行 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_send_offchannel(struct rpmsg_endpoint *ept, u32 src, u32 dst,
void *data, int len);
使用用户提供的 src 和 dst 地址向远程处理器发送消息。
调用者应指定端点、要发送的数据、其长度(以字节为单位)以及显式的源地址和目标地址。然后,该消息将发送到端点的通道所属的远程处理器,但将忽略端点的 src 地址和通道 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,失败时返回适当的错误值。
int rpmsg_trysend_offchannel(struct rpmsg_endpoint *ept, u32 src, u32 dst,
void *data, int len);
使用用户提供的源地址和目标地址向远程处理器发送消息。
用户应指定通道、要发送的数据、其长度(以字节为单位)以及显式的源地址和目标地址。然后,该消息将发送到该通道所属的远程处理器,但将忽略该通道的 src 和 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 驱动程序,总线将立即对其进行探测,然后可以开始向远程服务发送消息。
计划还通过 virtio 配置空间添加静态创建 rpmsg 通道的功能,但这尚未实现。