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

作者

Eli Billauer,Xillybus 有限公司 (http://xillybus.com)

电子邮件

eli.billauer@gmail.com 或按 Xillybus 网站上公布的地址。

简介

背景

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

FPGA 面临的挑战是所有东西都以非常低的级别实现,甚至比汇编语言更低。为了让 FPGA 设计人员能够专注于他们的特定项目,而不是一次又一次地重复造轮子,通常会使用预先设计的构建块,即 IP 核。这些是 FPGA 库函数的并行概念。IP 核可以实现某些数学函数、功能单元(例如 USB 接口)、整个处理器(例如 ARM)或任何可能方便的东西。把它们想象成一个构建块,侧面悬挂着电线用于连接其他块。

FPGA 设计中一项艰巨的任务是与成熟的操作系统(实际上是与运行它的处理器)进行通信:实现低级总线协议以及与主机(寄存器、中断、DMA 等)的更高层次的接口本身就是一个项目。当 FPGA 的功能是众所周知的(例如视频适配器卡或网卡)时,专门为项目设计 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 核上)被配置为同步或异步。对于同步管道,write() 仅在某些数据已提交并被 FPGA 确认后才成功返回。这会减慢批量数据传输,并且几乎不可能用于需要恒定速率数据流的场景:在 write() 调用之间没有数据传输到 FPGA,尤其是在进程失去 CPU 时。

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

对于 FPGA 到主机的管道,只要打开相应的设备文件,异步管道就允许从 FPGA 传输数据,无论数据是否已通过 read() 调用请求。在同步管道上,只传输 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。这类似于刷新请求,但它不会阻塞。还有一种自动刷新机制,它在大约最后一次 write() 调用后 XILLY_RX_TIMEOUT jiffies 触发一个等效的刷新。这使得用户可以不必关心底层的缓冲机制,同时仍能享受到类似流的接口。

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

数据粒度

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

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

探测

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

  1. 获取 IDT 的长度,以便为其分配缓冲区。这通过向设备发送 quiesce 命令来完成,因为此命令的确认包含 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 核可以配置为不发送它们,以略微减少带宽。