最近在写一本Xilinx的FPGA方面的书,现将HLS部分内容在这里分享给大家,希望大家喜欢,也欢迎批评指正。[原创www.cnblogs.com/helesheng]
在可编程逻辑器件被用于电子系统设计的前期,由于所含的逻辑资源较少,绝大部分情况下,它们被用于实现数据的传输和接口电路。工程师们习惯于使用寄存器传输级(RTL)的描述方式来开发可编程逻辑器件,以提高对逻辑资源的利用率。但正如我们在前面的章节中看到的,使用Verilog HDL这样的硬件描述语言进行RTL级的开发是一件非常费时、费力的事。
另一方面,随着摩尔定理的不断发展,集成在可编程逻辑器件中的逻辑资源呈指数级的快速增长,对于开发者而言节约器件中的逻辑资源的重要性不断降低。与此同时,随着深度学习算法和软件无线电(SDR)技术的成熟,可编程器件中需要由硬件实现的算法越来越复杂。再使用硬件描述语言低效率的实现这些复杂的算法,越来越无法满足时长对产品上市的时间要求。
提升可编程逻辑器件开发速度的方法无非有二:1、使开发工具能够自动处理尽可能多的细节,降低项目开发时间(提升抽象的层级)。2、同一段代码经过简单地修改就能够适用不同器件和优化要求,从而被尽可能多的项目使用(设计的重用)。而Vivado High Level Synthesis(高层次综合器,简称HLS)正式Xilinx在上述思路指导下设计的一种开发工具,它通过直接使用C、C++或System C等高级语言进行硬件开发,能够大幅度提升算法开发的抽象层次和设计重用率的高效开发工具。
8.1 高层次综合器(HLS)的概念与特点
1. 为什么需要高层次综合器
FPGA开发工具发展的一个总趋势是提高描述数字系统的抽象层级,从而隐藏更多的底层技术细节,以达到提高复杂算法开发的效率和不同器件之间设计重用的目的。在本书前述的硬件描述语言Verilog HDL能够实现的抽象,由低到高包括:结构级的抽象、寄存器传输级抽象和行为级的抽象。其中结构级的抽象直接描述查找表和寄存器的连接和配置;寄存器传输级的抽象只描述寄存器与寄存器之间的可以被解释和执行的操作,隐藏了具体的连接和配置方法,而将这部分细节留给硬件描述语言自动补齐;Verilog HDL能够实现的最高级别的抽象称为“行为级”,在这个级别上,硬件描述语言的综合器可以直接针对设计者所需要的电路行为和算法产生所需的电路,从而隐藏了大部分的电路细节。
而所谓“高层次综合器”完全摆脱了电路水平的硬件描述,开发者只需要通过C、C++和System C等传统意义上的计算机高级语言来描述算法本身,而不必过分关注硬件电路及其在FPGA内部的实现方式。从FPGA开发工具角度来理解,高层次综合器非上平行于硬件描述语言(HDL)的上述抽象层级(结构级、寄存器传输级和行为级)的独立描述层级。因为,高层次综合器的作用是将高级语言描述的算法和行为综合成寄存器传输级描述的硬件描述语言,再由Vivado以调用IP的形式,调用这些寄存器传输级的硬件描述语言,并将其进一步综合成FPGA可以直接使用的网络表。因此,高层次综合器的“综合”与硬件描述语言的“综合”的内涵并不相同,高层次综合器(HLS)也不能独立于Vivado单独使用。下图所示的是高层次综合器(HLS)的综合和硬件描述语言(HDL)的综合之间的关系,理解这幅图,有利于读者理解高层次综合器开发和优化流程。
图8.1 .1 高层次综合器(HLS)的综合和硬件描述语言(HDL)的综合之间的关系
总体而言,高层次综合器由于使用了更高层级的描述抽象,将更多的电路查找表和寄存器的连接、配置,以及信号传输的细节甩给了开发工具来自动完成,符合FPGA开发工具发展的趋势。虽然在这个过程中可能由于开发工具无法深刻理解具体应用的需求,从而导致FPGA资源的部分浪费,但带来的优势也非常明显:
其一,开发者无需关注细节的实现,导致开发效率成倍增加;开发者能够将精力放在对系统而言更加重要的算法开发上。
其二,用高级语言开发的算法代码,最大程度的做到了与具体器件“脱钩”,是的同一段代码经过简单的移植后就能够适用于使用不同器件的工程,提升了代码的重用率。
其三,在不同的项目中使用同一种算法时,可能会由于不同项目对算法执行时间和逻辑资源的要求不同,造成代码重复开发的问题。而高层次综合器HLS能够通过简单的配置,实现对同一段代码优化策略的重配置,进一步提升了代码的重用率。
2. 高层次综合器产生的电路模块
如图8.1.1所示,高层次综合器是一种能够将传统的高级语言描述的算法,转换为在可编程逻辑器件上实现的硬件描述语言的高效率开发工具。但一旦仔细推敲,细心的读者就会发现,传统的C/C++这类高级语言是建立在执行程序的CPU“逐条”取指执行的图灵机框架下的;而实现在可编程器件上的硬件电路却是相对固定,且需要实实在在接口硬件来完成算法数据的输入和结果输出的。因此高层次综合器的功能必然包括产生以下两部分电路:算法功能性电路,以及接口(也称为“顶层连接”)电路两个部分。图8.1.2所示的是一个典型的高层次综合器完成的硬件设计,其中包含了算法综合(Algorithm Synthesis)产生的功能性电路,以及接口综合(Interface Synthesis)产生的输入输出电路。而这两部分也是设计者使用高层次综合器开发时,需要根据要求着重设计和斟酌的。
图8.1.2 高层次综合器产生的典型电路
3. 使用高层次综合器的开发流程
使用高层次综合器进行开发的流程模型如下图所示。
图8.1.3 高层次综合器开发流程示意图
1)开发者需要给高层次综合器的,首先是一个能够实现所需算法功能的C/C++函数;其次是一个用于测试和验证该函数的标准输入和输出数据和调用该函数的代码。也就是上图中的“黄金参考”和“C测试集”(在很多设计中黄金参考被以数据的形式写入到C测试集的C/C++代码中)。
2)有了黄金测试和C测试集后,高层次综合器就可以进行功能性验证了。而通过功能性测试的算法函数代码就可以进入到高层次综合器的“本质工作”高层次综合阶段了。此时开发者可以通过图形化窗口或指令的方式配置图8.1.3所示的“算法综合”和“接口综合”的硬件细节。高层次综合完成后,将产生以硬件描述语言的RTL模型,包括HDL文件和所需的各种日志、输出文件,测试集、脚本等设计文件。
3)高层次综合器产生的RTL模型不一定与算法的预期设计完全相同,这就需要对RTL模型进行协同仿真。此时需要再次用C测试集,对RTL模型进行仿真,并比较仿真输出和黄金参考的差异。另外,在上图中的C/RTL协同仿真的同时,还可以对硬件实现所需的资源的数量、执行的延迟、最高工作时钟频率等进行“实现的评估”。若实现无法达到设计要求,这可以通过约束和指令来优化RTL模型。而这个优化过程,必然是一个设计反复迭代的过程。
4)高层次综合器最终产生的RTL输出可以是RTL文件(VHDL 或 Verilog 代码),也可以是打包好的IP包,以便被其他设计工具方便的调用。
4. 高层次综合器的核心工作
在上述过程中,高层次综合器完成的最重要,也是最复杂的是第2)步——由高级语言综合产生RTL描述模型的部分。这个步骤又可以细分为:提取数据通路和控制通路、调度和绑定、优化三个阶段。其中,“提取数据通路和控制通路”是指分析高级语言代码,解释所需的功能。将其分解为“数据通路”部分电路和“控制”部分电路,用以对数据流进行处理、运算,以及控制、协调数据流的计算过程。“调度和绑定”则将电路要完成的运算和控制工作分配到不同的时钟节拍和逻辑资源来完成。“优化”则是指通过图8.1.3中的约束和指令来限制高层次综合器的调度和绑定,从而实现反复迭代、改进综合结果的过程。
1)设计延迟和设计吞吐量
设计延迟和吞吐量是衡量高层次综合器产生RTL模型最重要的两个指标参数,在介绍高层次综合器的工作之前先将要介绍这两个概念。
设计延迟(Latency)是指不考虑器件资源性能限制条件下,实现算法所需要的时钟节拍数。
1 void foo(a,b,c,d,*x,*y){ 2 …… 3 func_A(a, b, t1); 4 func_B(a, b, t2); 5 func_C(c, t1, &x); 6 func_D(d, t2, &y); 7 }
代码8.1
使用高层次综合器实现代码8.1所示的算法,如不进行任何优化,将得到如图8.1.4所示的电路工作时序图,其设计延迟就是10个时钟周期。
图8.1.4 设计延迟举例
设计吞吐量(Throughput)是指不考虑器件资源性能限制条件下,两笔新输入(或输出)之间的周期数,图8.1.4中吞吐量也可能是10个时钟周期。
阅读至此读者一定充满了疑惑,既然设计延迟和设计吞吐量含义相同,为什么要用两个不同名字?其实我们图8.1.4中在讨论“吞吐量”的含义时,并未考虑电路“并发”的情况——硬件电路和高级语言实现算法不一样,可编程器件上不同部分的硬件电路可以同时执行多步运算,不像CPU执行高级语言语句只能一次逐条执行。这样当可编程器件中的一部分电路执行图8.1.4的func_B时,原来执行func_A的电路就可以再次取一笔新的数据执行,从而提高整个电路的吞吐量。如图8.1.5所示,电路的吞吐量就提高为4个周期。
图8.1.5 设计吞吐量举例
2)调度和绑定
下面以一段具体的C语言代码来说明高层次综合器可以如何通过“调度”将不同的数据操作映射到不同的时钟节拍中的。代码如下所示,其中变量t1、t2、t3和out之间存在依赖关系,只有依次计算得到上一个变量的值,才能得到下一个变量的值。
void foo{ …… t1 = a * b; t2 = c + t1; t3 = d * t2; out = t3 – e; }
代码8.2
如果设计要求不太关心上述算法所耗费的时间(设计延迟),则可以采用如图8.1.6所示的调度策略。即第一个时钟节拍实现a×b,第二个时钟节拍实现实现c+t1,第三个时钟节拍实现实现d×t2,第四个时钟节拍实现实现t3–e。总共花费4个时钟的时间,每个节拍对应的计算结果都采用触发器锁存输出,该调度方法对于所使用器件逻辑资源延迟的要求也最低。
图8.1.6 调度策略一示意图
若设计需要算法消耗较少的时间以实现更高的吞吐量,则可以通过去掉某些锁存触发器的方法,将更多的运算合并到单个时钟节拍中来完成。当然,这样做的代价是所消耗的器件资源能够在更短的时间内完成更多的组合逻辑运算。图8.1.7所示的是将a×b、c+t1、d×t2运算合并到一个时钟节拍中完成,从而将总耗时降低到2个时钟节拍的调度策略。
图8.1.7 调度策略二示意图
“绑定”是指把调度中涉及的运算“指派”给具体的逻辑资源来实现,即实现资源的映射,绑定策略可以大致分为共享和非共享两种。顾名思义,共享策略指多个运算共享同一个可编程硬件资源。当然共享的前提是不同运算不能再同一个时钟节拍中使用该资源。如图8.1.7所示的调度策略中,两个乘法运算都被分配到第一个时钟节拍中完成,这两个时钟节拍所使用的乘法器资源就是不可以共享的。
调度和绑定是一个相互关联的过程,它们共同建立在技术库的基础上,又受到用户指令的限制,实现合理的调度和绑定注定是一个反复试错和优化的过程。
图8.1.8 调度和绑定过程示意图
3)改善设计延迟和吞吐量
高层次综合器的核心工作,无非就是通过调度和绑定在满足设计占用资源/面积、时钟频率和功耗总体要求的前提下,获得最佳的延迟和吞吐量。而改善设计延迟和吞吐量的思路无非如下。
其一,利用数据之间的关联性,改善设计延迟。
再考虑代码8.1所示的算法,如果是在传统的CPU上执行这些代码,则只能如图8.1.4所示的时序依次执行func_A、func_B、func_C、func_D,设计延迟为10个时钟周期。但考虑各个函数模块之间的依赖关系是:只有完成func_A才能得到t1,才能执行func_C;只有完成func_B才能得到t2,以执行func_D。而func_A/func_C与func_B/func_D之间并没有制约关系,如图8.1.9所示。
图8.1.9 数据依赖关系的调整
则开发者可以通过相关约束和命令优化调度和绑定方式,将设计的数据通路调整为图8.1.10所示的两条,整体设计延迟降低为6个。
图8.1.10 设计延迟优化示意图
其二,在数据依赖关系允许的条件下,调整被绑定硬件资源的工作时序,最大程度的提高各个模块的并行度。使完成不同功能的硬件资源像“流水线”上的工人一样有序的持续工作,轮流对被处理的数据流中的具体数据分别进行“加工处理”,从而改善数据吞吐量。同样以上面的C语言伪代码为例,不考虑图8.1.10所示的延迟优化(四个函数顺序执行)。其中func_A、func_B、func_C和func_D相当于流水线中完成特定功能的“工人”,而不断进入的数据则相当于需要加工的“工件”。假设func_A、func_B、func_C和func_D所需的延迟分别为2、4、2、2个时钟周期,则可通过调度将重复执行的数据通路并行化,如图8.1.11所示。每个数据通路并行化后的延迟,为四个函数中执行时间最长的4个时钟周期。这样当大量数据依次通过数据通路后,电路的整体延迟虽然仍为10个时钟周期,但吞吐量缩减为3个时钟周期。
图8.1.11 设计吞吐量优化示意图