前言

FIFO(First in First out)代表先进去的数据先出来,后进去的数据后出来,它是FPGA应用当中非常重要的模块,广泛用于数据的缓存,跨时钟域数据处理等。Vivado软件中提供了FIFO的IP核 , 我们只需通过IP核例化一个FIFO,根据FIFO的读写时序来写入和读取FIFO中存储的数据。


一、FIFO简介

FIFO是在RAM的基础上增加了许多功能,FIFO的典型结构如下图所示。
在这里插入图片描述
FIFO主要分为读和写两部分,状态信号有空信号和满信号,同时还有数据数量状态信号。FIFO与RAM最大的不同是其没有地址线,因此不能进行随机地址读取数据(随机读取数据就是可以任意读取某个地址的数据),这样的好处是不用频繁地控制地址线。
虽然用户看不到地址线,但FIFO的内部还是有地址的操作的,用来控制RAM的读写接口,其地址在读写操作时如下图所示。
在这里插入图片描述
上图中的深度值就是一个FIFO里最大可以存放多少个数据。
在初始状态下,读写地址都为0,在向FIFO中写入一个数据后,写地址加1,从FIFO中读出一个数据后,读地址加1,此时FIFO的状态即为空,因为写了一个数据,又读出了一个数据。
如果写入数据的频率比读取数据的频率快,那么FIFO就会有满的时候,即到达深度值的时候。如果FIFO满了之后还继续写入数据就会溢出。
如果读取数据的频率比写入数据的频率快,那么FIFO就会有空的时候,所以判断FIFO空与满的状态,把握好读取数据与写入数据的频率,保持FIFO中一直有数据是一项艰巨的任务。
根据读写时钟的异同,FIFO可以分为同步FIFO(读和写的时钟相同)和异步FIFO(读和写的时钟不同)。同步FIFO控制比较简单,本节实验主要介绍异步FIFO的控制,其中读时钟为75MHz,写时钟为100MHz。
本实验中的端口定义如下表。
在这里插入图片描述
FIFO的数据写入和读出都是按时钟的上升沿操作的,当wr_en信号为高时写入FIFO数据,当almost_full信号有效时,表示FIFO只能再写入一个数据,一旦写入一个数据了,full信号就会拉高,如果在full的情况下wr_en仍然有效,也就是继续向FIFO写数据,则FIFO的overflow就会有效,表示溢出。
当rd_en信号为高时读FIFO数据,数据在下个周期有效,valid为数据有效信号,almost_empty表示还有一个数据读,当再读一个数据,empty信号有效,如果继续读,则underflow有效,表示下溢,此时读出的数据无效。


二、添加FIFO IP核

首先创建一个名为fifo_test的工程,创建工程的详细步骤可以参见:Vivado软件的使用——以led的交替闪烁为例
新工程到下图所示的界面后点击Finish即可完成工程的创建。
在这里插入图片描述
工程创建完成后依次按照下图中的序号找到FIFO Generator双击打开。
在这里插入图片描述
在弹出的配置页面中,先修改Component Name为"fifo_ip",这里可以选择读写时钟分开还是用同一个,一般来讲我们使用FIFO为了缓存数据,通常两边的时钟速度是不一样的,所以独立时钟是最常用的,我们这里选择"Independent Clocks Block RAM"。
在这里插入图片描述
在Native Ports配置界面下,选择数据位宽为16,FIFO深度选择512。
Read Mode有两种方式,一个是Standard FIFO,也就是标准的FIFO,数据滞后于读信号一个周期;还有一种方式为First Word Fall Through,即数据预取模式,简称FWFT模式,也就是FIFO会预先取出一个数据,当读信号有效时,相应的数据也有效,这里选择Standard FIFO。
在这里插入图片描述
切换到Data Counts配置界面下,勾选Write Data Count(已经写入FIFO的数据数量)和Read Data Count(FIFO中有多少数据可以读)两个选项,这样我们就可以通过这两个值来看FIFO内部的数据多少了。
在这里插入图片描述
到这里,需要修改的部分已经完成了,其他各选项下的参数保持默认即可,点击OK按钮,弹出如下对话框。
在这里插入图片描述
点击Generate就可以生成FIFO IP。


三、添加PLL IP核

用 PLL 产生出两路100MHz和75MHz的时钟,分别用于写时钟和读时钟,其中写时钟频率高于读时钟频率。
按照下图中的序号依次操作找到Clocking Wizard双击打开。
在这里插入图片描述
在Output Clocks下添加两个时钟输出,频率为100MHz和75MHz,其他保持默认,点击OK。
在这里插入图片描述
点击Generate就可以生成时钟IP核。
在这里插入图片描述


四、添加ILA IP核

本实验需要例化两个逻辑分析仪,分别连接写通道和读通道的信号。
首先添加写通道的ILA IP核,如下图所示。
在这里插入图片描述
按照代码给每个写通道探针设置位数如下图所示。
在这里插入图片描述
对应的代码如下。

reg	 [15:0] 		w_data			;	   		//FIFO写数据
wire      			wr_en			;	   		//FIFO写使能
wire       			full			;  			//FIFO满信号 
wire [8:0]  		wr_data_count	;  			//已写入数据数量

点击OK后弹出如下对话框,点击Generate即可生成写通道的ILA IP核。
在这里插入图片描述
接下来添加读通道的ILA IP核,如下图所示。
在这里插入图片描述
按照代码给每个读通道探针设置位数如下图所示。
在这里插入图片描述
对应的代码如下。

wire [15:0] 		r_data			;			//FIFO读数据
wire      			rd_en			;	   		//FIFO读使能
wire       			empty			;  			//FIFO空信号 
wire [8:0]  		rd_data_count	;  			//可读数据数量	

点击OK后弹出如下对话框,点击Generate即可生成读通道的ILA IP核。
在这里插入图片描述


五、编写测试程序

新建名为fifo_test的Verilog文件,依次按照下图中标注的序号进行即可。
在这里插入图片描述
在新建好的fifo_test.v中写入如下代码。

//该代码来自正点原子
`timescale 1ns / 1ps

module fifo_test
	(
		input 		clk,		         //50MHz时钟
		input 		rst_n	             //复位信号,低电平有效	
	);

reg	 [15:0] 		w_data			;	   		//FIFO写数据
wire      			wr_en			;	   		//FIFO写使能
wire      			rd_en			;	   		//FIFO读使能
wire [15:0] 		r_data			;			//FIFO读数据
wire       			full			;  			//FIFO满信号 
wire       			empty			;  			//FIFO空信号 
wire [8:0]  		rd_data_count	;  			//可读数据数量	
wire [8:0]  		wr_data_count	;  			//已写入数据数量
	
wire				clk_100M 		;			//PLL产生100MHz时钟
wire				clk_75M 		;			//PLL产生100MHz时钟
wire				locked 			;			//PLL lock信号,可作为系统复位信号,高电平表示lock住
wire				fifo_rst_n 		;			//fifo复位信号, 低电平有效

wire				wr_clk 			;			//写FIFO时钟
wire				rd_clk 			;			//读FIFO时钟
reg	[7:0]			wcnt 			;			//写FIFO复位后等待计数器
reg	[7:0]			rcnt 			;			//读FIFO复位后等待计数器

//例化PLL,产生100MHz和75MHz时钟
clk_wiz_0 fifo_pll
 (
  // Clock out ports
  .clk_out1(clk_100M),     	 	// output clk_out1
  .clk_out2(clk_75M),    		// output clk_out2
  // Status and control signals
  .reset(~rst_n), 			 	// input reset
  .locked(locked),       		// output locked
  // Clock in ports
  .clk_in1(clk)					// input clk_in1
  );      			

assign fifo_rst_n 	= locked	;	//将PLL的LOCK信号赋值给fifo的复位信号
assign wr_clk 		= clk_100M 	;	//将100MHz时钟赋值给写时钟
assign rd_clk 		= clk_75M 	;	//将75MHz时钟赋值给读时钟

/* 写FIFO状态机 */
localparam      W_IDLE      = 1	;
localparam      W_FIFO     	= 2	; 

reg[2:0]  write_state;
reg[2:0]  next_write_state;

always@(posedge wr_clk or negedge fifo_rst_n)
begin 
	if(!fifo_rst_n)
		write_state <= W_IDLE;
	else
		write_state <= next_write_state;
end

always@(*)
begin
	case(write_state)
		W_IDLE:
			begin
				if(wcnt == 8'd79)               //复位后等待一定时间,safety circuit模式下的最慢时钟60个周期
					next_write_state <= W_FIFO;
				else
					next_write_state <= W_IDLE;
			end
		W_FIFO:
			next_write_state <= W_FIFO;			//一直在写FIFO状态
		default:
			next_write_state <= W_IDLE;
	endcase
end
//在IDLE状态下,也就是复位之后,计数器计数
always@(posedge wr_clk or negedge fifo_rst_n)
begin 
	if(!fifo_rst_n)
		wcnt <= 8'd0;
	else if (write_state == W_IDLE)
		wcnt <= wcnt + 1'b1 ;
	else
		wcnt <= 8'd0;
end
//在写FIFO状态下,如果不满就向FIFO中写数据
assign wr_en = (write_state == W_FIFO) ? ~full : 1'b0; 
//在写使能有效情况下,写数据值加1
always@(posedge wr_clk or negedge fifo_rst_n)
begin
	if(!fifo_rst_n)
		w_data <= 16'd1;
	else if (wr_en)
		w_data <= w_data + 1'b1;
end

/* 读FIFO状态机 */
localparam      R_IDLE      = 1	;
localparam      R_FIFO     	= 2	; 
reg[2:0]  read_state;
reg[2:0]  next_read_state;

//产生FIFO读的数据
always@(posedge rd_clk or negedge fifo_rst_n)
begin
	if(!fifo_rst_n)
		read_state <= R_IDLE;
	else
		read_state <= next_read_state;
end

always@(*)
begin
	case(read_state)
		R_IDLE:
			begin
				if (rcnt == 8'd59)             	//复位后等待一定时间,safety circuit模式下的最慢时钟60个周期
					next_read_state <= R_FIFO;
				else
					next_read_state <= R_IDLE;
			end
		R_FIFO:	
			next_read_state <= R_FIFO ;			//一直在读FIFO状态
		default:
			next_read_state <= R_IDLE;
	endcase
end

//在IDLE状态下,也就是复位之后,计数器计数
always@(posedge rd_clk or negedge fifo_rst_n)
begin 
	if(!fifo_rst_n)
		rcnt <= 8'd0;
	else if (write_state == W_IDLE)
		rcnt <= rcnt + 1'b1 ;
	else
		rcnt <= 8'd0;
end
//在读FIFO状态下,如果不空就从FIFO中读数据
assign rd_en = (read_state == R_FIFO) ? ~empty : 1'b0; 

//实例化FIFO
fifo_ip fifo_ip_inst 
(
  .rst            (~fifo_rst_n    	),   // input rst
  .wr_clk         (wr_clk          	),   // input wr_clk
  .rd_clk         (rd_clk          	),   // input rd_clk
  .din            (w_data       	),   // input [15 : 0] din
  .wr_en          (wr_en        	),   // input wr_en
  .rd_en          (rd_en        	),   // input rd_en
  .dout           (r_data       	),   // output [15 : 0] dout
  .full           (full         	),   // output full
  .empty          (empty        	),   // output empty
  .rd_data_count  (rd_data_count	),   // output [8 : 0] rd_data_count
  .wr_data_count  (wr_data_count	)    // output [8 : 0] wr_data_count
);

//写通道逻辑分析仪
ila_wfifo ila_wfifo (
	.clk		(wr_clk			), 
	.probe0		(w_data			), 	
	.probe1		(wr_en			), 	
	.probe2		(full			), 		
	.probe3		(wr_data_count	)
);
//读通道逻辑分析仪
ila_rfifo ila_rfifo (
	.clk		(rd_clk			), 
	.probe0		(r_data			), 	
	.probe1		(rd_en			), 	
	.probe2		(empty			), 		
	.probe3		(rd_data_count	)
);
 	
endmodule

其中实例化FIFO部分的代码来自fifo_ip中的fifo_ip.veo文件,不过需要将括号内的参数做一修改。
在这里插入图片描述
实例化写通道ila逻辑分析仪部分的代码来自ila_wfifo中的ila_wfifo.veo文件,不过需要将括号内的参数做一修改。
在这里插入图片描述
实例化读通道ila逻辑分析仪部分的代码来自ila_rfifo中的ila_rfifo.veo文件,不过需要将括号内的参数做一修改。
在这里插入图片描述


六、分配管脚

本实验中需要分配管脚的只有时钟信号clk(管脚为U18)和复位信号rst_n(管脚为N15),按照下图中的数字顺序即可完成管脚的分配。
在这里插入图片描述
管脚分配完成后Ctrl+S保存,名称与工程名保持一致。
在这里插入图片描述
管脚分配的信息在fifo_test.xdc文件中。
在这里插入图片描述


七、Simulator仿真

右击Simulation Sources选择Add Source按照下图中序号依次新建TB文件。
在这里插入图片描述
在tb_fifo_test.v文件中写入如下代码。

//该代码来自正点原子
`timescale 1ns / 1ps

module vtf_fifo_tb;
// Inputs
reg clk;
reg rst_n;

// Instantiate the Unit Under Test (UUT)
fifo_test uut (
	.clk	(clk), 		
	.rst_n		(rst_n)
);

initial 
begin
	// Initialize Inputs
	clk = 0;
	rst_n = 0;

	// Wait 100 ns for global reset to finish
	#100;
      rst_n = 1;       

 end

always #10 clk = ~ clk;   //20ns一个周期,产生50MHz时钟源
   
endmodule

保存代码后选择SIMULATION下的Run Simulation,选择第一个行为仿真。
将所有代码中定义的信号拖入到波形仿真窗口,设置仿真时间为10us,运行后结果如下图所示。
在这里插入图片描述
由仿真结果可以看到,写使能wr_en 有效后开始写数据,初始值为 0001 ,从开始写到 empty 不空,是需要一定周期的,因为内部还要做同步处理。在不空后,开始读数据,读出的数据相对于 rd_en 滞后一个周期。
延长仿真周期为100us,如果FIFO 满了,根据程序的设计,满了就不向 FIFO 写数据了, wr_en 也就拉低了。
在这里插入图片描述
FIFO之所以会满,是因为写时钟比读时钟快,如果将写时钟与读时钟调换,也就是读时钟快,就会出现读空的情况。
交换读写时钟周期,再次仿真。
在这里插入图片描述
仿真结果如下图所示。
在这里插入图片描述
将FIFO的Read Mode改为First Word Fall Through,再进行仿真。
在这里插入图片描述
仿真结果如下图所示,可以看到,rd_en 有效的时候数据也有效,没有相差一个周期,这是First Word Fall Through仿真模式不同于Standard FIFO之处。
在这里插入图片描述
也就是在First Word Fall Through读模式下,FIFO会预先取出一个数据,当读信号有效时,相应的数据也有效。


八、硬件调试

连接开发板,点击Generate Bitstream生成比特流文件,将其下载到开发板上。
在这里插入图片描述
这时会生成两个ila文档,分别对应读通道和写通道。
在这里插入图片描述
先来看写通道的,对应的文件是hw_ila_2,仿真结果如下图所示。可以看到full信号为高电平时,wr_en为低电平,FIFO不再向里面写数据。
在这里插入图片描述
再试试读通道,对应的文件是hw_ila_1,仿真结果如下图所示。
在这里插入图片描述
如果以rd_en上升沿作为触发条件,点击运行,然后按下复位按键,也就是开发板上的PL KEY1按键,会出现下面的结果,这与仿真结果一致,标准FIFO模式下,数据滞后rd _en一个周期。
在这里插入图片描述


总结

以上就是ZYNQ之FPGA 片内FIFO读写测试实验的所有内容了,该实验过程与FPGA 片内RAM、ROM读写测试实验流程大体上相似,但也有明显的不同之处,本实验添加了PLL IP核,同时添加了两个ILA逻辑分析仪,这些都是比之前实验更复杂的地方,应当学习这种硬件测试的方法,以便应用在后续实验中。
本文参考资料:正点原子–course_s1_ZYNQ那些事儿-FPGA实验篇V1.06.pdf

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐