本帖最后由 正点原子运营 于 2021-1-18 16:16 编辑
1)实验平台:正点原子超越者FPGA开发板
2)平台购买地址:https://item.taobao.com/item.htm?&id=631660290421
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/fpga/zdyz-chaoyuezhe.html
4)本章实例源码下载:
第十八章 IP核之FIFO实验.zip
(3.15 MB, 下载次数: 5)
5)正点原子FPGA交流群:905624739 点击加入:![]()
6)关注正点原子公众号,获取最新资料更新
![]()
![]()
第十八章 IP核之FIFO实验
FIFO的英文全称是First In First Out,即先进先出。FPGA使用的FIFO一般指的是对数据的存储具有先进先出特性的一种缓存器,常被用于数据的缓存,或者高速异步数据的交互(即跨时钟域信号传递)。FIFO与FPGA内部的RAM和ROM的区别是FIFO没有外部读写地址线,采取顺序写入数据,顺序读出数据的方式,使用起来简单方便,由此带来的缺点就是不能像RAM和ROM那样可以由地址线决定读取或写入某个指定的地址。本章将通过对ISE软件自带的FIFO IP核进行读写测试,来向大家介绍Xilinx FIFO IP核的使用方法。 本章包括以下几个部分: 1 1.1 简介 1.2 实验任务 1.3 硬件设计 1.4 程序设计 1.5 下载验证
1.1 简介根据FIFO工作的时钟域,可以将FIFO分为同步FIFO和异步FIFO。同步FIFO是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作。异步FIFO是指读写时钟不一致,读写时钟是互相独立的。Xilinx的FIFO IP核可以被配置为同步FIFO或异步FIFO,其信号框图如下图所示。从图中可以了解到,当被配置为同步FIFO时,只使用WR_CLK,所有的输入输出信号都同步于WR_CLK信号。而当被配置为异步FIFO时,写端口和读端口分别有独立的时钟,所有与写相关的信号都是同步于写时钟WR_CLK,所有与读相关的信号都是同步于读时钟RD_CLK。
对于FIFO需要了解一些常见参数。 FIFO的宽度:FIFO一次读写操作的数据位宽N。 FIFO的深度:FIFO可以存储多少个宽度为N位的数据。 空标志:empty。FIFO已空时由FIFO的状态电路送出的一个信号,用以阻止FIFO的读操作继续从FIFO中读出数据从而造成无效数据的读出。 将空标志:almost_empty。FIFO即将被读空。 满标志:full。FIFO已满时由FIFO的状态电路送出的一个信号,用以阻止FIFO的写操作继续向FIFO中写数据从而造成溢出。 将满标志:almost_full。FIFO即将被写满。 读时钟:读FIFO时所遵循的时钟,读操作在时钟的上升沿触发。 请注意,“almost_empty”和“almost_full”这两个信号分别被看作“empty”和“full”的警告信号,它们距离真正的空(empty)和满(full)都会提前一个时钟周期拉高。本实验将使用这两个信号。 写时钟:写FIFO时所遵循的时钟,写操作在每个时钟的上升沿触发。 对于FIFO的基本知识先了解这些就足够了,可能有人会好奇为什么会有同步FIFO和异步FIFO,它们各自的用途是什么。之所以有同步FIFO和异步FIFO是因为各自的作用不同,同步FIFO常用于同步时钟的数据缓存,异步FIFO常用于跨时钟域的数据信号的传递。例如时钟域A下的数据data1传递给异步时钟域B,当data1为连续变化信号时,如果直接传递给时钟域B则可能会导致所收非所送的情况(即在采集过程中会出现包括亚稳态问题在内的一系列问题),使用异步FIFO能够将不同时钟域中的数据同步到所需的时钟域中。 1.2 实验任务本节的实验任务是使用ISE生成FIFO IP核,并实现以下功能:当FIFO为空时,向FIFO中写入数据,写入的数据量和FIFO深度一致,即FIFO被写满;然后从FIFO中读出数据,直到FIFO被读空为止,以此向大家详细介绍一下FIFO IP核的使用方法。 1.3 硬件设计本章实验只用到了输入的时钟信号和按键复位信号,没有用到其它硬件外设。本实验中各端口信号的管脚分配如下表所示: 表18.3.1 引脚分配表 对应的UCF约束语句如下所示: - <font size="4">NETsys_clk TNM_NET = sys_clk_pin;
- TIMESPECTS_sys_clk_pin = PERIOD sys_clk_pin 50000 kHz;
- NETsys_clk LOC =N8 | IOSTANDARD = "LVCMOS33";
- NETsys_rst_n LOC = G16 |IOSTANDARD = "LVCMOS33";</font>
复制代码 1.4 程序设计根据实验任务要求和模块化的设计思想,我们需要4个模块:fifo IP核、写fifo模块、读fifo模块以及顶层例化模块,其中顶层例化模块主要用来实现前三个模块的信号交互。由于FIFO多用于跨时钟域信号的处理,所以本实验使用异步FIFO来向大家详细介绍双时钟FIFO IP核的创建和使用。为了方便大家理解,这里将读/写时钟都用系统时钟来驱动。系统的功能框图如下图所示:
首先创建一个名为ip_fifo的工程,接下来创建fifo IP核。在ISE软件的左侧“Design”栏中右击工程名按钮,打开添加新文件选项栏,我们选择New Source,如下图所示:
打开NewSource之后选择IP(COREGenerator&Architecture Wizard),然后将FIFOIP核命名为fifo(也可以命名成别的名字,但是必须是英文命名),路径放在工程文件夹下,ISE会自动创建一个ipcore_dir文件夹用来存放IP核文件,如下图所示:
如上图所示步骤设置完成后点击Next,进入IP核选择界面。在SearchIP Catalog搜索栏中搜索fifo,然后选择FIFO Generator,如下图所示:
按上图所示步骤操作之后点击Next进入Summary界面,如下图所示:
在Summary界面直接点击Finish,接下来就是配置IP核参数的过程,如下图所示:
在上图中: Component Name:FIFOIP核的命名,这里会直接显示之前创建IP时的命名。 Interface Type:Native本机接口FIFO,可以利用区块RAM、分布式RAM或内置的FIFO资源;AX14除了支持Native的应用功能外,还可以用于AX14系统总线和点对点高速应用程序,但是AX14FIFO不支持内置的FIFO与移位寄存器的FIFO设置。 选择Native类型然后点击Next,进入下一页参数设置,如下图所示:
上图中: BlockRAM:FPGA内部硬件存在的块RAM资源。 DistributeRAM:分布式RAM资源,使用FPGA内部的寄存器和查找表搭建起来的RAM,当深度要求小于32的时候可以使用。 ShiftRegister:使用FIFO产生一个移位寄存器。 Commonclock:同步时钟。 Independentclock:异步时钟。 选择Independent clocks选项并使用Block RAM资源,然后点击Next进入下一页参数设置,如下图所示:
上图中: Read Mode:选择StandardFIFO,标准FIFO模型。 Write Width:写数据位宽。 Write Depth:写数据深度,可以理解为FIFO的数据容量。 Read Width:读数据位宽。 这里按照上图所示设置好后,点击Next进入下一个参数页面设置,如下图所示:
上图中: Almost Full Flag:将满标志。 Almost Empty Flag:将空标志。 Write Acknowledge Flag:写应答标志,可以在其下方设置高电平有效(ActiveHigh)还是低电平有效(ActiveLow)。 Overflow Flag:当写指针追到了读指针,然后还继续写就会给出OverflowFlag标志,可以在其下方设置高电平有效(ActiveHigh)或低电平有效(ActiveLow)用来表示写错误。 Vaild Flag:读有效,可以在其下方设置高电平有效(ActiveHigh)还是低电平有效(ActiveLow)。 Underflow Flag:当读指针追到了写指针,然后还继续读就会给出underflowFlag标志,可以在其下方设置高电平有效(ActiveHigh)或低电平有效(ActiveLow)用来表示读错误。 按照上图所示设置后,点击Next进入下一参数设置页面,如下图所示:
上图所示的这一页主要是复位的设置,这里我们不用作修改,保持默认,直接点击Next进入下一参数设置页面,如下图所示:
上图所示把WriteData Count和ReadData Count读写计数器选上,主要用来观察写进去了多少个数据,还剩多少个数据可读。注意读写计数器的位宽要足够大,保证计数器的最大值大于fifo的深度,否则计数器会溢出。之后点击Next进入下一参数设置页面,如下图所示:
上图所示的是要创建的FIFOIP核的Summary界面,检查一下设置,如果有问题就点击Back返回重新设置参数,如果没问题就点击Generate生成FIFO IP核。 回到Design工具栏下可以看到创建的FIFO IP核已经出现在工程栏下了,如下图所示:
现在就可以添加设计源文件了。本次是对FIFOIP核进行读写,所以需要一个读模块、一个写模块还要一个顶层例化模块。下面先来看顶层例化模块: - <font size="4">module fifo_top(
- input sys_clk ,// 时钟信号
- input sys_rst_n // 复位信号
- );
- wire [7:0 fifo_din ;
- wire fifo_wr_en ;
- wire fifo_rd_en ;
- wire [7:0 fifo_dout ;
- wire fifo_full ;
- wire almost_full ;
- wire wr_ack ;
- wire overflow ;
- wire fifo_empty ;
- wire almost_empty ;
- wire valid ;
- wire underflow ;
- wire [7:0fifo_wr_data_count ;
- wire [7:0fifo_rd_data_count ;
- fifo u_fifo (
- .wr_clk (sys_clk ), // input wr_clk
- .rd_clk (sys_clk ), // input rd_clk
- .din (fifo_din ), // input [7 : 0] din
- .wr_en (fifo_wr_en ), // input wr_en
- .rd_en (fifo_rd_en ), // input rd_en
- .dout (fifo_dout ),// output [7 : 0] dout
- .full (fifo_full ), // output full
- .almost_full (almost_full ), // output almost_full
- .wr_ack (wr_ack ), // output wr_ack
- .overflow (overflow ), // output overflow
- .empty (fifo_empty ), // output empty
- .almost_empty (almost_empty ), // output almost_empty
- .valid (valid ), // output valid
- .underflow (underflow ), // output underflow
- .rd_data_count (fifo_wr_data_count ),// output [7 : 0] rd_data_count
- .wr_data_count (fifo_rd_data_count ) //output [7 : 0] wr_data_count
- );
- fifo_wr u_fifo_wr (
- .clk ( sys_clk ), // 写时钟
- .rst_n ( sys_rst_n ), // 复位信号
-
- .fifo_wr_en ( fifo_wr_en ), // fifo写请求
- .fifo_wr_data (fifo_din ), // 写入FIFO的数据
- .almost_empty (almost_empty ), // fifo空信号
- .almost_full ( almost_full ) // fifo满信号
- );
- fifo_rd u_fifo_rd (
- .clk ( sys_clk ), // 读时钟
- .rst_n ( sys_rst_n ), // 复位信号
- .fifo_rd_en ( fifo_rd_en ), // fifo读请求
- .fifo_dout ( fifo_dout ), // 从FIFO输出的数据
- .almost_empty (almost_empty ), // fifo空信号
- .almost_full ( almost_full ) // fifo满信号
- );
- endmodule</font>
复制代码顶层模块主要是对FIFOIP核、写FIFO模块和读FIFO模块进行例化。需要注意的是从整个顶层来看,工程对外没有任何输出,所有的数据读写都是在FPGA内部进行的,编译器会认为我们的代码端口对外界不起作用,所以ISE会编译报错,不过没关系,我们可以将需要观察的信号设置成output型,这样既可以防止在Chipscope中信号被综合优化,又可以使工程编译通过。当然你也可以不设置为输出,创建一个Chipscope的在线调试文件(可以参考软件使用篇),将待观察信号接到在线调试IP核上,这样编译也可以编译通过,然后通过Chipscope观察信号。(特别注意:刚拿到我们的例程是没有对端口作处理的,直接编译会报错)。 顶层模块完成后,接着看看例化的子模块代码。fifoip核模块就不用看了,因为fifoip核是我们配置参数后软件自动生成的,主要来看看读写模块的代码。下面是写FIFO模块(fifo_wr.v)源文件的代码: - <font size="4">module fifo_wr(
- input clk , // 时钟信号
- input rst_n , // 复位信号
-
- input almost_empty, //FIFO将空信号
- input almost_full , //FIFO将满信号
- output reg fifo_wr_en , // FIFO写使能
- output reg [7:0 fifo_wr_data // 写入FIFO的数据
- );
- //reg define
- reg [1:0 state ; //动作状态
- reg almost_empty_d0 ; //almost_empty 延迟一拍
- reg almost_empty_syn ; //almost_empty延迟两拍
- reg [3:0 dly_cnt ; //延迟计数器
- //*****************************************************
- //** main code
- //*****************************************************
- //因为almost_empty 信号是属于FIFO读时钟域的
- //所以要将其同步到写时钟域中
- always@( posedge clk ) begin
- if( !rst_n ) begin
- almost_empty_d0 <= 1'b0 ;
- almost_empty_syn <= 1'b0 ;
- end
- else begin
- almost_empty_d0 <= almost_empty ;
- almost_empty_syn <= almost_empty_d0 ;
- end
- end
- //向FIFO中写入数据
- always @(posedge clk ) begin
- if(!rst_n) begin
- fifo_wr_en <=1'b0;
- fifo_wr_data <= 8'd0;
- state <= 2'd0;
- dly_cnt <= 4'd0;
- end
- else begin
- case(state)
- 2'd0: begin
- if(almost_empty_syn) begin //如果检测到FIFO将被读空(下一拍就会空)
- state <= 2'd1; //就进入延时状态
- end
- else
- state <= state;
- end
- 2'd1: begin
- if(dly_cnt == 4'd10) begin //延时10拍
- //原因是FIFO IP核内部状态信号的更新存在延时
- //延迟10拍以等待状态信号更新完毕
- dly_cnt <= 4'd0;
- state <= 2'd2; //开始写操作
- fifo_wr_en <= 1'b1; //打开写使能
- end
- else
- dly_cnt <= dly_cnt + 4'd1;
- end
- 2'd2: begin
- if(almost_full) begin //等待FIFO将被写满(下一拍就会满)
- fifo_wr_en <=1'b0; //关闭写使能
- fifo_wr_data <= 8'd0;
- state <= 2'd0; //回到第一个状态
- end
- else begin //如果FIFO没有被写满
- fifo_wr_en <=1'b1; //则持续打开写使能
- fifo_wr_data <= fifo_wr_data + 1'd1; //且写数据值持续累加
- end
- end
- default : state <= 2'd0;
- endcase
- end
- end
- endmodule</font>
复制代码fifo_wr模块的核心部分是一个不断进行状态循环的状态机,如果检测到FIFO为空,则先延时10拍,这里注意,由于FIFO的内部信号的更新比实际的数据读/写操作有所延时,所以延时10拍的目的是等待FIFO的空/满状态信号、数据计数信号等信号更新完毕之后再进行FIFO写操作。如果写满,则回到状态0,即等待FIFO被读空,进行下一轮的写操作。 读FIFO模块fifo_rd.v源文件的代码如下: - <font size="4">module fifo_rd(
- input clk , //时钟信号
- input rst_n , // 复位信号
- input [7:0 fifo_dout , // 从FIFO读出的数据
- input almost_full , // FIFO将满信号
- input almost_empty , //FIFO将空信号
- output reg fifo_rd_en // FIFO读使能
- );
- //reg define
- reg [1:0 state ; //动作状态
- reg almost_full_d0 ; //almost_full延迟一拍
- reg almost_full_syn ; //almost_full延迟两拍
- reg [3:0 dly_cnt ; //延迟计数器
- //*****************************************************
- //** main code
- //*****************************************************
- //因为fifo_full 信号是属于FIFO写时钟域的
- //所以要将其同步到读时钟域中
- always@( posedge clk ) begin
- if( !rst_n ) begin
- almost_full_d0 <= 1'b0 ;
- almost_full_syn <= 1'b0 ;
- end
- else begin
- almost_full_d0 <= almost_full ;
- almost_full_syn <= almost_full_d0 ;
- end
- end
- //读出FIFO的数据
- always @(posedge clk ) begin
- if(!rst_n) begin
- fifo_rd_en <= 1'b0;
- state <= 2'd0;
- dly_cnt <= 4'd0;
- end
- else begin
- case(state)
- 2'd0: begin
- if(almost_full_syn) //如果检测到FIFO被写满
- state <= 2'd1; //就进入延时状态
- else
- state <= state;
- end
- 2'd1: begin
- if(dly_cnt == 4'd10) begin //延时10拍
- //原因是FIFO IP核内部状态信号的更新存在延时
- //延迟10拍以等待状态信号更新完毕
- dly_cnt <= 4'd0;
- state <=2'd2; //开始读操作
- end
- else
- dly_cnt <= dly_cnt + 4'd1;
- end
- 2'd2: begin
- if(almost_empty) begin //等待FIFO将被读空(下一拍就会空)
- fifo_rd_en <= 1'b0; //关闭读使能
- state <= 2'd0; //回到第一个状态
- end
- else //如果FIFO没有被读空
- fifo_rd_en <= 1'b1; //则持续打开读使能
- end
- default : state <= 2'd0;
- endcase
- end
- end
- endmodule</font>
复制代码读模块的代码结构与写模块几乎一样,也是使用一个不断进行状态循环的状态机来控制操作过程,代码比较简单,这里就不再赘述。 下面我们来对写好的工程进行仿真验证一下功能,在仿真前我们先对顶层文件(fifo_top)做一下处理如下图所示:
将需要观察的信号全部重新定义一遍,并加上后缀为“_tb”,然后使用assign语句把原信号赋值给重新定义的待观察信号,最后把所有带后缀“_tb”的待观察信号当作输出端口输出,这样无论是写Tsetbench仿真文件还是使用Chipscope在线调试都比较方便,编译不会报错,抓取信号也不会被综合优化。 将顶层文件处理好后就可以生成Testbench文件了(不会生成Testbench文件的可以参考软件使用篇),模板生成完后写激励如下所示: - <font size="4">`timescale 1ns / 1ps
- module fifo_top_tb;
- // Inputs
- regsys_clk;
- regsys_rst_n;
-
- wire [7:0 fifo_din_tb ;//仿真在线调试专用
- wire fifo_wr_en_tb ;
- wire fifo_rd_en_tb ;
- wire [7:0 fifo_dout_tb ;
- wire fifo_full_tb ;
- wire almost_full_tb ;
- wire wr_ack_tb ;
- wire overflow_tb ;
- wire fifo_empty_tb ;
- wire almost_empty_tb ;
- wire valid_tb ;
- wire underflow_tb ;
- wire [7:0 fifo_wr_data_count_tb ;
- wire [7:0 fifo_rd_data_count_tb ;
-
- // Instantiate the Unit Under Test (UUT)
- fifo_top uut (
- .sys_clk (sys_clk ),
- .sys_rst_n (sys_rst_n ),
- .fifo_din_tb (fifo_din_tb ),
- .fifo_wr_en_tb (fifo_wr_en_tb ),
- .fifo_rd_en_tb (fifo_rd_en_tb ),
- .fifo_dout_tb (fifo_dout_tb ),
- .fifo_full_tb (fifo_full_tb ),
- .almost_full_tb (almost_full_tb ),
- .wr_ack_tb (wr_ack_tb ),
- .overflow_tb (overflow_tb ),
- .fifo_empty_tb (fifo_empty_tb ),
- .almost_empty_tb (almost_empty_tb ),
- .valid_tb (valid_tb ),
- .underflow_tb (underflow_tb ),
- .fifo_wr_data_count_tb (fifo_wr_data_count_tb ),
- .fifo_rd_data_count_tb (fifo_rd_data_count_tb )
-
- );
-
- initial begin
- // Initialize Inputs
- sys_clk =0;
- sys_rst_n =0;
-
- // Wait 100 ns for global reset to finish
- #100;
- sys_rst_n=1;
- // Add stimulus here
-
- end
- always #10sys_clk=~sys_clk;
- endmodule</font>
复制代码Testbench文件完成后点击保存,然后点击Modelsim仿真按钮,调用Modelsim进行仿真(不熟悉的可以参考Modelsim章节)。 仿真波形如下图所示:
上图是整体波形,从图中可以看到读写是交替进行的,处于写状态时写使能打开(fifo_wr_en_tb)、写数据跳变(fifo_din_tb)、写有效标志拉高(wr_ack_tb)、写数据计数器计数(fifo_wr_data_count_tb)、没有出现写错误,写错误信号保持低电平(overflow_tb),当数据写结束时先提前一个时钟拉高将满信号(almost_full_tb),再拉高写满信号(fifo_full_tb);读也是一样的,处于读状态时读使能打开(fifo_rd_en_tb)、读数据读出(fifo_dout_tb)、读有效标志拉高(valid_tb)、读数据计数器计数(fifo_rd_data_count_tb)、没有出现读错误,读错误信号保持低电平(underflow_tb)、当数据读结束时先提前一个时钟拉高将空信号(almost_empty_tb),再拉高读空信号(fifo_empty_tb); 我们放大波形图来看一下细节如下图所示:
上图是处于写状态时的细节,可以看到一共写了255个数(0~254),在快写满时确实是先拉高将满信号,再拉高写满信号;看完了写状态的细节我们再看看读状态的细节,如下图所示:
读状态也可以看到它读出的数据跟写入的一模一样,也是先拉高将空信号,再拉高读空信号。这就说明我们的FIFOIP核工程目前仿真功能是完全正常的,接下来就可以添加UCF文件进行管脚分配和时序约束了(不熟悉的可以参考软件使用篇),最后我们使用Chipscope工具进行下板子实际验证一下功能是否正确。 1.5 下载验证首先将下载器与超越者开发板上的JTAG接口连接,下载器另外一端与电脑连接,然后连接开发板的电源,并打开电源开关。 打开Chipscope工具,将刚刚生成好的逻辑分析仪IP核下载到板子上去,然后运行如下图所示:
上图中可以看到波形与仿真的波形是一样的,为了进一步确认功能,可以将波形放大看一下细节如下图所示:
上图是写数据时的细节,跟我们仿真图是一样的,说明写数据没有问题,我们再看看读数据时的波形细节,如下图所示:
可以看到读状态波形的细节也是符合预期,到此就可以说明本次FIFO IP核的功能是正常的,本次例程实验成功。
|