FPGA硬件加速学习 vivado hls --------------- 002

参考如下链接:
https://github.com/xupsh/pp4fpgas-cn

简介

需要充分理解 内存层级和带宽、空间局部性与时间局部性、并行结构和计算与存储之间的取舍和平衡。
可参考UCSD的 CSE 237C
总体来说,HLS可以自动完成以下曾经需要手动完成的工作:

  • HLS自动分析并利用一个算法中潜在的并发性
  • HLS自动在需要的路径上插入寄存器,并自动选择最理想的时钟
  • HLS自动产生控制数据在一个路径上出入方向的逻辑
  • HLS自动完成设计的部分与系统中其他部分的接口
  • HLS自动映射数据到储存单位以平衡资源使用与带宽
  • HLS自动将程序中计算的部分对应到逻辑单位,在实现等效计算的前提下自动选取最有效的实施方式

根据Vivado HLS的使用指南,我们将对我们的输入程序作出以下规范:

  • 不使用动态内存分配(不使用malloc(),free(),new和delete())
  • 减少使用指针对指针的操作 不使用系统调用(例如abort(),exit(),printf()),我们可以在其他代码例如测试平台上使用这些指令,但 是综合的时候这些指令会被无视(或直接删掉)
  • 减少使用其他标准库里的内容(支持math.h里常用的内容,但还是有一些不兼容)
  • 减少使用C++中的函数指针和虚拟函数
  • 不使用递归方程
  • 精准的表达我们的交互接口

FPGA的构造

FPGA由一个可编程逻辑模块的矩阵和与之相连的内存组成,通常这些模块是以查找表(LUT)的形式 存在,也就是说把地址信号输入进去,对应内存位置的内容会直接被输出出来。一个N位查找表可以以一个 N位输入真值表的方式来表示。
在这里插入图片描述

触发器(FF)是FPGA最基本的内存单位,通常触发器是配有查找表的,这样是为方便查找表之间的复 制与组合。在这基础上再加入一个规定它们的函数(例如全加器),就可以创建一个更为复杂的逻辑单 位,称为可配置逻辑块(CLB)或逻辑矩阵块(LAB)。有些设计工具中还会把它称作片(Slice)。为避 免歧义,我们将在下文中用slice作描述,这样读者可以对在Vivado设计工具中出现的Slice更加熟悉。一个 slice是几个查找表,触发器,和多路复用器(MUX)组合到一起而形成的更强大的可编程逻辑单位。每个 slice需要的小部件数视FPGA的架构而变,但总体来说每个slice真的只包含不多的几个部件。图1.1中的c部 分就是由1个三位输入查找表和1个触发器组成的slice。slice可以变得更加复杂一点,比如常见的全加器。 FPGA内部通常有一些定义好的全加器slice,这看起来有点违背FPGA的"可编写性"。但实际上使用全加器在 硬件设计中太过于常见,把所有的全加器每次重新编写成一个slice会降低效率。灵活性和高效综合考虑, 一些被配置好的slice是一个对整个系统有益的设计。

在这里插入图片描述
图1.2:由查找表和触发器组成的slice,通常slice比图上结构更加复杂一点,slice之间通过连线通道(routing channel)和开关盒(switchbox)相连,这两个用于连接的设备提供了一个同样可编写的互联和自定义的数 据传输方向。开关盒是一个包含很多开关(传输晶体管制成)的部分,提供了编写传输路径的能力

可编写的互联是FPGA最关键的特性之一,它能提供一个slice之间更灵活的连线网络。slice的输入与输 出全都与连线通道相连,连线通道也是通过配置比特来决定每个slice的输入输出通向哪里,而通道本身则 与开关盒相连。开关盒由很多传输晶体管充当的开关所组成,它的工作便是连接通道与通道。
图1.2展示了一个slice,连线通道和开关盒之间的连接方式。slice的每个输入输出都应与通道中的一条 路线相连。所谓路线,我们可以简单的把它想成一跟比特层级的跳线,在物理层级上这条线路是由传输晶 体管构成的,同样具有可编写性。

开关盒像是一个连接矩阵,沟通不同连接通道中的各个路线。FPGA一般有一个2D的形式,能给使用者 一个大概的2D计算模型,我们称之为岛状结构。在岛状结构里,每个slice都是一个逻辑岛,岛与岛之间通 过连线通道和开关盒相连。在这里每个开关盒在上下左右四个方向连接了四个连线通道。

在这里插入图片描述
图1.3:2D的FPGA岛状结构。每个slice内的逻辑与内存通过连接通道和开关盒相连。IO模块有一个对外接 口,可以通往内存,处理器,传感器等等。一些FPGA上IO直接与芯片引脚相连,一些FPGA则用起连接逻 辑架构和片上的一些资源。

在这里插入图片描述
图1.4:现代FPGA变得更加异构化,一些FPGA除了可编程部分之外,加入了很多预配好的结构比如寄存器 堆,自定义数据路径,高速互联等等。现如今FPGA通常配有一个或多个处理器,比如ARM或x86核,两者 协同控制系统。

我们说到FPGA上要承载的晶体管变得越来越多,这也是FPGA上多了很多预配好的资源的原因。这部 分硬件用于完成特定工作。很多设计都需要大量的加法和乘法,因此FPGA厂商把这部分的内容预配好以直 接使用。像DSP48数据路径已经被用一种高效的方法预配好,添加了乘法、加法、乘积、逻辑操作等一系 列算数。对于DSP48这样的模块来说,它们依旧保留了一定的可编写性,但不像其他可编程逻辑那样完全 灵活。这样综合而言,用户在DSP48这样的模块上进行乘法这样的操作会比重新编写高效的多。所以我们 说灵活性和效率有时候是此消彼长的。

块RAM(BRAM)是另一个预配好的模块。BRAM是一个支持多种内存形式和接口的可配置随机储存 器,可以储存字节,对字,全字,双字等等等。BRAM还可以把这些数据传给本地片上总线(与可编程逻 辑交流)或处理器总线(与片上处理器交流)等等接口。总体来说它有两个功能,一是芯片上各部分的数 据转移,二是储存大一些的数据集。slice经过编写也可以储存数据(通过触发器),但这样做会增加额外 消耗。

FPGA设计与处理

在这里插入图片描述
设想中的嵌入式FPGA设计结构图,包括接口核心(蓝色框),标准核(绿色框),和应用加速核 (紫色框)。注意应用加速核也可能自带流接口,内存对应接口。

图1.6中的系统通过两种方法可以实现。第一种方法是把HLS产生的加速器核当作一个普通的核。用 HLS创造出这种核之后把他们与IO接口核和标准核组合到一起(可以通过Vivado IP Inegrator这样的软 件),这样我们就得到了完整的设计。这个方法叫做以核为基础的设计方法,与使用HLS之前的FPGA设计 方法十分相似。第二种方法则着重于设计样板或平台,称为平台为基础的设计方法,这种方法下设计师先 用IO接口核和标准核组合出一个样板,然后再用HLS通过壳(shell)的接口将各式算法或对象组合进去。 只要壳支持双边的接口,加速器核在平台与平台之间的移动也非常容易。

设计优化

在这里插入图片描述
图1.7表示的是两种设计的实施设想,横向轴是时间轴(从左到右增大),纵向是设计中不同的函数单 位。红色表示的是输入有关的操作,橙色表示的是输出有关的操作,正在活跃的运算符用深蓝表示,不活 跃的则用浅蓝表示。每一个进入的箭头表示的是一个任务的开始,而出去的箭头表示任务的完成。左侧的 图表示的是一个每个周期都执行新任务的结构设计。与之对应的是完全流水(fully-pipelined)结构。右侧 表示的则是一个完全不一样的结构,系统每次读取四段输入,处理数据,然后再合成一个4段数据的输出。 这种结构的任务延迟和任务间隔是一样的(13个周期),并且每一周期内只有一个任务在执行。这个结构 和左边的流水形成了鲜明对比,左边的结构在同一周期内显然有多个任务在执行。HLS中的流水和处理器 中的流水概念相似,但是不再使用处理器中操作分5个阶段并把结果写入寄存器堆的方法,Vivado HLS工具 构造的是一个只适用于特定板子,可以完成特定程序的电路,所以它能更好的调整流水的阶段数量,初始 间隔(连续两组数据提供给流水之间的间隔),函数单位的数量和种类,还有所有部件之间的互联。

面积和产力的取舍

为了更深入的讨论使用HLS工具过程中的问题,我们需要分析一个简单但很常见的硬件函数----有限脉 冲响应(FIR)滤波器。FIR会对输入做固定系数下的卷积,它可以被用作充当各式滤波器(高通,低通, 带通),最简单的FIR可能就是一个移动平均滤波器。

#include "stdio.h"

#define NUM_TAPS 4
void fir(int input, int *output, int taps[NUM_TAPS]);

const int SIZE = 256;

int main() {
    int taps[] = {1, 2, 0, -3, 0, 4, -5, 0, 1, -2, 0, -3, 0, 4, -5, 0};
    int out = 0;
    for (int i = 0; i < SIZE; i++) {
        fir(i, &out, taps);
    }
    printf("result = %d\n", out);
    if (out == -1452) {
        return 0;
    } else {
        return 1;
    }
}

回到编译器,理解它的关键问题在于:这段代码中产生的是什么电路?这个问题的答案分多钟,还和 你所用的HLS工具有关。那么通常工具有以下几种合成方式:

第一种可能的产出电路是按照顺序执行每行代码产出的电路,这时候工具就像一个简单的RISC处理 器。下面的图1.9中的代码是图1.8中的代码在赛灵思Microblaze处理器下的汇编代码版本。虽然已经经过了 优化,但还是有很多指令用来执行计算数组索引(array index)和控制循环。这样的指令我们假设它每个循 环都要执行一次,那么我们在49个循环之后才能得到滤波器得出的结果。我们可以很明了地得到一个结 果,那就是一个周期内执行的指令数是影响性能的一个重要的壁垒。有时候对于一个架构的提升就是让它 处理的指令变得更复杂,让同一个指令能做的事情变得更多。HLS的一个特点就是在决定结构上的一些此 消彼长的设计时,不再需要考虑让它适用于指令集的结构限制。在HLS设计中,设计出一个在同周期内执 行成百上千个RISC级指令外加几百个周期程度流水的系统是非常常见的。

在这里插入图片描述
用户可以通过在代码中添加 #pagma HLS pipeline来指导Vivado HLS工具产生函数流水结构。这段指令 需要一个参数来规划流水的起始间隔,也就是一个函数流水的任务间隔。图1.10展示了一个可行的设计—每周期一抽头的架构。任务用到了一个乘法器和一个加法器完成滤波器。这种设计的任务间隔和任务延迟 都是4个周期。图1.11展示的是一个每周期一样本的结构,它使用了4个乘法器和3个加法器。这种设计的任 务延迟和任务间隔都是1个周期,所以它每个周期都接受一个新的输入。当然这两种之外还有很多可行的设 计,比如每周期两抽头设计,或每周期两样本设计,在一些特定应用中各自有各自的优势,我们将在第二 章中讨论更多优化。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

处理速率的限制

我们看到了很多改变架构会改变任务间隔的例子,这样做通常来讲可以提升处理速率。但是读者需要 意识到任何结构的任务间隔都是有一定的限度的。最关键的限制来自于递归和反馈循环,还有一些其他的 例如资源限制也很重要。

递归(recurrence),这里是指某个部件的计算需要这个部件之前一轮计算的结果,递归是限制产力的 重要因素,即使在流水结构中也是如此。分析算法中的递归并产出正确的硬件设计是非常关键的一 步,同样,选择一个尽量避免很多递归的算法也是设计中非常关键的一步。

另一个影响速率的关键因素就是资源限制,其中一种形式是设计边缘的跳线,因为一个同步电路中的 每根跳线在每周期只能传送抓取1个比特的数据。因此,如果 int32_t f(int32_t x)这样形式的函数作为一个单 独模块在100MHz的频率和1的任务间隔下运行,它最大的数据处理量就是3.2G比特。另一种资源限制来自 于内存,因为大多数内存每周期只支持一定次数的访问。还有一种资源限制来自于用户所给的限制,如果 用户规定了在综合中可用的操作数,这其实是给处理率添加了限制条件。

FIR滤波器

​编程实现11阶FIR滤波器代码如下图。这个函数有两个输入端口,一个端口是输入数据x和另一个端口 是输出结果y。由于每次执行该函数会提供一个函数输入数据并接收一个函数输出数据,多次调用这个函数 后,完成整个输出信号的计算。因为我们在获取更多信号时这段代码可以根据需求调用很多次,所以这段 代码可以方便地用流模式架构进行建模。

​代码对不同变量类型使用 typedef。虽然这不是必需的,但是可以方便地更改数据类型。正如我们后续 要讨论的位宽优化——特别是针对每个变量设定整数和分数位数——其在性能和资源方面优化显著。

#define N 11
#include "ap_int.h"  

typedef int coef_t;
typedef int data_t;
typedef int acc_t;

void fir(data_t *y,data_t x){
    coef_t C[N] = {
        53,0,-91,0,313,500,313,0,-91,0,53
    };
    static
    data_t shift_reg[N];
    acc_t acc;
    int i;
    acc = 0;
    Shift_Accum_Loop:
    for(i = N - 1;i >= 0;i--){
        if(i == 0){
            acc += x * C[0];
            shift_reg[0] = x;
        }else {
            shift_reg[i] = shift_reg[i - 1];
            acc += shift_reg[i] * C[i];
        }
    }
    * y = acc;
}

在这里插入图片描述
代码设计为流函数。它一次只接收一个数据,因此它必须存储以前的数据。由于这是一个11阶滤波器,我们必须存储之前的10个数据。 这是shift_reg[]矩阵的作用。因为矩阵数据在函数中被多次调用,所以该矩阵声明为静态类型。
编译时就知道变量类型的是静态类型;运行时才知道一个变量类型的叫做动态类型。

​每次 for 循环有两个基本运算。首先,它对输入数据执行乘累加操作(当前输入数据x和存储在shift_reg[]中之前输入的数据)。每次循环执行一个常量与一个输入数据的乘法,并将求和结果存储在变量acc中。同时该循环通过shift_array来实现数值移动,它的操作行为像FIFO。它存储输入数据x到shift_reg[0]中,并通过shift_reg将之前的元素“向前”移动:

Shift_Accum_Loop的标签不是必需的,但是它对调试很有帮助。Vivado HLS工具将这些标签添加到代码视图中。

在for循环完成后,acc变量是所有输入数据和FIR系数卷积运算的结果。最后的结果赋值到FIR滤波器函数输出端口y。这就完成了按照流处理流程计算得到的FIR滤波器的一个输出结果。
​这个函数没有高效地实现FIR滤波器。它是串行执行的,并且使用了大量不必要的控制逻辑。后续部分提供一些优化手段来提高它的性能。

操作链接

操作链接 是Vivado HLS为了优化最终设计而进行地一项重要优化。尽管设计人员没有太多控制权,但设计人员要明白它的工作原理尤其是在性能方面的影响,这是尤为重要的。考虑FIR滤波器在各阶运算中做的是乘法累加操作。假设加法运算需要2个ns,乘法运算需要3个ns。如果我们把时钟周期设为1ns(或者等效时钟频率1Ghz),那么它要花费5个时钟周期来完成乘累加操作。如下图所示。乘法操作需要3个周期,加法操作需要两个周期。乘累加操作总时间为5个时钟周期×1ns = 5ns。因此我们处理性能为1/5ns = 2亿次乘累加/秒。

在这里插入图片描述
随着时钟周期的延长,乘累加操作的性能也会发生变化。假设乘法运算需要3个ns,加法运算需要2个ns。图a)时钟周期为1ns时,一个乘累加操作需要5个周期,因此处理性能是2亿次乘累加/秒。图b)时钟周期为2ns,乘累加运算需要3个时钟周期,因此处理性能约1.67亿次乘累加/秒。图c)时钟周期个5ns。通过使用操作链接,乘累加操作需要一个时钟周期,因此处理性能是2亿次乘累加/秒。

如果我们将时钟周期增加到2ns,乘法运算用时将超过两个周期,那么加法操作必须等到第3周期才开始,它可以在一个周期内完成。因此乘累加操作需要3个周期,所以总共需要6ns。这时处理性能大约为1.67次乘累加操作 /秒。这个结果比之前1ns时钟周期的要低。这个可以用第2个时钟周期中没有执行任何操作的“死时间”来解释。
​然而增加时钟周期并不是在所有情况下都会导致更糟糕的结果。例如,如果我们将时钟周期设为5ns,我们可以使用操作链接将乘法和加法在同一个时钟周期内执行。如图2.2 c所示,因此乘累加操作在5ns的时钟周期下只需要1个时钟周期,因此我们可以执行2亿次乘累加 /秒。这与图2.2 a时钟周期更快(1ns)的处理性能相同。
​到目前为止,我们可以在一个时钟周期内只执行两个操作。可以在一个时钟周期内链接多个操作。例如如果时钟周期是10ns,我们可以按顺序执行5个加法操作,或者我们可以实现两个连续乘累加操作。

到目前为止,我们可以在一个时钟周期内只执行两个操作。可以在一个时钟周期内链接多个操作。例如如果时钟周期是10ns,我们可以按顺序执行5个加法操作,或者我们可以实现两个连续乘累加操作。

显而易见,时钟周期在Vivado HLS优化设计的过程中起着重要作用。时钟周期与Vivado HLS其他优化方式一起使用,使时钟周期对设计优化的影响变得更加复杂。完全理解Vivado HLS整个优化过程并不重要,这个观点是很明确的,因为每个新版本都在不断改进提升这个工具的性能。然而,重要的是要了解这个工具是如何工作的。这更有利于你更好地理解优化结果,甚至使你编写的代码更加优化。

Shift_Accum_Loop:
for(i = N-1;i > 0;i--){
    shift_reg[i] = shift_reg[i-1];
    acc += shift_reg[i] * C[i];
}

acc += x * C[0];
shift_reg[0] = x;

将for循环中条件语句删除,可以实现更有效的硬件结构。

​由于Vivado HLS对不同的时钟频率可以生成不同的硬件结构。因此整体性能的优化和最佳目标时钟周期的确认仍然需要用户的创造力。在大多数情况下,我们建议坚持在小范围时钟周期内进行优化。例如在本章项目中,我们建议将时钟周期设为10ns,并将重点放在理解不同优化手段(例如流水处理)如何用于创建不同的处理架构。100Mhz时钟频率是相对容易实现的,而且它提供了良好的初始结果。当然可以创建更快时钟频率的设计。200Mhz以及更快的频率是有可能设计的,但这时经常需要在时钟频率和其他优化策略之间进行更好的权衡。你可以通过更改时钟周期来观察处理性能上的差异。不幸的是,这里没有好的准则来选择最佳频率。

代码提升

for循环内部的if/else语句效率很低。在代码中每个控制结构,Vivado HLS会生成硬件逻辑电路来检查条件是否满足,这个检查在每个循环中都执行。此外这种条件结构限制了if或else分支中语句的执行;这些语句只有在解决了if条件语句之后才能执行。

当x==0时if语句进行检查,这个只发生在最后一次迭代中。因此if分支中的语句可以从循环中“升起”。也就是说,我们可以在循环结束后执行这些语句,然后在循环中删除if/else控制流。最后我们必须改变执行“第0次”迭代的循环边界。

循环拆分

我们在for循环中执行两个基本操作。第一个是通过shift_reg数组进行数据移位。第二个是进行乘累加运算来计算输出样本。循环分裂是分别在两个循环中实现各自操作。虽然这样做看起来不像是一个好主意,但是这样做允许我们在每个循环上分别进行优化。这可能是有利的,尤其是在对不同循环进行不同优化策略的情况下。
代码如下

TDL:
    for(i = N - 1;i > 0;i--){
        shift_reg[i] = shift_reg[i - 1];
    }
    shift_reg[0] = x;

    acc = 0;
    MAC:
    for(i = N-1;i >= 0;i--)
    {
        acc += shift_reg[i] * C[i];
    }

上面的代码显示了手动循环拆分优化结果。代码片段将图2.3的循环分割成两个循环。注意两个循环的标签名称,第一个是TDL 第二个是MAC。延时线(TDL)是数字信号处理中FIFO操作的术语;MAC是“乘累加”的缩写。

在这里插入图片描述
对应的analysis如下
在这里插入图片描述

每个循环单独拆分往往不能提高硬件实现效率。但是它可以实现每个循环独立地进行优化,这可能比优化原始的整体for循环更可能得到好结果。反之亦然;将两个(或多个)循环合并到一个循环中可能会产生最好的结果。这高度依赖于大多数优化都是正确的应用场景。一般来说对于如何优化代码没有“最优法则”。优化思路不一样导致优化过程也会有差异。因此重要的是要有很多可以使用的技巧,更好的是对优化工作原理有深入的了解。只有这样,你才能是得到最好的硬件算法实现。让我们继续学习一些额外的技巧……

循环展开

默认情况下Vivado HLS 将循环综合成顺序执行方式。该工具创建了一个数据路径,该路径实现了循环体中语句的执行。数据路径顺序执行每次循环迭代运算。这就创建了一个有效的区域架构;但是它限制了在循环迭代中可能出现的并行运算。

默认情况下Vivado HLS 将循环综合成顺序执行方式。该工具创建了一个数据路径,该路径实现了循环体中语句的执行。数据路径顺序执行每次循环迭代运算。这就创建了一个有效的区域架构;但是它限制了在循环迭代中可能出现的并行运算。

我们对第一个循环进行unroll优化。factor =2

在这里插入图片描述
可以看出,通过增加了unroll指令,周期数从44降到了34 .
主要是第一个TDL循环 次数变成了5,周期数变成了原来的一半。
下面是analysis图
在这里插入图片描述
tips:注意只有函数有输入输出的时候才能被优化。

  • #pragma HLS array_parition variable=shift_reg complete
    通过这种指令对数组进行优化,注意此时的factor=2.
    在这里插入图片描述
  • 如果把TDL循环的factor设置为5呢?

在这里插入图片描述
可以看出并没有得到什么提升。
让我们换一种对于数组的优化指令

  • #pragma HLS ARRAY_PARTITION variable=shift_reg block factor=1

在这里插入图片描述
可以看出这样和不加优化是一样的。说明本来就默认在block ram中。

BRAM有两个读端口和一个写端口。因此,我们可以在一个循环中可以执行两个读操作,但我们只能在两个周期内顺序进行写操作。

#define N 11
#include "ap_int.h"

typedef int coef_t;
typedef int data_t;
typedef int acc_t;

void fir(data_t *y,data_t x){
    coef_t C[N] = {
        53,0,-91,0,313,500,313,0,-91,0,53
    };
    static
    data_t shift_reg[N];
#pragma HLS ARRAY_PARTITION variable=shift_reg block factor=1
//#pragma HLS ARRAY_PARTITION variable=shift_reg complete dim=1
    acc_t acc;
    int i;
    //acc = 0;
    TDL:
    for(i = N - 1;i > 0;i--){
#pragma HLS UNROLL factor=2
        shift_reg[i] = shift_reg[i - 1];
    }
    shift_reg[0] = x;
    acc = 0;
    MAC:
    for(i = N - 1;i >= 3;i -= 4){
        acc += shift_reg[i] * C[i] +
        shift_reg[i - 1] * C[i - 1] +
        shift_reg[i - 2] * C[i - 2] +
        shift_reg[i - 2] * C[i - 2] +
        shift_reg[i - 3] * C[i - 3];
    }

    for(;i >= 0; i--){
        acc += shift_reg[i] * C[i];
    }
    * y = acc;
}

以展开因子为4对上面进行展开。
这样展开可以不依赖上一次的acc,即依赖关系优化没了。

在这里插入图片描述

如果是通过unroll命令呢?
在这里插入图片描述
还有一个指令时skip exit check

因为最大迭代计数是变量,所以Vivado
HLS可能无法确定其值,因此向部分展开的循环添加退出检查和控制逻辑。但是,如果您知道指定的展开因子(在此示例中为2)是最大迭代计数X的整数因子,则该
skip_exit_check选项允许您删除退出检查和关联的逻辑。这有助于最小化区域并简化控制逻辑。

把最后一个循环完全展开结果是这样的
在这里插入图片描述

循环流水

每次迭代执行一个乘累加(MAC)操作。在for循环体内部这个MAC运算四个操作:

  • 读取c[]:从c数组加载指定数据。
  • 读取 shift_reg[]:从shift_reg数组加载指定数据。
  • ∗:数组c[]和shifit_reg[]相乘。
  • +:将这些相乘的结果累积到acc变量中。
    在这里插入图片描述
    a)表示MAC for循环时序图。b)表示三个迭代运算用流水形式优化的MAC for循环。

下图使用了pipeline进行优化
间隔为1时
在这里插入图片描述
间隔为2时
在这里插入图片描述

  • 优化示意图
    在这里插入图片描述
  • 通过工具指定内存类型

#pragma HLS resource variable=shift_reg core=RAM 1P

强制vivado hls 使用单端口RAM。

#define N 11
#include "ap_int.h"

typedef int coef_t;
typedef int data_t;
typedef int acc_t;

void fir(data_t *y,data_t x){
    coef_t C[N] = {
        53,0,-91,0,313,500,313,0,-91,0,53
    };
    static
    data_t shift_reg[N];
#pragma HLS RESOURCE variable=shift_reg core=RAM_1P
//#pragma HLS ARRAY_PARTITION variable=shift_reg block factor=1
//#pragma HLS ARRAY_PARTITION variable=shift_reg complete dim=1
    acc_t acc;
    int i;
    //acc = 0;
    TDL:
    for(i = N - 1;i > 0;i--){
#pragma HLS PIPELINE
//#pragma HLS UNROLL factor=2
        shift_reg[i] = shift_reg[i - 1];
    }
    shift_reg[0] = x;
    acc = 0;
    MAC:
    for(i = N - 1;i >= 0;i--){
#pragma HLS PIPELINE
//#pragma HLS UNROLL
    	acc += shift_reg[i] * C[i];
    }

    * y = acc;
}

当使用该指令与循环流水优化相结合时,Vivado HLS工具将无法使用II=1来连接此循环。这是因为这段代码流水操作需要在同一周期中同时进行读写操作。

在这里插入图片描述
可以看到,我们期望的target是2. 但是单端口不能同时读写,最终的结果是1

RESOURCE指令允许用户强制设置Vivado HLS工具进行运算操作到硬件资源的映射方式。这种映射可以在矩阵(如上所示)同时也可以在变量中。思考代码 a=b+c;我们可以用RESOURCE指令#pragma HLS RESOURCE variable=a core=AddSub_DSP 来告诉Vivado HLS工具,加法操作应DSP48资源来实现。在Vivado HLS文档[63]中有各种硬件核资源描述。一般来说,建议让Vivado HLS自己选择实现资源。如果不能满足要求,设计人员再进行约束。

位宽优化

void acc(int a,int b,int *c)
{
	*c=a+b;
}

创建一个简单设计,实现代码a = b∗c。变量类型分别更改为char,short,int,long,long long。在每种情况下,相乘运算需要多少个周期?不同的运算类型分别需要多少资源?

  • char LUT = 15
  • int LUT = 39
  • long LUT=39
  • long long LUT=71

任意数据类型

  • 无符号:ap_uint
  • 有符号:ap_int

可以通过
在这里插入图片描述
定义所需要的长度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值