01signal.com

访问 Xillybus的设备文件(device files)

介绍

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

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

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

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

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

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

对应用程序接口缺乏了解可能导致的混淆主要有两种:

编程语言和操作系统

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

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

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

示例代码(code)

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

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

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

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

无论如何,示例代码位于 demoapps/ 子目录内部。请注意,在此代码中,每 I/O operation 中读取或写入的数据量很少,因为分配了一个较小的缓冲区(128 字节)。这样做的目的只是为了使代码更简单。在实际应用中,建议使用更大的缓冲区(32 kBytes 通常是一个不错的选择)。

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

话虽如此,示例代码背后的原理完全相同。

Buffered file I/O

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

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

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

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

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

从设备文件读取: 基础

从设备文件读取的示例代码是 streamread.c。此程序(program)可在 demoapps/ 目录中找到。但是,下面我将展示的代码来自不同的程序: memread.c (来自相同的目录(directory))。这个程序(program)的用途不同,但它包含一个名为 allread()的函数。用这个函数来演示几个主题会更方便。

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

int fd, len;
char *buf;

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

现在我们要将文件中的 @len 字节读入缓冲区。但不应允许部分结果: 我们想要一个始终读取所需数量的数据的函数。

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

allread(fd, buf, len);

这个函数定义如下:

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;
  }
}

请注意,此函数始终读取所请求的字节数量。如果无法做到这一点,函数将导致程序终止。对于正常使用场景来说,这可能过于激烈。 allread() 应被视为如何使用低级应用程序接口访问文件的简单演示。

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

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

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

rc = read(fd, buf, len);

read() 尝试从 file descriptor (@fd) 读取 @len 字节并将数据存储在缓冲区(@buf) 中。

至于 Xillybus 设备文件: 如果请求的数据(@len 字节) 数量不可用, read() 最多等待 10 ms 。经过这一短暂的时间后,函数返回的数据数量将少于所需数量(但至少有一个字节)。如果根本没有数据, read() 将无限期等待,直到 FPGA 的数据到达(但也有例外,下文将详细介绍)。此行为特定于 Xillybus的驱动程序(但仍然符合标准应用程序接口)。

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

received += rc;

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

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

while (received < len) { ... }

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

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

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

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

到目前为止,我重点关注了当 read() 能够读取数据时会发生什么。然而,有三种情况下 read() 不会读取任何内容。每种情况都由其自己的 if-statement处理。

POSIX signals

当您按下 CTRL-C 来停止程序时,操作系统会向 process发送 POSIX signal 。这是使程序终止的机制。当您将 "kill" 命令用于相同目的时,也会发生相同的情况。但还有许多其他类型的 signals ,在大多数情况下应忽略它们。

那么,如果 process 在对 read()进行函数调用的过程中收到 signal ,会发生什么情况?根据 Linux的约定, read() 必须立即将控制权返回给主程序。如果 read() 能够在发生这种情况之前读取数据,则不会发生任何异常情况: @rc 将包含字节的编号,并且不会显示已收到 signal 。

但如果没有新的数据到达, @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 可以在没有任何人工干预的情况下到达。

真正的错误

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

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

EOF

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

相关的代码是这样的:

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

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

回想一下, allread() 背后的想法是它始终读取所需数量的数据。如果这不可能,则函数将停止计算机程序。

与其他编程语言的相关性

上面的示例代码是用 C编写的,但它演示了一些无论使用哪种编程语言都相关的要点。

写入设备文件

用于写入文件的低级应用程序接口与从文件读取几乎相同。为了演示这一点,这是名为 allwrite()的函数,它可以在 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 标准,当要求 write() 写入零字节时, @rc 只能为零。但这在 while loop中从未发生过。

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

int fd, len;
char *buf;

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

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

allwrite(fd, buf, len);

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

Zero-length write

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

此类函数调用对于 Xillybus 设备文件具有特殊含义: 写入零字节意味着请求 flush。要理解这个意思,我们先看看数据写入一个设备文件会发生什么。

我们假设设备文件是异步数据流(asynchronous stream)。这个术语在不同的页面上有简要的解释,在文档中有更详细的解释。

当数据被写入设备文件(使用 write() )时, Xillybus 驱动程序会将此数据存储在 RAM 缓冲区中。此数据的部分或全部可能会立即发送到 FPGA 。但一般来说,一定量的数据可能仍留在缓冲区中,而执行函数调用的程序仍会继续。此机制的目的是提高性能,特别是在对 write()进行许多函数调用的情况下。

那么驱动程序的 RAM 缓冲区中的数据是什么时候发送到 FPGA的呢?有四种可能的情况:

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

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

write(fd, NULL, 0);

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

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
}

所有这些都是针对异步数据流说的。如果设备文件是同步数据流(synchronous stream),则作为对 write()的函数调用的结果,数据总是立即发送到 FPGA 。除此之外, write() 会等到数据到达 FPGA 后再返回( zero-length write() 不会这样做)。

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

概括

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

遵循这些准则很重要,以确保与 FPGA通信时行为一致。在编写时未考虑这些主题的程序可能会偶尔出现故障。这些故障通常似乎是 FPGA 或驱动程序的问题。因此,正确的编程技术将节省大量混乱和不必要的努力。

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