FIFO(二) —— 手写同步和异步FIFO

本文针对大佬手写FIFO设计进行学习与总结。

一、verilog实现同步FIFO

module SCFIFO1

//定义FIFO位宽和深度

#(
	 parameter   DATA_WIDTH = 'd8  ,							//FIFO位宽
    parameter   DATA_DEPTH = 'd16 ,							//FIFO深度
	 parameter   full_almost = 'd14  ,							//将满的位置
    parameter   empty_almost = 'd2 							//将空的位置
)


(
    
    input [DATA_WIDTH-1:0] data,
	 input clk,
	 input rst_n,
	 input wrreq,
	 input rdreq,
	 
	 
	 output empty,	    //空标志,高电平表示当前FIFO读空
	 output reg almost_empty,

	 output full ,       //满标志,高电平表示当前FIFO写满
	 output reg almost_full,
	 
	 output [$clog2(DATA_DEPTH) :0]usedw, //$clog2是以2为底取对数	
	 
	 output  reg [DATA_WIDTH-1:0] q

);

reg [$clog2(DATA_DEPTH) :0] fifo_cnt; //fifo计数器,对fifo内的数据进行计数

reg [DATA_WIDTH - 1 : 0] fifo_buffer[DATA_DEPTH - 1 : 0];	//用二维数组实现RAM	

reg [$clog2(DATA_DEPTH) - 1 : 0]	wr_addr;				//写地址
reg [$clog2(DATA_DEPTH) - 1 : 0]	rd_addr;				//读地址



//写操作:计算写地址

always @ (posedge clk or negedge rst_n) 
    if(!rst_n)
	     wr_addr <= 0;
	 else if(wrreq && !full)begin
	     wr_addr <= wr_addr + 1'b1;
		  fifo_buffer[wr_addr]<=data;
		  end

//读操作:计算读地址

always @ (posedge clk or negedge rst_n) 
    if(!rst_n)
	     rd_addr <= 0;
	 else if(rdreq && !empty)begin //读使能且未读空
	     rd_addr <= rd_addr + 1'b1;
		  q <=fifo_buffer[rd_addr];
		  end
		  
//usedw,计数器来计算fifo中的数据个数

always @ (posedge clk or negedge rst_n) begin
	if (!rst_n)
		fifo_cnt <= 0;
	else begin
		case({wrreq,rdreq})									   //拼接读写使能信号进行判断
			2'b00:fifo_cnt <= fifo_cnt;						//不读不写
			
			2'b01:	                               	   //只读不写
				if(fifo_cnt != 0)				   			   //fifo没有被读空
					fifo_cnt <= fifo_cnt - 1'b1;   			//fifo个数-1
					
			2'b10:                                       //只写不读
				if(fifo_cnt != DATA_DEPTH)         			//fifo没有被写满
					fifo_cnt <= fifo_cnt + 1'b1;   			//fifo个数+1
					
			2'b11:fifo_cnt <= fifo_cnt;	           			//读写同时
			default:;                              	
		endcase
	end
end
assign usedw = fifo_cnt;
//依据计数器状态更新指示信号
//还可设置将满和将空的位置,来判断,从而得到almost_full和almost_emputy信号
assign full  = (fifo_cnt == DATA_DEPTH) ? 1'b1 : 1'b0;		//空信号
assign empty = (fifo_cnt == 0)? 1'b1 : 1'b0;				//满信号

//将满
always @ (posedge clk or negedge rst_n) 
	if (!rst_n)
	    almost_full  <= 0;
	else if(wrreq && fifo_cnt == full_almost-1) //计数0开始,因此-1
	    almost_full  <= 1;
	else
	    almost_full  <= 0;
	    
//将空
always @ (posedge clk or negedge rst_n) 
	if (!rst_n)
	    almost_empty  <= 0;
	else if(rdreq && fifo_cnt == empty_almost+1)//计数0开始,因此+1
	    almost_empty  <= 1;
	else
	    almost_empty  <= 0;
	    

endmodule 

tb测试

`timescale 1ns/1ps

module fifo_tb(); //对同步fifo仿真

parameter   DATA_WIDTH = 'd8  ;							//FIFO位宽
parameter   DATA_DEPTH = 'd16 ;							//FIFO深度



	reg	[DATA_WIDTH-1:0]  data;
   reg	  clk;
	reg     rst_n;
	reg     wrreq;
	reg	  rdreq;
	
	wire empty;
   wire full;
   wire almost_full;
	wire almost_empty;
	wire	[DATA_WIDTH-1:0]  q;
	wire [$clog2(DATA_DEPTH) :0]usedw;


SCFIFO1  
#(
	.DATA_WIDTH	(DATA_WIDTH),			
   .DATA_DEPTH	(DATA_DEPTH)			

)
u1(	
	.data(data),
	.clk(clk),
	.rst_n(rst_n),
	.wrreq(wrreq),
	
	.rdreq(rdreq),
	
	.almost_empty(almost_empty),
	.almost_full(almost_full),
	
	.empty(empty),
	.full(full),	
	
	.q(q),
	.usedw(usedw)

);


//产生激励

initial begin
    clk = 0;
	 rst_n = 0;
	 
	 data =  0;
	 
	 rdreq = 0;
	 wrreq = 0;
	 #10;


//将FIFO写满	 
	 	repeat(16)begin
			rst_n = 1;
			wrreq = 1;
			#20;
			data = data + 1;
		end
		wrreq = 0;

		repeat(16)begin
			rdreq = 1;
			#20;
			data = data + 1;
		end
		rdreq = 0;
		
		#500;
		$stop;

	 
end


always #10 clk = ~clk;

endmodule

modelsim波形仿真

先wrreq=1,写入16个数据,将fifo写满,后rdreq = 1 ,将16个数据读出。
波形如下:
红色框中是定义的四个参数,FIFO位宽,深度以及将满和将空的位置。
在这里插入图片描述

分析写相关信号:
当wrreq = 1开始写数据,因此usedw开始+1,表示已用fifo的深度,当使用了14个深度的时候,是将满的位置,因此almost_full=1。同时当usedw=16,说明用了16深度,fifo写满,full=1。
在这里插入图片描述
分析读相关信号:
当rdreq = 1读数据,因此usedw开始从16不断-1,当usedw=2,表示将空的位置,因此almost_emoty=1。同时当usedw=0,说明fifo读空,empty=1。
可看到最终q的输出数据是0-15。实现了数据的先入先出。
在这里插入图片描述

二、verilog实现异步fifo

异步fifo

第一节介绍了同步fifo的设计方法,但同步fifo读写采用同一时钟,而异步fifo读写时钟不同,下面对其进行分析。参考

异步fifo设计的重点也是写满和读空的判断,参考大佬的博文,得到了如下的判断方式:

“写满”的判断: 将读指针同步到写时钟域,再与写指针判断
.
“读空”的判断:将写指针同步到读时钟域,再与读指针判断

如下是假读空:
左图:读指针没有超过写指针,说明没有读空。
右图:写指针同步到读时钟域需要T时间,经过T时间后,虽然在读时钟域中读写指针相同(读空),但左图才是实际的情况,因此这种情况是假读空。
在这里插入图片描述
如下是假写满:
左图:写指针没有超过读指针一圈,说明没有写满
右图:读指针同步到写时钟域需要T时间,经过T时间后,虽然在写时钟域中写指针超过了读指针一圈(写满),但左图才是实际的情况,因此这种情况是假写满。
在这里插入图片描述

格雷码

1、降低亚稳态
跨时钟域中可能出现亚稳态,最终导致fifo功能错误,由于格雷码每次只有一位发生变化,因此我们引入格雷码来降低亚稳态现象的出现。

2、格雷码判断空满
将格雷码进行时钟域同步,同步后的格雷码与该时钟域下的指针进行比较。

实现

1、分别构造读、写时钟域下的读、写指针,指针位数需拓展一位(最高位作为指示位,判断写指针是否超过读指针一圈,同时最高位不会影响指针表示的地址区间)。

注意这里写指针是否超读指针一圈的问题,写指针可超过读指针一圈,表明FIFO被写满。
但读指针超写指针一圈是不合理,FIFO内部都没数据了,再继续读的话没意义!因此当读指针刚好追上写指针的时候,我们就判断FIFO被读空。

首先思考为什么最高位是指示位,这里判断空满却采用的高两位?
.
因为我们分析读写指针是否超一圈的时候,其指针都是二进制编码指针,而我们为了降低亚稳态,引入了格雷码,二进制1000表示格雷码1100,而二进制0000,表示格雷码0000,因此不能仅对最高位进行比较了,所以我们后续在进行空满判断的时候需要比较高两位。

因此空满判断如下:
.
读空:若高两位相同,其余位相同,说明所有位相同,表示读刚好追上写,FIFO被读空。(同步后的格雷写指针和读指针比)
.
写满:若高两位相反,其余位相同,表示写超过读一圈,FIFO被写满。(同步后的格雷读指针和写指针比)

分别将读、写指针从二进制码转换成格雷码的方法:

assign 	wr_ptr_g = wr_ptr ^ (wr_ptr >> 1);
assign 	rd_ptr_g = rd_ptr ^ (rd_ptr >> 1);

2、将格雷码形式的读指针同步到写时钟域,将格雷码形式的写指针同步到读时钟域

//将读指针的格雷码同步到写时钟域,来判断是否写满
always @ (posedge wr_clk or negedge wr_rst_n) begin
	if (!wr_rst_n)begin
		rd_ptr_g_d1 <= 0;										//寄存1拍
		rd_ptr_g_d2 <= 0;										//寄存2拍
	end				
	else begin												
		rd_ptr_g_d1 <= rd_ptr_g;								//寄存1拍
		rd_ptr_g_d2 <= rd_ptr_g_d1;								//寄存2拍
	end	
end
//将写指针的格雷码同步到读时钟域,来判断是否读空
always @ (posedge rd_clk or negedge rd_rst_n) begin
	if (!rd_rst_n)begin
		wr_ptr_g_d1 <= 0;										//寄存1拍
		wr_ptr_g_d2 <= 0;										//寄存2拍
	end				
	else begin												
		wr_ptr_g_d1 <= wr_ptr_g;								//寄存1拍
		wr_ptr_g_d2 <= wr_ptr_g_d1;								//寄存2拍		
	end	
end

3、在写时钟域判断“写满”:同步到写时钟域的格雷码读指针与格雷码写指针相比,高2位相反,其余位相等——写满

在读时钟域判断“读空”,同步到读时钟域的写指针与格雷码读指针进行比较,高2位相等,其余位也相等——读空

//写满
assign	full  = ( wr_ptr_g == { ~(rd_ptr_g_d2[$clog2(DATA_DEPTH) : $clog2(DATA_DEPTH) - 1])
,rd_ptr_g_d2[$clog2(DATA_DEPTH) - 2 : 0]})? 1'b1 : 1'b0;
//读空
assign	empty = ( wr_ptr_g_d2 == rd_ptr_g ) ? 1'b1 : 1'b0;   

大佬写的都很清晰,可以学习一下,完整代码

//异步FIFO
module	DCFIFO1
#(
    parameter   DATA_WIDTH = 'd8,   //FIFO位宽                    
    parameter   DATA_DEPTH = 'd8 	//FIFO深度							
)		
(		
//写有关信号		
	input							wr_clk		,			//写时钟
	input							wr_rst_n	,       		//低电平有效的写复位信号
	input							wr_en		,       		//写使能信号,高电平有效	
	input	[DATA_WIDTH-1:0]		data_in		,     //写入的数据
	
//读有关信号			
	input							rd_clk		,			//读时钟
	input							rd_rst_n	,       		//低电平有效的读复位信号
	input							rd_en		,				//读使能信号,高电平有效						                                        
	output	reg	[DATA_WIDTH-1:0]	data_out	,	//输出的数据
	
//空满标志					
	output							empty		,				//空标志,高电平表示当前FIFO已被写满
	output							full		    			//满标志,高电平表示当前FIFO已被读空
);                                                              
 

//用二维数组实现RAM
reg   [DATA_WIDTH - 1 : 0]			   fifo_buffer[DATA_DEPTH - 1 : 0]; //深度为8,位宽为8的fifo

//定义读写指针,且指针位数拓展一位,用来作为空满指示位

reg  [$clog2(DATA_DEPTH) : 0]		wr_ptr;						//写指针,二进制
reg  [$clog2(DATA_DEPTH) : 0]		rd_ptr;						//读指针,二进制

wire [$clog2(DATA_DEPTH) : 0]		wr_ptr_g;					//写指针,格雷码
wire [$clog2(DATA_DEPTH) : 0]		rd_ptr_g;					//读指针,格雷码


reg  [$clog2(DATA_DEPTH) : 0]		wr_ptr_g_d1;				//写指针格雷码在读时钟域下打1拍
reg  [$clog2(DATA_DEPTH) : 0]		wr_ptr_g_d2;				//写指针格雷码在读时钟域下打2拍
reg  [$clog2(DATA_DEPTH) : 0]		rd_ptr_g_d1;				//读指针格雷码在写时钟域下打1拍
reg  [$clog2(DATA_DEPTH) : 0]		rd_ptr_g_d2;				//读指针格雷码在写时钟域下打2拍
	

wire [$clog2(DATA_DEPTH) - 1 : 0]	wr_ptr_true;			//真实写指针,作为写ram的地址
wire [$clog2(DATA_DEPTH) - 1 : 0]	rd_ptr_true;			//真实读指针,作为读ram的地址
 
//地址指针从二进制转换成格雷码
assign  wr_ptr_g = wr_ptr ^ (wr_ptr >> 1);					
assign  rd_ptr_g = rd_ptr ^ (rd_ptr >> 1);
 
//读写RAM地址赋值
assign  wr_ptr_true = wr_ptr [$clog2(DATA_DEPTH) - 1 : 0];	//拓展一位的写指针,去除最高位
assign  rd_ptr_true = rd_ptr [$clog2(DATA_DEPTH) - 1 : 0];	//拓展一位的读指针,去除最高位
 
//将写指针的格雷码同步到读时钟域,与格雷码读指针判断是否读空
always @ (posedge rd_clk or negedge rd_rst_n) begin
	if (!rd_rst_n)begin
		wr_ptr_g_d1 <= 0;										//打1拍
		wr_ptr_g_d2 <= 0;										//打2拍
	end				
	else begin												
		wr_ptr_g_d1 <= wr_ptr_g;					    //打1拍
		wr_ptr_g_d2 <= wr_ptr_g_d1;						//打2拍		
	end	
end
  
 
 //写操作,更新写地址
always @ (posedge wr_clk or negedge wr_rst_n) begin
	if (!wr_rst_n)
	    wr_ptr <= 0;
	else if (!full && wr_en)begin								//写使能有效且非满
		 wr_ptr <= wr_ptr + 1'd1;
		 fifo_buffer[wr_ptr_true] <= data_in;
	end	
end


//将读指针的格雷码同步到写时钟域,与格雷码写指针判断是否写满

always @ (posedge wr_clk or negedge wr_rst_n)
    if(!wr_rst_n)begin
	     rd_ptr_g_d1 <= 0;	
	     rd_ptr_g_d2 <= 0;	
	end
	else begin
	    rd_ptr_g_d1 <= rd_ptr_g;
		 rd_ptr_g_d2 <= rd_ptr_g_d1;
	end
	 
//读操作,更新读地址
always @ (posedge rd_clk or negedge rd_rst_n)
    if(!rd_rst_n)
	     rd_ptr <= 'd0;
	 else if(rd_en && !empty) begin								//读使能有效且非空
	     rd_ptr <= rd_ptr + 1'd1;
		  data_out <= fifo_buffer[rd_ptr_true];
	end
	 


//当高2位相等,其余位也相等,读空
//同步后的格雷码写指针 == 格雷码读指针,FIFO被读空
assign	empty = ( wr_ptr_g_d2 == rd_ptr_g ) ? 1'b1 : 1'b0;
 
//当高2位相反且其他位相等时,写指针超过读指针一圈,FIFO被写满

//格雷码写指针 ==  同步后的读指针格雷码高两位取反,再拼接上余下位

 
assign full = (wr_ptr_g == {~(rd_ptr_g_d2[$clog2(DATA_DEPTH) : $clog2(DATA_DEPTH) - 1]),rd_ptr_g_d2[$clog2(DATA_DEPTH) - 2 : 0]});
endmodule

tb文件:

`timescale 1ns/1ns	//时间单位/精度

module fifo_tb();
 
parameter   DATA_WIDTH = 8  ;		//位宽
parameter   DATA_DEPTH = 8 ;		//深度
 
reg							wr_clk	;		   
reg							wr_rst_n	;       	
reg							wr_en		;       		
reg	[DATA_WIDTH-1:0]	data_in	;        
 
reg							rd_clk	;			
reg							rd_rst_n	;       	
reg							rd_en		;									                                        
wire  [DATA_WIDTH-1:0]	data_out	;							
wire						   empty		;			//空标志,高电平表示被写满
wire						   full		;        //满标志,高电平表示被读空


DCFIFO1
#(
	 .DATA_WIDTH	(DATA_WIDTH),	
    .DATA_DEPTH	(DATA_DEPTH)
)
async_fifo_inst(

	.wr_clk		(wr_clk		),
	.wr_rst_n	(wr_rst_n	),
	.wr_en		(wr_en		),
	.data_in	   (data_in	   ),	
	.rd_clk		(rd_clk		),               
	.rd_rst_n	(rd_rst_n	),	
	.rd_en		(rd_en		),	
	.data_out	(data_out	),
	
	.empty		(empty		),		
	.full		   (full	   	)
);
 
//初始化
initial begin
	rd_clk = 1'b0;					
	wr_clk = 1'b0;					
	wr_rst_n <= 1'b0;				
	rd_rst_n <= 1'b0;				
	wr_en <= 1'b0;
	rd_en <= 1'b0;	
	data_in <= 'd0;
	#5
	wr_rst_n <= 1'b1;				
	rd_rst_n <= 1'b1;					
//写8次,让FIFO写满 	
	repeat(8) begin
		@(negedge wr_clk)begin		
			wr_en <= 1'b1;
			data_in <= data_in +1 ;	//8个数
		end
	end
	
//拉低写使能	
	@(negedge wr_clk)	wr_en <= 1'b0;
	
//读8次,将FIFO读空 	
	repeat(8) begin
		@(negedge rd_clk) rd_en <= 1'd1;		
	end
	
//拉低读使能
	@(negedge rd_clk) rd_en <= 1'd0;		
	
	//仅写4个数据,此时不读	
	repeat(4) begin
		@(negedge wr_clk)begin		
			wr_en <= 1'b1;
			data_in <= $random;	//生成8位随机数
		end
	end
	
//将读使能拉高
	@(negedge rd_clk) rd_en <= 1'b1;
	

//同时对FIFO写数据,此时实现同时读写	
	forever begin
		@(negedge wr_clk)begin		
			wr_en <= 1'b1;
			data_in <= data_in +1 ;	
		end
	end	


end
 

always #10 rd_clk = ~rd_clk;			//读时钟周期20ns
always #20 wr_clk = ~wr_clk;			//写时钟周期40ns
 
endmodule

波形:
在这里插入图片描述

  • 7
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting_FPGA

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值