介绍
博主之前在全国FPGA竞赛中使用过HLS的部分内容,所以抽一点时间来碎片化学习下HLS的相关知识。
简单来说吧,因为传统的开发FPGA流程相比缓慢,不如软件开发的效率高,所以说HLS运营而生,使用高级语言来进行转换为底层的硬件代码。虽然说是支持高级语言比如说C与C++但是最主要的核心来说, HLS的开发也是要求开发这对代码的背后的硬件与FPGA内部结构相对了解,所以说本文首先大致介绍了下HLS的相关内容,还有简单介绍了下FPGA简单的内部结构,(后面会更新专门的内部结构的分析的文章这里就不过多展开,主要针对的是HLS用到的部分做简单说明)本文内容来源网贴汇集+个人理解+HLS书籍(《FPGA并行编程》)+视频+自费视频
在传统的FPGA设计流程中,一般是自顶向下的模块化设计,这些模块包括用户自己编写的RTL或者是供应商提供的IP核。而在Xilinx新推出的高生产力设计流程中是以IP为核心的,把所有的模块都看做是IP,封装为IP,最主要的是IP的设计是基于C语言的,最后通过HLS将C语言代码转化为RTL,这能极大的加快设计进程。从这段时间的学习来看,HLS综合出来的电路比我自己写的RTL更省资源,在时序方面可能会差一些,,但它也满足了时序的要求。HLS还提供了非常多的经过深度优化的库,所以我个人觉得HLS综合的电路已经比很多刚接触FPGA不久的工程师要好了,传统的RTL设计可能不会消失,但很可能会越来越少了。
HLS在不添加任何优化指令的情况下默认综合电路为最节省资源的情况,即能使用分时复用就使用分时复用,能在一个周期中完成多个操作(满足时钟频率约束的要求)则绝不流水。从这里得到的启发是,设计不要过度的流水,只要能满足时钟频率的需求,我们在一个时钟周期内可以进行多个操作,于是在低频率的系统中,我们可以节省很多的寄存器资源。这样做还有一个好处是我们可以使用assign来完成逻辑设计,然后再对输出进行寄存,可以极大的减少代码量,增加代码的可读性,HLS综合后的RTL代码的风格就是这样的,当然因为命名的原因该代码几乎没有可读性,我们可以不用刻意去读懂它。
在学习Vivado HLS工具之前,我们需要了解HLS工具在FPGA开发流程中的作用。Vivado工具的设计理念是以IP为核心的,所有的功能模块都可以看做并且封装成一个IP,最后由Vivado集成,以实现最大化的设计复用。所以Vivado HLS可以看做是一个IP封装工具,它封装的是由C、C++、SystemC或者OpenCL等高级语言实现的功能函数。
HLS 完成的工作
- HLS自动分析一个算法中的潜在并发性
- HLS自动在需要的路径插入寄存器,并自动选择最理想的时钟
- HLS自动产生控制数据在一个路径上的出入方向的逻辑
- HLS自动完成设计部分与系统中的其他接口
- HLS自动映射数据到存储单元中以平衡资源使用与带宽
- HLS自动将程序中计算的部分映射到对应逻辑单元,在实现等效运算的前提下选择有效运算
大多数HLS工具需要用户提供功能的规范,交互的描述,一个对接的计算设备,和目标优化方向。而对于Vivado HLS来说,用户需要:
- 一个用C/C++/System C编写的函数
- 一个测试平台用于验证结果(C testbench)
- 一个FPGA开发版
- 期望的时钟周期
- 一个简单的实施指导
输入程序作出以下规范
根据Vivado HLS的使用指南,我们将对我们的输入程序作出以下规范:
- 不使用动态内存分配(不使用malloc(),free(),new和delete())
- 减少使用指针对指针的操作
- 不使用系统调用(例如abort(),exit(),printf()),我们可以在其他代码例如测试平台上使用这些指令,但是综合的时候这些指令会被无视(或直接删掉)
- 减少使用其他标准库里的内容(支持math.h里常用的内容,但还是有一些不兼容)
- 减少使用C++中的函数指针和虚拟函数
- 不使用递归方程
- 精准的表达我们的交互接口
当RTL级的设计可用时,大多数HLS工具会进行标准RTL设计流。而在赛灵思Xilinx Vivado设计套装里进行的是逻辑综合,将RTL级设计转换成一个FPGA逻辑部件的连线表,这份连线表不仅包含需要的逻辑部件还包含他们的连接方式。Vivado之后将连线表和目标设备中的可用资源相关联,这个过程被称作布局及布(PAR)。产出的FPGA配置被附在比特流(bitstream)上,用户可以将比特流上传到FPGA以实现想要的功能。比特流实质上是用二进制表示FPGA上每一个可用资源的配置,包括逻辑部件的使用,连线的方式,和片上的内存。大型FPGA例如赛灵思UltraScale FPGA拥有超过十亿个可配置比特,较小的FPGA上也至少有几亿个可配置比特。
FPGA构造
FPGA是由可编程逻辑矩阵和相连接的内存组成,通常是基于LUT(查找表,就和超市的购物小票相似,每个值都可以被索引到)。下图大致给出了FPGA的内部构造,在可编程的逻辑块中,现在的FPGA也已经高度集成了很多的资源。
逻辑单元
这部分主要是是实现逻辑运算的部分,基于查找表也就是LUT,实际的FPGA中大部分使用的是4–6位输入的查找表作为运算基础。在逻辑单元里,触发器FF是最近的内存单元,我们可以利用触发器的性质,储存一些少量的数据。
存储单元
前面提到了使用(FF)触发器可以存储少量的信息,图中也给出了(LUT in slicem)FPGA另外一部分的存储单元是Bram,他是支持多种内存接口,可以配置的随机存储器。总体来说有两个功能:
- 芯片各部分的数据转移
- 存储大的数据集
同时FPGA也可以通过驱动外部的存储器来实现上述的功能,这部分为外部存储,简称(外存)相比较来说,触发器有最好的带宽但是存储容量太小;而外存的存储密度高,但是带宽有限,而Bram相当于两者的中间值。
根据设计的需要,我们可以配置存储单元为各种形式,种类列举如图:
算术逻辑单元
因为对于算法的实现来说,说白了也就是进行计算的实现,算法优化的过程就像我们小学初中学习的化简,简便计算等等,所以我们也要对运算逻辑单元有个简单的了解。
这里我就不过多展开,大致了解到DSP48中包括的部分即可。
- 加法
- 乘法
- ALU运算单元
- 选择器
设计优化
对于HLS来说,优化基本算是他要解决的首要问题,那么我们应该从哪些角度来思考优化呢?在开始讨论怎么去优化之前,我们先要讨论一下判断一个设计特点的标准。计算时间就是一个衡量设计好坏的重要标准。很多人把时钟周期数作为一个同步电路性能的指标,但实际上对于两个使用不同时钟的电路这是不得当的,而时针不同又是HLS下的绝大多数情况。比如说,我们现在已经规定好了VivadoHLS的输入时钟限制,那么工具根据时钟的不同会从同一段代码中产生不同的结构,所以这不是一个很恰当的比较方式。秒数是一个更好的对应比较指标。Vivado HLS工具会提供一个周期数和周期频率的报告,用户可以用此得出某段代码的操作时间。
改变时钟频率有时候可以优化设计。
Vivado HLS工具把时钟频率作为一个输入,所以改变一个输入可以导致产出的结构完全不同。
我们用任务(task)这个术语来表示一个行为的基本单位,用户可以在Vivado HLS中发现与之对应的是调用函数。任务延迟就是任务开始到任务完成中间的这段时间。任务间隔则是任务开始到下一个任务开始之间的这段时间。所有的任务输入,输出和计算的时间都被算在任务延迟里,但是任务的开始并不同于读取输入,同样任务的结束也不等同于写出输出。在很多设计中,数据率是一个很重要的东西,它同时取决于任务间隔和函数参数的多少。
两种不同设计的任务间隔和任务延迟,上方的弧线指示的是任务间隔,下方的弧线指示的是任务延
迟。左右两个设计的区别在于,左边是流水线(pipeline),右边使用了更顺序化的设计。
图表示的是两种设计的实施设想,横向轴是时间轴(从左到右增大),纵向是设计中不同的函数单
位。红色表示的是输入有关的操作,橙色表示的是输出有关的操作,正在活跃的运算符用深蓝表示,不活跃的则用浅蓝表示。每一个进入的箭头表示的是一个任务的开始,而出去的箭头表示任务的完成。左侧的图表示的是一个每个周期都执行新任务的结构设计。
与之对应的是完全流水(fully-pipelined)结构。右侧表示的则是一个完全不一样的结构,系统每次读取四段输入,处理数据,然后再合成一个4段数据的输出。这种结构的任务延迟和任务间隔是一样的(13个周期),并且每一周期内只有一个任务在执行。这个结构和左边的流水形成了鲜明对比,左边的结构在同一周期内显然有多个任务在执行。
HLS中的流水和处理器中的流水概念相似,但是不再使用处理器中操作分5个阶段并把结果写入寄存器堆的方法,Vivado HLS工具构造的是一个只适用于特定板子,可以完成特定程序的电路,所以它能更好的调整流水的阶段数量,初始间隔(连续两组数据提供给流水之间的间隔),函数单位的数量和种类,还有所有部件之间的互联。
Vivado HLS工具通过计算一个任务输出到输入之间这个过程需要的寄存器数来决定周期。因此,0周期的任务延迟是可以实现的,也就是组合逻辑下路径上没有任何寄存器。另一个常用的工作是计算输入输出并把结果存到寄存器里,通过这些数据找到路径上的寄存器数。这样的计算有花费很多的周期
优化的取舍
理解编译器如何工作其实有助于设计师写出更高效的代码,这点对于HLS尤其重要,因为综合电路的构建方式有很多种,只理解它软件流是不够的。比如HLS设计师需要考虑流水,内存排布,I/O接口这些软件设计师不需要考虑的内容。
回到编译器,理解它的关键问题在于:这段代码中产生的是什么电路?这个问题的答案分多钟,还和你所用的HLS工具有关。
用户可以通过在代码中添加#pagma HLS pipeline来指导Vivado HLS工具产生函数流水结构。
复杂的设计在顺序化和并行化的结构之间会有很多取舍的考虑。这些取舍在Vivado HLS中很大程度上取决于设计者的决定和代码内容。
处理速率的限制
我们看到了很多改变架构会改变任务间隔的例子,这样做通常来讲可以提升处理速率。但是读者需要意识到任何结构的任务间隔都是有一定的限度的。最关键的限制来自于递归和反馈循环,还有一些其他的例如资源限制也很重要。递归(recurrence),这里是指某个部件的计算需要这个部件之前一轮计算的结果, 递归是限制产力的重要因素,即使在流水结构中也是如此[56,43]。分析算法中的递归并产出正确的硬件设计是非常关键的一步,同样,选择一个尽量避免很多递归的算法也是设计中非常关键的一步。递归在很多代码结构中都会出现,它存在于很多顺序化结构中,也有很多会随着改编成流水结构而消失。对于顺序化结构递归有时候不影响处理速率,但是在流水结构中是一个很不理想的状况。
另一个影响速率的关键因素就是资源限制,其中一种形式是设计边缘的跳线,因为一个同步电路中的每根跳线在每周期只能传送抓取1个比特的数据。因此,如果 int32_t f(int32_t x)这样形式的函数作为一个单独模块在100MHz的频率和1的任务间隔下运行,它最大的数据处理量就是3.2G比特。另一种资源限制来自于内存,因为大多数内存每周期只支持一定次数的访问。还有一种资源限制来自于用户所给的限制,如果用户规定了在综合中可用的操作数,这其实是给处理率添加了限制条件。
代码要求
- 强调重建代码对于高质量设计的重要性,比如在高性能和低使用面积上。
- 对常见的内容提供重建的代码
- 讨论重建对于硬件的影响
- 使用必要的HLS指令以实现最好的设计