01signal.com

访问 Xillybus的 device files

介绍

此页面基于Xillybus' programming guide for Linux 。如需更全面地了解以下主题,建议参阅本指南。 Windows也有类似的指南

如果你还没有做过“Hello, World”测试,建议先做

Xillybus IP core的通信通过 host上的 device files 进行。这些 device files 像普通文件一样被访问。但是,与普通文件不同, device file 并不代表存储在磁盘上的 data : 读取和写入 device file 会导致 I/O 操作。

因此,几乎所有编程语言都可以访问 Xillybus的 device files 。也可以为此目的使用 Linux command-line utilities 。所以有人可能想知道为什么有必要讨论这个话题: 如果您知道如何正确读取和写入文件,您就知道如何使用 Xillybus。

实际上,无需详细了解 API也可以编写访问常规文件的程序。然而,这对于使用 I/O是不够的: 与硬件的交互会产生普通文件很少发生的情况。作为一名程序员,有必要知道如何借助 API来处理这些情况。

因此,此页面的绝大部分内容都专门用于与访问常规文件相关的主题。 device files 的不同之处在于,他们在使用 API时对错误的容忍度较低。

对 API 缺乏了解可能导致的混淆主要有两种:

编程语言和操作系统

使用 Windows 时,使用 C、 C++、 C#、 Python、 Perl 以及 Cygwin自带的任何常用编程语言都没有问题。然而,一些工具(例如 MATLAB)可能拒绝与 Xillybus的 device files一起工作: 这些工具检测到 device file 不是常规文件,并将其视为错误。有针对这种情况的解决方法,通常是使用专为 low-level I/O而设计的 extensions 。

使用 Linux,任何可以访问文件的工具或编程语言都可以与 Xillybus一起使用。

下面的讨论基于 C 语言,但主题与所有编程语言相关。

示例代码

Xillybus的网站上有可供下载的编码示例。这些示例是用 C编写的,展示了如何正确使用低级 API 。

要下载示例,请转到您从中下载 demo bundle 的网页(即 Xillybus页面 XillyUSB页面)。

如果您使用的是 Linux,请下载 Linux driver。示例代码包含在同一个 .tar.gz 文件中。

如果您使用的是 Windows,请下载 Xillybus package for Windows。

无论哪种方式,示例代码都在 demoapps/ directory中。请注意,在此代码中,每个 I/O operation 中读取或写入的 data 的数量很小,因为分配了一个小的 buffer (128 bytes)。这样做的目的只是为了让代码更简单。在实际应用中,建议使用更大的 buffer (32 kBytes 通常是一个不错的选择)。

Linux 和 Windows 之间的区别很小但很重要。尤其:

也就是说,示例代码背后的原理是完全相同的。

Buffered file I/O

编程语言通常提供两个单独的 APIs 来访问文件: 一个高阶 API,一个低阶 API。高级 API 更常用,因为它更容易使用。

例如在 C 语言中,高层的 API 有 fopen()、 fread()、 fwrite()、 fprintf()、 fclose() 等,低层的 API 有 open()、 read()、 write()、 close() 等,这两个 APIs 的区别不在于 functions的名称略有不同: 高层 API 提供 user-space RAM buffers。这些 buffers 由 C run-time library实现。不要将它们与 DMA buffers混淆,后者由 kernel中的 driver 控制。

最重要的区别是 fwrite()的行为: call 到 function 的结果可能是 data 存储在 user-space buffer中。到 FPGA 的传输可能会延迟到以后。事实上, data 可以无限期地保留在 fwrite()的 buffer 中,直到文件关闭。这看起来像是 Xillybus的错误,因为 data 已写入文件,但什么也没有发生。

因此建议使用低级别 (non-buffering) API。这通常是可能的,即使大多数编程语言都提倡使用高级 API。如果工具或编程语言不支持低级 API,请改用可用的 API 。在这种情况下,重要的是要记住 I/O 何时发生是无法控制的。尽管如此,这在许多应用程序中已经足够好了,例如 data acquisition。

再次重申, buffered I/O 不应与 Xillybus的 buffers混淆。最重要的是, data 永远不会因为 Xillybus的 buffers而无限卡顿。这将在下面的 zero-length write()上下文中进一步解释。

从 device file读取: 基础

从 device file 读取的示例代码是 streamread.c。该程序可以在 demoapps/ directory中找到。但是,我将在下面显示的代码片段来自不同的程序: memread.c (来自同一个 directory)。该程序用于不同的目的,但它包含一个名为 allread()的 function 。用这个 function演示几个题目比较方便。

假设已使用此命令打开了一个文件:

int fd, len;
char *buf;

fd = open("/dev/xillybus_read_32", O_RDONLY);

现在我们要将文件中的 @len bytes 读入 buffer。但不应允许部分结果: 我们想要一个始终读取所需数量的 data的 function 。

这就是 allread() 的作用,当它像这样使用时:

allread(fd, buf, len);

这个 function 定义如下:

void allread(int fd, unsigned char *buf, int len) {
  int received = 0;
  int rc;

  while (received < len) {
    rc = read(fd, buf + received, len - received);

    if ((rc < 0) && (errno == EINTR))
      continue;

    if (rc < 0) {
      perror("allread() failed to read");
      exit(1);
    }

    if (rc == 0) {
      fprintf(stderr, "Reached read EOF\n");
      exit(1);
    }

    received += rc;
  }
}

请注意,此 function 始终读取请求的 bytes数量。如果这不可能, function 会导致计算机程序终止。对于正常的使用场景,这可能过于激烈。 allread() 应被视为如何使用低级 API访问文件的简单演示。

我们来解释一下这个 function。第一个有趣的部分是:

rc = read(fd, buf + received, len - received);

@received 在第一个 loop期间等于零。所以这一行等同于:

rc = read(fd, buf, len);

read() 尝试从 file descriptor (@fd) 读取 @len bytes 并将 data 存储在 buffer (@buf) 中。

至于 Xillybus device file: 如果请求的 data (@len bytes) 数量不可用, read() 最多等待 10 ms 。在这段短时间后,该函数返回的 data 少于所需的数量(但至少有一个 byte)。如果根本没有 data , read() 会无限期地等待,直到 data 从 FPGA 到达(但也有例外,下面会详细介绍)。此行为特定于 Xillybus的 driver (但仍然符合标准 API)。

如果 read() 能够读取某些内容,则 @rc 等于读取的 bytes 的数量。这意味着 @rc 为正数, loop 中的所有 if-statements 都被跳过。所以接下来会发生这种情况:

received += rc;

因此, @received 始终包含到目前为止已读取的 bytes 的总数。请注意, @rc 小于 @len (请求的 bytes 的数量)是完全合法且正常的。

while-loop 继续,直到 @received (已读取的 bytes 的总数)达到 @len。这在 while statement上有所体现:

while (received < len) { ... }

因此,如有必要,将阅读更多 data :

rc = read(fd, buf + received, len - received);

这一次, buffer 的起点移动到了 @received。要求的 bytes 的数量也减少了相同的数量。这些调整仅反映这是一次重复读取 data的尝试。

当 read() 不读取任何内容时

到目前为止,我一直关注 read() 能够读取 data时会发生什么。然而,在三种情况下 read() 不读取任何内容。这些情况中的每一种都由其自己的 if-statement处理。

POSIX signals

当您按 CTRL-C 停止程序时,操作系统会向 process发送一个 POSIX signal 。这是使程序终止的机制。当您出于相同目的使用“kill”命令时,也会发生同样的事情。但也有很多其他类型的 signals 在大多数情况下应该被忽略。

那么,如果 process 在对 read()的函数调用过程中接收到 signal ,会发生什么情况?按照 Linux的约定, read() 必须立即将控制权交还给主程序。如果在此之前 read() 能够读取 data ,则不会发生任何异常情况: @rc 将包含 bytes的编号,并且不会显示已收到 signal 。

但如果没有新的 data 到货, @rc 将为负数, @errno 将等于 EINTR。处理这种情况的标准方法如代码所示: 表现得好像什么都没发生,然后再试一次。

if ((rc < 0) && (errno == EINTR))
  continue;

这并不意味着 signal 被忽略: 例如,如果 signal 的原因是用户按下 CTRL-C,程序将照常终止。还有另一种机制可以解决这个问题。此 if-statement 的目的是处理不打算引起任何戏剧性的 signals 。如果 process 收到此类 signal , continue-statement 可确保不会发生任何异常情况。

例如,如果 process 用 CTRL-Z停止,则需要这个 if-statement 来保证程序在恢复执行时继续运行。此外,还有其他几款 signals 无需任何人工干预即可到货。

真正的 error

很自然地,尝试读取 data时可能会出错。发生这种情况时, @rc 将为负数,而 @errno的值将不同于 EINTR。示例代码只是报告此错误,并终止计算机程序:

if (rc < 0) {
  perror("allread() failed to read");
  exit(1);
}

EOF

如果 read() 无法提供任何 data ,因为已到达文件末尾,则此 function 返回值零。这对于常规文件当然是正确的。但 Xillybus 也有能力声明 data stream 已经结束。行为是一样的。

相关代码是这样的:

if (rc == 0) {
  fprintf(stderr, "Reached read EOF\n");
  exit(1);
}

这也被视为错误,并导致计算机程序终止: 这意味着在读取请求的 data (@len bytes) 数量之前已达到 EOF 。只有 @received 小于 @len才能达到这个 if-statement 。

回想一下 allread() 背后的想法是它总是读取所需数量的 data。如果这不可能,此功能将停止计算机程序。

与其他编程语言的相关性

上面的示例代码是用 C编写的,但它演示了一些与编程语言无关的重要要点。

写入 device file

用于写入文件的低级 API 与从文件读取几乎相同。为了演示这一点,这是名为 allwrite()的 function ,它可以在 streamwrite.c中找到:

void allwrite(int fd, unsigned char *buf, int len) {
  int sent = 0;
  int rc;

  while (sent < len) {
    rc = write(fd, buf + sent, len - sent);

    if ((rc < 0) && (errno == EINTR))
      continue;

    if (rc < 0) {
      perror("allwrite() failed to write");
      exit(1);
    }

    if (rc == 0) {
      fprintf(stderr, "Reached write EOF (?!)\n");
      exit(1);
    }

    sent += rc;
  }
}

将其与上图所示的 allread() 进行比较: 只有三个区别:

所以原则上写和读没有区别。

话虽如此,请注意 @rc 永远不应为零,因为在写入文件时 EOF 没有任何意义。根据 POSIX standard,当要求 write() 写入零 bytes时, @rc 只能为零。但这在 while loop中从未发生过。

总而言之, allwrite() 总是写入所请求的 bytes 的数量。唯一的选择是终止 process。也就是说,假设一个 device file 已经通过以下方式打开:

int fd, len;
char *buf;

fd = open("/dev/xillybus_write_32", O_WRONLY);

从 @buf 写入 @len bytes 的过程如下:

allwrite(fd, buf, len);

上述关于 allread() 的所有内容也适用于 allwrite() 。这包括与其他编程语言的相关性。

Zero-length write

允许使用零 bytes对 write() 进行函数调用。标准 API 并未说明在该特定情况下会发生什么。但是很明显,这意味着不会写 data 。

此类函数调用对于 Xillybus device file具有特殊含义: 写入零 bytes 意味着请求 flush。要理解这个意思,我们先看看 data 写入一个 device file会发生什么。

我们假设 device file 是 asynchronous stream。这个术语在不同的页面上有简要的解释,在文档中有更详细的解释。

当 data 写入 device file (带有 write() )时, Xillybus driver 将此 data 存储在 RAM buffer中。可能会将此 data 的部分或全部立即发送到 FPGA 。但一般来说, buffer中可能会残留一定数量的 data ,执行函数调用的计算机程序仍然会继续运行。此机制的目的是提高性能,尤其是在对 write()进行许多函数调用时。

那么 driver的 RAM buffer 中的 data 是什么时候发送到 FPGA的呢?有四种可能的情况:

所以 data 永远不会在 driver的 buffer 中停留太久。这是因为 data 总是在 10 ms的时间段内发送到 FPGA 。但在某些应用程序中,即使这种延迟也是不可接受的。在这种情况下, zero-length write() 可用于请求立即发送任何剩余的 data 。

这是用 C 语言执行此操作的方法:

write(fd, NULL, 0);

请注意, address 到 buffer 是 NULL。这没关系,因为要写入的 bytes 的数量为零。但是这个函数调用并不能保证请求成功。尽管它很可能会成功,但这是正确的方法:

while (1) {
  rc = write(fd, NULL, 0);

  if ((rc < 0) && (errno == EINTR))
    continue; // Interrupted. Try again.

  if (rc < 0) {
    perror("flushing failed");
    break;
  }

  break; // Flush successful
}

所有这些都是针对 asynchronous stream说的。如果 device file 是 synchronous stream,则作为对 write()的函数调用的结果, data 总是立即发送到 FPGA 。除此之外, write() 会等到 data 到达 FPGA 后再返回( zero-length write() 不会这样做)。

所以 zero-length write() 只与 asynchronous streams相关。除非必要,否则不应使用此功能,因为它会减慢与 FPGA的通信速度。

概括

如前所述,上面写的几乎所有内容对于访问任何文件都是正确的。只有几个主题是特定于 Xillybus的。

务必遵循这些准则以确保在与 FPGA通信时行为一致。不考虑这些主题而编写的计算机程序可能偶尔会出现故障。这些故障通常看起来是 FPGA 或 driver的问题。因此,正确的编程技术将避免很多混乱和不必要的努力。

此页面由英文自动翻译。 如果有不清楚的地方,请参考原始页面
Copyright © 2021-2024. All rights reserved. (6f913017)