1)实验平台:正点原子开拓者FPGA 开发板
2)摘自《开拓者FPGA开发指南》关注官方微信号公众号,获取更多资料:正点原子
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-13912-1-1.html
第十五章 IP核之FIFO实验
FIFO的英文全称是First In First Out,即先进先出。FPGA使用的FIFO一般指的是对数据
的存储具有先进先出特性的一个缓存器,常被用于数据的缓存或者高速异步数据的交互,也即
所谓的跨时钟域信号传递。它与FPGA内部的RAM和ROM的区别是没有外部读写地址线,采取顺序
写入数据,顺序读出数据的方式,使用起来简单方便,由此带来的缺点就是不能像RAM和ROM那
样可以由地址线决定读取或写入某个指定的地址。本章我们将对Quartus II软件生成的FIFO
IP核进行读写测试,来向大家介绍Altera FIFO IP核的使用方法。
本章包括以下几个部分:
15.1 FIFO IP核简介
15.2 实验任务
15.3 硬件设计
15.4 程序设计
15.5 下载验证
FIFO IP核简介
FIFO从输入时钟的角度来分,有两种类型:单时钟FIFO(SCFIFO)和双时钟FIFO(DCFIFO),
其中双时钟FIFO又可从输出数据的位宽的角度分为普通双时钟(DCFIFO)和混合宽度双时钟
FIFO(DCFIFO_MIXED_WIDTHS)。单时钟FIFO和双时钟FIFO的符号图如图 15.1.1所示。从图中
可以看到,单时钟FIFO具有一个独立的时钟端口clock,因此所有的输入输出信号都同步于
clock信号。而在双时钟FIFO结构中,写端口和读端口分别有独立的时钟,所有与写相关的信
号都是同步于写时钟wrclk,所有与读相关的信号都是同步于读时钟rdclk。在双时钟FIFO的符
号图中,位于图中上侧部分的以“data”和“wr”开头的信号为与写相关的所有信号,位于中
间部分的“q”和以“rd”开头的信号为与读相关的所有信号,位于底部的为异步清零信号。
图 15.1.1 单时钟 FIFO 与双时钟 FIFO 的符号图
对于FIFO需要了解一些常见参数:
FIFO的宽度:FIFO一次读写操作的数据位N;
FIFO的深度:FIFO可以存储多少个宽度为N位的数据。
空标志:对于双时钟FIFO又分为读空标志rdempty和写空标志wrempty。FIFO已空或将要空
时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效
数据的读出。
满标志:对于双时钟FIFO又分为读满标志rdfull和写满标志wrfull。FIFO已满或将要写满
时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出。
读时钟:读FIFO时所遵循的时钟,在每个时钟的上升沿触发。
写时钟:写FIFO时所遵循的时钟,在每个时钟的上升沿触发。
对于FIFO的基本知识先了解这些就足够了,可能有人会好奇为什么会有单时钟FIFO和双时
钟FIFO,它们各自的用途是什么。之所以有单时钟FIFO和双时钟FIFO是因为各自的作用不同。
单时钟FIFO常用于同步时钟的数据缓存,双时钟FIFO常用于跨时钟域的数据信号的传递,例如
时钟域A下的数据data1传递给异步时钟域B,当data1为连续变化信号时,如果直接传递给时钟
域B则可能会导致收非所送的情况,即在采集过程中会出现包括亚稳态问题在内的一系列问题,
使用双时钟FIFO能够将不同时钟域中的数据同步到所需的时钟域中。
实验任务
本节实验任务是使用Quartus II生成FIFO IP核,并实现当FIFO为空时就开始向FIFO中写
入数据,直到FIFO写满为止;当FIFO为满时则开始从FIFO中读出数据,直到FIFO读空为止的功
能,来向大家详细介绍一下FIFO IP核的使用方法。
硬件设计
本章实验只用到了输入的时钟信号和按键复位信号,没有用到其它硬件外设。
本实验中,各端口信号的管脚分配如下表所示。
表 15.3.1 IP实验管脚分配
程序设计
根据实验任务要求和模块化设计的思想,我们需要如下4个模块:fifo模块、写fifo模块、
读fifo模块以及顶层例化模块实现前三个模块的信号交互。由于FIFO多用于跨时钟域信号的处
理,所以本实验我们使用双时钟FIFO来向大家详细介绍双时钟FIFO IP核的创建和使用。为了
方便大家理解,这里我们将读/写时钟都用系统时钟来驱动。系统的功能框图如下所示:
图 15.4.1 系统框图
接下来我们创建一个名为ip_fifo的工程,在这里我们就不再给出Quartus II软件创建工
程的详细过程,如果大家对Quartus II软件的创建过程还不熟悉的话,可以参考“第四章
Quartus II软件的安装和使用”章节中的Quartus II软件的使用部分。新建后的工程如下所
示:
图 15.4.2 工程新建完成页面
创建好了工程以后,接下来我们创建FIFO IP核。我们在Quartus II软件的菜单栏中找到
【Tools】→【MegaWizard Plug-In Manager】按钮并点击打开,Tool工具栏打开面及打开后
弹出的页面如图 15.4.3和图 15.4.4所示。
图 15.4.3 工具栏打开IP核页面
图 15.4.4 创建IP核向导页面
在该页面中,可以看到有三个选项,第一个是创建一个新的IP核,第二个是编辑一个已经
创建好的IP核,第三个是复制一个已经创建好的IP核。因为我们这里是首次创建IP核,因此直接选择默认的第一个选项,然后点击【Next>】,进入如图 15.4.5所示页面。
图 15.4.5 选择FIFO IP核页面
在该页面中,我们可以在Memory Compiler下找到FIFO IP核,也可以直接在搜索框中输入
FIFO找到它。我们找到FIFO IP核以后,单击选中它,然后我们需要选择FIFO IP核保存的路径
及名称,首先大家先在工程所在路径par文件夹下创建一个文件夹ipcore,用于存放工程中用
到的IP核(如果之前没有创建ipcore文件夹的话),然后在“What name do you want for the
output file”一栏中输入IP存放的路径及名称,这里我们命名为fifo并且选择创建的IP核代
码为Verilog HDL(默认为Verilog HDL)。完成这些设置以后,我们点击【Next>】,进入如
图 15.4.6所示页面。
图 15.4.6 FIFO IP核模式配置页面
箭头1指向的位置用来设置FIFO的位宽,这里我们选择8bits,箭头2指向的位置用来设置
FIFO的深度,也就是能存放多少个指定位宽的数据,这里我们选择256words,这样设置以后FIFO
的容量大小为256个8bits。箭头6处所指的即界面左下角是资源使用情况,可以对FIFO消耗的
FPGA资源有个大概的了解。箭头3和箭头4所指向的位置用于设置FIFO的驱动时钟类型,箭头3
处用于选择单时钟FIFO,箭头4处用于选择双时钟FIFO。当选择双时钟FIFO时,箭头5所指向的
位置可以选择不同的输出位宽。这里我们选择双时钟FIFO,采用默认方式输出数据与输入数据
等宽,如下图所示:
图 15.4.7 选择双时钟FIFO
选择完之后,我们直接点击【Next>】,进入如图 15.4.8所示页面。
图 15.4.8 DCFIFO1配置页面
从该页面的“Which type of optimization do you want?”我们可以看出,该页面主要
是用于对我们的DCFIFO进行优化的,在箭头1、2、3所指处有三种针对读时钟同步、亚稳态保
护、面积和速度的优化类型,下面我们简单的介绍一下这三种优化类型:
⚫
Lowest latency but requires synchronized clocks(最低延迟,但要求同步时钟)
此选项使用一个同步阶段,没有亚稳态保护,适用于同步时钟。它是最小尺寸,提供良好
的Fmax。
⚫
Minimal setting for unsynchronized clocks(异步时钟时的最小设置)
这个选项使用两个同步阶段,具有良好的亚稳态保护。它是中等尺寸,提供良好的Fmax。
⚫
Best metastability protection, best fmax and unsynchronized clocks(异步时钟时最好的
亚稳态保护,最好的Fmax,不同步)
这个选项使用三个或更多的同步阶段,具有最好的亚稳态保护。它是最大尺寸,给出了最
好的Fmax。
在使用过程中,通常我们选择的是默认的中等优化,具体的优化主要还是看你的工程的要
求,假如你的工程对速度和稳定性要求很高,同时资源又很多,那么你就可以使用第三个选项,
假如你的工程资源很紧张,那么你可以选择使用第一个资源少的优化。在此我们保持默认的设
置,直接点击【Next>】,进入如图 15.4.9所示页面。
图 15.4.9 DCFIFO2配置页面
该页面用于选择可选的输出控制信号,从读方(Read side)和写方(Write side)分别
进行选择。下面我们简单的介绍一下:
⚫
rdfull和wrfull:FIFO满的标记信号,为高电平时表示FIFO已满,此时不能再进行写操
作。
⚫
rdempty和wrempty:FIFO空的标记信号,为高电平时表示FIFO已空,不能在进行读操
作。
⚫
rdusedw[]和wrusedw[]:显示存储在FIFO中数据个数的信号。
⚫
Add an extra MSB to usedw ports:将rdusedw和wrusedw数据位宽增加1位,用于保护
FIFO在写满时不会翻转到0。
⚫
Asynchronous clear:异步复位信号,用于清空FIFO。
这里我们选择读空、读满、读侧数据量和写空、写满信号、写侧数据量,然后继续点击
【Next>】,进入如图 15.4.10所示页面。
图 15.4.10 Rdreq Option,Blk Type的配置页面
该页面用于选择输出模式和存储器类型。最上面的红框选择输出模式,输出模式有两种:
正常模式和前显模式。对于正常模式,FIFO将端口rdreq看做正常的读请求并在该端口信号为
高电平进行读操作。对于前显模式,FIFO将端口rdreq看做读确认信号,将rdreq信号置为高电
平时将输出FIFO中的下一个数据字(如果存在)。如果使用前显模式,将会使设计性能下降。
这里我们使用默认值:正常模式。
接下来的红框用于指定实现存储器使用的存储块类型和存储器的存储深度,具体可选值与
使用的FPGA芯片有关,默认为Auto,我们一般使用默认值就可以了,所以这里直接点击【Next>】,
进入如图 15.4.11所示页面。
图 15.4.11 Optimization,Circuitry Protection 的配置页面
该页面主要用于选择是否禁止上溢检测和下溢检测的保护电路。如果你不需要上溢检测和
下溢检测保护电路,那么你可以通过Disable来禁止它们,以此来提高我们的FIFO性能。
上溢检测保护电路主要是用于在FIFO满时禁止wrreq端口,下溢检测保护电路主要是用于
在FIFO空时,禁止rdreq端口,它们默认的状态是打开的。这里我们使用默认设置。
“Implement FIFO storage with logic cells only, even if the device contains
memory blocks?”选项使用逻辑单元实现FIFO存储器,即使器件拥有存储块。这里使用默认
设置,用存储块实现FIFO。
然后直接点击【Next>】,进入如图 15.4.12所示页面。
图 15.4.12 第一个输出时钟c0配置页面
从该页面中,我们可以看出,如果我们想要仿真FIFO IP核,那么我们就需要用到
“altera_mf”这个仿真库。如果我们想要将此FIFO IP核用在其他的EDA工具上,我们可以通
过选择“Generate netlist”这个选项来生成IP_syn.v文件,用于其他的EDA工具中。这里需
要注意的是,并不是所有的第三方EDA工具都支持。点击【Next>】,进入如图 15.4.13所示页
面。
图 15.4.13 Summary 的配置页面
在该页面,我们可以看到,该IP核能生成的所有文件都在该页面中,在这么多的文件中,
我们只要选择FIFO_inst.v文件就可以了,方便对FIFO的例化。至此,关于FIFO IP核的配置就
讲解完了。然后我们点击【Finish】完成整个IP核的创建。接下来Quartus II软件会在ipcore
文件夹下创建FIFO的IP核文件,然后询问我们是否添加至工程,点击“YES”按钮将生成的IP
核添加至工程,如下图所示页面。
图 15.4.14 IP核添加至工程确认界面
接下来返回到工程界面,在File界面里,我们可以看到生成的fifo.qip和fifo.v已经添加
到工程中,如图 15.4.15所示界面。
图 15.4.15 FIFO添加至工程界面
打开fifo.v文件,我们可以看到使用Modelsim对fifo文件进行仿真时需要添加一个名为
altera_mf的仿真库,如下图所示:
图 15.4.16 FIFO_clk文件注释说明
至此,FIFO IP核的创建已经全部完成,如果需要修改IP核的话,点击在Quartus II软件
的菜单栏中找到【Tools】→【MegaWizard Plug-In Manager】按钮并点击打开,图 15.4.17
为打开后的页面。
图 15.4.17 修改IP核页面
和我们第一次创建IP核不同的是,这一次我们选择第二个选项,修改已经存在的IP核,然
后点击【Next>】,进入选择IP核路径页面,双击ipcore文件夹,进入如图 15.4.18所示页面。
然后双击fifo.v文件,点击【Next>】开始重新配置FIFO IP核。
图 15.4.18 选择需要修改的IP核路径页面
接下来我们设计一个verilog文件来对FIFO写入数据,文件名为fifo_wr.v,编写的verilog
代码如下:
1 module fifo_wr(
2 //mudule clock
3 input clk , // 时钟信号
4 input rst_n , // 复位信号
5
6 //user interface
7 input wrempty, // 写空信号
8 input wrfull , // 写满信号
9 output reg [7:0] data , // 写入FIFO的数据
10 output wrreq // 写请求
11 );
12
13 //reg define
14 reg wrreq_t ; // 写信号
15 reg [1:0] flow_cnt/*synthesis preserve*/; // 状态流转计数
16
17 //*****************************************************
18 //** main code
19 //*****************************************************
20
21 assign wrreq = ~wrfull & wrreq_t;
22
23 //向FIFO中写入数据
24 always @(posedge clk or negedge rst_n) begin
25 if(!rst_n) begin
26 wrreq_t <= 1'b0;
27 data <= 8'd0;
28 flow_cnt <= 2'd0;
29 end
30 else begin
31 case(flow_cnt)
32 2'd0: begin
33 if(wrempty) begin //写空时,写请求拉高,跳到下一个状态
34 wrreq_t <= 1'b1;
35 flow_cnt <= flow_cnt + 1'b1;
36 end
37 else
38 flow_cnt <= flow_cnt;
39 end
40 2'd1: begin //写满时,写请求拉低,跳回上一个状态
41 if(wrfull) begin
42 wrreq_t <= 1'b0;
43 data <= 8'd0;
44 flow_cnt <= 2'd0;
45 end
46 else begin //没有写满的时候,写请求拉高,继续输入数据
47 wrreq_t <= 1'b1;
48 data <= data + 1'd1;
49 end
50 end
51 default: flow_cnt <= 2'd0;
52 endcase
53 end
54 end
55
56 endmodule
写fifo模块主要完成向fifo中写入数据的功能,当fifo为空时,向fifo中写入数据;当fifo
写满之后,停止写入数据,然后重新判断fifo是否为空。
然后设计一个verilog文件来对FIFO进行读取数据,文件名为fifo_rd.v,编写的verilog
代码如下:
1 module fifo_rd(
2 //system clock
3 input clk , // 时钟信号
4 input rst_n , // 复位信号(低有效)
5
6 //user interface
7 input [7:0] data , // 从FIFO输出的数据
8 input rdfull , // 读满信号
9 input rdempty, // 读空信号
10 output rdreq // 读请求
11 );
12
13 //reg define
14 reg rdreq_t ;
15 reg [7:0] data_fifo; // 读取的FIFO数据
16 reg [1:0] flow_cnt /*synthesis preserve*/; // 状态流转计数
17
18 //*****************************************************
19 //** main code
20 //*****************************************************
21
22 assign rdreq = ~rdempty & rdreq_t;
23
24 //从FIFO中读取数据
25 always @(posedge clk or negedge rst_n) begin
26 if(!rst_n) begin
27 rdreq_t <= 1'b0;
28 data_fifo <= 8'd0;
29 end
30 else begin
31 case(flow_cnt)
32 2'd0: begin
33 if(rdfull) begin
34 rdreq_t <= 1'b1;
35 flow_cnt <= flow_cnt + 1'b1;
36 end
37 else
38 flow_cnt <= flow_cnt;
39 end
40 2'd1: begin
41 if(rdempty) begin
42 rdreq_t <= 1'b0;
43 data_fifo <= 8'd0;
44 flow_cnt <= 2'd0;
45 end
46 else begin
47 rdreq_t <= 1'b1;
48 data_fifo <= data;
49 end
50 end
51 default: flow_cnt <= 2'd0;
52 endcase
53 end
54 end
55
56 endmodule
读fifo模块主要完成向fifo中读出数据的功能,当fifo数据为满的状态时,开始向fifo中
读出数据;当fifo读空之后,停止读数据,然后重新判断fifo是否为满的状态。
最后我们创建一个顶层文件,文件名为ip_fifo.v,实例化刚才创建的FIFO IP核和读写
FIFO文件,编写的verilog代码如下。
1 module ip_fifo(
2 input sys_clk , // 时钟信号
3 input sys_rst_n // 复位信号
4 );
5
6 //wire define
7 wire wrreq ; // 写请求信号
8 wire [7:0] data ; // 写入FIFO的数据
9 wire wrempty ; // 写侧空信号
10 wire wrfull ; // 写侧满信号
11 wire wrusedw ; // 写侧FIFO中的数据量
12
13 wire rdreq ; // 读请求信号
14 wire [7:0] q ; // 从FIFO输出的数据
15 wire rdempty ; // 读侧空信号
16 wire rdfull ; // 读侧满信号
17 wire rdusedw ; // 读侧FIFO中的数据量
18 //*****************************************************
19 //** main code
20 //*****************************************************
21
22 //例化FIFO模块
23 fifo u_fifo(
24 .wrclk ( sys_clk ), // 写时钟
25 .wrreq ( wrreq ), // 写请求
26 .data ( data ), // 写入FIFO的数据
27 .wrempty ( wrempty ), // 写空信号
28 .wrfull ( wrfull ), // 写满信号
29 .wrusedw ( wrusedw ), // 写侧数据量
30
31 .rdclk ( sys_clk ), // 读时钟
32 .rdreq ( rdreq ), // 读请求
33 .q ( q ), // 从FIFO输出的数据
34 .rdempty ( rdempty ), // 读空信号
35 .rdfull ( rdfull ), // 读满信号
36 .rdusedw ( rdusedw ) // 读侧数据量
37 );
38
39 //例化写FIFO模块
40 fifo_wr u_fifo_wr(
41 .clk (sys_clk ), // 写时钟
42 .rst_n (sys_rst_n), // 复位信号
43
44 .wrreq (wrreq ), // 写请求
45 .data (data ), // 写入FIFO的数据
46 .wrempty (wrempty ), // 写空信号
47 .wrfull (wrfull ) // 写满信号
48 );
49
50 //例化读FIFO模块
51 fifo_rd u_fifo_rd(
52 .clk (sys_clk ), // 读时钟
53 .rst_n (sys_rst_n), // 复位信号
54
55 .rdreq (rdreq ), // 读请求
56 .data (q ), // 从FIFO输出的数据
57 .rdempty (rdempty ), // 读空信号
58 .rdfull (rdfull ) // 读满信号
59 );
60
61 endmodule
代码中例化了fifo模块、写FIFO模块wr_fifo、读FIFO模块rd_fifo,各模块的信号连接图
如下图所示:
图 15.4.19 各模块信号连接图
将FIFO模块输出的写空信号wrempty和写满信号wrfull连接至写FIFO模块(fifo_wr),当
写FIFO模块检测到写空信号wrempty拉高时,向FIFO模块发送写请求信号wrreq并写入数据,检
测到写满信号wrfull拉高时,停止向FIFO模块写入数据。将FIFO模块输出的读空信号rdempty
和读满信号rdfull连接至读FIFO模块(fifo_rd),当读FIFO模块检测到读满信号rdfull拉高
时,向FIFO模块发送读请求信号rdreq并从FIFO模块读取数据,检测到读空信号rdempty拉高时,
停止从FIFO模块读取数据。
ip_fifo模块添加至工程后,如果工程名字和ip_fifo模块名字不一致的话,必须先将
ip_fifo模块设置为顶层模块,设置方法是右键选择ip_fifo.v文件,点击Set as Top-Level
Entity。
图 15.4.20 ip_fifo设置为顶层文件
接下来点击Start Compliation图标编译工程。
图 15.4.21 开始编译工程界面
工程编译成功后,打开Pin Planner配置FPGA的管脚,管脚按照本章“15.3节 硬件设计”
中表 15.3.1的列表来分配,分配完成后重新编译工程。
接下来我们对FIFO IP核进行仿真,来验证对FIFO的读写操作是否正确。首先在Modelsim
软件中创建一个名为ip_fifo_tb的工程,在这里我们就不再给出软件创建工程的详细过程,如
果大家对Modelsim软件的创建过程还不熟悉的话,可以参考“第五章Modelsim软件的安装与使
用”章节中的Modelsim软件使用部分。
ip_fifo_tb仿真文件源代码如下:
1 `timescale 1 ns/ 1 ns
2 module ip_fifo_tb;
3
4 //parameter define
5 parameter PERIOD = 20; //定义系统时钟周期
6
7 //reg define
8 reg sys_clk;
9 reg sys_rst_n;
10
11 //初始化
12 initial begin
13 sys_clk <= 1'b0;
14 sys_rst_n <= 1'b0;
15 #(20*PERIOD + 1)
16 sys_rst_n <= 1'b1;
17 end
18
19 always #(PERIOD) sys_clk = ~sys_clk;
20
21 ip_fifo u1_ip_fifo(
22 .sys_clk (sys_clk),
23 .sys_rst_n (sys_rst_n)
24 );
25
26 endmodule
我们还需要将Testbench添加至我们的Quartus II软件的仿真设置中,添加好以后,我们
就可以点击Quartus II软件菜单栏中的找到【Tools】→【Run Simulation Tool】→【RTL
Simulation】点击打开,弹出如下图所示页面。
图 15.4.22 FIFO空时
可以看到在读空FIFO时,读空信号rdempty先拉高,过两个时钟周期后写空信号wrempty才
拉高,这是由FIFO内部结构决定的,并且在写请求信号wrreq拉高后的第4个时钟周期读空信号
rdempty才拉低。
图 15.4.23 FIFO满时
由上图可以看到在写满信号wrfull拉高2个时钟周期后,读满信号rdfull才有效,并且在
读请求信号rereq拉高后的第3个时钟周期写满信号wrfull才拉低。
下载验证
首先我们打开IP核之FIFO实验工程,在工程所在的路径下打开ip_fifo/par文件夹,在里
面找到“ip_fifo.qpf”并双击打开。注意工程所在的路径名只能由字母、数字以及下划线组
成,不能出现中文、空格以及特殊字符等。工程打开后如图 15.5.1示:
图 15.5.1 IP核之FIFO实验工程
将下载器一端连接电脑,另一端与开发板上对应端口连接,最后连接电源线并打开电源开关。
开拓者开发板硬件连接图如下图所示:
图 15.5.2 硬件连接实物图
接下来我们使用SignalTap II软件对FIFO IP核进行调试,首先我们在Quartus II软件中
创建一个SignalTap II调试文件,我们将wrempty、wrfull、wrreq、data、rdreq、rdempty、
rdfull和q这八个信号添加至SignalTap II调试文件中,下图为SignalTap II软件采集到的
FIFO空时的波形图。
图 15.5.3 FIFO空时
可以看到在读空FIFO时,读空信号rdempty先拉高,过两个时钟周期后写空信号wrempty才拉高,这是由FIFO内部结构决定的,并且在写请求信号wrreq拉高后的第4个时钟周期读空信
号rdempty才拉低。
下图为SignalTap II软件采集到的FIFO满时的波形图。
图 15.5.4 FIFO满时
可以看到在写满信号wrfull拉高2个时钟周期后,读满信号rdfull才有效,并且在读请求
信号rereq拉高后的第三个时钟周期写满信号wrfull才拉低。