【小白入门】Verilog实现异步FIFO

 

 之前也在CSDN上面写过两个FIFO相关的文章,不过代码看起来比较复杂,且注释也比较少,不利于新手入门。很多时候都没有耐心继续看下去。

http://t.csdn.cn/0dPX6

http://t.csdn.cn/lYvoY 

因为自己本身是一个初学者,就从初学者的视角来看待并学习FIFO。

为什么选择学习FIFO?

在学完双端口RAM之后看待FIFO,会觉得为什么要用FIFO呢?双端口的RAM也可以实现数据的存储与读取,读写的时钟也可以不一样,为什么不用RAM而要基于RAM来设计一个FIFO呢?

FIFO与RAM的区别是先进先出,不需要读地址和写地址。 写时钟下,将数据写入FIFO中,在读时钟下将先写入的数据先读出来,不需要向FIFO输入写地址和读地址即可完成数据的存储以及不同时钟下的读写操作,这样一听是非常方便的。在SDRAM的学习过程中,我们知道有突发长度这个东西,当突发长度为1的时候,即一个写地址对应一个写数据,这样是非常麻烦的,所以很多SDRAM如DDR3这种,都会将突发长度设置为8,即给一个首地址,后面连续读取8个数据。

【小白入门】Verilog实现异步FIFO_第1张图片

 

再贴一张异步FIFO的图

 

在写代码之前,需要了解几个概念。

 先思考,FIFO的存储空间和RAM是一样的,就像一个n行x1列的表格,每个表格里面存放一个数据,并且对应一个地址,在读写的过程中肯定会存在表格写满的情况和读的时候里面没有数据的情况,那应该怎么判断呢?

读写同时进行时

①首先是在读的视角,如果如果读一行数据的时候,刚好也在往这一行数据里面写数据,那这个时候即可判断读空了,如果再继续向下读的话,里面就没有写进的数据,读出的数据也不是我们写进去的,就是无效的。

所以读空的判断条件是:在读时钟的视角下,写时钟同步过来的地址等于我目前正在读的地址。

【小白入门】Verilog实现异步FIFO_第2张图片

关于跨时钟域的问题,大家可以去搜索一下跨时钟域以及亚稳态。也可以看我的这篇文章。

http://t.csdn.cn/hvJTa

②在写的视角下, 那什么时候写满呢?因为地址是有限的嘛,当读完一个数据的时候,读对应哪个地址的数据就已经不需要了,因为我们以及读了,即读完的那个“位置”空了。所以当写完一圈,并且追上下一轮的读的时候,就代表写满了。

所以写满判断的条件是:在写的时钟下,写完一圈对应的地址,等于同步过来的读地址。

【小白入门】Verilog实现异步FIFO_第3张图片

 

其次在写代码的时候,还需要了解格雷码,地址是按照0000-0001-0010-xxxx这种增长的,但是在地址变化的过程中,地址中的位数会存在”跳变“,如从0001-0010这两个相邻码的时候,有两位发生了变化,这样是不好的。 具体可以参考这篇文章

http://t.csdn.cn/OiesB

以下是代码Verilog的代码

`timescale 1ns / 1ps

module asyn_fifo1
(
		input		             rst_n		,
		
		input			         wr_clk		,
		input	                 wr_en		,
		input		 [7:0]       data_in	,
		input			         rd_clk		,
		input			         rd_en		,
		
		output		             full	    ,
		output			         empty		,
		output reg	[7:0]	     data_out	
);

		reg		[7:0]    ram_mem[255:0]    	;    //定义一个位宽为8bit深度为256的双端口RAM
		
		wire	[7:0]        rd_addr	    ;
		wire	[7:0]        wr_addr		;
		reg		[8:0]    rd_addr_ptr	    ;    //格雷码需要移位运算,且判断写满信号也需要多一位
		reg		[8:0]    wr_addr_ptr	    ;    
		
		wire  	[8:0]    rd_addr_gray	    ;
		reg		[8:0]    rd_addr_gray1	    ;
		reg		[8:0]    rd_addr_gray2	    ;
		wire	[8:0]    wr_addr_gray	    ;
		reg		[8:0]    wr_addr_gray1	    ;
		reg		[8:0]    wr_addr_gray2	    ;
		
		assign	rd_addr[7:0] = rd_addr_ptr[7:0];
		assign	wr_addr[7:0] = wr_addr_ptr[7:0];
		assign	rd_addr_gray = (rd_addr_ptr>>1) ^ rd_addr_ptr;		//bin to gray
		assign	wr_addr_gray = (wr_addr_ptr>>1) ^ wr_addr_ptr;
		
		//dual port ram
		integer	i;
		always @(posedge wr_clk or negedge rst_n)        //写时钟下初始化RAM
		begin
			if(rst_n == 1'b0)
				for(i=0;i<256;i=i+1)
					ram_mem[i] <= 1'b0;
			else if(wr_en && ~full)                      //写使能且没有写满
				ram_mem[wr_addr] = data_in;
			else
				ram_mem[wr_addr] = ram_mem[wr_addr];
		end
		
		//rd_addr_ptr++ and wr_addr_ptr++
		always @(posedge rd_clk or negedge rst_n)        //读时钟下,对读地址进行操作
		begin
			if(rst_n == 1'b0)
				rd_addr_ptr <= 1'b0;
			else if(rd_en && ~empty)
				rd_addr_ptr <= rd_addr_ptr + 1'b1;
			else
				rd_addr_ptr <= rd_addr_ptr;
		end
		always @(posedge wr_clk or negedge rst_n)        //写时钟下,对写地址进行操作
		begin
			if(rst_n == 1'b0)
				wr_addr_ptr <= 1'b0;
			else if(wr_en && ~full)
				wr_addr_ptr <= wr_addr_ptr + 1'b1;
			else
				wr_addr_ptr <= wr_addr_ptr;
		end
		
		//gray and two regsiter
		always @(posedge wr_clk or negedge rst_n)       //写时钟视角下,把读时钟同步到自己的时钟下
		begin
			if(rst_n == 1'b0)begin
				rd_addr_gray1 <= 1'b0;
				rd_addr_gray2 <= 1'b0;
			end
			else begin
				rd_addr_gray1 <= rd_addr_gray;
				rd_addr_gray2 <= rd_addr_gray1;
			end
		end
		always @(posedge rd_clk or negedge rst_n)        //读时钟视角下,把写时钟同步到自己的时钟下
		begin
			if(rst_n == 1'b0)begin
				wr_addr_gray1 <= 1'b0;
				wr_addr_gray2 <= 1'b0;
			end
			else begin
				wr_addr_gray1 <= wr_addr_gray;
				wr_addr_gray2 <= wr_addr_gray1;
			end
		end
		
		//data_out
		always @(posedge rd_clk or negedge rst_n)
		begin
			if(rst_n == 1'b0)
				data_out <= 1'b0;
			else if(rd_en && ~empty)
				data_out <= ram_mem[rd_addr];
			else
				data_out <= 1'b0;
		end
		
		assign empty = (rd_addr_gray == wr_addr_gray2)?1'b1:1'b0;    //判断读没读空
		assign full = (wr_addr_gray[8:7] != rd_addr_gray2[8:7]) && (wr_addr_gray[6:0] == rd_addr_gray2[6:0]);        //判断是否写满
endmodule

 

以下是tb仿真文件代码

`timescale 1ns / 1ps


module asyn_fifo1_tb();

        reg	                       	rst_n		;
		reg			                wr_clk		;
		reg	          	            wr_en		;
		reg		 [7:0]              data_in	    ;
		reg			                rd_clk		;
		reg			                rd_en		;

		wire		                full	    ;
		wire			            empty		;
		wire     [7:0]	            data_out	;
		
asyn_fifo1    asyn_fifo1_inst
(
		.rst_n	       (rst_n)	     ,	
		.wr_clk	       (wr_clk)      ,
		.wr_en	       (wr_en) 	     ,
		.data_in       (data_in)	 ,
		.rd_clk        (rd_clk)	     ,
		.rd_en	       (rd_en) 	     ,
		.full	       (full)        ,
		.empty	       (empty)       ,
		.data_out	   (data_out)
);
		initial wr_clk = 0;
		always #10 wr_clk = ~wr_clk;        //写时钟为50MHz
		
		initial rd_clk = 0;
		always #30 rd_clk = ~rd_clk;        //读时钟频率为写时钟的1/3
		
		always @(posedge wr_clk or negedge rst_n)    //不停的向FIFO中写0-255的数据
		begin
			if(rst_n == 1'b0)
			     data_in <= 0;
			else if (wr_en)
			     data_in <= data_in+1'b1;
			 else
			      data_in <= data_in;
		  end
			  initial  begin        
			     rst_n = 0;
			     wr_en = 0;
			     rd_en = 0;
			     #200;                //时间为200ns时,允许写入
			     rst_n = 1;
			     wr_en = 1;
			     #20000;               //时间再过20000ns时,不允许写,开始读
			     wr_en = 0;
			     rd_en = 1;
			     #20000;                //时间再过20000ns时,读停止
			     rd_en=0;
			     $stop;
			  end
endmodule

波形分析

200ns时,数据开始写入FIFO中,如下图

【小白入门】Verilog实现异步FIFO_第4张图片 写满时,full信号拉高,后面继续不停写入,但满了,都是无效的写入。

【小白入门】Verilog实现异步FIFO_第5张图片 

20200ns时,开始读数据

【小白入门】Verilog实现异步FIFO_第6张图片 读完后,empty信号拉高,表示读空。

【小白入门】Verilog实现异步FIFO_第7张图片 

由于写时钟为读时钟的三倍,从整体的波形图中也可以看出,写满数据的时间是读完数据时间的1/3.

【小白入门】Verilog实现异步FIFO_第8张图片 

 

 

 

 

 

 

你可能感兴趣的:(FPGA,就业相关,Verilog,fpga开发)