介绍
此页面基于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是不够的: 与硬件的交互会产生普通文件很少发生的情况。作为一名程序员,有必要知道如何借助应用程序接口来处理这些情况。
因此,此页面的绝大部分内容都专门用于与访问常规文件相关的主题。设备文件的不同之处在于,他们在使用应用程序接口时对错误的容忍度较低。
对应用程序接口缺乏了解可能导致的混淆主要有两种:
- I/O operation 的数据数量可能与预期不同(通常小于预期)。
- 与 FPGA 的通信比预期晚。
编程语言和操作系统
使用 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 之间的区别很小但很重要。尤其:
- 函数的名称略有不同: _open() 与 open() 等
- 对于 Windows: 打开文件时必须使用 _O_BINARY 。
话虽如此,示例代码背后的原理完全相同。
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编写的,但它演示了一些无论使用哪种编程语言都相关的要点。
- read() 返回的数据可能比要求的少。这也可能发生在 buffered I/O 上(例如 fread() )。但是对于 buffered I/O,只有在出现错误或到达 EOF 时才会发生这种情况。相比之下,这对于 read()来说是正常的,并不表示发生了特殊情况。
- 程序必须正确处理 POSIX signals 。
- 在读取所有数据并达到 end-of-file (EOF) 后,read() 返回零值。
写入设备文件
用于写入文件的低级应用程序接口与从文件读取几乎相同。为了演示这一点,这是名为 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() 进行比较: 只有三个区别:
- 使用 write() 而不是 read()。但是这些函数的使用方法完全一样。
- 变量 @received (variable @received)的名字改成了 @sent。不过区别只是变量(variable)的名字不同。这款变量的含义和用法是完全一样的。
- 文本输出被改编: 它在之前说“read”的地方说“write”。
所以原则上写和读没有区别。
话虽如此,请注意 @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的呢?有四种可能的情况:
- RAM 缓冲区变满了。
- 设备文件关闭。
- 10 ms 的时间段已经过去 (automatic flush)
- 执行 zero-length write()
所以数据永远不会在驱动程序的缓冲区中停留太久。这是因为数据总是在 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 或驱动程序的问题。因此,正确的编程技术将节省大量混乱和不必要的努力。