以下内容摘自正点原子的:《领航者 ZYNQ 之 HLS 开发指南》
为了尽快把新产品推向市场,数字系统的设计者需要考虑如何加速设计开发的周期。设计加速主要可以从“设计的重用”和“抽象层级的提升”这两个方面来考虑。Xilinx 推出的 Vivado HLS 工具可以直接使用C、C++或 System C 来对 Xilinx 系列的 FPGA 进行编程,从而提高抽象的层级,大大减少了使用传统 RTL描述进行 FPGA 开发所需的时间。
一、高层综合简介
如图 1.1.1 所示,FPGA 设计中从底层向上一共存在着四种抽象层级,依次为:结构性的、RTL、行为性的和高层。其中最底层的抽象(结构性的)涉及到对底层硬件单元直接的例化,比如逻辑门,甚至是更底层的 LUT 或者触发器。设计者更常用的是在“寄存器传输级(Register Transfer Level,RTL)” 进行设计,这个层级的抽象隐藏了底层的细节,是在描述寄存器和寄存器之间可执行的操作。更上层的“行为性的”描述是对电路的算法描述,也就是描述电路表现出什么样的功能(行为),而不是描述每个寄存器该如何进行操作。
前面介绍的几种抽象层级都是在使用硬件描述语言 HDL 进行设计,可以看出,随着抽象层级的提升,设计最终在硬件上实现的细节逐渐被弱化。而本章重点介绍的“高层”设计方法则直接使用高级语言,如C/C++进行设计,然后由 Vivado HLS 编译器将 C 代码综合成 HDL 描述,最后再进行逻辑综合得到网表,这个网表最终会被映射到具体的 FPGA 器件上。
就像 C 语言或者其他高级语言针对不同的处理器架构有着不同的编译器,Xilinx Vivado High-Level Synthesis(高层综合,HLS)工具同样是一种编译器,只不过它是用来将 C 或者 C++程序部署到 FPGA 上,而不是部署到传统的处理器上。
在 Vivado HLS 中可以使用三种语言进行设计开发,分别是 C、C++ 和 SystemC。
二、HLS 设计流程
Vivado HLS 的功能简单地来说就是把 C、C++ 或 SystemC 的设计转换成 RTL 实现,然后就可以在Xilinx FPGA 或 Zynq 芯片的可编程逻辑中综合并实现了。需要注意的是,这里我们说的使用 C/C++完成的设计与运行在处理器(ZYNQ 中的 ARM 处理器或 MicroBlaze 软核处理器)中的软件代码是截然不同的。在 HLS 中,所有的 C 设计都是要在可编程逻辑中实现的,也就是说,我们仍然是在进行硬件设计,只不过使用的不再是硬件描述语言。
使用 Vivado HLS 进行设计的流程如下图所示:
HLS 设计的主要输入是一个 C/C++/SystemC 设计,以及一个基于 C 的测试集(TestBench)。我们首先要知道 C 语言的本质就是函数,那么这个测试集就是用于验证 C 设计中的函数,验证过程需要一个“黄金参考”。这个“黄金参考”类似于一个标准答案,用来和 C 设计中函数所产生的输出做比对。
在对 HLS 设计进行综合之前,我们要先对其进行“功能性验证”,也就是 C 仿真,其目的是验证 HLS 输入的 C 代码的功能是否正确。验证的方式就是在 TestBench 中调用 C 设计的函数,然后将其输出与“黄金参考”进行比对,如果与黄金参考有差异就需要先对 C 设计进行修改调试。
接下来就是对设计进行高层综合,即 HLS 过程本身。该过程涉及到分析和处理基于 C 的代码,加上用户所给出的指令和约束,来创建 RTL 描述。高层综合结束后会产生一组输出文件,包括以 Veilog 或者VHDL 语言编写的 RTL 设计文件。
综合过程结束后得到的 RTL 模型,可以在 Vivado HLS 中进行 C/RTL 协同仿真,来进一步验证综合得到的 RTL 设计的正确性。在这个过程中 Vivao HLS 会自动产生一个测试集为 RTL 设计提供输入,然后拿它的输出与预期的值做比对。C 功能性验证和 C/RTL 协同仿真的区别如下图所示:
在图 1.2.2 左侧的功能性验证(C 仿真)中,原始测试集是用户输入的测试文件 TestBench。而右侧的C/RTL 协同仿真所需的 RTL 测试集是由 Vivado HLS 自动产生的,这样就不再需要人工创建了,所产生的测试集包括了原始测试集和被测 RTL 模块之间的数据传递。
除了对功能进行验证,我们还要评估 RTL 设计的实现和性能。比如,在 FPGA 中所需的资源的数量,设计的延迟、所支持的最高时钟频率等是否满足要求。如果不满足要求,那么就需要设计者通过修改指令和约束,然后再次进行高层综合,如图 1.2.1 中右侧的回路所示。一个设计可能要做多次 HLS 设计迭代,来找到“最佳 ”的解决方案。如果有必要,设计者也可以返回修改 C 设计代码,然后从头开始重新对设计进行验证。
在设计被验证了之后,而且实现也满足了期望的设计目标,那么就可以集成进更大的系统里了。我们可以直接使用 HLS 过程所产生的 RTL 文件(即 VHDL 或 Verilog 代码),更方便的做法是使用 Vivado HLS 的 IP 打包功能。对 Vivado HLS 所产生的输出打包意味着 HLS 设计能够以 IP 核的形式引入其他 Xilinx 工具中,比如 Vivado 中的 IP 集成器。这两种类型的输出如下图所示:
三、 接口综合
在做 HLS 的时候,设计者需要分析设计的两个主要方面:
- 设计的接口,也就是它的顶层连接;
- 设计的功能,也就是它所实现的算法;
我们给出一个 HLS 设计中接口和功能的概念图,如图 1.3.1 所示。
在上图中,两端的绿色区域表示设计的输入和输出接口,其中展示了部分接口类型,如 RAM 接口、FIFO 接口,以及总线类型的接口等。这些接口可以是工具从代码中通过接口综合(Interface Synthesis)得到的,也可以由设计者手动指定具体的接口类型。
图中间黄色的区域表示 HLS 设计具体能够实现的功能,对于不同的应用,其功能也各不相同。在 Vivado HLS 设计中,功能是从输入的代码中,经过算法综合(Algorithm Synthesis)的过程得到的。
在这里我们先简单介绍一下接口综合。顾名思义,Interface Synthesis 指的是 HLS 设计中对接口的综合,综合出来的接口能够与系统中的其他模块通信,还有可能需要与系统中的处理器进行通信。
这里接口的概念既包括端口(port),也包含所使用的协议。所有端口的细节(如类型、位宽和方向)是从 C/C++ 文件中顶层函数的参数和返回值里推断出来的;而协议是从端口的表现(行为)推断出来的。比如,最简单的接口可以是一条 1 比特的线(wire),而更复杂的接口,可能要用总线或 RAM 接口。接口综合能够推断出来的接口类型包括:线、寄存器、单向和双向握手信号、FIFO、存储器和总线等。
下面我们给出一个简单的 C 设计的顶层函数,函数名为 find_average_of_best_X(),其参数如下图所示:
图 1.3.2 中函数内部工作的详细情况无关紧要,不过每个参数的读/写操作将决定综合出来的端口的方向。这个函数定义包含三个参数,数组“sample”和整数“X”是函数的输入,而 average 作为函数的输出。因此,简单来说,这三个函数参数要被 HLS 转换成两个输入接口和一个输出接口,如下图所示:
需要注意的是,图 1.3.3 只是一个简化了的接口示意图。根据所用的协议,这些接口可能包括数据端口自身以外的控制输入或输出,如下图所示:
图 1.3.4 是函数 find_average_of_best_X()经 HLS 综合出来的完整的 RTL 模块的接口图。从图中可以看到由函数的三个参数所综合出来的接口分别拥有了各自的协议,如 ap_memory 协议、ap_none 协议和 ap_vld协议。同时模块还多出来了一些端口,如 ap_clk 和 ap_rst 等,它们使用的是 ap_ctrl_hs 协议。这些协议决定了相应的接口是如何与系统中其他模块进行交互的,至于各协议具体的含义以及如何为接口选择其协议,我们将在后续的章节中介绍。
四、算法综合
算法综合关注的是设计的功能,即设计所期望的行为,它是由输入的 C 设计所描述的。算法综合从代码中推出各种运算操作,然后转换成一组 RTL 语句。
算法综合包括三个主要阶段,依次是:
- 解析出数据通路和控制电路;
- 调度和绑定;
- 优化;
解析出数据通路和控制电路
HLS 的第一个阶段是分析 C/C++/SystemC 代码,并且解释所需的功能。Vivado HLS 从以下几个方面分析程序:逻辑和算法的运算、条件语句和分支、数组运算和循环等。
所产生的实现会具有一个数据通路元件,一般还会有一个控制元件。需要澄清的是,这里的“数据通路”处理指的是在数据样本上作的运算,而“控制”是需要协同数据流处理所需的电路。算法的本质定义出数据通路和控制元件,设计者可以在 HLS 中采取专门的步骤来最小化控制元件的复杂度。
调度和绑定
HLS 是由两个主要过程组成的:调度(Scheduling)和绑定(Binding)。它们是交替进行的,彼此互相影响,如下图所示:
-
调度是把由 C 代码解释得到的 RTL 语句翻译成一组运算,每个运算都关联着一定的执行时间,以时钟周期为单位。这个阶段所作的决策,受时钟频率和不确定度、目标芯片的技术和用户所施加的指令所影响。
-
绑定是调度好了的运算和目标芯片上的实际资源联系起来的过程。这些资源的功能和时序特征可能会影响调度,因此绑定信息会反馈给调度过程。比如使用 DSP48x 资源就表明关键路径比采用逻辑资源的方案要短。
优化
-
约束 :设计者可以对设计的某些指标加以限制。比如,可以指定最低的时钟周期。这样就能确保实现结果能够满足要集成进去的系统的要求。类似的,设计者可以选择约束资源的利用情况或其他的指标,从而优化应用的设计。
-
指令:设计者可以通过指令对 RTL 的实现参数施加更具体的影响。有各种类型的指令,分别映射在代码的某些特征上,比如让设计者可以指定 HLS 引擎如何处理 C 代码中识别出来的循环或数组,或是某个特定运算的延迟。这能导致 RTL 输出的巨大改变。因此,具有了指令的知识,设计者就可以根据应用的需求来做优化了。
五、 HLS 库
Vivado HLS 中包含了一系列的 C 库(包括 C 和 C++),方便对一些常用的硬件结构或功能使用 C/C++进行建模,并且能够综合成 RTL。在 Vivado HLS 中提供的 C 库有下面几种类型:
1、任意精度数据类型库
2、HLS Stream 库
3、HLS 数学库
4、HLS 视频库
5、HLS IP 库 6、HLS 线性代数库
在 HLS 设计中调用库中的函数可以大大提高开发效率。