用于通用 FPGA 接口的 Xillybus 驱动程序

作者:

Eli Billauer, Xillybus Ltd. (http://xillybus.com)

电子邮件:

eli.billauer@gmail.com 或如 Xillybus 网站上所宣传的那样。

简介

背景

FPGA(现场可编程门阵列)是一种逻辑硬件,可以被编程为几乎任何通常作为专用芯片组的功能:例如,显示适配器、网络接口卡,甚至带有外围设备的处理器。 FPGA 是硬件的乐高:基于某些构建模块,您可以按照自己喜欢的方式制作自己的玩具。重新实现市场上已经作为芯片组提供的某些东西通常毫无意义,因此 FPGA 主要用于需要某些特殊功能且产量相对较低时(因此不值得开发 ASIC)。

FPGA 的挑战在于,一切都在非常低的级别实现,甚至低于汇编语言。为了让 FPGA 设计人员专注于他们的特定项目,而不是一遍又一遍地重新发明轮子,通常会使用预先设计的构建模块、IP 核。这些是 FPGA 并行的库函数。 IP 核可以实现某些数学函数、功能单元(例如 USB 接口)、整个处理器(例如 ARM)或任何可能派上用场的功能。将它们视为一个构建块,侧面悬挂着电线,用于连接到其他模块。

FPGA 设计中一项艰巨的任务是与一个成熟的操作系统(实际上是与运行它的处理器)进行通信:实现低级总线协议和与主机(寄存器、中断、DMA 等)的更高层接口本身就是一个项目。当 FPGA 的功能是众所周知的(例如,视频适配器卡或 NIC)时,专门为该项目设计 FPGA 的接口逻辑是有意义的。然后编写一个特殊的驱动程序,将 FPGA 作为众所周知的接口呈现给内核和/或用户空间。在这种情况下,没有理由将 FPGA 与总线上的任何设备区别对待。

然而,期望的数据通信不符合任何众所周知的外部设备功能是很常见的。此外,为数据交换设计优雅的抽象通常被认为过于复杂。在这些情况下,寻求一种更快且可能不太优雅的解决方案:驱动程序实际上是作为用户空间程序编写的,而内核空间部分只保留基本的数据传输。这仍然需要为 FPGA 设计一些接口逻辑,并为内核编写一个简单的临时驱动程序。

Xillybus 概述

Xillybus 是一个 IP 核和一个 Linux 驱动程序。它们共同构成了一个 FPGA 和主机之间基本数据传输的工具包,提供带有简单用户界面的管道式数据流。它旨在为混合 FPGA-主机项目提供低成本解决方案,在这种情况下,驱动程序的项目特定部分在用户空间程序中运行是有意义的。

由于通信需求可能因 FPGA 项目而异(每个方向上所需的数据管道数量及其属性),因此没有一个特定的逻辑块是 Xillybus IP 核。相反,IP 核是根据其最终用户给定的规范进行配置和构建的。

Xillybus 提供独立的数据流,这些数据流类似于用户管道或 TCP/IP 通信。在主机端,字符设备文件就像任何管道文件一样使用。在 FPGA 端,硬件 FIFO 用于流式传输数据。这与通过固定大小缓冲区进行通信的常用方法相反(即使 Xillybus 在底层使用此类缓冲区)。单个 IP 核上可能有超过一百个此类流,但根据配置,也可能只有一个。

为了简化 Xillybus IP 核的部署,它包含一个简单的数据结构,该结构完全定义了核的配置。 Linux 驱动程序在其初始化过程中获取此数据结构,并相应地设置 DMA 缓冲区和字符设备。因此,可以使用单个驱动程序来开箱即用地处理任何 Xillybus IP 核。

刚刚提到的数据结构不应与 PCI 的配置空间或扁平设备树混淆。

用法

用户界面

在主机上,所有与 Xillybus 的接口都是通过 /dev/xillybus_* 设备文件完成的,这些文件在驱动程序加载时自动生成。这些文件的名称取决于加载到 FPGA 中的 IP 核(请参阅下面的探测)。要与 FPGA 通信,请打开与您要从中发送数据或接收数据的硬件 FIFO 对应的设备文件,并使用普通的 write() 或 read() 调用,就像使用常规管道一样。特别是,这样做是完全有意义的

$ cat mydata > /dev/xillybus_thisfifo

$ cat /dev/xillybus_thatfifo > hisdata

可能在某个阶段按下 CTRL-C,即使 xillybus_* 管道能够发送 EOF(但可能不使用它)。

驱动程序和硬件旨在像管道一样以合理的方式运行,包括

  • 支持非阻塞 I/O(通过在 open() 上设置 O_NONBLOCK)。

  • 支持 poll() 和 select()。

  • 在负载下高效利用带宽(使用 DMA),但也通过自动刷新来处理发送过来的少量数据(如 TCP/IP)。

设备文件可以是只读的、只写的或双向的。双向设备文件被视为两个独立的管道(除了在实现代码中共享一个“通道”结构之外)。

同步

Xillybus 管道配置(在 IP 核上)为同步或异步。对于同步管道,只有在 FPGA 提交并确认某些数据后,write() 才会成功返回。这会减慢批量数据传输,并且几乎不可能用于需要以恒定速率传输数据的流:在 write() 调用之间没有数据传输到 FPGA,尤其是在进程失去 CPU 时。

当管道配置为异步时,如果缓冲区中有足够的空间存储缓冲区中的任何数据,write() 将返回。

对于从 FPGA 到主机的管道,无论 read() 调用是否已请求数据,异步管道都允许数据在相应的设备文件打开后立即从 FPGA 传输。在同步管道上,只传输 read() 调用请求的数据量。

总之,对于同步管道,主机和 FPGA 之间的数据仅在满足驱动程序当前处理的 read() 或 write() 调用时才传输,并且这些调用会等待传输完成才返回。

请注意,同步属性与 read() 或 write() 完成的字节数少于请求的字节数的可能性无关。有一个单独的配置标志(“allowpartial”)决定是否允许此类部分完成。

可寻址管道

可以将同步管道配置为将流的位置暴露给 FPGA 上的用户逻辑。这样的管道也可以在主机 API 上寻址。通过此功能,可以将内存或寄存器接口连接到 FPGA 侧的可寻址流。通过寻址到所需的地址并根据需要调用 read() 或 write() 来读取或写入附加内存中的特定地址。

内部结构

源代码组织

Xillybus 驱动程序由一个核心模块 xillybus_core.c 和依赖于特定总线接口的模块(xillybus_of.c 和 xillybus_pcie.c)组成。

当内核找到合适的设备时,会探测总线特定的模块。由于 DMA 映射和同步功能(它们本质上依赖于总线)由核心模块使用,因此在初始化时,xilly_endpoint_hardware 结构会传递到核心模块。此结构填充了指向包装函数的指针,这些函数在总线上执行与 DMA 相关的操作。

管道属性

每个管道都有一些属性,这些属性在构建 FPGA 组件(IP 核)时设置。它们由 xillybus_core.c 中的 xilly_setupchannels() 从 IDT(定义内核配置的数据结构,请参阅下面的探测)中获取,如下所示

  • is_writebuf:管道的方向。非零值表示它是从 FPGA 到主机的管道(FPGA“写入”)。

  • channelnum:主机和 FPGA 之间通信中管道的标识号。

  • format:底层数据宽度。请参阅下面的数据粒度。

  • allowpartial:非零值表示 read() 或 write()(无论哪个适用)返回的字节数可能少于请求的字节数。常见的选择是非零值,以匹配标准的 UNIX 行为。

  • synchronous:非零值表示管道是同步的。请参阅上面的同步。

  • bufsize:每个 DMA 缓冲区的大小。始终为 2 的幂。

  • bufnum:为此管道分配的缓冲区数量。始终为 2 的幂。

  • exclusive_open:非零值强制独占打开关联的设备文件。如果设备文件是双向的,并且已经只在一个方向上打开,则可以打开相反的方向一次。

  • seekable:非零值表示管道是可查找的。请参阅上面的“可查找的管道”。

  • supports_nonempty:非零值(通常情况)表示硬件将发送必要的消息,以支持此管道的 select() 和 poll() 操作。

主机永远不从 FPGA 读取数据

尽管 PCI Express 通常是热插拔的,但典型的母板并不希望卡突然消失。但是,由于 PCIe 卡是基于可重编程逻辑的,因此当主机运行时意外重新编程 FPGA 很可能导致突然从总线上消失。实际上,在这种情况下不会立即发生任何事情。但是,如果主机尝试从映射到 PCI Express 设备的地址读取数据,会导致某些主板上的系统立即冻结,即使 PCIe 标准要求正常恢复。

为了避免这些冻结,Xillybus 驱动程序完全避免从设备的寄存器空间读取数据。从 FPGA 到主机的所有通信都通过 DMA 完成。特别是,中断服务例程在被调用时不会遵循检查状态寄存器的常见做法。相反,FPGA 准备一个包含简短消息的小缓冲区,这些消息通知主机中断是关于什么的。

出于统一性的考虑,此机制也用于非 PCIe 总线。

通道、管道和消息通道

呈现给用户的每个(可能是双向的)管道都在 FPGA 和主机之间分配一个数据通道。通道和管道之间的区别仅在通道 0 的情况下是必要的,通道 0 用于来自 FPGA 的中断相关消息,并且没有连接的管道。

数据流

尽管在双方都向用户呈现了非分段的数据流,但该实现依赖于为每个通道分配的一组 DMA 缓冲区。为了说明问题,让我们以 FPGA 到主机的方向为例:当数据流入 FPGA 中相应通道的接口时,Xillybus IP 核将其写入其中一个 DMA 缓冲区。当缓冲区满时,FPGA 会通知主机(附加一个 XILLYMSG_OPCODE_RELEASEBUF 消息到通道 0,并在必要时发送中断)。主机通过使数据可以通过字符设备读取来响应。当所有数据都被读取后,主机写入 FPGA 的缓冲区控制寄存器,允许覆盖该缓冲区。双方都存在流量控制机制,以防止下溢和溢出。

这不足以创建类似 TCP/IP 的流:如果在 DMA 缓冲区被填满之前数据流暂时停止,那么直观的期望是缓冲区中的部分数据无论如何都会到达,尽管缓冲区尚未完成。这是通过在 XILLYMSG_OPCODE_RELEASEBUF 消息中添加一个字段来实现的,通过该字段,FPGA 不仅通知提交的是哪个缓冲区,还通知它包含多少数据。

但是,只有在主机指示的情况下,FPGA 才会提交部分填充的缓冲区。当 read() 方法已阻塞 XILLY_RX_TIMEOUT jiffies(当前为 10 毫秒)时,就会发生这种情况,之后主机命令 FPGA 尽快提交 DMA 缓冲区。此超时机制在总线带宽效率(防止发送大量部分填充的缓冲区)和保持较低的数据尾部延迟之间取得平衡。

在主机到 FPGA 的方向上使用类似的设置。但是,部分 DMA 缓冲区的处理方式略有不同。用户可以通过将字节计数设置为零来发出 write(),从而告诉驱动程序将缓冲区中的所有数据提交给 FPGA。这类似于刷新请求,但不会阻塞。还有一个自动刷新机制,在大约最后一次写入后 XILLY_RX_TIMEOUT jiffies 后触发等效刷新。这允许用户忽略底层的缓冲机制,并享受类似流的接口。

请注意,对于具有非零“同步”属性的管道,部分缓冲区刷新的问题无关紧要,因为同步管道无论如何都不允许数据在 read() 和 write() 之间在 DMA 缓冲区中停留。

数据粒度

数据以 8 位、16 位或 32 位宽的字到达或发送到 FPGA,由 “format” 属性配置。在可能的情况下,当以与其自然对齐方式不同的方式访问管道时,驱动程序会尝试隐藏这一点。例如,从具有 32 位粒度的管道读取单个字节可以正常工作。将单个字节写入具有 16 位或 32 位粒度的管道也可以工作,但是驱动程序无法将部分完成的字发送到 FPGA,因此,最多一个字的传输可能会被延迟,直到它完全被用户数据占用。

这在某种程度上使主机到 FPGA 的流的处理变得复杂,因为当缓冲区被刷新时,它可能包含多达 3 个字节,这些字节在 FPGA 中不构成一个字,因此无法发送。为了防止数据丢失,这些剩余的字节需要移动到下一个缓冲区。xillybus_core.c 中以某种方式提到 “leftovers” 的部分与此复杂性相关。

探测

如前所述,驱动程序加载时创建的管道数量及其属性取决于 FPGA 中的 Xillybus IP 核。在驱动程序初始化期间,包含配置信息(接口描述表(IDT))的 blob 从 FPGA 发送到主机。引导过程分三个阶段完成:

  1. 获取 IDT 的长度,以便可以为其分配缓冲区。这是通过向设备发送静止命令来完成的,因为此命令的确认包含 IDT 的缓冲区长度。

  2. 获取 IDT 本身。

  3. 根据 IDT 创建接口。

缓冲区分配

为了简化防止 PCIe 数据包非法边界交叉的逻辑,应用以下规则:如果缓冲区小于 4kB,则它不得跨越 4kB 边界。否则,它必须是 4kB 对齐的。xilly_setupchannels() 函数通过从内核请求整页来分配这些缓冲区,并根据需要将它们划分为 DMA 缓冲区。由于所有缓冲区的大小都是 2 的幂,因此可以打包任何此类缓冲区的集合,最多浪费一页内存。

所有缓冲区都在驱动程序加载时分配。这是必要的,因为有时会请求较大的连续物理内存段,这在系统刚启动时更有可能可用。

缓冲区内存的分配按照它们在 IDT 中出现的顺序进行。驱动程序依赖于管道在 IDT 中按缓冲区大小递减排序的规则。如果请求的缓冲区大于或等于一页,则从内核请求必要数量的页,并将这些页用于此缓冲区。如果请求的缓冲区小于一页,则从内核请求一页,并且部分使用该页。或者,如果已经有一个部分使用的页面,则将缓冲区打包到该页面中。可以证明,以这种方式从内核请求的所有页面(最后一个可能除外)都得到了 100% 的利用。

“非空”消息(支持 poll)

为了支持 “poll” 方法(以及 select()),在 FPGA 到主机的方向上有一个小的注意事项:FPGA 可能已在 DMA 缓冲区中填充了一些数据,但尚未提交该缓冲区。如果主机等待 FPGA 提交缓冲区,则 FPGA 端可能会已发送数据,但 select() 调用仍然会阻塞,因为主机尚未收到任何关于此的通知。这通过 FPGA 发送的 XILLYMSG_OPCODE_NONEMPTY 消息来解决,当通道从完全空变为包含一些数据时会发送此消息。

这些消息仅用于支持 poll() 和 select()。可以配置 IP 核不发送这些消息以略微减少带宽。