6 特定编程技术
6.1 可寻址流(Seekable Streams)
同步 Xillybus 流(Synchronous Stream)可以配置为可寻址。流的位置通过独立的信号线以地址形式呈现给 FPGA 中的应 用逻辑,因此与 FPGA 中的存储器阵列或寄存器接口非常直接,如演示包和示例代码所示。
此功能特别适用于设置 FPGA 中的控制寄存器。流的同步特性确保在底层 I/O 函数返回之前,FPGA 中的寄存器已被设 置。
以下代码片段演示如何将 len 字节的数据写入 FPGA 中存储器或寄存器空间的地址 address,假设这两个变量已预先设置。
int rc, sent;
if (_lseek(fd, address, SEEK_SET) < 0) {
perror("Failed to seek");
exit(1);
}
for (sent = 0; sent < len;) {
rc = _write(fd, buf + sent, len - sent);
if ((rc < 0) && (errno == EINTR))
continue;
if (rc <= 0) {
perror("Failed to write");
exit(1);
}
sent += rc;
}
假定 fd 是从 _open() 函数调用返回的值,其中文件以写或读写方式打开,且 buf 指向包含待写入数据的缓冲区。
此示例是第 3.3 段所示示例的 扩展。
此代码中唯一特别之处是对 _lseek() 的函数调用,它设置了地址。调用 _lseek() 函数时,应仅使用 SEEK_SET 选项作为第 三个参数。
后续的函数调用会根据 I/O 流的位置更新地址,因此在调用 _lseek() 函数后,可以连续进行多次顺序写入,没有限制。
对于在 FPGA 中以 16 位或 32 位字访问的流,赋予 _lseek() 的地址必须分别是 2 或 4 的倍数。呈现给 FPGA 应用逻辑的地 址始终保持为流的 I/O 位置(最初由 _lseek() 设置)分别除以 2 或 4 的结果。对于更宽的字,适用相同的对数规则。
_tell() 函数可能返回流中的正确位置(即当前地址),但并非可靠的信息来源。如有疑问,请再次 调用 _lseek()。
_lseek() 可以类似方式用于读取数据。请参阅演示应用程序包中的 memwrite.c 和 memread.c(以及它们在 在 Windows 主机上入门 Xillybus中的描述)。
6.2 双向流同步
在某些应用中,需要同步多个流,可能方向相反。例如,主机上可能实现一个无线电传输系统,接收来自 A/D 转换器的数字 样本,该转换器连接到射频接收器。同样,它可能向 D/A 转换器发送数字样本,该转换器连接到射频发射器。在这类场景中,通常需要生成 用于传输的数字样本,以便传输时间与接收样本之间的关系已知。了解接收信号的精确时间也可能很重要。
幸运的是,这可以通过简单的 FPGA 逻辑实现。一种解决方案是忽略接收到的数字样本,直到第一个用于传输的样本到达 FPGA:
主机首先打开用于从 FPGA 读取样本的流。此阶段该流处于空闲状态,因为 FPGA 丢弃其接收样本。然后主机打开用于向 FPGA 写入传输样本的流,并开始向其写入数据。当第一个样本到达 FPGA 时,它停止忽略接收到的样本,并开始向主机发送它们。
因此,从 FPGA 读取的第一个样本将与写入 FPGA 的第一个样本匹配。主机上的应用程序因此可以仅通过匹配它们在各自流 中的位置,将任何传输样本的时间与任何接收样本的时间对应起来。可能需要轻微校正以补偿 FPGA 中的延迟以及 A/D 和 D/A 的延迟,但这 种延迟是常数且已知的。
流必须始终保持连续。如何实现这一点已在第 4 节讨 论。
如果仅需保持传输与接收之间的相对时间关系,此解决方案即可满足要求。当样本需要与外部事件或其他时间参考同步时, 可根据需要调整相同的样本跳过原则以达到预期结果。
关于如何监控任意时刻驱动缓冲区中持有的数据量,请参见 Xillybus FPGA 设计者指南中“监控缓冲数据量”一节。
6.3 数据包通信(Packet Communication)
某些应用需要将数据流划分为长度可变的数据包。建议的解决方案使用两个独立的流,并且不要求数据发送方在开始通过通 道提交数据包本身时知道数据包的长度。
固定已知长度数据包的平凡情况简单地通过在一个流上一个接一个地传输它们来解决。另一端的接收方仅为每个数据包读取 该固定数量的字。这是视频帧采集或视频回放应用中的典型解决方案。
对于长度可变的数据包情况,让我们考虑一个上游应用,其中 FPGA 向主机发送字节数据包。假设 FPGA 仅在最后一个字节 到达时才知道数据包的长度。
FPGA 端(即发送端)的实现如下:
-
FPGA 将所有数据包字节写入第一个 Xillybus 流。
-
FPGA 在写入数据包的第一个字节时重置字节计数器,并为其写入的每个额外字节递增该计数器。
-
当写入数据包的最后一个字节时,FPGA 将计数器的值发送到第二个 Xillybus 流。该值包含数据包的长度(减一)。
此解决方案的一个重要特性是 FPGA 无需在发送之前存储整个数据包。它只是在数据到达时将其传递过去。
主机端的用户应用程序运行如下循环:
-
从第二个流读取一个词,其中包含下一个数据包的字节数。
-
如有必要,分配所需大小的缓冲区内存。
-
从第一个流中将给定数量的字节读取到专用于该数据包的缓冲区中。
请注意,主机在访问数据之前获取要读取的字节数,但 FPGA 以相反的顺序将其写入流。使用独立的 Xillybus 流允许这种反 转。
当数据包从主机发送到 FPGA 时,类似的安排也适用。使用两个流(一个用于数据,一个用于字节计数)的原则保持不 变。FPGA 的应用逻辑现在可以在从另一个流获取数据之前从一个流读取字节数。
此安排也可扩展到在非数据流中传递其他元数据,例如数据包的目的地或在某些网络中的路由(这在第一个字节到达时有时 未知)。
6.4 模拟硬件中断(Emulating Hardware Interrupts)
在小型微控制器项目中,通常使用硬件中断来通知软件发生了某些事件,软件需要采取某些行动。当软件作为 Windows 中 的用户空间进程运行时,硬件中断是不可能的。
针对基于 Xillybus 的系统,建议的解决方案是分配一个专门的流用于承载消息。在其最简单的形式中,通过在该专用流上发 送一个单字节来模拟硬件中断。
在主机端,用户空间应用程序尝试从该流读取数据。结果是,当没有“中断”信号时,应用程序休眠(阻塞),直到一个字节 到达并将其唤醒。应用程序处理该事件,然后尝试从专用流读取另一个字节,因此如有必要再次进入休眠,依此类推。
为了实现主应用程序与中断例程之间的适当交互,该专用流可以由一个单独的软件线程或进程读取。通过这种安排,主代码 无论读取专用消息流的线程如何都继续流动,而后者根据发送的消息休眠和唤醒。
此方法的一种变体使用传输的字节值来传递有关模拟中断性质的信息。此外,如果实现中有意义,每条消息可以长于单个字 节。
这种方法看似浪费逻辑资源,但 Xillybus 最初设计时就是为了在添加每个流时不会消耗太多逻辑,以使此类解决方案合 理。
6.5 协处理 / 硬件加速
协处理(也称 硬件加速)是一种技术,它允许应用程序利用逻辑电路的灵活性,以比 给定处理器更快、更便宜、能耗更低或其他更高效的方式执行特定操作。无论动机如何,高效的数据传输流对于使协处理成为可行的解决方 案至关重要。
需要认识到,基于协处理的应用程序中的数据流与常见的编程数据流有根本区别。为了说明这一差异,我们以需要计算浮点 数平方根的计算机程序为例。
程序员直接的方式是将数字作为参数传递给 sqrt(),调用它,然后等待函数返回。
假设希望在 FPGA 的逻辑电路中计算平方根。一个常见的 错误 是用一个特殊函数替换 sqrt(),该函数将值发送到 FPGA 进行计算,等待完成,然后返回结果。尽管这确实是 sqrt() 的一个简单直接替代,但它很可能比原始的 sqrt() 更慢,且在其他方面效率更低:数据在总线上双向传输所需的时间,加上 FPGA 进行计算所需的 时间,很可能远大于 sqrt() 所需的处理器周期。话虽如此,如果数据流设计得当,在 FPGA 上计算平方根可能会快得多。
为了克服总线和 FPGA 逻辑带来的延迟,需要对软件进行重组。特别是,单线程程序中的任务需要拆分为两个或多个线程 (或进程)。如果无法或不希望使用多个线程,可以利用其他编程技术来模拟多线程的行为,但编程范式仍然是多线程的。
回到 sqrt() 的例子,对该函数的调用被分为两个线程:第一个线程将要计算平方根的数据发送到硬件(或某种表示操作请求 的数据结构)。第二个线程从硬件接收结果,并从算法中的该点继续处理。
单独看一个数据项时,这似乎没有意义,但协处理的动机意味着要处理大量数据项。因此,第一个线程发送一个 数据流 进行计算,第二个线程接收一个 结果流。
这种 流水线 技术最大限度地减少了硬件延迟的影响,因为两个线程实际上都不在等待 这个延迟。相反,延迟影响的是位于两个线程之间的处理项数目——但 吞吐量 仅取决于两个线程和 FPGA 逻辑的处理能力。
以下概念图总结了这一思想。
加速计算 sqrt() 是一个相对简单的例子,但它涵盖了利用协处理时的许多挑战。几乎所有情况下,计算机程序的大部分代码 都需要重写,以便一切由流水线的数据流驱动。
另一个需要注意的问题是,由于 Xillybus 使用 read() 和 write(),因此将多个数据项分组后再写入流向 FPGA 的流中可能是有 益的。同样,尝试在每个 read() 调用中读取超过一个结果项可能会提高性能。其中的原因在于,read() 和 write() 是具有一定开销的系统调 用。如果数据元素很小且以高频率传输,这些系统调用的开销可能相当显著。sqrt() 的例子很好地说明了这一点:双精度浮点数通常长 8 字 节。这么短的 I/O 系统调用效率相当低下,因此将多个双精度浮点数元素合并到一次系统调用中会带来显著的改善。
此外,值得一提的是,并非所有应用都涉及固定长度的数据块。例如,使用协处理计算任意字符串的哈希(如 SHA1)时, 处理的各个数据元素长度可能不同。第 6.3 节针对这一问题 提出了解决方案。
