1 Vivado设计
1.1 FPGA基本知识
- Xilinx Atrix-7使用6输入LUT结构(0-63)
- CLB:可配置逻辑块
- Slice:每个CLB包含2个Slice(包含查找表LUT和8位寄存器REG)
- 布线池:围绕在CLB周围,衔接FPGA的资源调度
- I/O块:FPGA芯片四周对外连接的短线(工程中需要对此进行管脚约束)
- FPGA其他资源:Block RAM、DSP Slice(数字信号处理块)、高速串行收发器、PLL时钟发生器(锁相环)、外部存储器控制器(硬件IP)、模数转换模块(XADC)
- BRAM:
(1)FPGA内嵌的存储单元包括BRAM和分布式RAM(基于CLB的查找表LUT)
(2)BRAM可用于随机存取存储器(RAM)、只读存储器(ROM)、FIFO或移位寄存器。 - 时钟资源:
(1)FPGA内部会将时钟布线资源划分到不同的时钟区,每个时钟区对应一定数量的IO口、逻辑资源、存储器资源、DSP等。
(2)时钟走线:时钟源到各个时钟有一段延时,延时不整齐,最大的延时就会限制FPGA的最大时钟频率。 - 数字信号处理块(DSP):由三个不同的链路块组成。
- 高速串行收发器
- 外部存储器控制器
- 模数转换模块
1.2 Vivado工程创建
1.2.1 设计流程
1、HLS设计:
C/C++/System C -> High-level Synthesis(Vivado HLS)-> IP核(设计核心) --->RTL系统级 ---> 综合 ---> 实现
(Tools >Validate Design)
2、HDL设计:
HDL代码 ---> 综合 ---> 实现
*资源:逻辑网表(EDIF)、约束(XDC)、物理数据(XDEF)
1.2.2 工程文件存放
打开新建的工程文件文件夹,基本内容如下:
(1) .xpr
为打开工程文件的文件,Open Project选择此文件。
(2).srcs
文件夹存放HDL代码(sources_1)、约束文件XDC(constrs_1)
(3).sim
文件夹存放仿真代码
(4).runs
文件夹存放综合和实现后产生的文件 (impl_1和synth_1), .bit文件存放在impl_1文件夹下
IP文件夹存放一些IP核
1.2.3 操作流程
(1)新建文件,选择芯片型号。
(2)新建RTL代码(寄存器级).v
(3)头文件可以用.v也可以用.vh。
(4)加入IP核(.xci)
(5)加入约束(.xdc):包括引脚约束和时序约束。
(6)加入仿真文件(.v)
1.2.4 设计流程(Flow Navigator)
(1) 仿真(Simulation):仿真代码逻辑功能是否完善。
(2) 综合(Synthesis):可以查看Schemastic、网表文件、时序报告、资源利用率。
(3) 实现(Implementation):将网表根据引脚和时序约束在FPGA上的电路中进行计算实现。
(4) 生成比特流文件(Generate Bitstream):文件类型为.bit
,用于下载到芯片内和debug。
(5) 生成.mcs文件(烧录进Flash):在Implement Design状态下进入Settings->Bitstream->Config...->Configuration Rate->SPI Configuration->Bus Width:4,Enable:Yes->重新Generate Bitstream...
需要先擦除程序,Program Configuration Memory Device中选择Erase
1.2.5 命令行tcl(略)
详情查看:ug835
1.2.6 Verilog语法
1、语法分为可综合和不可综合:可综合的较少,用于创建工程;不可综合的有很多,主要用于测试(testbench)。
2、阻塞赋值与非阻塞赋值:
代码1:非阻塞赋值,执行顺序并行,边沿触发,生成时序逻辑
c
always @(posedge clk_i or negedge rst_n) begin
if(rst_n) cout <= 1'd1;
else if(cout == 4'd10) cout <= 1'd0; //逻辑运算一定要写完整
else cout <= cout + 1'd1;
end
代码2:阻塞赋值,执行顺序串行,电平触发,生成组合逻辑
c
always @(rst_n) begin
if(cout == 4'd5) cout = 1'd1;
else begin
a=b;
c=a;
cout = cout + 1'd1;
end
end
3、关键字:
c
parameter 参数
wire 线
reg 寄存器
assign 逻辑简单的语句
always 一直执行
begin end
case(value) endcase
1.2.7 仿真验证 Simulation
1、搭建testbench测试平台:
(1)输入激励(clk,rst_n等)
(2)例化顶层验证设计
(3)响应
(4)对比输出
2、代码:
c
`timescale 1ns / 1ps
module simulation();
reg clk_in;
parameter CYCLE = 10;
always #(CYCLE) clk_in = ~clk_in; //每10ns,时钟反转一次
reg rst_n;
wire LED_out;
//模块名 例化名
TOP TOP_Init
( //端口相互连接,前面的是TOP模块内的input和output,后面的是此模块的变量
.clk_in(clk_in),
.rst_n(rst_n),
.LED_out(LED_out)
);
initial begin //初始化
clk_in = 0;
rst_n = 0;
#100;
rst_n = 1;
end
endmodule
3、要求:
(1)激励接口reg
输入到例化的模块中
c
reg clk_in;
reg rst_n;
(2)响应接口wire
从例化中输出到仿真代码中
c
wire counter;
(3)待验证设计例化
例如:
c
counter_top uut_counter_top( //先写模块名称,再写例化的名称
.clk_in(clk_in),
.rst_n(rst_n),
.counter(counter)
);
(4)虚拟的时钟:
c
always #10 clk_in = ~clk_in;
1.2.8 约束文件
1、约束分为:时序约束、IO约束、时序例外约束(有先后顺序)。
1.2.8.1 时序约束
时序约束分为外部输入延时,内部延时,数据路径延时,输出延时。
(1)create_clock :主时钟必须最早创建,端口进来的主时钟以及GT输出的时钟都必须由用户使用create_clock
自主创建。主时钟通常有两种情况:一是时钟由外部时钟源提供,通过引脚进入FPGA,该时钟引脚绑定为主时钟;另一种是告诉收发器(GT)的时钟RXOUTCLK或TXOUTCLK。对于7系列的FPGA,需要对GT的这两个时钟手工约束:对UltraScale FPGA,只需要对GT的驶入时钟约束即可,Vivado会自动对这两个时钟约束。
如果是差分输入的时钟,可以仅在查分对的P侧用get_ports
获取端口,并使用create_clock
创建。
create_clock -name <name> -period <period> -waveform {<rise_time> <fall_time>} [get_ports <input_port>]
参数 | 含义 |
---|---|
-name | 时钟名称 |
-period | 时钟周期,单位为ns |
-waveform | 波形参数,第一个参数为时钟的第一个上升沿时刻,第二个参数为时钟的第一个下降沿时刻 |
-add | 在同一时刻源上定义多个时钟时使用 |
- 针对vivado自动推导的衍生时钟,比如MMCM/PLL/BUFR的输出时钟,可以由Vivado自动推导,用户无需创建。(MMCM可以调整相位,PLL占用的面积较小)
但是 ,工具不能自动推导出使用寄存器和组合逻辑搭建的分频器等衍生的时钟,必须有用户使用create_generated_clock
来创建。
c
create_clock -name clk1 -period 10.000[get_ports CKP1]
create_generated_clock -name clk2 [get_pins REGA/Q] -source [get_ports CKP1] -divide_by 2
- clk1是原本就有的主时钟,clk2是衍生出来的时钟。
(2)create_generated_clock:
c
create_generated_clock -name <generated_clock_name> \
-source <master_clock_source_pin_or_port> \
-multiply_by <mult_factor> \
-master_clock <master_clk> \
<pin_or_port>
参数 | 含义 |
---|---|
-name | 时钟名称 |
-source | 产生该时钟的源时钟 |
-multiply_by | 源时钟的多少倍频 |
-divide_by | 源时钟的多少分频 |
这个约束是在FPGA内部产生的衍生时钟,所以参数中有一个-source
,就是制定这个时钟是从哪里来的,这个时钟叫master clock
,是指上级时钟,区别于primary clock
。它可以是上面讲的primary clock,也可以是其他的衍生时钟。该命令不是设定周期或波形,而是描述时钟电路如何对上级时钟进行转换, 转换可以是如下关系:
- 简单的频率分频
- 简单的频率倍频
- 频率倍频与分频的组合,获得一个非整数的比例,通常由MMCM或PLL完成
- 相移或波形反相
- 占空比改变
- 上述所有关系的组合
衍生时钟 又分为两种情况:
①Vivado自动推导的衍生时钟
②用户自定义的衍生时钟
首先来看第一种,如果使用PLL或者MMCM,则Vivado会自动推导出一个约束。在xdc文件中,不对着两个输入时钟进行约束,只对输入的clk进行约束,也可以看到vivado生成了约束(有三个约束,因为PLL会自动输出一个反馈时钟)。
- 自动推导的好处:当MMMCM/PLL/BUFR的配置改变而影响到输出时钟的频率和相位时,用户无需改写约束。
- 劣势:用户不清楚自动推导出的衍生钟的名字,当设计层次改变时,衍生时钟的名字可能改变,但由于该衍生时钟的约束并非我们自己定义的,因此可能会没有关注到它名字的改变,当我们使用这些衍生时钟进行别的约束时,就会出现错误。
解决办法:用户手动写出自动推导的衍生时钟的名字,只需要写名字,其余的不写。
c
create_generated_clock -name <generated_clock_name> \
-source <master_clock_source_pin_or_port>
这一步很容易会被提示critical warning,其实有个很简单的方法,就是name和source都按照vivado中生成的来。具体我们到后面的例子中会讲到。
(3)set_clock_group :
使用方法:
c
#第一种
set_clock_groups -asynchronous -group <clock_name_1> -group <clock_name_2>
#第二种
set_clock_groups -physically_exclusive -group <clock_name_1> -group <clock_name_2>
这个约束常用的方法有三种,第一种用法是当两个主时钟是异步关系 时,使用-asynchronous
来指定。这个在我们平时用的还是比较多的,一般稍微大点的工程,都会出现至少两个主时钟,而且这两个时钟之间并没有任何的相位关系,这时就要指定:
c
create_clock -period 10 -name clk1 [get_ports clk1]
create_clock -period 8 -name clk2 [get_ports clk2]
set_clock_groups -asynchronous -group clk1 -group clk2
第二种用法是当我们需要验证同一个时钟端口在不同时钟频率下能否获得时序收敛时使用。比如有两个异步主时钟clk1和clk2,需要验证在clk2频率为100MHz,clk1频率分别为50MHz、100MHz和200MHz下的时序收敛情况,我们就可以这样写。
c
create_clock -name clk1A -period 20.0 [get_ports clk1]
create_clock -name clk1B -period 10.0 [get_ports clk1] -add
create_clock -name clk1C -period 5.0 [get_ports clk1] -add
create_clock -name clk2 -period 10.0 [get_ports clk2]
set_clock_groups -physically_exclusive -group clk1A -group clk1B -group clk1C
set_clock_groups -asynchronous -group "clk1A clk1B clk1C" -group clk2
第三种用法就是当我们使用BUFGMUX时,会有两个输入时钟,但只会有一个时钟被使用。比如MMCM输入100MHz时钟,两个输出分别为50MHz和200MHz,这两个时钟进入了BUFGMUX。
<如图:FPGA时序约束理论篇之时钟周期约束01.png>
在这种情况下,我们需要设置的时序约束如下:
c
set_clock_groups -logically_exclusive \
-group [get_clocks -of [get_pins inst_mmcm/inst/mmcm_adv_inst/CLKOUT0]] \
-group [get_clocks -of [get_pins inst_mmcm/inst/mmcm_adv_inst/CLKOUT1]]
(4)创建虚拟时钟
虚拟时钟通常用于设定对输入和输出的延时约束,这个约束其实是属于IO约束中的延迟约束。虚拟时钟和前面讲的延迟约束的使用场景不太相同。顾名思义,虚拟时钟,就是没有与之绑定的物理管脚。
虚拟时钟主要用于以下三个场景:
- 外部IO的参考时钟并不是设计中的时钟
- FPGA I/O路径参考时钟来源于内部衍生时钟,但与主时钟的频率关系并不是整数倍
- 针对I/O指定不同的jitter和latency
简而言之,之所以要创建虚拟时钟,对于输入来说,是因为输入到FPGA数据的捕获时钟是FPGA内部产生的,与主时钟频率不同;或者PCB上有Clock Buffer导致时钟延迟不同。对于输出来说,下游器件只接收到FPGA发送过去的数据,并没有随路时钟,用自己内部的时钟去捕获数据。
如下图所示,在FPGA的A和B端口分别有两个输入,其中捕获A端口的时钟是主时钟,而捕获B端口的时钟是MMCM输出的衍生时钟,而且该衍生时钟与主时钟的频率不是整数倍关系。
<图片:FPGA时序约束理论之时钟周期约束02.png>
这种情况下时序约束如下:
c
create_clock -name sysclk -period 10 [get_ports clkin]
create_clock -name virclk -peroid 6.4
set_input_delay 2 -clock sysclk [get_ports A]
set_input_delay 2 -clock virclk [get_ports B]
可以看到,创建虚拟时钟用的也是create_clock约束,但后面并没有加get_ports参数,因此被称为虚拟时钟。
再举个输出的例子,我们常用的UART和SPI,当FPGA通过串口向下游器件发送数据时,仅仅发过去了uart_tx这个数据,下游器件通过自己内部的时钟去捕获uart_tx上的数据,这就需要通过虚拟时钟来约束;而当FPGA通过SPI向下游器件发送数据时,会发送sclk/sda/csn三个信号,其中sclk就是sda的随路时钟,下游器件通过sclk去捕获sda的数据,而不是用自己内部的时钟,这是就不需要虚拟时钟,直接使用set_output_delay即可。
注意,虚拟时钟必须在约束I/O延迟之前被定义。
(5)最大最小延迟约束 :
顾名思义,就是设置路径的max/min delay,主要应用场景有两个:
- 输入管脚的信号经过组合逻辑后直接输出到管脚
- 异步电路之间的最大最小延迟
设置方式:
c
set_max_delay <delay> [-datapath_only] [-from <node_list>][-through <node_list>]
set_min_delay <delay> [-from <node_list>] [-to <node_list>] [-through <node_list>]
参数 | 含义 |
---|---|
-from | 有效的起始节点包含:时钟,input(input)端口,或时序单元(寄存器,RAM)的时钟引脚。 |
-to | 有效的终止节点包含:时钟,output(output)端口或时序单元的数据端口。 |
-through | 有效的节点包含:引脚,端口,线网 |
max/min delay的约束平时用的相对少一些,因为在跨异步时钟域时,我们往往会设置asynchronous或者false_path。对于异步时钟,我们一般都会通过设计来保证时序能够收敛,而不是通过时序约束来保证。
1.2.8.2 IO约束
不加任何IO约束的端口,时序要求被视为无穷大。set_input_delay
和set_output_delay
是从系统角度来约束的。-min
是hold slack
时间,不大于周期,-max
是setup slack
时间,需要<=0。一般默认都是设置0。
1.2.8.3 时序例外约束
包括set_max_delay
,set_min_delay
,set_multicycle_path
,set_false_path
等,这类约束除了要满足xdc的先后优先级外,还要遵循自身的优先级限制。
-
总的准则是:针对同一条路径,对约束目标描述越具体的优先级越高。
-
注:XDC里面每一行相当于一条指令,Vivado按照行序从前往后读取XDC指令,所以越后面的XDC指令,其优先级越高。比如当有2条XDC指令约束同一个东西时,后面指令会因为执行的比较晚,而覆盖前一条指令的效果。
因为XDC中的指令有先后顺序,所以推荐的XDC文件组织方式一般是把timing约束放在前面,而把物理位置约束放在后面。
c
## Timing Assertions Section
# Primary clocks
# Virtual clocks
# Generated clocks
# Clock Groups
# Input and output delay constraints
## Timing Exceptions Section
# False Paths
# Max Delay / Min Delay
# Multicycle Paths
# Case Analysis
# Disable Timing
## Physical Constraints Section
1.2.8.4 高级时钟约束
1、时序的零起点:
用create_clock
定义的主时钟的起点即时序的"零起点 ",在这之前的上游路径延时都被工具自动忽略 。
create-clock -name sysclk -period 10 [get_ports sys_clk]
create-clock -name sysclk_bad -period 10 [get_pins clk_infra_i/sys_clk_buf/0]
<图片:高级时钟约束01/02>
2、时钟定义的先后顺序:
时钟的定义也遵从XDC/Tcl的一般优先级,即:在同一个点上,由用户定义的时钟会覆盖工具自动推导的时钟,且后定义的时钟会覆盖先定义的时钟。若要二者并存,必须使用-add
选项。
<图片:高级时钟约束03/04>
c
create_clock -name sysclk -period 10 [get_ports sys_clk]
create_generated_clock -name clkbufg -source [get_ports sys_clk] -divide_by 1 [get_pins clk_infra_i/clkfsm_buf/0]
create_generated_clock -name clkbufr -source [get_ports sys_clk] -divide by 1 [get_pins clk_infra_i/sys_clk_buf/0] -add -master_clock sysclk
- 这个例子添加主时钟的衍生时钟clkbufg
-add -master_clock sysclk
上述例子中BUFG的输出端由用户自定义了一个衍生钟clkbufg,这个衍生钟便会覆盖此处原有的sysclk。此外,图示BUFR工作在bypass模式,其输出不会自动创建衍生钟,但在BUFR的输出端定义一个衍生钟clkbufr,并使用-add 和 -master_clock 选项后,这一点上会存在sysclk和clkbufg两个重叠的时钟。如下的Tcl命令验证了我们的推论。
<图片:高级时钟约束05>
4、同步时钟与异步时钟:
<图片:高级时钟约束06>
在XDC中,所有的时钟都会被缺省认为是相关的,也就是说,网表中所有存在的时序路径都会被Vivado分析。这也意味着FPGA设计人员必须通过约束告诉工具,哪些路径是无需分析的,哪些时钟域之间是异步的。
1.2.9 综合 Synthesis
1、移位寄存器:srl_style
用LUT建立:
c
(* srl_style = "register" *) reg [WIDTH-1:0] shreg;
interger i;
always @(posedge clk) begin
if(clken) begin
for(i=0;i<WIDTH-1;i=i+1)
shreg[i+1] <= shreg[i];
shreg[0] <= SI;
end
end
assign SO = shreg[WIDTH - 1];
- srl_reg: ->SRL->FF->
- reg_srl: ->FF->SRL->
- reg_srl_reg: ->FF->SRL->FF-> (高性能)
- register: ->FF->FF->FF->FF->
- srl: ->SRL-> (占用的资源少)
- 注:srl为查找表(不支持复位),FF为触发器
2、ram_style和rom_style
利用vivado综合生成memory。支持:Block RAM,分布式资源(LUT RAMs)。
Verilog:
c
(*ram_style = "distributed"*) reg [data_size-1:0] myram[2**addr_size-1:0]
3、use_dsp48
- 利用综合工具综合算术运算(dsp48):乘法、乘加/乘减、乘累加。
- 但是,加法,减法,累加利用常规逻辑运算实现。
代码:
c
use_dsp48 yes/no
[例1]如果有24-bit加24-bit,再存入寄存器;需要24个LUT,25个Register。因此需要利用dsp48,来减少资源消耗和加速性能。
4、其他属性:
① black_box :综合工具对此不进行综合。value:yes/no,这个属性能被放在module,entity,component内。
1.3 IP创建使用
1.3.1 IP使用方法
- IP management创建IP工程 (IP Integrator)
- 在当前工程中定制IP(写好Verilog HDL,综合,>Tools>Create and Package New IP)
- OOC:综合后的IP核
globally:只生成RTL代码,在工程内综合 - IP版本控制,能锁定,能更新
- IP导入工程(.xci:Add Existing IP,
.dcp:Add or Create Design Sources)
调用IP:新建文件夹,复制IP进去,在Tools > Settings > IP > Repository > 导入路径 - 使用IP:IP Catalog(有官方IP、第三方IP、User IP) ---> 选择IP ---> IP实例化到top.v文件内
- IP综合后,会生成单独的xdc约束文件(一般为只读)。
使用IP内的FIFO(先入先出存储器):FIFO Generator;
PLL(锁相环,分频/倍频时钟管理电路):Clock Wizard;
*---------------------------------------------------------------------------
Note: CMT(Clock Management Tile)包括MMCM(Mixed-mode Clock Manager)和PLL(Phase-locked loop)
*---------------------------------------------------------------------------
1.4 模块化设计
c
顶层模块(top)---module A----module A1
----module A2
----module A3
---module B
---module C----module C1
2 Vivado HLS 学习笔记
2.1 HLS 概述------Vivado HLS加速FPGA算法开发
Vivado集成了HLS工具,使用C、C++以及System C语言对Xilinx的FPGA器件进行编程。用户通过高层次综合生成HDL级的IP核,从而加速IP创建。
缺点是不方便HDL的细节调整。
HLS(high-level synthesis,高层次综合)支持C/C++/System C(也可以使用matlab输出.c/.h)。
2.1.1 开发流程:
(1)创建工程,编写设计输入(.c/.cpp/.h),测试文件testbench(.c/.cpp)
(2)Run C Simulation
(3)Run C Synthesis
(4)Run C/RTL Cosimulation
(5)Export RTL
*注意:.h文件不要添加到工程内,
C Testbench文件非常重要,用于输出结果自检,生成RTL Testbench.
在C Synthesis步骤中添加Constraints/Directives.
资料:ug871,ug902
2.1.2 Expolrer、Toolbars使用:
-
Explorer:可以有多个Solution,下面包括:constraints,csim,impl,sim,syn
-
每个solution都有自己的directives,用来优化C综合,可以写在source file,也可以写在directive file里(推荐)。
写在ditective.tcl内,作为一个tcl命令,每个solution都有自己的命令,可以查看不同的综合效果。移植到第三方软件,也需要移植directive.tcl。
【例1】实现简单的数组运算
头文件:VectorAdd.h
c
#define N 5 //向量长度
typedef int data_t;
void VectorAdd(data_t A[N], data_t c, data_t B[N]);
模块代码:VectorAdd.cpp
c
#include "VectorAdd.h"
void VectorAdd(data_t A[N], data_t c, data_t B[N])
{
unsigned int i;
myloop: //此为directive标签,使用ditective.tcl
for(i=0; i<N; i++)
{
//#pragma HLS PIPELINE //此为嵌入C语言的ditective
B[i] = A[i] + c;
}
}
testbench代码:
c
#include <iostream>
#include <iomanip>
#include "VectorAdd.h"
using namespace std;
int main() //main函数必须为int类型,0正确,1错误
{
data_t A[N] = {-4, -3, 0 ,1 ,2}; //输入
data_t c = 5; //输出
data_t B[N] = {0}; //输出
data_t RefB[N] = {1, 2, 5, 6, 7}; //参考
unsigned int i = 0;
unsigned int errcnt = 0; //错误次数
VectorAdd(A,c,B); //函数
cout << setfill('-' ) << setw(35) << '-' << '\n';
cout << setfill(' ' ) << setw(10) << left << 'A';
cout << setfill(' ' ) << setw(10) << left << 'c';
cout << setfill(' ' ) << setw(10) << left << 'B';
cout << setfill(' ' ) << setw(10) << left << "RefB" << '\n';
cout << setfill('-' ) << setw(35) << '-' << '\n';
for(i=0; i<N; i++)
{
cout << setfill(' ') << setw(10) << left << A[i];
cout << setfill(' ') << setw(10) << left << c;
cout << setfill(' ') << setw(10) << left << B[i];
cout << setfill(' ') << setw(10) << left << RefB[i];
if(B[i] == RefB[i])
{
cout << '\n';
}
else
{
cout << '(' << RefB[i] << ')' << '\n';
errcnt++;
}
}
cout << setfill('-') << setw(35) << '-' << '\n';
if(errcnt > 0)
{
cout << "Test Failed" << '\n';
return 1; //错误
}
else
{
cout << "Test Pass" << endl;
return 0; //正确
}
}
注:main函数必须为int类型,0正确,1错误
3、Toolbars:有New Solution,Run C Simulation,Run C Synthesis,Run C/RTL Cosimulation,Export RTL,Open Report,Open Wave Viewer,Compare Reports
4、C语言中不可被综合的部分:①动态内存分配②涉及到系统层面的操作
2.1.3 C Sythesis(C综合)
查看报告:Outline:Performance,Lantency和Utilization
可以和其他的solution比较,查看性能差异
2.1.4 Run C/RTL Cosimulation
Vivado Simulator,Verilog,Dump Trace:port
仿真报告:Status:pass,就可以查看仿真波形
2.2 处理任意精度数据类型
2.2.1 C语言支持
- 任意精度的数据类型
语言 | 整型数据类型 | 需要的头文件 |
---|---|---|
C | [u]int(1024 bits) | #include <ap_cint.h> |
C++ | ap_[u]int(1024 bits)能被拓展到32Kb宽 | #include <ap_int.h> |
C++ | ap_[u]fixed<W,I,Q,O,N> | #include <ap_fixed.h> |
有兴趣参考ug902,Table 1-7
1、例子:18*18的乘法器
Input data type :
ap_int<18>
Product data type : ap_int<36>
使用任意精度的数据类型可以获得:更高的时钟频率,更好的数据吞吐率、消耗更少的资源。
2、 提示:
(1)定义数据类型最好在头文件 内。
(2)在debug中非常有用。
C++头文件
c
#include <ap_int.h>
#define W 18
#define __NO_SYNTH__
#ifdef __NO_SYNTH__
typedef int data_t;
typedef int prod_t;
#else
typedef ap_int<W> data_t;
typedef ap_int<2*W> prod_t;
#endif
在Vivado HLS Outline 中会高亮显示。
(3)sizeof()返回值返回数据类型或变量的数据长度。
sizeof(type/variable)
(4)数据类型
种类 | 类型 | 最小位宽 | Note |
---|---|---|---|
bollean | bool | 1 btye | |
character | char | 1 byte | |
wchar_t | 1 byte | 可能是unsigned/signed | |
char16_t | 2 bytes | c++11 type(HLS不支持) | |
char32_t | 4 bytes | c++11 type(HLS不支持) | |
interger | short | 2 bytes | |
int | 4 bytes | ||
long | 4 bytes | ||
long long | 8 bytes | c99/c++11 type | |
floating point | float | 4 bytes | |
double | 8 bytes | ||
long double | 8 bytes |
sizeof()代码:
c
cout << "ap_int<1>:\t" << sizeof(ap_int<1>) << "bytes" << endl;
cout << "ap_int<16>:\t" << sizeof(ap_int<16>) << "bytes" << endl;
cout << "ap_int<20>:\t" << sizeof(ap_int<20>) << "bytes" << endl;
ap_fixed<4,1> a = 0.125;
cout << "ap_fixed<4>:\t" << sizeof(a) << "bytes" << endl;
输出:
bash
ap_int<1>:1 bytes 1->8->8/8=1 byte
ap_int<16>:2 bytes 16->16->16/8=2 bytes
ap_int<20>:4 bytes 20->32->32/8=4 bytes
ap_fixed<4>:1 bytes 4->8->8/8=1 byte
MVSC可以编译HLS,需要进行设置。
2.3 数据类型的转换
- 数据初始化:
c
int var_i = -6;
ap_int<6> a_6bit_var_c = -22; //copy初始化
ap_int<6> a_6bit_var_d(-22); //direct初始化
//ap_int<6> a_6bit_var_u{-22}; //uniform初始化,不支持
ap_int<6> a_6bit_var_r2("0b101010",2);
ap_int<6> a_6bit_var_r8("0o52",8);
ap_int<6> a_6bit_var_r10("-22",10);
ap_int<6> a_6bit_var_r16("0x2A",16);
- ap_[u]fixed变量定义与初始化:
ap_[u]fixed<W,I,Q,O>
W:总字长
I:整数位字长
W-I:小数位字长
Q:量化模式,default:AP_TRN_ZERO
O:溢出模式,default:AP_WRAP
[例1] var1和var2的真值:
c
ap_fixed<3,2> var1 = 1.25; //1 [01.0]
ap_fixed<3,2,AP_RND> var2 = 1.25; //1.5 [01.1]
[例2] var3和var4的真值:
c
//19的字长为: [010011.]
ap_fixed<4,4> var = 19; //[0011.]因此为3
ap_fixed<4,4,AP_RND,AP_SAT> var4 = 19; //[0111.]四位有符号数的最大值7
- f代表单精度的浮点数float
c
double vf2(5.0);
float vf3(5.0f);
- Vivado HLS的math库(hls_math.h)提供了可综合的C/C++库的扩展支持。
- ①隐式数据转换:promotion:小类型数据变大类型数据。
conversion:大类型数据变小类型,会导致损失或错误。
②显式数据类型转化:
c
ap_uint<3> i3 = 4;
ap_uint<4> i4 = 10;
ap_ufixed<6,4> i5 = i4/i3;
cout << "The value of i5:\t" << i5 << "\n";
ap_ufixed<6,4> i6 = (ap_ufixed<6,4>)i4/i3;
cout << "The value of i6:\t" << i6 << "\n";
ap_ufixed<6,4> i9 = (ap_ufixed<6,4>)(i4)/i3;
cout << "The value of i6:\t" << i9 << "\n";
输出:
bash
The value of i5: 2
The value of i6: 2.5
The value of i9: 2.5
- 二进制运算符
- 两个变量相加(M+N):位宽为ap_int<max(M,N)+1>
- 两个变量相乘(M*N):位宽为ap_int<M+N>
- 两个变量相除(M/N):位宽为ap_int
- 两个变量取余(M%N):位宽为ap_int<min(M,N)>
保证大数据不溢出,小数据不损失。 - typeid获取数据类型
c
#include <iostream>
#include <iomanip>
#include <typeinfo>
#include <ap_int.h>
#include <ap_fixed.h>
using namespace std;
int main()
{
ap_int<4> v1 = 3;
int v2 = 6;
ap_fixed<6,2> v3 = 1.25;
cout << left << setw(30) << setfill("-") << "-" << "\n";
cout << left << "v1" << typeid(v1).name() << "Value:"<< v1 << "\n";
cout << left << "v2" << typeid(v2).name() << "Value:"<< v2 << "\n";
cout << left << "v3" << typeid(v3).name() << "Value:"<< v3 << "\n";
cout << left << setw(30) << setfill("-") << "-" << "\n";
return 0;
}
2.4 复合数据类型
有结构体和数组。数组被映射为memory端口。
1、结构体:当结构体被用在顶层函数(top-level)时,
- 结构体元素被映射为scalar端口。(先在头文件中声明)
[例2]头文件中定义结构体:
c
//--------------------------StructPort.h
#include <ap_int.h>
#define W 4
#define N 4
typedef ap_int<W> A_t;
typedef ap_uint<W> B_t;
typedef struct {
A_t A; //占据4 bits,1 byte
B_t B[N]; //占据4*4=16 bits,但是为4 bytes(4 bits占一位)
}data_t;
data_t StructPort(data_t i_val);
c
//-------------------------.c
#include "StructPort.h"
data_t StructPort(data_t i_val)
{
data_t o_val;
int i;
o_val.A = i_val.A + 2;
for(i=0;i<N;i++)
{
o_val.B[i] = i_val.B[i] + 2;
}
return o_val;
}
bash
cout << "the size of this struct is " << sizeof(data_t) << "\n";
输出:the size of this struct is 5
- 结构体优化方式:Byte_pad :filed_level 和struct_level
1.1 数据包选择(Data Pack Mode):插入Directives,DATA_PACK,field_level,OK------->%HLS DATA_PACK variable=i_val field_level
- field_level的含义:结构体内所有的元素的位宽必须是以8为边界,4bit->8bit,12bit->16bit
- struct_level的含义:每个元素的实际位宽保留,但是在封装后的位宽以8bit的整数倍为边界。
- 可以看出Data Pack可以降低lantency和initial interval,提高线路吞吐率,这里利用的是for循环展开。
2、枚举类型 (enum
erated type):数值定义为一个符号常量
枚举类型会自动分配整数,第一个是0,后面的依次加1。
[例3]
c
//------------------------EnumApp.h
#include <ap_int.h>
#define W 4
typedef ap_int<W> a_t;
typedef enum {
M_INIT, //0
M_ADD, //1
M_SUB, //2
M_HOLD //3
} mymode_t;
a_t EnumApp(a_t A,mytype_t mode);
c
//-----------------------.c
#include "EnumApp.h"
a_t EnumApp(a_t A,mymode_t mode)
{
static a_t res;
switch(mode)
{
case M_INT: res = A; break;
case M_ADD: res = res + A; break;
case M_SUB: res = res - A; break;
case M_HOLD:break;
}
return res;
}
- mode会被自动综合成2 bits
3、小结:
- Vivado HLS支持struct和enum。
- struct和enum都可以成为top-level函数的接口。
- 如果struct出现在top-level函数接口:能被data pack封装。
- 如果enum出现在top-level函数接口:实际上是一个整数,HLS能自动推断数据位宽。
2.5 C/C++基本运算
[例4] 加一个常数。
c
//----------------------AddConst.h
#include <ap_fixed.h>
#define W 10
#define I 2
typedef ap_ufixed<W,I> din_t;
void AddConst(din_t din,din_t &sum);
c
//---------------------AddConst.cpp
#include "AddConst.h"
void AddConst(din_t din,din_t &sum)
{
sum = din + din_t(0.25); //din_t(0.25)为常数
}
c
//-------------------AddConst_tb.cpp
#include <iostream>
#include <iomanip>
#include "AddConst.h"
using namespace std;
int main()
{
for(i=0;i<10;i++>)
{
AddConst(va,sum);
RefSum = va + din_t(0.25);
va = va + din_t(0.125);
if(sum == RefSum)
{
cout << '\n';
}
else
{
ErrCnt++;
cout << '(' << "Failed" << ')' << '\n';
}
}
}
- 逻辑运算:! && ||返回true/false
- 位运算:>> << ~ & | ^ 返回
[例5] Complex Multiplier
c
#include "CmpMult.h"
void CmpMult(t_a_cmp a,t_b_cmp b,t_p_cmp &p)
{
data_a_t ar = std::real(a);
data_a_t ai = std::imag(a);
data_b_t br = std::real(b);
data_b_t bi = std::imag(b);
#ifndef Solution1
#define
//法1:
data_p_t pr;
data_p_t pi;
//4次乘法,2次加法:使用4个dsp48
pr = (ar * br) - (ai * bi);
pi = (ar * bi) + (ai * br);
//法1 end
#else
//法2:
data_p_t pc;
data_p_t pr;
data_p_t pi;
//3次乘法,5次加法:使用3个dsp48,19个FF,1个LUT
pc = bi * (ar -ai);
pr = pc + ar * (br - bi);
pi = pc + ai * (br + bi);
#endif
p.real() = pr;
p.imag() = pi;
}
[例6] 欧几里得算法:计算最大公约数
-GCD(1071,462)
a | b | Computation Process |
---|---|---|
1071 | 462 | 1071=2*462+147 |
462 | 147 | 462=3*147+21 |
147 | 21 | 147=7*21+0 |
21 | 0 |
- b=a%b => b==0 (判断,循环)
c
//---------------------------gcd.h
#include <ap_int.h>
#define LW 11
#define SW 10
typedef ap_uint<LW> da_t;
typedef ap_uint<SW> db_t;
//Make sure the port da is the larger one
//the port db is the smaller one
db_t gcd(da_t da,db_t db);
c
//--------------------------gcd.cpp
#include "gcd.h"
db_t gcd(da_t da,db_t db)
{
if(db == 0)
{
return da;
}
else
{
return gcd(db,da % db); //HLS不支持递归函数
}
}
2.6 C/C++测试平台的基本架构------描述高效的C testbench
-
C++描述testbench:Driver/Stimulus -> Reference Model & DUT(design under test) -> Monitor -> Scoreboard(得分板):参考模型和设计输出对比。
-
C仿真比RTL/Verilog仿真快。
-
C testbench的作用:验证C函数的正确性(C simulation),验证RTL设计(C/RTL Cosimulation)。
-
Testbench的要求:
①top_level函数多次执行,验证多种可能性。
②输出比较
③返回值:0:正确,1:有错误。
[例7]
c
#include "ScalarMult.h"
prod_t ScalarMut(data_t A,data_t B)
{
prod_t prod;
prod = A * B;
return prod;
}
c
#include <iostream>
#include <iomanip>
#include "ScalarMult.h"
using namespace std;
int main()
{
data_t A[4] = {-4,4,0,5};
data_t B[4] = {4,-4,1,5};
prod_t RefP[4] = {-16,-16,0,25};
prod_t P;
unsigned int i;
unsigned RrrCnt = 0;
cout << left << setw(30) << setfill('-') << '-' << '\n';
cout << left << setw(10) << setfill(' ') << 'A';
cout << left << setw(10) << setfill(' ') << 'B';
cout << left << setw(10) << setfill(' ') << 'P' << '\n';
cout << left << setw(30) << setfill('-') << '-' << '\n';
for(i=0;i<4;i++)
{
P = ScalarMult(A[i],B[i]);
cout << left << setw(10) << setfill(' ') << A[i];
cout << left << setw(10) << setfill(' ') << B[i];
cout << left << setw(10) << setfill(' ') << P;
if(P == RefP[i])
{
cout << '\n';
}
else
{
cout << '(' << RefP[i] << ')' << endl;
ErrCnt++;
}
}
cout << left << setw(30) << setfill
}
3 硬件加速设计方法
3.1 高质量VerilogHDL描述方法
3.1.1基本认知
- HDL语言仅是对已知硬件电路的文本表现形式编写前,对所需实现的硬件电路"胸有成竹"。
- 互联性:wire型变量描述各个模块之间的端口与网线连接关系
- 并发:可以有效地描述并行的硬件系统
- 时间:定义了绝对和相对的时间度量,可综合操作符具有物理延迟
- 可综合的语句:always,if-else,case,assign
- 不可综合的语句:function,for,fork-join,while(用于testbench)
3.1.2 映射的硬件结构
1.if-else:多路选择器(multiplexing hardware)
输出结果由输入的选择条件决定。
c
if (Aflag = '1') then
OutData <= A + B;
else
OutData <= C + D;
endif
[./001-multiplexing.jpg]
重构if-else映射的硬件结构:加法器结构复杂,减少了一个加法器,减少了硬件的面积。
c
if(Aflga == 1'b1)
begin
Op1 <= A;
Op2 <= B;
end
else
begin
Op1 <= C;
Op2 <= D;
end
OutData <= Op1 + Op2;
[./001-multiplexing2.jpg]
但第一种元件控制信号Aflag的延迟只有一个选择器,第二种元件控制信号Aflag的延迟有控制器和加法器之和。第二种电路性能可能比第一种性能差。
- 单if语句:无优先级的判断结构
推荐初学者尽量使用单if语句(if...else if...else if)描述多条件判断结构
c
always @(a or b or c or d or sel0 or sel1 or sel2 or sel3) begin
z = 0;
if(sel3) z = d;
else if(sel2) z = c;
else if(sel1) z = b;
else if(sel0) z = a;
end
- 多if语句:具有优先级的判断结构
c
always @(a or b or c or d or sel0 or sel1 or sel2 or sel3) begin
z = 0;
if(sel0) z = a;
if(sel1) z = b;
if(sel2) z = c;
if(sel3) z = d;
end
- 最后一级选择信号具有最高优先级。
- 具有优先级的多选结构会消耗组合逻辑。不推荐这种写法
- 某些设计中,有些信号需要先到(如关键使能信号,选择信号等),有些需要后到达(如慢速信号、有效时间较长的信号等),此时则需要使用if...if结构。
- 设计方法:最高优先级给最迟到达的关键信号
- case:无优先级的判断结构
c
always @(a or b or c or d or sel0 or sel1) begin
case({sel0,sel1})
2'b00: z = d;
2'b01: z = c;
2'b10: z = b;
2'b11: z = a;
default: z = 1'b0;
endcase
end
- 与单if语句的区别:条件互斥
- 多用于指令译码电路
- latch:异步电路、门控时钟(慎用)
- 不能过滤毛刺,能用D触发器,不用latch
- 容易引入latch的途径:使用不完备的条件判断语句,缺少else,缺少default
4 备忘录
-
Docnav使用,FPGA基础,Vivado使用,HLS使用
-
Verilog语句讲解:
组合逻辑
时序逻辑
-
模块例化
-
Vivado需要加环境变量
-
Vivado生成bit流文件后,软核设计完成。
-
File > Lauch SDK。打开Xilinx SDK(软件开发套件),会生成一个.hdf文件(硬件设计文件),可以查看寄存器地址。
-
烧录方法:
(1)准备两条线,先烧写bit流文件,再烧写软件。
(2)生成.bit文件和.elf/.hex文件后,使用脚本,将软核和软件整合到一起(生成两个文件.bit(断电丢失)和.mcs(烧写到flash内部)),使用Vivado烧写。
-
打开C_Sky DebugServer就可以看到连接是否成功。
-
设计结构:
PS :Processing System(系统)
PL :Programmable Logic(E902软核)
PS和PL通过AXI来通信
-
为e902添加自己的模块,查看平头哥的官方手册,dummy为空模块,选择一个dummy,加入自己/官方的IP,只要接口一样就能加入,AHB总线,AXI总线,有一个AHB到AXI的桥IP(AHB-Lite to AXI Bridge).
-
建立Block design,加入IP,选中IP,ctrl+T加入引脚。
-
自定义IP:Tools->Create and Package New IP->Create AXI4 Peripheral,完成后,在IP Catalog中,右键编辑IP。
-
打开verilog文件,按照提示输入代码。加入输出引脚,输入寄存器register到输出,一层层例化。
-
在Package IP里确认。打包Re-Package IP
-
在Block Design内使用此IP,加入互联IP(AXI Interconnect),设置slave接口1个,master接口1个,S连M(x2),再自动连线,删除复位模块,连复位线。输出模块的引脚右键make external,或者ctrl+T。验证设计(validate design)。
-
分配地址,Address Editor,查看用户手册,加入地址。完成
-
sources,模块右键Generate Output Products。Create HDL Wrapper。打开生成的.v文件。
-
加入myio_top顶层文件,例化上面的.v文件,