4 实现数据采集(data acquisition)

4.1 引言

将数据从 FPGA 捕获到计算机的需求经常出现,例如:

  • 从视频信号源进行帧捕获。

  • 来自模数转换器(ADC)的数据。

  • 接收来自 FPGA 的调试信息。

对于此类应用,数据速率可能很高。尽管如此,必须保证数据流的连续性: 不允许丢失数据。

使用 Xillybus 实现数据采集应用非常简便,只需将数据写入 FIFO 即可。本节重点讨论如何确保到达主机的数据是连续 的。

从理论上讲,无法保证外设与计算机之间维持稳定的数据速率,因为操作系统可能会任意长时间地剥夺应用程序对 CPU 的 使用权。

然而,仍有方法可以维持连续的数据流。实现这一目标的首要且显而易见的条件是,使用能够以所需速率传输数据的 Xillybus 流。除此之外,还应采用特定的主机编程技术。这两个编程指南中对此问题进行了详细讨论:

请特别注意这两份指南的第 4 节,其中讨论了如何处理高数据速率。

对于高带宽应用,还建议参考其中一份指南的第 5 节,其中包含几个需要注意的主题:

但即使设计完美无缺,数据流的连续性仍然有可能被破坏: 操作系统的本质决定了它可以任意长时间地剥夺应 用程序对 CPU 的使用权。

因此,首要目标是确保数据流的连续性几乎永远不会被破坏。第二个目标是确保,如果尽管付出努力仍然发生这种情况,该 事件能被注意到。更重要的是,要保证到达主机的所有数据都是连续的。

为了实现第二个目标,应用逻辑应在连续性被破坏的点停止数据流。在此之后发送一个 EOF 到主机,以告知主机出现问 题。这样,应用程序就可以依赖到达的数据确实是连续的。

理想情况下,这种停止机制应该永远不会被激活。但当它被激活时,可以让我们意识到问题,并有机会解决它。

接下来,将展示如何使用 Xillybus 从连续数据源捕获数据。本节的重点是确保所有到达主机的数据都是数据源的可靠副 本。

4.2 示例代码

有两种方法可以修改设计以包含停止机制:

  • 使用修改后的 FIFO,如果连续性被破坏,它会停止数据流。这个修改后的 FIFO 是标准 FIFO 的包装器。它也会产生 EOF 信号。

  • 对使用 FIFO 的逻辑进行修改。

下面展示并解释的示例代码可以通过以下链接下载:

https://xillybus.com/downloads/xillycapture.zip

该 zip 文件包含三个文件:

  • eof_fifo.v,用 Verilog 编写

  • xillycapture.v,用 Verilog 编写

  • xillycapture.vhd,用 VHDL 编写

有两种方法可以尝试该示例。在这两种情况下,都应编辑 xillydemo.v 或 xillydemo.vhd。

第一种可能的方法是使用 eof_fifo.v: 将 fifo_32x512 的实例化替换为 eof_fifo 的实例化。然后将 user_r_read_32_eof 连接到该 FIFO 的 eof 端口。

除此之外,建议生成一个数据源来代替回环。可以是仅用于测试的假数据源(例如计数器)。

第二种可能的方法是使用 xillycapture.v 或 xillycapture.vhd: 断开 demo bundle 中与 read_32 相关的信号,并 改为插入这些文件中的示例代码。

请注意,在这两个文件中,有一个名为“slowdown”的信号。该信号的目的是降低假数据源的数据速率。当使用真实数据源 时,应移除该信号。

在这两种方法中,示例代码都实例化了一个标准的双时钟 FIFO。该 FIFO 的宽度为 32 位。在尝试对示例代码进行综合之 前,请使用工具(例如 Vivado 或 Quartus)生成此 FIFO。该 FIFO 的名称应为 async_fifo_32。深度为 512 个字就足够了。

本节的其余部分基于 xillycapture.v 和 xillycapture.vhd。但所解释的原理同样适用于理解 eof_fifo.v。

4.3 FIFO 连接

假设数据源与 capture_clk 同步。因此,数据以常规方式连接到一个标准的双时钟 FIFO。该 FIFO 连接在数据源和 Xillybus IP 核之间。

在 Verilog 中:

   async_fifo_32 fifo_32
     (
       .rst(!user_r_read_32_open),
       .wr_clk(capture_clk),
       .rd_clk(bus_clk),
       .din(capture_data),
       .wr_en(capture_en),
       .rd_en(user_r_read_32_rden),
       .dout(user_r_read_32_data),
       .full(capture_full),
       .empty(user_r_read_32_empty)
       );

在 VHDL 中:

  fifo_32 : async_fifo_32
    port map(
      rst        => reset_32,
      wr_clk     => capture_clk,
      rd_clk     => bus_clk,
      din        => capture_data,
      wr_en      => capture_en,
      rd_en      => user_r_read_32_rden,
      dout       => user_r_read_32_data,
      full       => capture_full,
      empty      => user_r_read_32_empty
      );

  reset_32 <= not user_r_read_32_open;

这与 demo bundle 非常相似: 文件关闭时复位 FIFO,其 user_r_read_32_* 信号按照 demo bundle 中的方式连 接。

4.4 数据采集控制

capture_en 信号用作写使能信号。有三种情况会阻止向 FIFO 写入数据:

  • 当文件关闭时

  • 当 FIFO 已满(full)时

  • 当自文件打开以来 FIFO 出现过已满(full)状态时

因此,capture_en 的条件(在 Verilog 中)归结为:

assign capture_en = capture_open && !capture_full &&
                    !capture_has_been_full ;

在 VHDL 中:

  capture_en <= capture_open and not capture_full
                and not capture_has_been_full ;

capture_open 信号是 user_r_read_32_open 在 capture_clk 时钟域中的副本。

在实际应用中,通常还有其他条件需要写入 FIFO。例如,等待视频帧的开始,或等待特定的错误条件(当使用数据采集进 行调试时)。此类条件可以根据需要通过逻辑 AND 添加到该表达式中。

信号 capture_has_been_full 在 FIFO 已满时变为高电平,并且仅在文件关闭时才恢复为低电平。因此,当 FIFO 已满时,数 据采集停止,并且在文件保持打开期间不会重新开始。

重要 的:
在示例代码中,对 capture_en 有不同的定义,这有助于减慢假数据源的速度。对于实际应用,应将 capture_en 更改为上 述定义。

现在来看实现 capture_has_been_full 的代码,在 Verilog 中:

always @(posedge capture_clk)
  begin
    if (!capture_full)
      capture_has_been_nonfull <= 1;
    else if (!capture_open)
      capture_has_been_nonfull <= 0;

    if (capture_full && capture_has_been_nonfull)
       capture_has_been_full <= 1;
    else if (!capture_open)
       capture_has_been_full <= 0;
   end

VHDL 版本:

  process (capture_clk)
  begin
    if (capture_clk'event and capture_clk = '1') then
      if ( capture_full = '0' ) then
        capture_has_been_nonfull <= '1' ;
      elsif ( capture_open = '0' ) then
        capture_has_been_nonfull <= '0' ;
      end if;

      if (capture_full = '1' and capture_has_been_nonfull = '1') then
        capture_has_been_full <= '1' ;
      elsif ( capture_open = '0' ) then
        capture_has_been_full <= '0' ;
      end if;

    end if;
  end process;

当 FIFO 的 capture_full 变为高电平时,capture_has_been_full 变为高电平。当文件关闭时,capture_has_been_full 变为低 电平。

另一个信号 capture_has_been_nonfull 解决了另一个问题: 只要 FIFO 处于复位状态,其 ’full’ 信号就是高电 平。如果由于这个原因导致 ’full’ 信号为高电平,则 capture_has_been_full 不应为高电平。换句话说,只有当 capture_full 曾经为低电平(意 味着 FIFO 退出复位)然后又变为高电平(意味着 FIFO 确实已满)时,capture_has_been_full 才应为高电平。

因此,这段代码有点复杂,但一旦理解了原理,就相当直接了。

4.5 生成 EOF

当满足以下两个条件时,生成文件结束符(EOF):

  • FIFO 中的所有数据都已被消耗(即所有数据都已被 IP 核读取)。

  • 不再有数据写入 FIFO,因为 FIFO 在过去已满过。

在 Verilog 中,这样写:

assign user_r_read_32_eof = user_r_read_32_empty && has_been_full;

在 VHDL 中(注意这是一个组合函数):

user_r_read_32_eof <= user_r_read_32_empty and has_been_full;

如示例代码所示,has_been_full 通过时钟域跨越将 capture_has_been_full 的值复制到 bus_clk 域。

请注意,user_r_read_32_eof 按照 API 允许的方式从低电平变为高电平。这是因为它与 user_r_read_32_empty 进行了逻辑 与运算,如第 3.3 节所建议的。

4.6 测试运行

重要 的:
此测试运行故意展示了一个IP核配置不合适的错误示例。此故意错误的目的在于演示 EOF 如何发挥作用。 本 次测试使用的 IP 核具有较小的缓冲区且使用同步流(synchronous stream)。这些对于数据采集应用来说是不正确的选择。正确配置的 IP 核不会表现出如下所示的糟糕性能。

为了确保传输数据的可重复性,数据源被选为一个简单的计数器,它计算发送的字数。直到 EOF 的数据量是随机的: EOF 发生在计算机因忙于处理其他事务而暂时忽略读取设备文件的任务时。

测试运行在 Linux 上展示,但也可以在 Windows 上运行。有关运行命令行工具的更多信息,请参阅以下指南:

测试运行可能如下所示:

$ cat /dev/xillybus_read_32 > first
$ cat /dev/xillybus_read_32 > second
$ ls -l
total 77740
-rw-rw-r--. 1 liveuser liveuser 71727100 Jul 13 15:31 first
-rw-rw-r--. 1 liveuser liveuser 7874556 Jul 13 15:31 second

因此,第一次尝试收集了约 71 MB,但第二次尝试只收集了 7 MB。每次运行的数据量取决于在操作系统为了处理其他事情 而忽略读取进程之前接收了多少数据。很可能是为了写入磁盘而短暂停止了读取进程。

但即使将所有数据丢弃到 /dev/null,它最终也会停止(尝试使用“man dd”了解更多关于 dd 工具的信息):

$ dd if=/dev/xillybus_read_32 of=/dev/null bs=1M
0+34365 records in
0+34365 records out
140756988 bytes (141 MB) copied, 18.0364 s, 7.8 MB/s
$ dd if=/dev/xillybus_read_32 of=/dev/null bs=1M
0+6027 records in
0+6027 records out
24684540 bytes (25 MB) copied, 3.16028 s, 7.8 MB/s

在这两次测试中,移动计算机鼠标都会停止数据流。这足以分散操作系统的注意力。

再次强调: 这些结果非常糟糕,因为使用了同步流(synchronous stream)。使用异步流(asynchronous stream)和适量的 DMA 缓冲区,根本不会出现此类问题。

最后,我们来看看其中一个文件的内容:

$ hexdump -C -v first | head
00000000 f8 fb a2 01 f9 fb a2 01 fa fb a2 01 fb fb a2 01 |................|
00000010 fc fb a2 01 fd fb a2 01 fe fb a2 01 ff fb a2 01 |................|
00000020 00 fc a2 01 01 fc a2 01 02 fc a2 01 03 fc a2 01 |................|
00000030 04 fc a2 01 05 fc a2 01 06 fc a2 01 07 fc a2 01 |................|
00000040 08 fc a2 01 09 fc a2 01 0a fc a2 01 0b fc a2 01 |................|
00000050 0c fc a2 01 0d fc a2 01 0e fc a2 01 0f fc a2 01 |................|
00000060 10 fc a2 01 11 fc a2 01 12 fc a2 01 13 fc a2 01 |................|
00000070 14 fc a2 01 15 fc a2 01 16 fc a2 01 17 fc a2 01 |................|
00000080 18 fc a2 01 19 fc a2 01 1a fc a2 01 1b fc a2 01 |................|
00000090 1c fc a2 01 1d fc a2 01 1e fc a2 01 1f fc a2 01 |................|

如预期,数据包含递增序列。用于生成数据的计数器从未复位,因此序列不是从 0 开始。

4.7 监控缓冲数据量

通常需要跟踪 Xillybus 缓冲区中属于特定流的数据量。这有助于控制延迟、防止溢出(overflow)或下溢(underflow),或 者防止应用程序在调用 read() 或 write() 时进入休眠。

例如,对于从 FPGA 到主机的数据流: 可能有一定量的数据存储在缓冲区中,因为 IP 核已从 FPGA 中的 FIFO 读取了这些数据,但应用程序尚未消耗这些数据。通常需要知道有多少数据正在等待处理。

同样,对于相反方向: 可能有应用程序已写入流但尚未到达 FPGA 中 FIFO 的数据。直接原因是 FPGA 中的 FIFO 已满(full),因此无法再从 IP 核接受数据。然而,真正的原因是数据正在等待被应用逻辑消耗。

Xillybus 没有提供专门的功能来估计缓冲区中的数据量。但是,有一种简单的方法可以利用 Xillybus 的现有功能来实现此功能,如下所 示。

为了解释所提出的解决方案,假设 demo bundle 中的一个流(FPGA 到主机,32 位)用于数据采集。

以下计数器用于统计自文件打开以来从 FIFO 中取出的数据字数(由 IP 核读取):

reg [31:0] count_data;

always @(posedge bus_clk)
  if (!user_r_read_32_open)
    count_data <= 0;
  else if (user_r_read_32_rden)
    count_data <= count_data + 1;

count_data 可以是寄存器阵列中的一个寄存器,如第 3.4 节所建议的。

另一种解决方案是向 IP 核添加另一个 Xillybus 流(从 FPGA 到主机)。该流用于将 count_data 的值发送到主机,通过将 count_data 直接连接到此附加流的数据端口(即通常连接到 FIFO 数据输出的端口)来实现。

该流的 ’eof’ 端口和 ’empty’ 端口应保持恒定为低电平。该流应配置为同步流 (synchronous stream),方法是在 IP Core Factory 中将其“use”参数设置为 “Command and status”。这样,应用程序可以随时从此流中读取 4 个字节,以获取 count_data 的更新 值。

请注意,count_data 与 bus_clk 同步,因此可以直接连接到 Xillybus IP 核的数据端口。

缓冲区中的数据量可以计算为 count_data 与自设备文件打开以来应用程序从其设备文件读取的数据量之间的差值(在此示例 中为 /dev/xillybus_read_32)。当然,软件必须跟踪它从该流读取的数据量。

在相反方向(从主机到 FPGA),可以在 FPGA 中维护一个类似的计数器,使用:

reg [31:0] count_data;

always @(posedge bus_clk)
  if (!user_w_write_32_open)
    count_data <= 0;
  else if (user_w_write_32_wren)
    count_data <= count_data + 1;

其工作原理相同: 应用程序跟踪写入相关设备文件的数据量。当需要知道缓冲区中存储了多少数据时,应用程 序读取 count_data。该数据量计算为已写入(自设备文件打开以来)的数据量与 count_data 值之间的差值。

请注意,在到目前为止的讨论中,FIFO 中的数据未包含在计算中: 只考虑了 Xillybus 在其缓冲区中保存的数 据。 有时需要获得端到端的数字,包括存储在 FIFO 中的数据。为此,应统计 FIFO 另一侧的操作。换句话说,对于从 FPGA 到 主机的流,这是写入 FIFO 的元素数量。对于相反方向,这是从 FIFO 读取的元素数量。

但是,如果 FIFO 的另一侧与不同的时钟同步(例如前面提到的 capture_clk),则实现起来可能更困难。这是因为 count_data 也需要与该其他时钟同步。因此,需要时钟域跨越才能将 count_data 的值连接到 IP 核。因此,当 FIFO 连接到两个不同的时钟 时,在准确性和简便性之间存在权衡。