概述
Xillybus 最常见的应用场景就是数据采集(data acquisition)。本页介绍如何将数据从 FPGA 传输到主机。
有关 FPGA 内部与 Xillybus 交互的更详细说明,请参阅 Xillybus FPGA 设计指南。
如果应用的数据速率很高,还建议阅读 Linux 快速入门指南或Microsoft Windows 快速入门指南中关于这一主题的章节 5。
主机端的处理
为了理解 Xillybus 的工作方式,最简单的是从计算机端入手:下面这条命令可以用来把采集到的数据写入磁盘上的一个文件中:
$ cat /dev/xillybus_read_32 > capture-file.dat
cat 是 Linux 的标准命令,会从一个文件读取所有数据,并把这些数据写到标准输出。在这个例子中,输入并不是一个普通文件,而是一个设备文件。这个输入由来自 FPGA 的数据流组成。由于对标准输出进行了重定向,这些数据会被写入磁盘上的一个文件。
这并不是一个人为拼凑的例子:在某些应用场景中,这正是实际使用 Xillybus 做数据采集的正确方式。如果希望获取特定数量的数据,使用 dd 可能更好。更常见的情况是,用专门的计算机程序从设备文件中读取数据。每个应用程序都有自己喜欢的数据消费方式。
因此,不管你偏好哪种编程语言,也不管你用的是 Linux 还是 Windows,都没有关系。从 FPGA 接收数据的计算机软件只需要做和 cat 相同的事情:打开一个文件,然后从中读取。有一个单独的页面介绍标准的文件 I/O 编程技巧。关于 Xillybus 编程技巧的更详细信息,请分别参见面向 Linux 和 Windows 的编程指南。
用于数据采集的逻辑
现在来看看 FPGA 这边发生了什么。Xillybus IP 核(IP core)与应用程序逻辑通过一个 FIFO 交互:应用程序逻辑把数据写入 FIFO,然后 Xillybus 确保这些数据到达主机。如果你对 FIFO 的概念不熟悉,有一个单独的页面介绍 FIFO 的工作原理。
Xillybus 的演示包(demo bundle)中包含一个名为 xillydemo.v 的文件。这是与 IP 核对接的 Verilog 代码。演示包中还有一个 VHDL 文件:xillydemo.vhd。不过下面的例子是 Verilog 代码。
Xillybus IP 核的例化(instantiation)发生在 xillydemo.v(或 xillydemo.vhd)中。下面是与上面那条 cat 命令的数据采集例子相关的部分(其余部分省略)。
// Wires related to /dev/xillybus_read_32
wire user_r_read_32_rden;
wire user_r_read_32_empty;
wire [31:0] user_r_read_32_data;
wire user_r_read_32_eof;
wire user_r_read_32_open;
[ ... ]
xillybus xillybus_ins (
[ ... ]
// Ports related to /dev/xillybus_read_32
// FPGA to CPU signals:
.user_r_read_32_rden(user_r_read_32_rden),
.user_r_read_32_empty(user_r_read_32_empty),
.user_r_read_32_data(user_r_read_32_data),
.user_r_read_32_eof(user_r_read_32_eof),
.user_r_read_32_open(user_r_read_32_open),
[ ... ]
.bus_clk(bus_clk),
[ ... ]
);
IP 核各端口的含义在 Xillybus 逻辑 API 指南中有详细描述。
在 xillydemo.v 中有一个 FIFO 的例化。这个 FIFO 是单时钟 FIFO,适用于原始 Verilog 代码中所演示的回环应用。对于数据采集应用来说,双时钟 FIFO 更合适,因为数据采集逻辑通常使用自己的时钟。
因此,你可以把 xillydemo.v 改成下面这样,让它执行数据采集:删除单时钟 FIFO 的例化(它叫 fifo_32x512),然后插入以下内容:
assign user_r_read_32_eof = 0;
dualclock_fifo_32 fifo_32
(
.rd_clk(bus_clk),
.rst(!user_r_read_32_open),
.rd_en(user_r_read_32_rden),
.dout(user_r_read_32_data),
.empty(user_r_read_32_empty),
.wr_clk(capture_clk),
.wr_en(capture_en),
.din(capture_data),
.full(capture_full)
);
双时钟 FIFO
dualclock_fifo_32 是一个标准的双时钟 FIFO。需要把它作为 IP 用 FPGA 开发软件生成。这个 FIFO 的深度应为 512 个单元或更多。由于不同 FPGA 软件之间存在差异,端口名称可能与上面显示的不同。不过,推断出如何连接各个 FIFO 端口应该并不困难。
再次提醒,如果你对 FIFO 还不熟悉,这里有一个介绍 FIFO 的页面。
FIFO 有几个端口直接连接到 Xillybus 的 IP 核:rd_clk、rd_en、dout 和 empty。请注意,这些连线方式与演示包中的完全相同。IP 核使用这四个信号从 FIFO 中拉取数据。把这些端口与 IP 核相连,始终都应该采用这种正确方式。
请注意,rd_clk 连接到 bus_clk。这个信号来自 Xillybus 的 IP 核。换句话说,FIFO 的一侧使用由 IP 核决定的时钟。
至于 rst,请注意它连接到 !user_r_read_32_open。只有当主机上的相关设备文件处于打开状态时,user_r_read_32_open 才为高电平。因此,当文件没有打开时,FIFO 会处于复位状态。这样,当主机打开设备文件时,FIFO 是空的:如果 FIFO 中有上一次会话遗留的数据,也会在设备文件关闭时被清除。
这种表现通常正是我们对一个数据源所期望的。但如果你希望在设备文件关闭时 FIFO 仍保留数据,可以把 rst 连到别的东西上,或者干脆让 rst 保持低电平。
请注意,在这个例子中以及演示包中,user_r_read_32_eof 均为 0。这个信号可以用来向主机发送文件结束(end-of-file)指示。更多信息见API 指南。
与应用程序逻辑的接口
在数据采集应用中,总有某种应用程序逻辑负责产生要发送到主机的数据。这部分因应用而异,与本次讨论无关。我们重点关注如何把这些数据发送到主机。
这一部分出乎意料地简单:应用程序逻辑把数据写入 FIFO 即可。写入 FIFO 的数据,最终会以连续数据流的形式到达主机上的计算机程序。
因此,应用程序逻辑只需遵循标准的 FIFO 写入约定即可。在上面的例子中,这一点由 capture_clk、capture_en、capture_data 和 capture_full 体现。逻辑只需要把数据放到 capture_data 上,并控制 capture_en,就能正确地向 FIFO 写入数据。请注意,应用程序逻辑是使用自己的时钟向 FIFO 写入数据的。
数据流
下面是一个简化的框图,说明了从应用程序逻辑到主机上用户应用程序的数据流。
请注意,这个框图中省略了两个技术细节:PCIe 模块和内核驱动没有画出来,因为它们与用户对数据流的感知无关。使用 Xillybus 的正确方法就是忘掉这些细节,把注意力集中在应用程序逻辑和应用程序软件上。
不需要把数据组织成数据包:应用程序逻辑与计算机之间的通信通道是一条连续的数据流。IP 核和驱动会确保数据流表现得像其他流协议一样,例如 Linux 中程序之间的管道(pipe)。另一种行为类似的协议是 TCP/IP。换句话说,向 FIFO 写入多少数据并不重要,这些数据很快都会到达主机上的用户应用程序。
一个常见的错误是把数据组织成包,并让 IP 核的 DMA 缓冲区去适应这些包的大小。这样做没有任何好处。即使发送的数据是按固定大小分包的,也没有必要让 IP 核去迁就这个大小。
如果 FIFO 满了怎么办?
FIFO 的基本规则之一是:如果 full 端口为高电平,wr_en 必须为低电平。简而言之:不要往一个满的 FIFO 里写数据。那么如果发生了这种情况呢?处理这种状况会大大增加应用程序逻辑的复杂度。
简短的回答是:FIFO 永远不会满。在正常工作条件下,这一点是被主动保证的:IP 核不断从 FIFO 读取数据,并把这些数据复制到主机的 RAM 中。这一过程进行得足够快,因此 FIFO 不会被填满。通常,FIFO 的深度不需要超过 512 个数据单元。
但是,如果应用程序逻辑写入 FIFO 的速度过快,FIFO 仍可能被填满。换句话说,如果应用程序逻辑的平均数据速率超过了 IP 核的限制(每种类型的 IP 核都会标明这一限制),IP 核就来不及把数据从 FIFO 中读走。
另一种可能是,用户应用程序软件(例如上面例子中的 cat)没有及时从设备文件中读取数据。结果,主机上的 RAM 缓冲区会被填满,这同样会阻止 IP 核从 FIFO 中读取数据(因为 IP 核没有地方可以写入数据了)。最终导致溢出(overflow)。这可能是因为用户应用程序软件写得不好。另一个可能的原因与操作系统有关,稍后会进一步讨论。
主机端 RAM 缓冲区的大小取决于 Xillybus IP 核。例如,对于 xillybus_read_32 和 xillybus_write_32(位于演示包所包含的 IP 核中),这个大小是 4 MB。IP Core Factory 可以创建请求更大缓冲区的自定义 IP 核。
总之:防止溢出,归根结底是为 IP 核选择正确的参数:首先,所选 IP 核必须能够承受数据速率。此外,主机上的 RAM 缓冲区要足够大。这样才能保证即使用户应用程序暂时没有从设备文件中读取数据,数据流也能持续进行。
如果即便如此 FIFO 还是满了,常见原因就是系统设计有误。溢出的常见原因之一,是高估了计算机处理该数据速率的能力。
尤其是当数据要写入磁盘上的文件时(比如上面的 cat 命令),最大数据速率可能比你想象的要低。原因是操作系统通常有很大的磁盘缓存(可能有数 GB)。如果用比磁盘缓存还小的数据量去测量磁盘的数据速率,结果会过于乐观:操作系统会在数据真正写完之前就假装已经写完。实际上,数据只是到了缓存里,真正的磁盘写入要在之后才发生。只有处理更大数量的数据时,这个问题才会暴露出来。
CPU 被剥夺
不幸的是,有一种溢出是不可避免的:操作系统(Linux 或 Windows)有权让任何用户空间进程无限制地暂停对 CPU 的使用。换句话说,读取数据的计算机程序可能会突然停止工作一段时间,然后再恢复正常。这段时间没有上限。任何非实时操作系统都允许像这样随机地暂停进程。
但是,在这些操作系统上仍然可以进行数据采集。这主要是因为,长时间的 CPU 剥夺通常被视为一种糟糕的行为。因此这些暂停通常都很短暂。
在这些暂停期间,IP 核会继续填充主机上的 RAM 缓冲区(这借助 DMA 完成,不需要处理器介入)。当计算机程序重新获得 CPU 后,它可以迅速消费掉所有已积累的数据。通常,能补偿 10 ms 暂停的 RAM 缓冲区就足够了。不过,如果在 IP Core Factory 创建自定义 IP 核,你可以请求大得多的缓冲区。
话虽如此,暂停时间过长的情况仍然可能发生。其结果是 RAM 缓冲区变满,进而 FPGA 上的 FIFO 也变满。这种溢出的后果是数据丢失。这种事情不应该发生,而且很可能也不会发生。但如果真的发生了呢?
检测溢出
建议的解决方案是:如果 FIFO 变满,就终止数据流。具体做法是:在连续数据的最后一个单元被读出后,逻辑立即向主机发送一个 EOF。那么我们来想想,如果主机像前面建议的那样用 cat 命令消费数据,会发生什么:
$ cat /dev/xillybus_read_32 > capture-file.dat
通常情况下,这条命令会一直运行,直到你用 CTRL-C 停止它。但如果 FPGA 中的 FIFO 变满了,这条命令会正常结束,就像它复制完一个普通文件后自然结束一样。输出文件中将包含 FIFO 变满之前采集到的所有数据。
总结一下这种方法的要点:写入 capture-file.dat 的所有数据都保证是正确无误的、连续的。如果数据采集系统因为 CPU 被剥夺而无法维持连续性,结果是输出文件变短。但文件的内容是可信赖的。
要实现这一方案,请把 dualclock_fifo_32 的例化替换为下面这样:
eof_fifo fifo_32
(
.rd_clk(bus_clk),
.rst(!user_r_read_32_open),
.rd_en(user_r_read_32_rden),
.dout(user_r_read_32_data),
.empty(user_r_read_32_empty),
.wr_clk(capture_clk),
.wr_en(capture_en),
.din(capture_data),
.full(),
.eof(user_r_read_32_eof)
);
eof_fifo 的定义在另一个页面上给出。
请注意,user_r_read_32_eof 连接到了这个 FIFO 的 eof 端口。这就是逻辑在必要时向主机发送 EOF 的方式。另请注意,这个 FIFO 的 full 端口没有连接任何东西:不再需要监控这个信号了。如果 FIFO 变满,反正也没什么可做的。EOF 机制确保在主机消费完所有有效数据之后,数据流可以重新开始。
数据回放
那相反方向呢?比如像下面这样的用法:
$ cat playback-data.dat > /dev/xillybus_write_32
其原理相同:cat 命令从磁盘上的文件读取数据,然后写入设备文件。在 FPGA 端,IP 核把这些数据写入一个 FIFO,应用程序逻辑再从 FIFO 中读取数据。思路完全一样,只是方向反了过来。
下面是一个简化的框图,说明了从主机上用户应用程序到应用程序逻辑的数据流。
与数据采集类似,这种应用不存在数据丢失的风险:IP 核在 FIFO 已满时不会向它写入数据。这样一来,主机 RAM 中的缓冲区也可能变满。此时,主机上的用户应用程序会等待(通过睡眠),直到应用程序逻辑从 FIFO 中读走了足够多的数据。
与数据采集的另一个相似之处是:只要用户应用程序持续地以足够快的速度写入数据,FIFO 就永远不会变空。为了保证这一点,需要考虑的因素是相同的:IP 核的规格以及用户应用程序都必须支持所要求的数据速率。
当这些条件得到满足时,应用程序逻辑就可以在需要数据时从 FIFO 中消费数据。下溢(underflow)永远不会发生。
异步数据流与同步数据流
这个话题与上面的内容并不直接相关,但仍然值得简单讨论一下。
在数据采集应用中,首要目标是保持连续的数据流。因此,IP 核会尽快把数据从 FIFO 搬运到主机的 RAM 缓冲区。主机上的用户应用程序此刻是否正在请求数据(通过调用 read() 等函数)并不重要:只要设备文件处于打开状态且 FIFO 中有数据,数据流就会继续。
这意味着,主机没有办法控制来自 FPGA 的数据流(除了打开和关闭设备文件,或者专门设计一种应用相关的方案之外)。不过,在绝大多数实际的数据采集应用中,并不需要控制数据流:设备文件一打开,数据流就开始,这完全没问题。至于每一个数据单元是在哪一刻从 FIFO 中被读走的,并不重要。
行为类似这样的设备文件被称为异步数据流(asynchronous stream)(这里使用 Xillybus 的术语)。
但在另一些应用中,数据是何时被采集的非常重要。例如,FPGA 上的应用程序逻辑发送的可能不是 FIFO 中的数据,而是一个状态寄存器的内容。演示包中名为 xillybus_mem_8 的设备文件就演示了这种用法。在这种情况下,控制 FPGA 端数据采集的时机就非常重要:主机读取设备文件,是为了获取该时刻的状态信息,而不是某个未知的过去时刻的状态。
针对这类应用,Xillybus 提供了同步数据流(synchronous stream):IP 核总是尽可能少地从 FPGA 收集数据。换句话说,IP 核只有在主机上发生 read() 等函数调用时才会去采集数据。这样就由主机控制着何时从 FPGA 采集数据。
同步数据流的缺点是数据流中会出现暂停。这些暂停的主要问题在于,当数据流暂时中断时,FPGA 上的 FIFO 可能会变满。这些暂停还会降低数据流的效率,因此最大数据速率会变低。不过,这两个缺点只对数据采集类应用才有影响。而数据采集应用本来就应该使用异步数据流。
对于相反方向的设备文件,异步数据流与同步数据流之间也有区别。在这个方向上,区别体现在 write() 函数调用的返回值上:对于异步数据流,write() 只要把数据写入了 RAM 缓冲区就会返回。因此,在大多数情况下,write() 根本不需要睡眠。而同步数据流则相反,write() 会一直等待,直到数据被送到 FPGA 端。这一点在通信通道用于发送命令时很重要。但对数据采集应用来说,这又是不利的。
在演示包中,只有 /dev/xillybus_mem_8 是同步数据流。其余四个设备文件都是异步数据流。
在 IP Core Factory 中,选择同步数据流还是异步数据流,取决于你选择的应用类型(即 “use” 下拉菜单)。例如,如果选择 “Data acquisition / playback”(数据采集/回放),该工具会生成异步数据流。如果选择 “Command and status”(命令与状态),则会得到同步数据流。你还可以关闭 “Autoset internals” 来手动选择。
关于异步数据流和同步数据流的更多信息,请参阅 Linux 编程指南(或 Windows 编程指南)的第 2 节。关于 IP Core Factory,请参阅Guide to defining a custom Xillybus IP core。
总结
用 Xillybus 可以既简单又快速地搭建一个虽然简约但实际可用的数据采集系统:应用软件那么简单,用一个标准的 Linux 命令(cat)就够了。在 FPGA 端,与 Xillybus IP 核的交互也只是把数据写进一个 FIFO。
用 Xillybus 采集到的数据保证是正确无误并且连续的。但是,由于操作系统的固有特性,无法保证溢出永远不会发生。既然这是不可避免的,最好的办法就是确保一旦发生溢出,就能被检测到。Xillybus 为此提供了一种简单的机制:向主机发送一个 EOF。

