本帖最后由 正点原子运营 于 2023-3-30 10:21 编辑
第十三章 IP核之FIFO实验
1)实验平台:正点原子 DFZU2EG/4EV MPSoC开发板
2) 章节摘自【正点原子】DFZU2EG/4EV MPSoC之FPGA开发指南 V1.0
6)FPGA技术交流QQ群:994244016
FIFO的英文全称是First In First Out,即先进先出。FPGA使用的FIFO一般指的是对数据的存储具有先进先出特性的一个缓存器,常被用于数据的缓存,或者高速异步数据的交互也即所谓的跨时钟域信号传递。它与FPGA内部的RAM和ROM的区别是没有外部读写地址线,采取顺序写入数据,顺序读出数据的方式,使用起来简单方便,由此带来的缺点就是不能像RAM和ROM那样可以由地址线决定读取或写入某个指定的地址。本章我们将对Vivado软件生成的FIFO IP核进行读写测试,来向大家介绍Xilinx FIFO IP核的使用方法。 本章包括以下几个部分: 13.1 FIFO IP核简介 13.2 实验任务 13.3 硬件设计 13.4 程序设计 13.5 下载验证
13.1 FIFO IP核简介根据FIFO工作的时钟域,可以将FIFO分为同步FIFO和异步FIFO。同步FIFO是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作。异步FIFO是指读写时钟不一致,读写时钟是互相独立的。Xilinx的FIFO IP核可以被配置为同步FIFO或异步FIFO,其信号框图如下图所示。从图中可以了解到,当被配置为同步FIFO时,只使用wr_clk,所有的输入输出信号都同步于wr_clk信号。而当被配置为异步FIFO时,写端口和读端口分别有独立的时钟,所有与写相关的信号都是同步于写时钟wr_clk,所有与读相关的信号都是同步于读时钟rd_clk。 图13.1.1Xilinx的FIFO IP核的信号框图
对于FIFO需要了解一些常见参数: FIFO的宽度:FIFO一次读写操作的数据位N。 FIFO的深度:FIFO可以存储多少个宽度为N位的数据。 将空标志:almost_empty。FIFO即将被读空。 空标志:empty。FIFO已空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的读出。 将满标志:almost_full。FIFO即将被写满。 满标志:full。FIFO已满或将要写满时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出。 读时钟:读FIFO时所遵循的时钟,在每个时钟的上升沿触发。 写时钟:写FIFO时所遵循的时钟,在每个时钟的上升沿触发。
这里请注意,“almost_empty”和“almost_full”这两个信号分别被看作“empty”和“full”的警告信号,他们相对于真正的空(empty)和满(full)都会提前一个时钟周期拉高。
对于FIFO的基本知识先了解这些就足够了,可能有人会好奇为什么会有同步FIFO和异步FIFO,它们各自的用途是什么。之所以有同步FIFO和异步FIFO是因为各自的作用不同。同步FIFO常用于同步时钟的数据缓存,异步FIFO常用于跨时钟域的数据信号的传递,例如时钟域A下的数据data1传递给异步时钟域B,当data1为连续变化信号时,如果直接传递给时钟域B则可能会导致收非所送的情况,即在采集过程中会出现包括亚稳态问题在内的一系列问题,使用异步FIFO能够将不同时钟域中的数据同步到所需的时钟域中。
13.2 实验任务本节的实验任务是使用Vivado生成FIFO IP核,并实现以下功能:当FIFO为空时,向FIFO中写入数据,写入的数据量和FIFO深度一致,即FIFO被写满;然后从FIFO中读出数据,直到FIFO被读空为止,以此向大家详细介绍一下FIFO IP核的使用方法。
13.3 硬件设计本章实验只用到了输入的时钟信号和按键复位信号,没有用到其它硬件外设。 本实验中,各端口信号的管脚分配如下表所示。 表 13.3.1 IP实验管脚分配
对应的XDC约束语句如下所示: - #IO管脚约束
- #时钟周期约束
- create_clock -name sys_clk_p -period 10.000 [get_portssys_clk_p]
- #时钟
- set_property IOSTANDARD DIFF_HSTL_I_12 [get_portssys_clk_p]
- set_property IOSTANDARD DIFF_HSTL_I_12 [get_portssys_clk_n]
- set_property PACKAGE_PIN AE5 [get_ports sys_clk_p]
- set_property PACKAGE_PIN AF5 [get_ports sys_clk_n]
- #复位
- set_property -dict {PACKAGE_PIN AH11 IOSTANDARD LVCMOS33}[get_ports sys_rst_n]
复制代码
13.4 程序设计根据实验任务要求和模块化设计的思想,我们需要如下4个模块:fifo IP核、写fifo模块、读fifo模块以及顶层例化模块实现前三个模块的信号交互。由于FIFO多用于跨时钟域信号的处理,所以本实验我们使用异步FIFO来向大家详细介绍双时钟FIFO IP核的创建和使用。为了方便大家理解,这里我们将读/写时钟都用系统时钟来驱动。系统的功能框图如下图所示:
图 13.4.1 系统框图
首先创建一个名为ip_fifo的工程,接下来我们创建fifo IP核。在Vivado软件的左侧“Flow Navigator”栏中单击“IP Catalog”,“IP Catalog”按钮以及单击后弹出的“IP Catalog”窗口如下图所示。
图 13.4.2 “IP Catalog”按钮
图 13.4.3 “IP Catalog”窗口
在“IP Catalog”窗口中,在搜索栏中输入“fifo”关键字,这时Vivado会自动查找出与关键字匹配的IP核名称,我们双击“FIFO Generator”,如下图所示。
图 13.4.4 搜索栏中输入关键字
弹出“CustomizeIP”窗口,如下图所示。
图 13.4.5 “Customize IP”窗口
接下来就是配置IP核的时钟参数的过程。
最上面的“ComponentName”一栏设置该IP元件的名称,这里保持默认即可。在第一个“Basic”选项卡中,“Interface Type”选项用于选择FIFO接口的类型,这里我们选择默认的“Native”,即传统意义上的FIFO接口。“Fifo Implementation”选项用于选择我们想要实现的是同步FIFO还是异步FIFO以及使用哪种资源实现FIFO,这里我们选择“Independent Clocks Block RAM”,即使用块RAM来实现的异步FIFO。如下图所示。
图 13.4.6 “Basic”选项卡
接下来是“NativePorts”选项卡,用于设置FIFO端口的参数。“Read Mode”选项用于设置读FIFO时的读模式,这里我们选择默认的“Standard FIFO”。“Data Port Parameters”一栏用于设置读写端口的数据总线的宽度以及FIFO的深度,写宽度“Write Width”我们设置为8位,写深度“Write Depth”我们设置为256,注意此时FIFO IP核所能实现的实际深度却是255;虽然读宽度“Read Width”能够设置成和写宽度不一样的位宽,且此时读深度“Read Depth”会根据上面三个参数被动地自动设置成相应的值;但是我们还是将读宽度“Read Width”设置成和写宽度“Write Width”一样的位宽,这也是在实际应用中最常用的情况。由于我们只是观察FIFO的读写,所以最下面的“Reset Pin”选项我们可以不使用,把它取消勾选。其他设置保持默认即可,如下图所示。
图 13.4.7 “Native Ports”选项卡
“Status Flags”选项卡,用于设置用户自定义接口或者用于设定专用的输入口。这里我们使用“即将写满”和“即将读空”这两个信号,所以我们把它们勾选上,其他保持默认即可,如下图所示。
图 13.4.8 “Status Flags”选项卡
“Data Counts”选项卡用于设置FIFO内数据计数的输出信号,此信号表示当前在FIFO内存在多少个有效数据。为了更加方便地观察读/写过程,这里我们把读/写端口的数据计数都打开,且计数值总线的位宽设置为满位宽,即8位,如下图所示。
图 13.4.9 “Data Counts”选项卡
最后的“Summary”选项卡是对前面所有配置的一个总结,在这里我们直接点击“OK”按钮即可,如下图所示。
图 13.4.10 “Summary”选项卡
接着就弹出了“GenarateOutput Products”窗口,我们直接点击“Generate”即可,如下图所示。
图 13.4.11 “Genarate Output Products”窗口
之后我们就可以在“DesignRun”窗口的“Out-of-Context Module Runs”一栏中出现了该IP核对应的run“fifo_generator_0_synth_1”,其综合过程独立于顶层设计的综合,所以在我们可以看到其正在综合,如下图所示。
图 13.4.12 “fifo_generator _0_synth_1”run
在其Out-of-Context综合的过程中,我们就可以进行RTL编码了。首先打开IP核的例化模板,在“Source”窗口中的“IP Sources”选项卡中,依次用鼠标单击展开“IP”-“fifo_generator _0”-“Instantitation Template”,我们可以看到“fifo_generator_0.veo”文件,它是由IP核自动生成的只读的verilog例化模板文件,双击就可以打开它,如下图所示。
图 13.4.13 “fifo_generator_0.veo”文件
我们创建一个verilog源文件,其名称为ip_fifo.v,作为顶层模块,其代码如下: - 1 module ip_fifo(
- 2 input sys_clk_p , //系统差分输入时钟
- 3 input sys_clk_n , //系统差分输入时钟
- 4 input sys_rst_n // 复位信号
- 5 );
- 6
- 7 //转换差分信号
- 8 IBUFDS diff_clock
- 9 (
- 10 .I (sys_clk_p), //系统差分输入时钟
- 11 .IB(sys_clk_n), //系统差分输入时钟
- 12 .O (sys_clk) //输出系统时钟
- 13 );
- 14
- 15 //wire define
- 16 (*mark_debug = "true"*) wire fifo_wr_en ; // FIFO写使能信号
- 17 (*mark_debug = "true"*) wire fifo_rd_en ; // FIFO读使能信号
- 18 (*mark_debug = "true"*) wire [7:0 fifo_din ; // 写入到FIFO的数据
- 19 (*mark_debug = "true"*) wire [7:0 fifo_dout ; // 从FIFO读出的数据
- 20 (*mark_debug = "true"*) wire almost_full ; // FIFO将满信号
- 21 (*mark_debug = "true"*) wire almost_empty ; // FIFO将空信号
- 22 (*mark_debug = "true"*) wire fifo_full ; // FIFO满信号
- 23 (*mark_debug = "true"*) wire fifo_empty ; // FIFO空信号
- 24 (*mark_debug = "true"*) wire [7:0 fifo_wr_data_count ;
- 25 // FIFO写时钟域的数据计数
- 26 (*mark_debug = "true"*) wire [7:0 fifo_rd_data_count ;
- 27 // FIFO读时钟域的数据计数
- 28
- 29 //*****************************************************
- 30 //** main code
- 31 //*****************************************************
- 32
- 33 //例化FIFOIP核
- 34 fifo_generator_0 u_fifo_generator_0(
- 35 .wr_clk (sys_clk), // input wire wr_clk
- 36 .rd_clk (sys_clk), // input wire rd_clk
- 37 .din (fifo_din), // input wire [7 : 0] din
- 38 .wr_en (fifo_wr_en), // input wire wr_en
- 39 .rd_en (fifo_rd_en), // input wire rd_en
- 40 .dout (fifo_dout), // output wire [7 : 0] dout
- 41 .full (fifo_full), // output wire full
- 42 .almost_full (almost_full), // output wire almost_full
- 43 .empty (fifo_empty), // output wire empty
- 44 .almost_empty (almost_empty), // output wire almost_empty
- 45 .rd_data_count (fifo_rd_data_count),
- 46 // output wire [7 : 0]rd_data_count
- 47 .wr_data_count (fifo_wr_data_count)
- 48 // output wire [7 : 0]wr_data_count
- 49 );
- 50
- 51 //例化写FIFO模块
- 52 fifo_wr u_fifo_wr(
- 53 .clk ( sys_clk ), // 写时钟
- 54 .rst_n ( sys_rst_n ), // 复位信号
- 55
- 56 .fifo_wr_en ( fifo_wr_en ) , // fifo写请求
- 57 .fifo_wr_data ( fifo_din ) , // 写入FIFO的数据
- 58 .almost_empty ( almost_empty ), // fifo空信号
- 59 .almost_full ( almost_full ) // fifo满信号
- 60 );
- 61
- 62 //例化读FIFO模块
- 63 fifo_rd u_fifo_rd(
- 64 .clk ( sys_clk ), // 读时钟
- 65 .rst_n ( sys_rst_n ), // 复位信号
- 66
- 67 .fifo_rd_en ( fifo_rd_en ), // fifo读请求
- 68 .fifo_dout ( fifo_dout ), // 从FIFO输出的数据
- 69 .almost_empty ( almost_empty ), // fifo空信号
- 70 .almost_full ( almost_full ) // fifo满信号
- 71 );
- 72
- 73 endmodule
复制代码顶层模块主要是对FIFOIP核、写FIFO模块、读FIFO模块进行例化,除此之外本实验还生成并例化了一个ILA IP核,用于对顶层模块信号的进行在线捕获观察。
写FIFO模块fifo_wr.v源文件的代码如下: - 1 module fifo_wr(
- 2 input clk , // 时钟信号
- 3 input rst_n , // 复位信号
- 4
- 5 input almost_empty, // FIFO将空信号
- 6 input almost_full , // FIFO将满信号
- 7 output reg fifo_wr_en , // FIFO写使能
- 8 output reg [7:0] fifo_wr_data // 写入FIFO的数据
- 9 );
- 10
- 11 //reg define
- 12 reg [1:0] state ; //动作状态
- 13 reg almost_empty_d0 ; //almost_empty 延迟一拍
- 14 reg almost_empty_syn ; //almost_empty 延迟两拍
- 15 reg [3:0] dly_cnt ; //延迟计数器
- 16 //*****************************************************
- 17 //** main code
- 18 //*****************************************************
- 19
- 20 //因为 almost_empty 信号是属于FIFO读时钟域的
- 21 //所以要将其同步到写时钟域中
- 22 always@( posedge clk ) begin
- 23 if( !rst_n ) begin
- 24 almost_empty_d0 <= 1'b0 ;
- 25 almost_empty_syn <= 1'b0 ;
- 26 end
- 27 else begin
- 28 almost_empty_d0 <= almost_empty ;
- 29 almost_empty_syn <= almost_empty_d0 ;
- 30 end
- 31 end
- 32
- 33 //向FIFO中写入数据
- 34 always @(posedge clk ) begin
- 35 if(!rst_n) begin
- 36 fifo_wr_en <= 1'b0;
- 37 fifo_wr_data <= 8'd0;
- 38 state <= 2'd0;
- 39 dly_cnt <= 4'd0;
- 40 end
- 41 else begin
- 42 case(state)
- 43 2'd0: begin
- 44 if(almost_empty_syn) begin //如果检测到FIFO将被读空(下一拍就会空)
- 45 state <= 2'd1; //就进入延时状态
- 46 end
- 47 else
- 48 state <= state;
- 49 end
- 50 2'd1: begin
- 51 if(dly_cnt == 4'd10) begin //延时10拍
- 52 //原因是FIFO IP核内部状态信号的更新存在延时
- 53 //延迟10拍以等待状态信号更新完毕
- 54 dly_cnt <= 4'd0;
- 55 state <= 2'd2; //开始写操作
- 56 fifo_wr_en <= 1'b1; //打开写使能
- 57 end
- 58 else
- 59 dly_cnt <= dly_cnt + 4'd1;
- 60 end
- 61 2'd2: begin
- 62 if(almost_full) begin //等待FIFO将被写满(下一拍就会满)
- 63 fifo_wr_en <= 1'b0; //关闭写使能
- 64 fifo_wr_data <= 8'd0;
- 65 state <= 2'd0; //回到第一个状态
- 66 end
- 67 else begin //如果FIFO没有被写满
- 68 fifo_wr_en <= 1'b1; //则持续打开写使能
- 69 fifo_wr_data <= fifo_wr_data + 1'd1; //且写数据值持续累加
- 70 end
- 71 end
- 72 default : state <= 2'd0;
- 73 endcase
- 74 end
- 75 end
- 76
- 77 endmodule
复制代码 fifo_wr模块的核心部分是一个不断进行状态循环的小状态机,如果检测到FIFO为空,则先延时10拍,这里注意,由于FIFO的内部信号的更新比实际的数据读/写操作有所延时,所以延时10拍的目的是等待FIFO的空/满状态信号、数据计数信号等信号的更新完毕之后再进行FIFO写操作,如果写满,则回到状态0,即等待FIFO被读空,以进行下一轮的写操作。
读FIFO模块fifo_rd.v源文件的代码如下: - 1 module fifo_rd(
- 2 input clk , // 时钟信号
- 3 input rst_n , // 复位信号
- 4
- 5 input [7:0] fifo_dout , // 从FIFO读出的数据
- 6 input almost_full , // FIFO将满信号
- 7 input almost_empty , // FIFO将空信号
- 8 output reg fifo_rd_en // FIFO读使能
- 9 );
- 10
- 11 //reg define
- 12 reg [1:0] state ; //状态
- 13 reg almost_full_d0 ; //almost_full延迟一拍
- 14 reg almost_full_syn ; //almost_full延迟两拍
- 15 reg [3:0] dly_cnt ; //延迟计数器
- 16
- 17 //*****************************************************
- 18 //** main code
- 19 //*****************************************************
- 20
- 21 //因为 fifo_full 信号是属于FIFO写时钟域的
- 22 //所以要将其同步到读时钟域中
- 23 always@( posedge clk ) begin
- 24 if( !rst_n ) begin
- 25 almost_full_d0 <= 1'b0 ;
- 26 almost_full_syn <= 1'b0 ;
- 27 end
- 28 else begin
- 29 almost_full_d0 <= almost_full ;
- 30 almost_full_syn <= almost_full_d0 ;
- 31 end
- 32 end
- 33
- 34 //读出FIFO的数据
- 35 always @(posedge clk ) begin
- 36 if(!rst_n) begin
- 37 fifo_rd_en <= 1'b0;
- 38 state <= 2'd0;
- 39 dly_cnt <= 4'd0;
- 40 end
- 41 else begin
- 42 case(state)
- 43 2'd0: begin
- 44 if(almost_full_syn) //如果检测到FIFO被写满
- 45 state <= 2'd1; //就进入延时状态
- 46 else
- 47 state <= state;
- 48 end
- 49 2'd1: begin
- 50 if(dly_cnt == 4'd10) begin //延时10拍
- 51 //原因是FIFO IP核内部状态信号的更新存在延时
- 52 //延迟10拍以等待状态信号更新完毕
- 53 dly_cnt <= 4'd0;
- 54 state <= 2'd2; //开始读操作
- 55 end
- 56 else
- 57 dly_cnt <= dly_cnt + 4'd1;
- 58 end
- 59 2'd2: begin
- 60 if(almost_empty) begin //等待FIFO将被读空(下一拍就会空)
- 61 fifo_rd_en <= 1'b0; //关闭读使能
- 62 state <= 2'd0; //回到第一个状态
- 63 end
- 64 else //如果FIFO没有被读空
- 65 fifo_rd_en <= 1'b1; //则持续打开读使能
- 66 end
- 67 default : state <= 2'd0;
- 68 endcase
- 69 end
- 70 end
- 71
- 72 endmodule
复制代码读模块的代码结构与写模块几乎一样,也是使用一个不断进行状态循环的小的状态机来控制操作过程,读者参考着代码应该很容易能够理解,这里就不再赘述。
我们对代码进行仿真,TestBench中只要送出时钟的复位信号即可。TB文件如下: - 1 module tb_ip_fifo( );
- 2 // Inputs
- 3 reg sys_clk_p;
- 4 reg sys_clk_n;
- 5 reg sys_rst_n;
- 6
- 7 // Instantiate the Unit UnderTest (UUT)
- 8 ip_fifo u_ip_fifo (
- 9 .sys_clk_p (sys_clk_p),
- 10 .sys_clk_n (sys_clk_n),
- 11 .sys_rst_n (sys_rst_n)
- 12 );
- 13
- 14 //Genarate the clk
- 15 always #5 sys_clk_p = ~sys_clk_p;
- 16 always #5 sys_clk_n = ~sys_clk_n;
- 17
- 18 initial begin
- 19 //Initialize Inputs
- 20 sys_clk_p = 1'b0;
- 21 sys_clk_n = 1'b1;
- 22 sys_rst_n = 0;
- 23 //Wait 100 ns for global reset to finish
- 24 #100 ;
- 25 sys_rst_n = 1;
- 26 //Add stimulus here
- 27 end
- 28
- 29 endmodule
复制代码写满后转为读的仿真波形图如下图所示: 图 13.4.14 Vivado仿真波形1
由波形图可知,当写满255个数据后,fifo_full满信号就会拉高。经过延时之后,fifo_rd_en写使能信号拉高,经过一拍之后就开始将fifo中的数据送到fifo_dout端口上。 写满后转为读的仿真波形图如下图所示: 图 13.4.15 Vivado仿真波形2
由波形图可知,当读完255个数据后,fifo_empty空信号就会拉高。经过延时之后,fifo_wr_en写使能信号拉高,经过一拍之后就开始向fifo中继续写入数据。
13.5 下载验证 编译工程并生成比特流.bit文件。将下载器一端连接电脑,另一端与开发板上的JTAG下载口连接,然后连接电源线,最后将开发板的电源拨码开关按键往电源指示灯的方向拨动对开发板进行上电,上电成功后开发板的电源指示灯会亮蓝灯。
点击Vivado左侧“Flow Navigator”窗口最下面的“Open Hardware Manager”,此时Vivado软件识别到下载器,点击“Hardware”窗口中“Progam Device”下载程序,在弹出的界面中选择“Program”下载程序。
下载完成后,接下来在Vivado中会自动出现“hw_ila_1”Dashboard窗口。如下图所示:
图 13.5.1 将探针信号添加到波形窗口中
将有关探针信号添加到波形窗口中,这里我们已经完成信号的添加,方法是点击“hw_ila_1”Dashboard窗口左上角的“+”。同时我们在窗口右下角将“fifo_rd_en”信号添加到触发窗口中且设置为上升沿触发, 单击左上角的触发按钮,如下图所示:
图 13.5.2 触发按钮
最后就看到了ILA捕获得到的数据,展开波形图如下图所示:
图 13.5.3 捕获得到的波形图
从捕获得到的波形图中可以看出,其逻辑行为与仿真波形图中的一致,证明我们的代码正确地实现了预期的功能。 |