前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >FPGA Xilinx Zynq 系列(二十七)Vivado HLS: 近视 之 项目剖析

FPGA Xilinx Zynq 系列(二十七)Vivado HLS: 近视 之 项目剖析

作者头像
FPGA技术江湖
发布2020-12-30 11:13:50
发布2020-12-30 11:13:50
2.2K0
举报
文章被收录于专栏:FPGA技术江湖FPGA技术江湖
大侠好,欢迎来到FPGA技术江湖,江湖偌大,相见即是缘分。大侠可以关注FPGA技术江湖,在“闯荡江湖”、"行侠仗义"栏里获取其他感兴趣的资源,或者一起煮酒言欢。

今天给大侠带来FPGA Xilinx Zynq 系列第二十七篇,开启十五章,讲述Vivado HLS: 近视之项目剖析等相关内容,本篇内容目录简介如下:

15. Vivado HLS: 近视

15.1 一个 Vivado HLS 项目的剖析

15.2 Vivado HLS 用户界面

15.2.1 图形用户界面

15.2.2 命令行界面 (CLI)

15.3 数据类型

15.3.1 C 和 C++ 的自有数据类型

15.3.2 Vivado HLS 的 C 和 C++ 任意精度数据类型

15.3.3 SystemC 的任意精度类型

15.3.4 浮点数据类型和运算

15.3.5 任意精度模式的验证

15.4 接口规格和综合

15.4.1 C/C++ 函数定义

15.4.2 端口级别接口的综合

15.4.3 端口接口协议类型

15.4.4 端口接口协议的综合

15.4.5 包级别的接口端口和协议

15.4.6 接口综合指令

15.4.7 人工接口设定

本系列分享来源于《The Zynq Book》,Louise H. Crockett, Ross A. Elliot,Martin A. Enderwitz, Robert W. Stewart. L. H. Crockett, R. A. Elliot, M. A. Enderwitz and R. W. Stewart, The Zynq Book: Embedded Processing with the ARM Cortex-A9 on the Xilinx Zynq-7000 All Programmable SoC, First Edition, Strathclyde Academic Media, 2016。

Vivado HLS: 近视

Xilinx 设计方法中最重要的进步之一,就是引入了一个能做高层综合的工具:Vivado HLS。在之前的章节阐述了使用 HLS 的理由之后,现在来仔细看看如何用 Vivado HLS 做设计。

为此,本章要涉及几个话题,包括数据类型的定义及其对电路综合的意义、端口建立和包级别接口,以及算法综合的问题。也会展示如何用指令和约束来影响HLS所产生的解决方案。

必须指出的是,HLS 的功能是如此丰富多样,仅仅本章是完全不足以全面覆盖的,所以我们的目标是给出吸引人的介绍,然后引导读者参考 [17][18] 和 [19] 来获得更详细的评论和教程材料。不过,本章会给出一个关于循环的研究例子,作为演示某些 Vivado HLS 的特性和优化能力的基础,从而让读者了解用 HLS 能做什么。

和 Vivado Design Suite 的其他部分一样,HLS 也是着眼于集成和设计重用, 因此 Vivado HLS 包含了打包 IP 以方便地集成进系统设计的工具。在本章快结束的时候会简单地看一下这个工具的情况。

15.1 一个 Vivado HLS 项目的剖析

我们应该从 Vivado HLS 综合过程的高层模型开始(注意之前在 269 页的 14.4.3 节说过,仿真和验证也是用 C 测试集和从 C 测试集自动产生的 RTL 版本来做的)。这个过程的输入有:

  • C, C++ 或 SystemC 文件 — 这些文件里有要综合的函数。在一个简单的设计中,可以是含有单个函数的单个文件,或者在一个较复杂的情况下,可以是成系列的子函数分布在多个文件中。
  • C 测试集文件 — C测试集文件是验证 C和 HLS过程所产生的 RTL代码的基础。
  • 约束 — 设计者施加时序约束(期望的时钟周期),以及时钟不确定度指标和目标芯片的细节数据。这些约束和指令加在一起影响综合的过程。
  • 指令 — 设计者施加的指令影响高层描述 (输入的 C 代码)所产生的实现的方式,比如流水线和并行性的情况。下面列出所产生的输出。设计者可以选择要创建的输出种类。
  • SystemC 模型 — 这是从 HLS 过程输出的 RTL 级别的模型,也就是对输入的 SystemC 文件的另一种类型的描述。SystemC 输出不是用于综合的,而是仅用于 RTL 仿真。
  • VHDL 或 Verilog 文件 — 按照用户的偏好设置,Vivado HLS过程产生以VHDL 或 Verilog 语言写的 RTL 级别的输出。这是要集成进项目,用来产生在 FPGA或 Zynq 芯片上编程的位流 (*.bit 文件)的综合代码。
  • 给 Vivado、System Generator 或 XPS 用的打包好了的 IP — 打包好了的输出可以方便地用于直接包含进 IP Integrator 项目、XPS 项目或 System Generator 设计中。

上面所说的各种文件,形成了一个 Vivado HLS 项目的基础。接下来,我们继续介绍 Vivado HLS 开发环境。

图 15.1: Vivado HLS 综合过程的概述

15.2 Vivado HLS 用户界面

Vivado HLS 工具既提供了图形用户界面(GUI),也提供了命令行界面(CLI), 它们可以各自独立使用,也可以根据各自的偏好设置而互相配合使用。两种方法都能调用相同的功能,而从用户使用的角度具有不同的优势。

15.2.1 图形用户界面

Vivado HLS GUI 就像一般的软件开发环境一样,具有项目管理、代码编辑和调试的功能。除此之外,还有 HLS 专用的功能,用来指导 HLS 的过程和评估所综合的硬件。这些是值得着重介绍的,下面的小节逐一简述。

图 15.2 说明了这些专用的部分和 Vivado HLS GUI 的关系。GUI 实际上具有三种不同的视图:Debug (调试)、Synthesis (综合)和 Analysis (分析),每个视图默认地在 GUI 中列出相关的数据区。这里着重讲的是和综合视图与分析视图相关的功能。

综合视图:项目组织

Vivado HLS 开发常见的形式就是基于 “ 解决方案 ” 的概念 —— 同一份源代 码,根据用户不同的要求做的不同的实现。Vivado HLS 项目的结构正反映了这个形 式:项目中有不同的文件夹来存放源代码和测试集,加上所包含的头文件,然后有一组解决方案文件夹。每个解决方案代表了一个不同的实现,并包含有与那个实现相关的文件、报告和结果。用户可以产生所需的任意数量的解决方案,然后做评估和比较。在任意时刻,只有其中一个解决方案可以是 “ 活跃 (active)” 的,指令和分析也是这样的。

图 15.2 展示了一个例子项目的项目结构,活跃的项目是粗体的。另外还有 HLS 输出文件和报告的文件夹。

综合视图:指令区 (Directives pane)

Vivado HLS 也配备了用于设置和管理指令的区域,这些指令能影响 HLS 过程的行为。指令区只反映 “ 活跃 ” 的解决方案,只有当主窗口中打开了源代码的时候才是可见的。

注意指令可以是两者之一:

  • 与源代码分离,在指令文件中的 TCL 命令;或
  • 作为编译指示 (pragma)集成进源文件。

选择是把指令插入代码,还是放在分开的文件中,会影响到在指令区的指令如何显示。如图 15.2 所示,一个井号 (#)表示是编译指示 (嵌入在源代码中),而分号 (%)表示是在专门的指令文件中。

图 15.2: Vivado HLS GUI 视图

两种方法都各有优点,在 15.4.6 节会在界面中加以讨论。

综合视图:综合报告

Vivado HLS 为每个解决方案产生一个综合报告,给出与特定的实现相关的统计数据的合并数据。具体包括:

  • 时钟数据,及与约束的比较;
  • 延迟统计;
  • 在代码中识别出来的循环的细节 (如循环次数、每轮循环的延迟);
  • 估算的以 PL 资源表示的实现成本;
  • 综合出来的 RTL 接口端口的列表,包括方向、大小和相关的协议。

对于指定的一组解决方案,可以产生一个独立的比较报告。这样就能在不同的解决方案之间直接比较关键的实现度量指标,这是朝向最优设计发展的非常有用的工具。

分析视图

除了综合结果之外,GUI 中还有一个分析视图,它能给出综合出来的设计的运算和控制步骤的一个图形化的可视化表达。这个数据是和资源链接在一起的,而且能和原本综合所用的 C/C++ 代码交叉索引。

分析视图有用是因为它让设计者能更深入欣赏设计是如何被综合出来的,从而指导下一步的修改。观看综合出来的设计的细节,有助于设计者识别出导致瓶颈问题的运算,这对于下一步的优化是有好处的。

15.6 节将仔细讨论分析视图。

15.2.2 命令行界面 (CLI)

命令行界面 /TCL 脚本方法在做重复性或预先定义的任务的时候特别合适,因为所需的步骤可以自动化地执行,从而节约时间而且确保可重现的结果。

命令是通过 TCL 语言来输入的。这是一种开源的脚本语言,广泛用于 ASIC 和 FPGA 开发。在 Vivado HLS 里,TCL 可以被用于运行诸如设置项目和运行仿真等基本的任务,直到用预定的参数和指令集来驱动丰富的测试组都行。从使用的角度来说,预 先 准 备 好 脚 本 然 后 执 行 总 是 比 较 方 便 的 (比 如 做 好“`my_hls_script.tcl`”,里面放所需的全部设置和命令)。还可以直接在命令行输入每一条命令来执行。

另有一份关于 Vivado HLS 用的全部 TCL 命令的全面的指南,这是想要开发驱动这个软件的脚本的关键资源 [18]。在 Xilinx 的 Vivado HLS 教程中也加入了使用脚本的例子 [17],在 Vivado Design Suite 的 TCL 指南中也有一些基本的介绍 [16]。也可以遵循本书所附的教程之一来获得这个方法的经验,请参考第 16 章的详细描述。

15.3 数据类型

在使用 FPGA 的传统设计方法的时候,数据类型的规格是重要的,因为这对设计的一致性和实现的关键度量指标(也就是资源利用、时序性能和功耗)都有直接的影响。对数值型数据类型而言,采用较短的字长会牺牲精度,而采用过长的字长导致资源消耗和功耗的增长,以及不良的最大时钟频率。因此有必要仔细设定数据类型。

这个问题在 Vivado HLS 中,和在其他方法比如 HDL 开发或基于包的设计中一样重要,即使在设计入口处的数据类型是不同的。理解可用的 C、C++ 和 SystemC 的数据类型以及它们的综合,是开发有效而且高效的设计的基础。为此,这一节致力于回顾可用的类型,并解释它们会如何被转换进 RTL 设计中,并进一步成为硬件。我们先考虑 C 和 C++ 语言自有的数据类型,然后讨论任意精度类型。

15.3.1 C 和 C++ 的自有数据类型

C和C++语言有一些自有的数据类型,是从四个基本数值类型派生出来的:char、 int、float 和 double,具体总结于表 15.1。

以 char、int 和派生出来的类型而言,默认是有符号的,不过也可以指定为无符号的 (或有符号的以免误解)。特别是标准的 int 类型和 short、long 及 longlong 版本的 int 类型,等价于最小大小。这里,选择了的是典型的值 [5][9]。

表 15.1: C 语言的自有数据类型

a. 根据 C 语言的定义,某个类型的位数不是固定的,而是与具体的实现相关的。这里给出的是一组有代表性的数值。

b. 所给的范围是基于前述的每种类型的位数大小的。

还有一些有意思的类型,下面简单列举一下:

  • 布尔类型,bool,引用了 “stdbool.h” 头文件后可用,它定义了 {`true,false`} 两个标准值。
  • 通过 “complex.h” 库的头文件支持了复数类型。这是和浮点有关的一种类型。
  • 有一种精度更高的浮点类型 long double,不过实际上可能和 double 类型是相同的。

从表 15.1 中可能已经看出,C/C++ 自身的数据类型是基于 8 位的 (8 位、16 位、32 位和 64 位),这表明软件代码往往是用于这样的大小的处理器的。不过,这样的限制对于产生有效的硬件架构来说并不理想。

为了优化硬件实现,不应该有超过必须使用的位存在,因为那样会导致额外的硬件开销。需要支持任意字长来满足电路需要的任意程度的精度。实际上,如果要限制字长是 8 位的整倍数,在某些 PL 的专用资源上,问题可能会更严重。比如,两个 18 位的数字 A 和 B 的乘法,会产生 36 位的结果 S,这样就会需要 A 和 B 用 32 位来表示,而 S 用 64 位。这样会导致一个低效的乘法器的实现,用上四个 DSP48x 片而不是一个 —— 300% 的额外开销!(如果有需要,请参考 25 页 2.2.2 节关于 DSP48x 架构的描述)。不仅于此,用于 A、B 和 S 的任何寄存器和其他运算资源都会是尺寸超大的。

那么显然,有理由要支持任意精度的数据类型,就像在HDL和System Generator 那些设计手段中用的一样。因此 Vivado HLS 特地支持任意精度的 C/C++ 数据类型。另外,SystemC具有自己的任意精度的数据类型作为语言的一部分,当然Vivado HLS 也完全支持这个特性。

15.3.2 Vivado HLS 的 C 和 C++ 任意精度数据类型

理解了对任意精度算术,也就是能形成高效硬件实现的需要之后,直接的结果就是任意精度整数类型。不过这对于大多数硬件设计者来说还不够满意,他们希望对于某些应用能有定点算术。因此,Vivado HLS 也支持任意精度的定点类型,但是只能在 C++ 中使用。

任意精度整数类型

对任意精度整数类型的支持,是由 C 和 C++ 输入语言的不同的类型和相关的库来实现的,具体见表 15.2。两种语言的字长都可以是 1 位到 1024 位,也就是说 1≤ N ≤ 1024。

表 15.2: 在 C 和 C++ Vivado HLS 设计中使用的任意精度整数数据类型

注意在 C 中使用任意精度整数类型的时候,必须使用另一个编译器(apcc 而不是 gcc),细节见 [18]。C++ 无需如此。图 15.3 是 C 和 C++ 两个等价的代码片段,说明如何使用任意精度的整数类型。注意所用的语法稍有不同。

图 15.3: 在 C (左边)和 C++ (右边)中使用任意精度整数类型

任意精度定点类型

图 15.4 是定点数的一般格式,在二进制小数点的左边是确定位数的整数,而二进制小数点右边的是小数部分。作为二进制整数,MSB (最高位)对于无符号数字为正,对于有符号数字则为负。

为了和 Xilinx Vivado HLS 文档一致 [17][18],在我们的讨论中,整个字长记做 W,整数的位数记做 I,而小数的位数记做 B,也就是说,W = I+B。在图 15.4 的 例子中,I = 5、B = 7,而 W = 12。

图 15.4: 定点字格式的例子

Vivado HLS 的 C++ 的定点数格式如表 15.3 所定义,注意 C 语言并不支持定点 数。这里,W 和 I 就是上面所定义的,Q 是表示量化模式的字符串,O 定义溢出模式,而 N 表示溢出卷绕模式时的饱和位的数量 (也就是 N 个最高位要置为 1)。这些选项的细节在 [18] 可以找到。后面三个参数是可选的,如果没有指定的话,量化模式 Q 默认是截断为 0,而溢出模式 O 默认为卷绕。

表 15.3: Vivado HLS 设计的任意精度定点数据类型

和之前整数数据类型类似的,有必要厘清用表 15.3 所给的通用类型定义来声明变量的语法。图 15.5 给出的代码例子中,创建了一些变量,每个都有不同的整数和小数部分的字长。代码也展示了量化和溢出模式的使用。

表 15.4 列出了用于 Q (量化模式)和 O (溢出模式)的字符串 [18],而其中黑体的是默认值。

表 15.4: C++ 的 ap_fixed 和 ap_ufixed 类型的量化和溢出模式 (黑体的是默认的)

15.3.3 SystemC 的任意精度类型

前面提过,SystemC 自己就支持整数和定点类型。如表 15.5 所示,使用这些类型的方式与使用 C++ 的非常类似。不过,请注意 SystemC 有两种不同的数据类型来适合小型 (最多 64 位)和大型 (最多 512 位)的整数字长,而 C 和 C++ 能用最多1 024 位字长的数据类型。

SystemC 的代码例子,以及与表 15.4 相当的模式字符串,可以另外在 [18] 中找到。

如 15.5: C++ 例子代码,表示如何声明定点变量

表 15.5: SystemC 数据类型的总结

15.3.4 浮点数据类型和运算

Vivado HLS 支持使用浮点数据类型和运算,这些是作为 Xilinx 技术库的核提供的。比如,标准的算术运算,诸如加法、减法、乘法和除法都有对应的 Xilinx核,可以由 Vivado HLS 调用。引入对应的头文件,还能使用更多的数学函数 [18]。不过,并非所有的浮点运算都被HLS所支持,所以写代码的时候得要记着这些限制。

作为 HLS 可用的浮点运算的例子,图 15.6 的代码中出现了(单精度)浮点变量的乘法和加法运算,这些是可以成功地由 Vivado HLS 用浮点加法器和乘法器核的实例来成功综合出来的。通过检查 Vivado HLS 所产生的报告可以确认这一点。

15.3.5 任意精度模式的验证

可以用原本的 C/C++ 数据类型实现的等价的函数,来比较和验证用任意精度算术写的函数,通常开始开发 Vivado HLS 设计的时候,用的就是传统的 C/C++ 类型。这是调整算术字长,也就是实现正好精确满足需求,又不浪费一点硬件资源的快速而有效的方法。

某个变量 (或某些变量)可以用两种类型规格来开发:(一)原本的类型,用在最初验证过的函数中的;和 (二)降低了精度的版本,也就是准备用于综合的。在某次仿真中只会使用其中一种,通过 C 的宏来实现切换 [18]。这样,功能性仿真就可以便捷地为每种变量类型执行一次,然后结果可以快速地比较。所有的参数都和原本的参考设计保持一致,而不会有本质的差别。

图 15.6: 演示 “float” 类型使用的例子

如果结果表明降低精度的版本产生了所需的结果,那么自然这是应该用于综合的,因为它降低了硬件成本。

15.4 接口规格和综合

正如 14.4.1 节中所提到过的,标识一个 HLS 函数的接口的机制,在不同的 HLS 输入语言中是不同的:C 和 C++ 支持从高层描述来做接口的综合,而 SystemC 需要另外做详细的人工定义 (偶有例外)。这里我们着重讨论从 C 和 C++ 而来的综合。

在 Vivado HLS 中,所设计的顶层 C/C++ 函数的输入参数和返回值被综合成 RTL 数据端口,每个端口带有相关的协议。整合起来,这些端口和协议就形成了端口接口。端口接口是用来和其他子系统通信的,而且,如果可能还要和系统中的处理器通信。除了从 C 函数参数推导出来的端口接口,包级别的协议和相关的端口也被用来负责子系统之间的数据交换。

接下来几页,我们首先考虑推导出接口的那些顶层 C/C++ 函数的定义。然后解释从这种定义所得出的端口和协议的综合、包级别的协议的功能性,最后回顾如何用指令来影响接口综合的过程。

15.4.1 C/C++ 函数定义

Vivado HLS 设计的功能性部分是一个 C/C++ 函数,可能以层次结构的方式包含其他的子函数。这个顶层的函数,也就是在层次的最高层的函数,形成了接口综合过程的基础。

作为一个例子,图 15.7 中的代码,表示了一个简单的 C 设计的顶层函数 `find_average_of_best_X()`。这个函数内部工作的详细情况无关紧要,不过每个参数的读 / 写操作能决定综合出来的端口的方向,这会在 15.4.2 中讨论。

图 15.7: 一个 HLS 顶层函数的例子

这个函数定义包含三个参数,那个 8 单元的数组 `sample` 应该被理解为是一个输入,那个整数 X 也是如此,而 average 其实是函数的输出。因此,简单来说,这三个函数参数要被 HLS 转换成两个输入接口和一个输出接口,如图 15.8 所示。

图 15.8: 例子函数 `find_average_of_best_X()` 的简化接口图

需要注意的是,根据所用的协议,这些接口可能包括数据端口自身以外的控制输入或输出。本章后面,在介绍了端口协议之后,我们还会回到这个接口图,并加以改进。

15.4.2 端口级别接口的综合

看过了 15.4.1 节里那个有意思的接口综合的例子之后,就该来看看在从 C/C++ 代码综合一个 RTL 端口级别的接口时,所采用的更正式的方式(注意包级别的接口会在 15.4.5 涉及,包级别与端口级别是有所不同的)。

对一个端口的 RTL 级别的描述包括以下内容:

  • 端口的名称;
  • 端口的方向 (输入、输出或输入输出);
  • 数据类型和尺寸。

因此,在用 Vivado HLS 做设计的时候,所有这些属性都必须从高层 C/C++ 代码中 综合出来。本节接下来,会逐一讨论每个属性。

端口名称

端口的名称是从对应的函数参数的名称来的。比如,图 15.7 的函数中,“sample” 是函数的一个数组类型输入参数,因此 “samples” 这个名字也就会用来做数组数据端口的名字。一种例外的情况是从函数的 return语言综合出来的端口,会指定用 “ap_return” 的名字 (在我们之前的那个例子里没有 return 语句)。在某些情况下,额外的控制信号和相关的端口会和数据端口一起综合,这是由所用的协议决定的。在 15.4.3 节会涉及更详细的协议的细则,此刻,需要指出的是与一个综合出来的数据端口相关联的任何控制信号会得到相同的名字,然后有表达控制类型的相应的后缀。

端口方向

端口方向的解释,是遵循一系列规则的,表 15.6 总结了这些规则。比如,C/C++函数的一个参数,如果只会被那个函数读,而永远不会写入,就会被综合成一个 RTL输入端口。类似的,一个只会写入而永远不会读的参数,会被转换成一个输出端口。

表 15.6: 端口方向的综合

数据类型和尺寸

从 C/C++ 函数的参数综合出来的端口的数据类型和大小遵循相同的一般数据类型的规则,就是 15.3 节所讨论过的那些。有些接口协议 (下一节讨论)需要产生额外的控制端口,这些通常是 1 比特的信号,当然也有例外的。

15.4.3 端口接口协议类型

除了端口本身,还有相关的协议来定义通过那个端口所发生的交互的形式。Vivado HLS 制定了一组可用的协议,复杂程度从 “none” (也就是没有明显的协 议)到 “hs” (握手协议)、“ack” (确认协议),甚至 AXI 协议都有。下面列出了所有可用的协议,而且给出了每个协议在 Vivado HLS 工具中所用的名称,以及 简单的解释 [18]。

  • ap_none — 这是最简单的协议类型,没有明显的接口协议,没有额外的控制信号,没有相关联的额外硬件。不过,这个协议也附带表明输入和输出操作的时序是独立的而且各自正确地处理的。
  • ap_stable — 这个协议和 ap_none 类似,其中不涉及到额外的控制信号或相关的硬件。区别在于 ap_stable 倾向用于输入 (只是输入)变化不频繁的端口,一旦重启之后就基本上是稳定的,比如配置数据。它的输入并非是常数,但是也不需要用寄存器来处理。
  • ap_ack — 这个协议的行为,输入和输出是不同的。对于输入,要附加一个输出确认端口,在输入被读的那个时钟周期要保持高电平。对于输出端口,要附加一个输入确认端口。每次写入输出端口 . 之后,设计中必须等待收到输入确认信号,然后才能继续下一步操作。
  • ap_vld — 要提供一个额外的端口来验证数据。对于输入端口,要附加一个有效输入控制端口,它负责校验输入端口是否有效。对于输出端口,要附加一个输出有效端口,在输出数据有效的时钟周期里给出信号。
  • ap_ovld — 这个端口和 ap_vld 是一样的,但是只能用于输出端口,或是一个 inout (双向)端口的输出部分。
  • ap_hs — 这个协议的 “_hs” 后缀表明这是用于 “ 握手(handshaking)” 的协议,它是 ap_ack、ap_vld 和 ap_ovld 的超集。ap_hs 协议既可以用作输入端口也可以用作输出端口,它具有数据的生产者和消费者之间的双向的握手过程,同时包括验证和确认的对话。因此,它需要两个控制端口和相应的额外开销。不过,它是传递数据的可靠的方法,而且不需要外部来确认时序。
  • ap_memory — 这个基于存储器的协议支持存储器的随机访问会话,可以用在输入、输出和双向的端口上。唯一能用这个协议的参数类型是数组类型,因为数组类型对应的是存储器的结构。ap_memory 协议需要时钟和写使能的控制信号,以及一个地址端口。
  • bram — 和 ap_memory 相同,不过在用 IP Integrator 绑定的时候,所用到的端口是集合成单个端口,而不是每一个单独列出来的。
  • ap_fifo — FIFO 协议也能用于数组参数,只是通过这个协议只能顺序访问数据而不能随机访问。它不需要产生任何寻址数据,因此实现起来比 ap_memory接口要容易些。ap_fifo 协议可以用做输入或输出端口,但是不能用于双向端口。它附带的控制端口用来根据端口的方向指出 FIFO 的满或空,并确保有需要时过程会停下以避免出现数据饱和或欠缺。
  • ap_bus — 这个协议是泛泛的没有指定具体总线标准的总线接口,可以用于和总线桥的通信,这样就能受系统总线的仲裁了。ap_bus 协议支持单次读操作、单次写操作和批量传输,这些是由一组控制信号所协调的。除了这个通用的总线结构之外,对 AXI 总线接口的特殊支持,可以在较晚的阶段,用接口综合指令来集成进去。
  • axis — 这个指定接口做 AXI 流操作。
  • s_axilite — 这个指定接口做 AXI Slave Lite (简化从机协议)。
  • m_axi — 这个指定接口做 AXI 主机协议。

详细解释每个协议的机制超出了本章的范畴。不过在 [18] 中可以找到大量的进一步的数据,包括详细的时序图。另外在 [17] 中也有不少相关的实际的例子。

15.4.4 端口接口协议的综合

定义了可用的协议之后,现在来关注如何从高层描述来做特定协议的综合,以及有什么相应的限制。

设计者可以通过施加恰当的指令来为每个端口选择协议。所支持的协议集受到这些因素的限制 (一)端口的方向,和 (二)C/C++ 函数参数的类型,即表 15.7所表明的 [18]。如果协议不能直接由指令所定义,或者如果错误地选择了一个不被支持的协议,Vivado HLS 会采用默认协议。默认的也是上面 (一)和 (二)的功 能,如表 15.7 所示。

表 15.7: 协议综合:所支持的类型和默认值 (S =支持,D =默认)[18]

a. 在这行中:I= 输入端口;IO= 输入输出 (双向)端口;O= 输出端口。

根据协议、端口类型和方向之间的相关性,在开发高层 C/C++ 描述时,考虑 C/ C++ 函数参数的类型是很重要的。根据表 15.7 的各列的表头,可以传入传出 C/C++函数的值有四种不同的参数类型,也就是:(一)变量;(二)指针;(三)数组和 (四)引用。也就是说,一种特定的参数类型只对应于有限的几种协议。比如,传入一个数组参数作为参输入,能使用的协议就只有:ap_hs、ap_memory、bram、ap_fifo、ap_bus、axis 和 m_axi,其中 ap_memory 是默认的。

为了把这个知识和设计项的实际问题联系起来,请参考前面图 15.7 里的函数定 义,注意那个函数有三个参数:

  • samples — 数组输入;
  • X — 整数 (标量)输入,传值;
  • average — 用作输出的指针变量。

那么,根据表 15.7,就需要注意 samples 的默认的协议是 ap_memory,X 的默 认的协议是 ap_none,并且 average 默认的协议是 ap_vld。考虑到这些协议所需的额外的控制端口,我们就能更新原本在图 15.8 中所给出的综合了的 RTL 接口,成为图 15.9 的样子。注意这里的数据端口是 32 位的,这是因为用了 C 的 int 数据类型的缘故。

图 15.9: “`find_average_of_best_X()`” 函数的 RTL 接口图

采用了默认的端口级别端口和协议。

还要注意的是,这个图只显示了端口和端口级别的协议 —— 并没有包括任何包级别的协议,那是下一节的内容。时钟信号会在后面做为包级别协议的一部分加进来。

15.4.5 包级别的接口端口和协议

除了从 C/C++ 函数的参数推导得到的逻辑端口及其相关的协议,还可以给设计加上包级别的协议 (这些协议也被称作是函数级别的协议)。这样就有了一种机制来控制子系统的执行,在把一个或多个 Vivado HLS 包集成进系统,然后还要管理包之间的数据流动的时候,这会特别有用。

在定义协议之前,有必要先来简要地在图 15.10 的帮助下确定一些术语。该图描绘了五个层叠的包,并标注了数据流的方向。实际上可能还有包之间的 FIFO 缓冲区,但是并没有画在图上 (要理解的是,在选择这个例子的时候,我们只是用到了Vivado HLS 包的一种可能的使用模型 —— 我们并不是要暗示链状的包关系是唯一 或典型的使用场景)。

图 15.10: 在 Vivado HLS 的包之间的数据流

以 D 包为参考点,它左边的包 (C、B...)被认为是上行的数据流,而右边的 (E、F...)则被认为是下行的数据流。在考虑任何两个包之间的接口的时候,上行的包输出数据,并传递给下行的包。比如,考虑 C 包和 D 包之间的接口,C 包是数据的生产者而 D 包则是数据的消费者。

有的时候,我们希望一个包向它的上行包施加 “ 反向压力 “。换句话说,这个消费者包也许希望阻止那个生产者包制造更多的数据,直到消费者准备好接受新的数据 (如果有必要,这个压力会像涟漪效应一样向更上游的包扩散)。这是一种可以用包级别的控制可以实现的功能。

有三种类型的包级别协议,以下按照在 Vivado HLS 中所用的术语列出:

  • ap_ctrl_none — 选择这个选项只是表明没有添加包级别的协议,而是完全在端口接口级别用端口级别协议来做控制。
  • ap_ctrl_hs — 有握手的包级别控制协议。由一个 ap_start 控制输入来通知这个包要开始操作了,然后这个包会产生三个输出控制信号 (ap_ready、ap_idle 和 ap_done)来表明它的操作阶段。具体来说,ap_ready 信号表明包已经就绪来读新的输入,ap_idle 表明包正在处理数据,而 ap_done 会在输出数据已经可读的时候发出。一个实用的例子是,ap_ctrl_hs 协议适用于单个HLS 包与做控制的处理器接口时。
  • ap_ctrl_chain — 这个协议与 ap_ctrl_hs 类似,但是多了一个输入控制信号 ap_continue,这是设计用来把多个 Vivado HLS 包串起来的。ap_continue 输 入表明下行包能接受新数据了,因此有必要的话,它可以向上行包施加反向压力。如果 ap_continue 信号没有生效,这个包会完成它当前的计算,直到能把结果呈现到输出端,然后它就会停止,直到 ap_continue 再次被置为有效。

如果用了一个包级别协议,它的运作和每个端口所采用的任何端口级别的协议都是无关的。不过,无论选择了怎样的包级别协议,有两个输入协议都会施加到包上:ap_clk 和 ap_rst。这是必须的,因为包内部的操作是同步的,因此它需要一个时钟信号 ap_clk,而 ap_rst 则是因为包必须能从外部被重置。

一般来说,值得注意的是,一个 AXI4-Lite 总线接口可以被加到包级别接口协议,因此能让包级别控制信号在包和做控制的处理器之间传递。某些情况下,还可以把包级别的控制端口和端口级别的接口捆绑起来,形成一个一体的 AXI4-Lite 接口 [18]。

做为讨论的结尾,考虑给之前的那个例子函数增加 ap_ctrl_hs 包级别控制协议 (默认的协议) ,如图 15.11 所示。在这里,增加了六个额外的端口:ap_clk 和ap_reset (所有的 Vivado HLS 设计都需要);ap_start 控制输入和 ap_done、ap_ready 及 ap_idle 控制输出。这些新的包级别的接口端口都显示在图的上端。

图 15.11: `find_average_of_best_X()` 的 RTL 接口图,

既有默认的端口级别的端口和协议,也有默认的包级别的端口和协议

15.4.6 接口综合指令

指令是使设计者能对所设计的 C/C++/SystemC 代码的实现施加高层控制的机制。HLS 有一组指令是专门和接口综合相关的,针对之前几页所讨论的两种接口的形式,这些指令既可以用于端口级别,也可以用于包级别。除了直接指定协议之外,也可以指定接口的其他一些属性,比如数组输入的形式,或用来实现存储器或 FIFO缓冲区的资源。

指令类型

有两种类型的指令是可以施加在端口级别和包级别的接口上的,陈述如下:

  • Array Map — 数组映射。把几个数组合并起来形成一个较大的数组,目的是利用较少的 FIFO 或 RAM 资源和控制端口来实现这个接口。
  • Array Partition — 数组划分。把数组接口划分成几个较小的区域,从而形成一组扩展的端口、控制信号和实现资源,但是提升了带宽。
  • Array Reshape — 数组重塑。在这种情况下,原本的数组被分割成几个较小的数组,然后再合并来形成新的单元数量较少但是每个单元更大的数组。这样会占用较少的存储器位置,形成较短的地址。
  • Interface — 接口。这个指令可以被用来直接指定一个端口级别的接口的协议为某个可用的协议 (即第 300 页的表 15.7 所列的),或是如 15.4.5 节所述,指定包级别的协议为 ap_none、ap_ctrl_block 或 ap_ctrl_chain。
  • Resource — 资源。可以选择某个特定的资源来实现这个接口。比如,可以把一或两个端口 RAM 指定给一个 ap_memory 接口,或是一个 ap_fifo 接口可以实现在一个 Block RAM 或 LUT 构建的 FIFO 上。
  • Stream — 流。这个指令指定这个接口作为流式端口,用上 FIFO 来实现,并能直接选择 FIFO 的深度。

对端口级别的接口类型的指定是通过相应的函数参数做的,而包级别的接口是 施加在顶层函数上的。

通过施加所需的指令,图 15.11 中的设计例子,可以被输出成图 15.12 中的 IP Integrator 包。图 15.13 展示了把端口和包级别的接口固化成单个 AXI4-Lite 接口的结果(注意数组输入 samples 是独立的,因为它需要流式接口)。通常会这样做,以实现运行在处理器或单片机上的软件的控制。

进一步的选项

除了前面详细解释的指令之外,还有其他的针对每个端口的选项,就是说额外的时钟周期的延迟可以加到那个端口接口上。这样就有可能改善设计的时序性能。

图 15.12: 以图 15.11 中指定的端口和包级别的协议,

从 `find_average_of_best_X`函数所产生的 IP Integrator 包。

图 15.13: 从 `find_average_of_best_X` 函数所产生的 IP Integrator 包,

其中包级别协议的端口和部分端口级别协议的端口被合并进了一个 AXI4-Lite 接口。

源文件还是指令文件?

指令可以合起来放在 Vivado HLS 项目的一个独立的文件中,也可以继承进源文件作为 #pragma。图 15.14 给出了使用 pragma 的例子。Vivado HLS 会自动把这些插入进去,直接放在函数体的最上面(如第 296 页的图 15.7 所指出的那样),因此就不需要在代码中再手工输入这些行了 (当然如果你愿意也是可以这样做的!)。无论哪种方式,都应该要理解 pragma 是如何构成的。

在图 15.14 的例子中,有三个 pragma:开头两个设置接口指令,而第三个设置资源指令。具体来说,第一个 pragma 指定将 ap_memory 协议用于 samples 输入端口,类似的,第二行设置 ap_vld 协议用于 X 这个输入端口。还要注意的是,在 X 端口插入了寄存器。第三个指令制定samples输入端口用一个特定的硬件资源来实现,在这里是一个用LUT实现的单口ROM(也就是说用逻辑片而不是 Block RAM资源)。

图 15.14: 插入 C/C++ 源代码中做接口综合的 pragma 的例子

由于接口通常是系统中静态的部分,而且设计者因为关心函数实现的优化的效果,会创建不同的解决方案,所以接口指令往往是以 pragma 的形式放在源文件中的。这就意味着接口设置就可以方便地在 Vivado HLS 的各个解决方案之间搬来搬去,而不需要每次重新制定,或是特别操心如何把这些指令从一个方案抄到另一个去。不过,设计者也许会为了某些美好的愿望,而宁愿在一个文件中给出这些指令,这样就能保持指令和源代码的分离 —— 这也是一种不错的好方法。

总的来说,插入 pragma 会让代码不那么好移植,因此 pragma 最适合的场景是在设计者想要做出修补的时候。比如,可能想要用 pragma 来调整正在设计的接口的配置。

那些在不同的“解决方案”不同的属性,也就是设计者想要对代码做的优化, 应该用单独的指令文件来指定,而不是嵌入在代码中。保持代码和指令的分离能给高层设计更高的自由度。

15.4.7 人工接口设定

直到这里,我们还只是在讨论从 C 和 C++ 函数出发的接口综合,不过接口也是可以自由指定的。在用 SystemC 编程的时候,这是唯一的手段 (除了 ap_bus 和ap_memory 接口类型是可以被综合出来的之外),而在用 C 或 C++ 的时候,这也是可以用的手段。

在 SystemC 中指定接口

在 SystemC 中,任何设计的部件都以一个从基类 SC_MODULE 派生的 C++ 类来表 达。这个类用来定义部件的接口和功能 [1]。在 SystemC 中指定接口和 HDL 级别的描述是类似的,包括每个接口的类型、方 向和大小的直接定义。图15.15就是一个简单的计数器的例子,它给出了my_counter这个模块 (类)的第一段定义,在这个定义中给出了端口的声明。你可能注意到了,挑出来的这段代码和 VHDL 入口声明是很相似的。由于所有的端口数据都是人工指定的,这个例子中就不再需要接口综合了。

图 15.15: 人工指定用 SystemC 做的计数器设计的端口

在 C/C++ 中指定接口

有些时候,也许会想要定义和 Vivado HLS 所提供的具有不同的控制端口和相关联的协议的接口。这可以在 C 或 C++ 中加入额外的代码块来指定协议信号和在这些控制端口上所需的会话组来实现(还记得这些内容一般不是由用户指定的,用户会指定的是数据端口,而控制端口是由 HLS 过程来引入的)。在 Vivado HLS pragma 实现中,会话的顺序是在 ap_wait() 函数的帮助下确定的,这个函数会让 VivadoHLS 在一段特殊标注的代码段(一个 “ 协议区 ”)内的 IO 操作之间插入时钟周期。

在 [18] 可以找到一个演示这个人工接口指定技术的代码。

第二十七篇到此结束,下一篇将带来第二十八篇,Vivado HLS: 近视 之 算法综合等相关内容。欢迎各位大侠一起交流学习,共同进步。

END

后续会持续更新,带来Vivado、 ISE、Quartus II 、candence等安装相关设计教程,学习资源、项目资源、好文推荐等,希望大侠持续关注。

大侠们,江湖偌大,继续闯荡,愿一切安好,有缘再见!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-05-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FPGA技术江湖 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档