6 Vivado HLS 集成

6.1 概述

本节演示如何将一个简单的 C 函数编译成 IP 模块,并将其集成到 Xillybus 的 Block Design 流程中。

本节所基于的示例项目可从以下地址下载:

https://xillybus.com/downloads/hls-axis-starter-1.0.zip

建议将下载的文件解压到一个与 Xillybus 项目容易关联的目录中,因为后续无法移动。

重要的是要区分示例项目中的两种不同 C 源码:

  • 用于执行的代码:运行在计算机或嵌入式平台(“主机 (host)”)上,如同任何计算机程序一样,并使用 FPGA 来卸载某些操 作。

    在示例项目中,示例文件位于 host/ 子目录下。

  • 用于综合的代码:旨在由 Vivado HLS 翻译成逻辑。

    在示例项目中,它位于 coprocess/example/src/main.c 中。

与常见的 C/C++ 编程不同,主机程序并不直接调用综合后的函数。而是将要执行函数所需的数据组织成一种数据结构,并使 用一种简单的 API(下文会介绍)将其传输给综合后的函数。稍后,它再通过类似的 API 从综合后的函数接收返回数据,作为另一种数据结 构。

6.2 HLS 综合

本节使用的示例 C 代码概要见第 6.4 节。

启动 Vivado HLS,并打开 HLS 项目:在欢迎页面上选择“Open Project”,导航到 HLS 项目套件解压的位置,选择名 为“coprocess”的文件夹。

更改项目的部件号:选择 Solution >Solution Settings... >Synthesis,将“Part Selection”更改为目标 FPGA。

通过选择 Solution >Synthesis >Active Solution(或单击工具栏上相应的图标)启动项目编译(“synthesize”)。控制台中将 显示大量文本,包括若干警告(这是正常的)。不应出现错误。

成功编译的标志是 HLS 控制台选项卡的最后几行中出现以下消息:

Finished C synthesis.

只有在综合成功时,综合报告才会显示在控制台选项卡上方。

有关 Vivado HLS 的更多信息,请参阅其用户指南 (UG902)。

6.3 与 FPGA 项目集成

在 Vivado HLS 中,选择 Solution >Export RTL,然后选择“IP Catalog”作为 Format Selection。对于“Evaluate Generated RTL”,选择 Verilog,并且不要勾选其下方的任何复选框。单击 OK。

这可能需要几分钟,最终会显示类似以下内容:

Finished export RTL.

现在在 Vivado 中(不是 Vivado HLS)打开 Xillydemo 项目(按照第 2.1 节设置),并打开区块设计 (Block Design)。如果使用 Xillinux (Zynq),请打开名为“blockdesign”的区块。

添加 HLS IP 模块如下:在区块设计图区域中某处右键单击,选择“IP Settings...”。在“Repository Manager”选项卡下,单击 绿色的加号按钮以添加仓库。导航到并选择与第 6.2 节中相同的“coprocess”目录,以 打开 HLS 项目。Vivado 应弹出一个窗口,指示已添加了一个仓库。单击两次“OK”按钮以确认。

现在将 IP 模块添加到区块设计中:再次在区块设计图区域中某处右键单击。选择“Add IP...”,并从列表中选择 Xillybus_wrapper IP(在搜索框中输入“wrapper”可能会更方便)。

一个名为 xillybus_wrapper_0 的新模块将出现在图中。断开 to_host_read_32 与 from_host_write_32 之间的连线(即断开环 回 (loopback))。

然后按如下方式连接 xillybus_wrapper 模块:

  • data_in 连接到 from_host_write_32

  • data_out 连接到 to_host_read_32

  • ap_rst_n 连接到 to_host_read_32_open

  • ap_clk 连接到 ap_clk(这也是时钟向导 (Clocking Wizard) 的 clk_out1 输出)

结果应类似于以下所示(以基于 Xillinux 的区块设计为例):

ap_rst_n 与 to_host_read_32_open 之间的连接使 xillybus_wrapper 模块内部的逻辑保持在复位状态,除非主机上打开了 xillybus_read_32 设备文件(当文件未打开时 to_host_read_32_open 为低电平,复位输入为低电平有效)。假设主机上运行的软件在尝试与 该模块通信之前打开了此设备文件,这将确保每次运行软件时逻辑产生一致的响应。

此时,可以执行实现 (implementation) 以获得比特流 (bitstream):在 Vivado 窗口底部,选择“Design Runs”选项卡,右键单 击“synth_1”并选择“Reset Runs”。确认重置 synth_1。

然后在左侧栏中单击“Generate Bitstream”。

6.4 示例综合代码

为了阐明 HLS 如何与 Xillybus 配合工作,示例演示了三角正弦的计算和一个简单的整数操作,两者都包含在一个简单的自定 义函数 mycalc() 中。

coprocess/example/src/main.c 的开头如下:

#include <math.h>
#include <stdint.h>

extern float sinf(float);

int mycalc(int a, float *x2) {
  *x2 = sinf(*x2);
  return a + 1;
}

与往常一样,有几个 #include 语句。包含“math.h”是正弦函数所必需的。

这是一个简单的函数 mycalc(),它充当“综合函数 (synthesized function)”。这是一个非常简单的函数,演示了浮点数和整数 的算术运算。高级综合指南 UG902 提供了关于如何实现更有用任务的更多信息。

接下来在 main.c 中,是包装函数 xillybus_wrapper(),它是综合函数与 Xillybus 之间的桥梁,因此负责打包和解包来回传输 的数据。

在此示例中,它通过一个数据流(由“data_in”参数表示)从主机接收整数和浮点数格式的数字。它使用“data_out”参数返回整 数加一的结果以及浮点数的(三角)正弦值。

void xillybus_wrapper(int *data_in, int *data_out) {
#pragma AP interface axis port=data_in
#pragma AP interface axis port=data_out
#pragma AP interface ap_ctrl_none port=return

  uint32_t x1, tmp, y1;
  float x2, y2;

  // Handle input data
  x1 = *data_in++;
  tmp = *data_in++;
  x2 = *((float *) &tmp); // Convert uint32_t to float

  // Run the calculations
  y1 = mycalc(x1, &x2);
  y2 = x2; // This helps HLS in the conversion below

  // Handle output data
  tmp = *((uint32_t *) &y2); // Convert float to uint32_t
  *data_out++ = y1;
  *data_out++ = tmp;
}

xillybus_wrapper() 声明了两个指针,都指向 int 类型的变量。这些函数参数将变成待生成 IP 模块的两个 AXI Stream 端口,以便包含在区 块设计中:每个参数都有一个 #pragma 语句,告知 HLS 它们应被视为类型为“axis”的接口。

“#pragma AP”和“#pragma HLS”可以互换——前者基于 C 综合器先前的名称 (Auto Pilot),后者则见于 AMD 近期的文档 中。

由于 HLS 将“int”视为 32 位字,相应的 AXI Stream 接口将具有 32 位宽的数据接口。

当然,可以更改参数列表以及 pragma 以获得任意一组 AXI Stream 输入和输出。

ap_ctrl_none 的 pragma 声明告诉编译器不要为(不存在的)返回值生成端口。

接下来是一些“执行”代码:获取输入数据。每个 *data_in++ 操作都会获取一个来自主机的 32 位字。在所示代码中,第一个 字被解释为无符号整数,并放入 x1。第二个字被视为 32 位浮点数,并存储在 x2 中。

然后调用 mycalc() 函数,即“综合函数”。此函数通过返回值返回一个结果,第二段数据通过更改 x2 返回。

包装函数将 x2 的更新值复制到新变量 y2 中。这看起来像是冗余操作,如果此代码的编译目标是在处理器上执行,那确实如 此。然而在使用 HLS 时,这对于使编译器稍后处理到浮点数的转换是必要的。这反映了 HLS 编译器有些怪异的行为,但这是使用指针时的 一个微妙问题:尽管 C 代码中定义了内存数组和指向它的指针,但 HLS 编译器并不会生成它们中的任何一个。使用指针只是关于我们想要实 现什么的一个提示,有时需要对这些提示进行一些引导。

最后,结果被发送回主机:每个 *data_out++ 将一个 32 位字发送到计算机,并完成从浮点数到整数的适当转换。

请注意,*data_in++ 和 *data_out++ 运算符实际上并不移动指针,也不存在底层的内存数组。相反,它们象征着将数据移入 和移出 AXI 流接口(并最终移入和移出 Xillybus 流)。因此,使用“data_in”和“data_out”变量的唯一方式是 *data_in++ 和 *data_out++(高级 综合指南提供了其他可能性,特别是固定大小的数组)。

还要注意,由于这段代码被翻译成逻辑,而不是由处理器运行,这些 C 命令的唯一意义是根据给定的输入数据流产生预期的 输出数据流。然而对于数据 何时 发出,并没有承诺(除了 HLS 报告中给出的一系列可能的延迟 (latency) 范围)。

因此,输入数据赋值的顺序在某种意义上很重要,因为它强制了如何解释传入的数据。另一方面,由于发送的第一个输出 y1 仅依赖于第一个到达的输入 x1,因此允许在第二个输入到达之前发送第一个输出。这与代码执行的直观顺序性相矛盾,但在硬件加速的上下 文中没有意义,因为最终结果是相同的。

此外,如果 data_in AXI 流持续提供数据,包装函数会“重复”运行,就好像 它写 着:

  while (1) // This while-loop isn't written anywhere!
    xillybus_wrapper(data_in, data_out);

新数据会尽快通过 *data_in++ 命令获取,极有可能填满逻辑的内部流水线 (pipeline)(在示例项目中,根据 HLS 报告,该流 水线长度超过 70 级)。 因此,与处理器的代码执行不同(处理器会先获取一对字,处理它们,发出两个输出字,然后才获取第 二对字),HLS 的解释很可能在 data_out AXI 流上输出任何内容之前,先从 data_in 获取 70 个字。

6.5 对用于综合的 C/C++ 代码的修 改

可以通过向包装函数添加参数,并将这些参数声明为接口端口来创建额外的 AXI Stream 端口,如示例中所示。

当然也可以对示例设计的 C 代码进行其他更改。

建议采用与 *data_in++ 和 *data_out++ 相同的风格实现 I/O,或参考高级综合指南 (UG902) 了解其他可能性。它也是学习编 码技巧的推荐来源。

重要 的:
不要在做出更改后直接在 Vivado 中单击“Generate Bitstream”: 如果不按下面详述的方式更新模块而重复实现 比特流,很可能会导致看似成功的比特文件实现,但实际上是基于过时版本的 HLS 模块。

在示例项目中做出更改后,从第 6.2 节的“HLS 综合”重新开始,一直 进行到在 Vivado 中实现,并在 Vivado 中更新 HLS 模块。

换句话说:

  • Vivado HLS:在 HLS 中运行项目编译。HLS 综合器总是在开始新的编译之前清理先前编译生成的文件。

  • Vivado HLS:导出为 IP Catalog 套件。

  • 在 Vivado 中(不是 Vivado HLS),升级 xillybus_wrapper 模块(实际上,是在其更改后更新 它):打开区块设计视图,并响应页面顶部的消息,该消息指出该模块需要升级。如果未找到此消息,请在 Tcl 控制台中输 入“report_ip_status -name status”。单击底部的“Upgrade Selected”按钮。随后会出现一个确认升级成功的对话框,以及一个要求生成输出产 品的对话框。在第二个对话框上单击“Skip”。

  • Vivado:验证设计运行是否已失效:在 Vivado 窗口底部,选择“Design Runs”选项卡。synth_1 的 Status 列应显示 Synthesis Out-of-date。

  • Vivado:除非设计运行已失效, 否则尝试以下操作: 刷新 IP 目录:在区块设计图区域中 某处右键单击,选择“IP Settings...”。在“Repository Manager”选项卡下,单击底部的“Refresh All”按钮。可能还需要在同一对话框 的“General”选项卡上单击“Clear Cache”。之后,返回升级 xillybus_wrapper 模块。

    如果在上面的前一项中发现设计运行已失效,则这些操作均非必需。

  • Vivado:重置 synth_1 运行

  • Vivado:生成比特流 (Generate bitstream)

6.6 simple.c:主机程序示例

在示例项目中,有两个 C 文件作为示例主机程序:simple.c 和 practical.c。它们演示了项目的主机端。

两者均为 Linux 主机编写,可使用例如以下命令进行编译:

# gcc -O3 -Wall simple.c -o simple

但它们很容易适应 Windows(见下文)。

重要 的:
simple.c 不应作为实际主机编程的示例,特别是由于其以下缺点:
仅处理单个元素。在 write() 和 read() 函数调用对上循环将导致性能不佳。
必须检查 write() 和 read() 操作的返回值以确保正常运行。为简单起见此处省略了检查,但这会使程序不可靠。

6.7 节概述了更好的编码技巧。

simple.c 文件以 #include 语句开头:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#include   <sys/types.h>
#include   <sys/stat.h>
#include   <fcntl.h>
#include   <stdint.h>

随后是经典的 main() 函数声明以及一些变量声明:

int main(int argc, char *argv[]) {
  int fdr, fdw;

  struct {
    uint32_t v1;
    float v2;
  } tologic, fromlogic;

结构体变量将在下面讨论。

程序首先打开两个设备文件,它们的行为类似于命名管道 (named pipes),并用于与逻辑通信:/dev/xillybus_read_32 和 /dev/xillybus_write_32。回忆一下 Xillybus 套件的设置,这两个文件由 Xillybus 驱动程序生成。

如第 6.3 节 所述,在区块设计图中 ap_rst_n 连接到 to_host_read_32_open,因此打开 /dev/xillybus_read_32 会使逻辑退出复位。这就是在数据传输之 前打开这两个文件的原因。

  fdr = open("/dev/xillybus_read_32", O_RDONLY);
  fdw = open("/dev/xillybus_write_32", O_WRONLY);

  if ((fdr < 0) || (fdw < 0)) {
    perror("Failed to open Xillybus device file(s)");
    exit(1);
  }

接下来是实际执行。用一对值填充“tologic”结构以传输给逻辑,然后将其直接从内存写入 xillybus_write_32。实际上,这写入 了 8 个字节,或者更准确地说,是两个 32 位字。第一个是放入 tologic.v1 的整数 123,第二个是 tologic.v2 中的浮点数。因此,tologic 结构 的设置与逻辑对数据的期望相匹配:一个整数由第一个 *data_in++ 指令获取,一个浮点数由第二个指令获取。

  tologic.v1 = 123;
  tologic.v2 = 0.78539816; // ~ pi/4

  // Not checking return values of write() and read(). This must
  // be done in a real-life program to ensure reliability.

  write(fdw, (void *) &tologic, sizeof(tologic));
  read(fdr, (void *) &fromlogic, sizeof(fromlogic));

  printf("FPGA said: %d + 1 = %d and also "
         "sin(%f) = %f\n",
         tologic.v1, fromlogic.v1,
         tologic.v2, fromlogic.v2);

回顾第 6.4 节,包装代码从 data_in 流中获取两个 32 位字。第一个字进入“x1”,第二个进入“tmp”,然后“tmp”立即转换为浮点数。这与“tologic”结构的两 个 32 位元素相匹配。

随后是从 FPGA 读回数据。同样的原则适用于“fromlogic”。

simple.c 以常见的收尾结束:

  close(fdr);
  close(fdw);

  return 0;
}

至关重要的是,发送到 /dev/xillybus_write_32 的数据量必须与包装函数中 *data_in++ 操作的数量相匹配。如果发送的数据 太少,综合函数可能根本不会执行。如果太多,后续执行可能会出错。

在此示例中,为“tologic”和“fromlogic”选择了相同的结构格式,但不必拘泥于此。重要的是发送和接收的数据必须与包装函数 的 *data_in++ 和 *data_out++ 操作次数同步。

此程序的执行结果应如下:

# ./simple
FPGA said: 123 + 1 = 124 and also sin(0.785398) = 0.707107

最后,给 Windows 用户的一个提示:可能需要做出以下全部或部分调整:

  • 将文件名字符串从“/dev/xillybus_read_32”改为“\\\\.\\xillybus_read_32”(Windows 上的实际文件名是 \\.\xillybus_read_32, 但需要转义)。第二个文件名改为“\\\\.\\xillybus_write_32”。

  • 将 unistd.h 的 #include 语句替换为 io.h

  • 将函数调用 open(), read(), write() 和 close() 替换为 _open(), _read(), _write() 和 _close()

6.7 practical.c:实用的主机程序

simple.c 示例以简洁的方式展示了数据交换,但在实际系统中需要做一些更改:

以下差异最为显著:

  • 不是生成单组数据进行处理,而是分配并发送一个结构体数组。同样,从逻辑接收一个数据数组。这减少了 I/O 开销以及由 软件和硬件引起的延迟 (latency) 的影响。这是通过硬件加速获得性能提升的关键方法。

  • 程序通过 fork() 分成两个进程,一个用于写入数据,一个用于读取数据。使这两个任务相互独立可防止因任一方缺少处理数 据而导致处理停滞。这种独立性可以通过线程(特别是在 Windows 中)或使用 select() 函数调用来实现。

  • 正确使用 read() 和 write() 函数调用,以确保可靠的 I/O。为此添加的 while 循环可能看起来繁琐,但它们是正确响应这些函 数调用部分完成(未读取或写入所有字节)所必需的,这在负载下是常见情况。还按需处理 EINTR 错误,以正确响应可能发送给运行进程的 POSIX 信号(可能意外发送)。

现在简要介绍 practical.c。首先是头文件:

#include <stdio.h>
#include <unistd.h>

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

以及相同的结构体,加上定义 N,即每块数据中的元素数量。

#define N 1000

struct packet {
   uint32_t v1;
   float v2;
};

一个常见的 main() 函数定义和一些变量:

int main(int argc, char *argv[]) {

  int fdr, fdw, rc, donebytes;
  char *buf;
  pid_t pid;
  struct packet *tologic, *fromlogic;
  int i;
  float a, da;

像之前一样打开文件:

  fdr = open("/dev/xillybus_read_32", O_RDONLY);
  fdw = open("/dev/xillybus_write_32", O_WRONLY);

  if ((fdr < 0) || (fdw < 0)) {
    perror("Failed to open Xillybus device file(s)");
    exit(1);
  }

实际执行从 fork() 分成两个进程开始。

  pid = fork();

  if (pid < 0) {
    perror("Failed to fork()");
    exit(1);
  }

父进程准备要处理的数据并将其写入 FPGA。它关闭读取文件描述符,因为该进程不使用它。保持打开状态会使设备文件保 持打开,直到两个进程都关闭其文件描述符(或退出),这不是这里期望的行为。

  if (pid) {
    close(fdr);

    tologic = malloc(sizeof(struct packet) * N);
    if (!tologic) {
       fprintf(stderr, "Failed to allocate memory\n");
       exit(1);
    }

接下来,用数据填充一个结构体数组。这解释了为什么为每组待处理数据定义一个结构体是有意义的。

    // Fill array of structures with just some numbers
    da = 6.283185 / ((float) N);

   for (i=0, a=0.0; i<N; i++, a+=da) {
     tologic[i].v1 = i;
     tologic[i].v2 = a;
   }

    buf = (char *) tologic;

注意,“buf”被定义为指向 char 缓冲区的指针,指向结构体数组。这种转换是必需的,因为发送数据的 while 循环将缓冲区视 为任何要传输的数据块。

接下来是用于写入数据的 while 循环。它可能看起来不必要地复杂,但却是确保数据可靠写入的最短方式。建议在实际应用 中直接采用此代码。

    donebytes = 0;

   while (donebytes < sizeof(struct packet) * N) {
     rc = write(fdw, buf + donebytes,
                sizeof(struct packet) * N - donebytes);

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

         if (rc <= 0) {
           perror("write() failed");
           exit(1);
         }

         donebytes += rc;
    }

在此示例中,只发送了一个块(并在另一端接收)。在实际代码中,应循环执行上述两段代码。

性能测试表明,32 kBytes 的块大小通常能给出最佳结果。

由于此示例只发送一个块,进程会退出。在关闭文件之前休眠一秒钟可确保逻辑在数据全部排出之前不会复位。当区块设计 如第 6.3 节所示时,这没有 意义,因为 ap_rst_n 连接到 to_host_read_32_open,而 from_host_write_32_open 根本没有连接。

尽管如此,这演示了一个好的惯例:除非需要快速退出,否则不要立即关闭文件描述符。当项目变得更复杂时,这可以避免 一些混淆。

    sleep(1); // Let the output drain

    close(fdw);
    return 0;

接下来是子进程,以类似方式开始:

  } else {
    close(fdw);

   fromlogic = malloc(sizeof(struct packet) * N);
   if (!fromlogic) {
     fprintf(stderr, "Failed to allocate memory\n");
     exit(1);
   }

    buf = (char *) fromlogic;

再次强调,这是从设备文件读取数据的推荐方式:

    donebytes = 0;

   while (donebytes < sizeof(struct packet) * N) {
     rc = read(fdr, buf + donebytes,
               sizeof(struct packet) * N - donebytes);

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

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

         if (rc == 0) {
           fprintf(stderr, "Reached read EOF!? Should never happen.\n");
           exit(0);
         }

         donebytes += rc;
    }

然后打印数据:

    for (i=0; i<N; i++)
      printf("%d: %f\n", fromlogic[i].v1, fromlogic[i].v2);

   sleep(1); // Let the output drain

   close(fdr);
   return 0;
  }
}

再次强调,进程在关闭文件描述符之前休眠一秒钟,同样,在这种特定情况下也不是必需的:关闭文件描述符确实会复位逻 辑,但在此处是无害的,因为到达这一点时所有输出都已被获取。

如前所述,除非快速退出是有益的,否则这一秒钟的休眠可以避免混淆,特别是在生成了其他输出流(例如用于调试)的情 况下。

6.8 设计考虑

6.8.1 使用多个 AXI 流

示例项目展示了每个方向一个流的基本情况。然而,通过向包装函数添加参数,并添加 pragma 将这些参数声明为 AXI 流, 可以很轻易地在 IP 模块上添加用于输入和/或输出的流。

例如,三个输入流代替一个:

void xillybus_wrapper(int *d1, int *d2, int *d3, int *data_out) {
#pragma AP interface axis port=d1
#pragma AP interface axis port=d2
#pragma AP interface axis port=d3
#pragma AP interface axis port=data_out
#pragma AP interface ap_ctrl_none port=return


  *data_out++ = thefunc(*d1++, *d2++, *d3++);

}

向 Xillybus IP 核添加流同样简单,只需配置一个定制 IP 核即可,如第 5 节所述。

额外的流在多种场景中可能很有用,其中包括:

  • 将数据和元信息放在单独的流中发送。例如,如果需要将数据分成数据包,可以在一个专用流中发送它们的长度,在另一个 流中发送数据。这样可以在长度已知之前就开始发送数据包的起始部分。

  • 发送自然分开组织的数据,例如不同图像的像素扫描(下文会进一步说明)。

  • 用于调试:将中间数据发送到主机进行验证。

当使用多个流时,重要的是要记住它们全部:如果任何输入流缺少数据,或者某个输出流的相应设备文件未打开(或发生数 据溢出 (overflow)),逻辑的执行流程可能会停滞。这一点尤为重要,特别是当某个输出流用于调试时:在正常操作期间使用系统时,很容易 忘记用于调试的流。由于来自此流的数据未被消耗,通常会在几个数据周期后导致令人困惑的执行停止。

通常,以乍一看可能不合适的方式向逻辑硬件提供数据是明智的。 例如,上面显示的三输入示例对于需要每个 操作三个数据元素的图像处理算法可能很有用:假设图像从左到右、从上到下扫描。为了生成像素输出,算法需要来自前两幅图像和当前图 像中各个像素。在这种情况下,可以同时通过一个流将当前图像发送到 FPGA,并通过另外两个流并行发送前两幅图像。

这看起来像是浪费 I/O 数据带宽和大量不必要的内存拷贝。特别是,感觉处理器过多地参与了“数据混洗”。撇开主观感受不 谈,内存拷贝实现在每个现代处理器架构上都是高度优化的任务,而处理器通常还负载着其他与应用相关的任务,这使得内存拷贝的负载可 以忽略不计。

因此,尽管从资源利用率的角度来看,直接向逻辑提供数据并非最优,但考虑到处理器通常还有其他繁重任务要处理,对处 理器的额外负载通常相当小。对于显著简化设计而言,这通常是一个合理的代价。

6.8.2 应用时钟的频率

由 HLS 生成的逻辑由区块设计的应用时钟驱动,该时钟由 stream_clk_gen 模块生成。由于此时钟是逻辑的时基,其执行速 率与时钟频率成正比。除非 AXI 流端口的数据传输成为瓶颈,否则更高的应用时钟频率意味着处理吞吐量的成比例提升。

然而,应用时钟的频率能有多高是有限制的,具体取决于 FPGA 的逻辑资源以及它们如何被利用来实现所需的任务。以下是 设计过程中的相关里程碑:

  1. Vivado HLS 允许用户设置设计的目标时钟频率,指定应用时钟的期望频率(通过 Solution >Solution Settings)。此参数仅作为 HLS 的 提示,允许其在必要时和可能的情况下做出额外努力以产生更快的逻辑。

  2. 当 Vivado HLS 完成编译时,它会提供一个可能达到的时钟频率估计(位于 HLS GUI 的 Synthesis 选项卡中“Performance Estimates”部分的“Timing”下)。

  3. 用户在 Vivado 的区块设计中设置应用时钟的频率,如第 3.2 节(特别是第 3.2.2 节)所述。自然的选择是第 2 项中 估计的时钟频率或更低。注意这是在不 Vivado HLS 中完成的。

  4. 当 Vivado 完成整个设计到 FPGA 比特流的实现时,它会告知用户是否成功组织了逻辑以满足与时钟相关的所有要求。这包 括满足第 3 项中设置的时钟频率。

因此,最终归结为最后一个里程碑,以及 Vivado 是否能够满足与第 3 项中选择的应用时钟频率相关的时序约束。

HLS 以及 stream_clk_gen 中的默认时钟周期是 10 ns (100 MHz)。通常最好保留此选择,除非:

  • Vivado 无法满足时序约束,在这种情况下应选择更慢的时钟。

  • 有动机提高处理吞吐量,在这种情况下应尝试要求更快的时钟。这通常是一个迭代过程,需要调整时钟频率以及对设计本身 和 HLS pragma 做出更改,以达到改进的结果。

6.8.3 复位逻辑

由于 C/C++ 代码被翻译成逻辑,它并不实际运行,而是维持其自身执行流的状态。为了使逻辑模仿处理器执行程序的行为, 必须确保执行从程序的开头开始。这通过复位逻辑实现。

在大多数情况下,直观的行为是:当主机程序开始执行时,FPGA 中的程序从其开头开始。由于主机上运行的任何进程在访 问设备文件之前都会打开它们,并且这些文件至少在进程终止时会被关闭,因此在一个或多个设备文件关闭时复位逻辑是很自然的。

Xillybus IP 核中的每个流都有一个 *_open 端口,当相应的设备文件打开时,该端口为高电平 (’1’)。由于 HLS 模块默认具有低电平有效的 复位输入 ap_rst_n,将 *_open 输出直接连接到 ap_rst_n 输入可产生期望的结果:当文件关闭时,*_open 信号为低电平 (’0’)。这使逻辑保持 在复位状态。

可能希望组合多个 *_open 端口,以将逻辑保持复位状态直到所有设备文件都打开,或者直到其中任何一个被打开。这可以 通过添加简单的逻辑门模块来实现,这些模块在 Vivado 的 IP 目录中可用。如何生成复位信号的选择取决于主机程序的设置。

无论哪种方式,重要的是确保主机在打开设备文件(如需要以使复位信号变为非激活状态)之前,不要尝试与 HLS 模块交换 数据。为简单起见,最好在开始与 HLS 模块进行任何数据交换之前打开与该模块相关的所有设备文件,并在清理时全部关闭。