3 I/O 编程实践

3.1 概述

Xillybus 能与任何能够访问文件的编程语言正常工作,任何用于访问文件的 API 都适用。

本指南侧重于底层 API 集合,该集合基于 open()、read()、write() 和 close() 等函数。之所以选择这一集合而非其他常见集 合(例如 fopen()、fwrite()、fprintf() 等),是因为底层 API 的函数没有额外的缓冲层。这些缓冲层对性能可能产生正面影响,但有了它们, 我们就无法控制实际的 I/O 操作。

当数据持续传输,且预期软件操作与硬件 I/O 之间不存在直接关联时,这一点就显得不那么重要。

额外的缓冲层也可能引起混淆,让人误以为存在软件错误,而实际上并没有。例如,调用 fwrite() 可能只是将数据存储在 RAM 缓冲区中,直到文件关闭才执行任何 I/O 操作。对此不了解的开发人员可能会被误导,认为 fwrite() 失败是因为 FPGA 端没有任何反 应,而实际上数据正在缓冲区中等待。

本部分描述了推荐的 UNIX 编程实践,使用了底层 C 运行时库函数。在此阐述这些内容是为了完整性,因为其中没有任何与 Xillybus 特定相关之处。

代码片段取自介绍在 在 Linux 主机上入门 Xillybus中的示例应用程序。这些示例中的设备文件名对应于用于 PCIe / AXI 的 Xillybus IP 核。对于 XillyUSB, 前缀是 xillyusb_00_* 而非 xillybus_*。

3.2 读取数据指南

假设已声明如下变量:

int fd, rc;
unsigned char *buf;

使用底层 open 函数打开设备文件(文件描述符为整数格式):

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

if (fd < 0) {
  perror("Failed to open devfile");
  exit(1);
}

如果该设备文件已被另一个进程打开用于读取(可根据请求进行非独占式打开),则会返回“Device or resource busy”(errno = EBUSY)错误。如果出现“No such device”(errno = ENODEV),则很可能是试图打开一个只写流。

在文件成功打开且 buf 指向已分配的内存缓冲区后,数据读取操作如下:

while (1) {
  rc = read(fd, buf, numbytes);

numbytes 是要读取的最大字节数。

返回值 rc 包含实际读取的字节数(如果函数调用异常完成则为负值)。

请注意,如果请求的数据量 numbytes 在内部可用,则 read() 会立即返回。否则,如果有任 何数据可用,它将在约 10 ms 后返回。如果完全没有可用数据,则 read() 会休眠,直到有数据可以返回。

驱动程序会检查数据是否可用,即 IP 核已从 FPGA 中的应用逻辑接收到该数据。DMA 缓冲区的机制对 read() 函数的调用者 而言是透明的,并且绝不会因为 DMA 缓冲区尚未填满而延迟数据传递至 read() 调用,如附录中第 A.3.5 节所述。

重要 的:
即使 read() 成功返回,也无法保证所有请求的字节都已从文件中读取。如果完成的数据量不满足要求,调用者有责任再次 调用 read()。

对 read() 的调用之后应检查其返回值,如下所示(“continue”和“break”语句假设处于 while 循环上下文中):

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

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

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

  // do something with "rc" bytes of data
}

第一个 if 语句检查 read() 是否因信号而提前返回。这是由于进程从操作系统接收到了信号。

这实际上并不是错误,而是一种迫使驱动程序立即将控制权返回给应用程序的条件。使用 EINTR 错误码只是向函数调用者表 明并未读取到数据。程序以“continue”语句响应,从而重新尝试以相同参数调用 read() 函数。

如果在信号到达时缓冲区中已有部分数据,驱动程序将返回 rc 中已读取的字节数。应用程 序不会知道信号已到达,并且根据 UNIX 编程惯例,它也无需关心:如果信号需要采取行动(例如来自键盘 CTRL-C 的 SIGINT),则由操作 系统或注册的信号处理器负责该行动。

请注意,有些信号不应影响执行流程,因此如果未按上述方式检测信号,程序可能会无故报告错误。

处理 EINTR 场景也是允许进程被停止(如 CTRL-Z)并正确恢复所必需的。

第二个 if 语句在报告用户可读的错误消息后,如果发生真实错误,则终止循环。

第三个 if 语句检测是否已到达文件末尾,返回值为零表示如此。当从 Xillybus 设备文件读取时,发生这种情况的唯一原因是 应用逻辑已置位流的 _eof 引脚(该引脚是 FPGA 上 IP 核接口的一部分)。

3.3 写入数据指南

假设已声明如下变量:

int fd, rc;
unsigned char *buf;

使用底层 open 函数打开设备文件(文件描述符为整数格式):

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

if (fd < 0) {
  perror("Failed to open devfile");
  exit(1);
}

如果该设备文件已被另一个进程打开用于写入(可根据请求进行非独占式打开),则会返回“Device or resource busy”(errno = EBUSY)错误。如果出现“No such device”(errno = ENODEV),则很可能是试图打开一个只读流。

在文件成功打开且 buf 指向已分配的内存缓冲区后,数据写入操作如下:

while (1) {
  rc = write(fd, buf, numbytes);

numbytes 是要写入的最大字节数。

返回值 rc 包含实际写入的字节数(如果函数调用异常完成则为负值)。

重要 的:
即使 write() 成功返回,也无法保证所有请求的字节都已写入文件。如果完成的数据量不满足要求,调用者有责任再次调用 write()。

对 write() 的调用之后应检查其返回值,如下所示(“continue”和“break”语句假设处于 while 循环上下文中):

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

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

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

  // do something with "rc" bytes of data
}

第一个 if 语句检查 write() 是否因信号而提前返回。这是由于进程从操作系统接收到了信号。

这实际上并不是错误,而是一种迫使驱动程序立即将控制权返回给应用程序的条件。使用 EINTR 错误码只是向函数调用者表 明并未写入任何数据。程序以“continue”语句响应,从而重新尝试以相同参数调用 write() 函数。

如果在信号到达之前已写入部分数据,驱动程序将返回 rc 中已写入的字节数。应用程序不 会知道信号已到达,并且根据 UNIX 编程惯例,它也无需关心:如果信号需要采取行动(例如来自键盘 CTRL-C 的 SIGINT),则由操作系统 或注册的信号处理器负责该行动。

请注意,有些信号不应影响执行流程,因此如果未按上述方式检测信号,程序可能会无故报告错误。

处理 EINTR 场景也是允许进程被停止(如 CTRL-Z)并正确恢复所必需的。

第二个 if 语句在报告用户可读的错误消息后,如果发生真实错误,则终止循环。

第三个 if 语句检测是否已到达文件末尾,返回值为零表示如此。当写入 Xillybus 设备文件时,这种情况绝不应该发生。

3.4 对异步下游流(Asynchronous Downstream)执行刷新(Flush)

如第 2.4 节所述,写入 PCIe / AXI IP 核上的异步流(Asynchronous Stream)的数据并不一定会立即发送到 FPGA,除非 DMA 缓冲区已填满(存在多个 DMA 缓冲 区)。这种行为通过确保分配的缓冲空间得到充分利用来提高性能。这也提高了 PCIe / AXI 总线上所发送数据包的效率。

如之前所述,XillyUSB IP 核几乎会立即发送数据,即使流是异步的,因为 USB 接口为此提供了高效的安排。因此,对于 XillyUSB IP 核,执行刷新仅在需要等待传输完成时才具有意义。

指向 FPGA 的流在关闭文件描述符时会自动进行刷新,但这是一种尽力而为的机制,不可依赖。对 close() 的调用会被延 迟,直到所有数据都到达 FPGA,其方式与同步流(Synchronous Stream)上 write() 函数调用被延迟的方式类似。显著的区别在于,close() 最多等待一秒以完成刷新。如果刷新在此时未完成,close() 无论如何都会返回,并在系统日志中发出警告消息。但请注意,在某些罕见的场 景下,关闭文件描述符时最后几个剩余数据字可能会丢失而不产生任何警告。

也可以显式请求对异步流进行刷新,方法是调用 write() 函数并传入一个长度为零的缓冲区,即:

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
}

请注意以下事项:

  • UNIX 的手册页并未定义 write() 函数调用在计数为零时应如何操作,而是将选择权留给每个设备驱动程序。这种刷新方法是 Xillybus 特有 的。

  • 与 close() 不同,上述 write() 会立即返回,无论数据何时在 FPGA 侧被消耗。

  • 因此,这种 write() 对于 XillyUSB 来说毫无意义。它无事可做,也确实什么都不做:数据无论如何几乎会立即发送,并且 write() 函数调用在任何情况下都不会等待。

  • 由于不会从缓冲区读取数据,write() 函数调用中的缓冲区参数可以取任意值,包括 NULL,如上所示。

  • 使用更高级别的 API,并传入长度为零的缓冲区,可能完全没有任何效果。例如,调用 fwrite() 写入零字节可能只是简单地返 回而没有任何操作,因为该函数通常所做的是将数据添加到 C 运行时库创建的缓冲区中。

  • fflush() 与此无关:它执行的是高级别缓冲区的刷新,但不会向底层驱动程序发送刷新命令。

  • 无需对另一个方向(从 FPGA 到主机)的流执行刷新,也没有办法这样做。这是因为当主机尝试读取数据即将使进程进入休 眠(即阻塞)时,会自动对这些流进行刷新。

3.5 select() 与非阻塞 I/O

尽管不推荐,但适用于 Linux 的 Xillybus 驱动程序支持非阻塞调用和 select() 函数。请注意,适用于 Windows 的驱动程序不 支持任何类似功能,因此使用此功能会使应用程序在必要时更难移植。处理多个来源的推荐方法是使用多线程(最好使用 RAM FIFO),如 fifo.c 示例程序所示,该程序在第 4.4 节中讨论。

对 select()、pselect() 和 poll() 的函数调用可以像任何 UNIX 文件描述符一样使用,用于读取和写入均可。

在 IP Core Factory 中设置为“Windows only”的 Xillybus IP 核中,非阻塞调用和 select() 功能未启用。

为完整起见,我们将重新审视第 3.2 节中读取数据的 代码大纲,使用非阻塞读取。此代码仅演示了 UNIX 中对文件进行非阻塞读取的常规方法。

使用 O_NONBLOCK 标志打开文件:

fd = open("/dev/xillybus_ourdevice", O_RDONLY | O_NONBLOCK);

if (fd < 0) {
  perror("Failed to open devfile");
  exit(1);
}

文件的读取方式、参数或返回值的含义均无差异:

while (1) {
  rc = read(fd, buf, numbytes);

但现在返回值多了一个检查:如果 rc 为负且错误码为 EAGAIN,则表示没有数据可读。更 准确地说,驱动程序的缓冲区中没有数据,并且 FPGA 中的 FIFO 为空(empty)。

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

  if ((rc < 0) && (errno == EAGAIN)) {
    // do something else
    continue;
   }

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

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

  // do something with "rc" bytes of data
}

请注意,除非在函数调用返回 EAGAIN 时做了某些有意义的事情,否则上述代码毫无意义。否则,它只会通过 while 循环空 转浪费 CPU 时间,而不是在没有数据可读时休眠。

对于非阻塞写入,请在第 3.3 节的示例中进行 相应更改。

3.6 监控驱动缓冲区中的数据量

本主题在 Xillybus FPGA 设计者指南中名为“监控缓冲数据量”的章节中讨论。

3.7 XillyUSB:监控物理数据链路质量的需求

与 PCIe 不同,使用 USB 3.0 的物理数据链路已被观察到会产生位错误。这种情况并不常见,表明其中一个相关组件(很可 能是主机的 USB 端口或线缆)存在问题。

USB 协议提供了多种机制来克服发生位错误的情况,然而这些错误的随机性使得链路协议进入了极少达到的状态。结果,这可能会暴露 主机 USB 控制器中的错误。此类错误(如果存在)通常会被隐藏,并导致各种奇怪的行为。

因此,如果物理数据链路频繁出现位错误,则存在重大风险:USB 连接可能会卡住、自发断开,或者在极少数情况下,甚至 导致应用程序数据错误。

XillyUSB 通过一个专用的设备文件 /dev/xillyusb_NN_diagnostics 提供了监控物理数据链路健康状况的方法。showdiagnostics 实用程序 (在此网页上解释)会公开收集到的相关信息。

强烈建议基于 XillyUSB 的应用程序持续监控 showdiagnostics 实用程序显示的前五个计数器(与坏包、检测到的错误和恢复 请求相关),并确保它们不增加。如果它们确实增加,特别是如果它们反复增加,则应用程序软件应建议纠正措施,可能包括以下之 一:

  • 断开 USB 插头并重新连接到另一个端口。这可能有助于,因为某些主板的不同端口连接到不同品牌的 USB 主机控制器(通 常是为了支持更高版本的 USB 3.x 协议)。

  • 断开并重新连接同一端口上的 USB 插头。如果模拟信号均衡器(用于消除物理信号路径引起的衰减和反射)最终处于次优 状态,这可能会有所帮助。

  • 尝试使用不同的 USB 线缆。

即使存在位错误,应用程序也很有可能继续完美运行。因此,最佳做法是在考虑用户可能未遇到任何明显问题的情况下建议 纠正措施。

showdiagnostics.pl 实用程序是一个 Perl 脚本,可以用作参考代码。或者,可以参考适用于 Windows 的诊断实用程序(以 C 源代码形式 提供)。

请注意,这些问题并非 XillyUSB 特有。相反,这些问题同样可能影响任何 USB 3.0 设备,但 XillyUSB 提供了检测它们的手 段。同样值得重申的是,PCIe 链路不以存在任何类似问题而闻名,这很可能是由于物理连接和信号路由得到了更好的控制。