3 I/O 编程实践

3.1 概述

Xillybus 可与任何具备文件访问能力的编程语言正常工作,任何用于访问文件的 API 均适用。

本指南重点介绍基于 _open()、_read()、_write() 和 _close() 等函数的底层经典 C API。这些带下划线前缀的函数与不带下划 线的对应函数(即 _read() 和 read() 为同一函数)行为完全相同。不过,Microsoft 编译器在使用不带下划线的函数时可能会发出警告。

Microsoft API(即 CreateFile()、ReadFile() 等)也可使用(参见第 3.5 段),但本指南 出于以下原因主要忽略该 API:

  • Windows API 提供的额外特性并非必需(尽管该 API 能提供更准确的错误报告)。

  • 经典 C API 更为人所知,更简单,且更易于程序员采用。

  • 经典 C API 在不同版本的 Windows 之间不太可能发生修改。

  • 经典 C API 可使代码具有可移植性。

之所以选择底层 API 而非其他知名 API(例如 fopen()、fwrite()、fprintf() 等),是因为底层 API 的函数没有额外的缓冲层。 这些缓冲层可能对性能有积极影响,但随之而来的是无法控制实际的 I/O 操作。

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

额外的缓冲层还可能造成混淆,让人误以为存在本不存在的软件缺陷。例如,调用 fwrite() 可能仅仅将数据存储到 RAM 缓冲 区中,直到文件关闭时才执行任何 I/O 操作。不了解此情况的开发者可能会因为 FPGA 端没有任何反应而误以为 fwrite() 失败,而实际上数据 正在缓冲区中等待。

本节描述推荐的 UNIX 编程实践,使用底层 C 运行时库函数。此处给出详细说明是为了完整性,因为这些实践与 Xillybus 本 身并无特定关联。

代码片段取自 在 Windows 主机上入门 Xillybus中介绍的演示应用程序。这些示例中的设备文件名对应于用于 PCIe 的 Xillybus IP 核。

以下示例针对 Xillybus 的 PCIe 变体给出。它们同样适用于 XillyUSB,但文件名前缀为 xillyusb_00_* 而非 xillybus_*。如果 连接了多个 XillyUSB 设备,“00”部分将替换为发现设备时空闲的最低索引,即 01、02 等。一旦设备分配了索引,只要它保持连接,索引就 不会改变。

这些示例的典型头文件包括:

#include <io.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

3.2 读取数据的指南

假设变量已声明如下:

int fd, rc;
unsigned char *buf;

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

fd = _open("\\\\.\\xillybus_ourdevice", O_RDONLY | _O_BINARY);

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

文件名中的 \\\\.\\ 前缀在经过反斜杠转义后变为 \\.\

请注意 _O_BINARY 标志,它告知 Windows 将该流视为非文本数据。缺少此标志 时,Windows 会转换换行字符,并将 Ctrl+Z (0x1a) 视为 EOF(文件结束符)。

如果该设备文件已被另一个进程打开用于读取(非独占文件打开模式可根据请求提供),则会发出“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 缓冲区未满(如附录第 A.3.5 节所述), 因此永远不会延迟将数据传递给 _read() 函数调用。

重要 的:
即使 _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)并正确恢复也是必要的。

请注意,信号属于 UNIX 世界,因此尽管上面说了这么多,但尚不清楚它们在 Windows 计算机上是否会到达。无论如何,相 关的 if 语句至少是无用但无害的。

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

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

3.3 写入数据的指南

假设变量已声明如下:

int fd, rc;
unsigned char *buf;

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

fd = _open("\\\\.\\xillybus_ourdevice", O_WRONLY | _O_BINARY);

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

文件名中的 \\\\.\\ 前缀在经过反斜杠转义后变为 \\.\

请注意 _O_BINARY 标志,它告知 Windows 将该流视为非文本数据。缺少此标志 时,Windows 会转换换行字符,并将 Ctrl+Z (0x1a) 视为 EOF(文件结束符)。

如果该设备文件已被另一个进程打开用于写入(非独占文件打开模式可根据请求提供),则会发出“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)并正确恢复也是必要的。

请注意,信号属于 UNIX 世界,因此尽管上面说了这么多,但尚不清楚它们在 Windows 计算机上是否会到达。无论如何,相 关的 if 语句至少是无用但无害的。

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

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

3.4 在异步下游流(Asynchronous Downstream)上执行刷新

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

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

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

也可以通过调用长度为 0 的 _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
}

请注意以下几点:

  • 对于 _write() 函数调用在计数为零时应执行什么操作,系统缺乏明确定义,由各设备驱动程序自行选择。此刷新方法特定于 Xillybus。

  • 与 _close() 不同,如上所示的 _write() 会立即返回,无论数据何时在 FPGA 端被消耗。

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

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

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

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

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

3.5 使用 Microsoft 原生 API

尽管不推荐使用,但为了完整性,给出了使用 Microsoft 原生 API 从流中读取数据的示例。完整代码可在演示应用程序包中 的 winstreamread.c 中找到(参见 在 Windows 主机上入门 Xillybus)。

首先,定义一个用于打印错误的辅助函数。它是 Windows 中 perror() 的对应函数:其目的是将错误代码转换为人类可读的消 息。

它接受一个用于描述尝试操作的字符串以及错误代码。作为响应,该函数打印给定的字符串、错误代码以及 Windows 翻译 的人类可读错误描述。

void errorprint(char *what, DWORD dw) {
  LPVOID lpMsgBuf;

  FormatMessage(
                   FORMAT_MESSAGE_ALLOCATE_BUFFER |
                   FORMAT_MESSAGE_FROM_SYSTEM |
                   FORMAT_MESSAGE_IGNORE_INSERTS,
                   NULL,
                   dw,
                   MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                   (LPTSTR) &lpMsgBuf,
                   0, NULL );



  fprintf(stderr, "%s: Error=%08x:\n%s\n",
          what, dw, lpMsgBuf);

  LocalFree(lpMsgBuf);
}

假设已声明以下变量:

HANDLE fh;
DWORD rc;

使用 CreateFile() 函数调用打开文件,尽管其名称不一定创建文件,而是打开文件。

fh = CreateFile("\\\\.\\xillybus_ourdevice",   //   file to open
                GENERIC_READ,                  //   open for reading
                0,                             //   do not share
                NULL,                          //   no security
                OPEN_EXISTING,                 //   existing file only
                FILE_ATTRIBUTE_NORMAL,
                NULL);                         // no attr. template

CreateFile() 始终以“binary mode”(二进制模式)打开文件。实际上,它不支持称为“text mode”(文本模式)的换行符转换。

文件名中的 \\\\.\\ 前缀在经过反斜杠转义后变为 \\.\

检查文件打开是否成功,如果失败则调用上面定义的函数 errorprint()。

if (fh == INVALID_HANDLE_VALUE) {
  errorprint("Failed to open file", GetLastError());
  return 1;
}

成功打开文件且 buf 指向内存中已分配的缓冲区后,使用以下代码读取数据:

if (!ReadFile(fh, buf, numbytes, &rc, NULL)) {
    errorprint("ReadFile", GetLastError());
    return 1;
}

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

rc 由 ReadFile() 写入,包含实际读取的字节数。

如果为零,则表示已到达文件末尾。从 Xillybus 设备文件读取时,发生这种情况的唯一原因是应用逻辑已拉高流的 _eof 引脚 (这是 IP 核在 FPGA 上接口的一部分)。

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

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

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

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

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

与 PCIe 不同,已观察到使用 USB 3.0 时的物理数据链路会产生位错误。这并不常见,表明涉及的某个组件存在问题,很可 能是主机的 USB 端口或线缆。

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

因此,如果物理数据链路频繁遭受位错误,则存在显著风险,USB 连接可能会卡住、意外断开,或者在极少数情况下,甚至 导致应用数据出错。

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

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

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

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

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

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

showdiagnostics.pl 实用程序可用作参考代码,因为其 C 源代码与可执行文件位于同一个 zip 文件中。

请注意,这些问题都不是 XillyUSB 特有的。相反,这些问题同样可能影响任何 USB 3.0 设备,但 XillyUSB 提供了检测它们 的手段。另外,值得重申的是,PCIe 链路已知不会遭受任何类似的问题,这很可能是由于物理连接和信号布线受到更好的控制。