异步FIFO:Verilog代码设计与实现

1.异步FIFO的定义
FIFO:First in first out的缩写,即先进先出,可以理解为一个双端口ram。异步fifo是指读写时钟不一样,有两个时钟信号,分别是读时钟信号和写时钟信号。与之对应的是同步fifo,它的读和写都由同一个时钟触发,只有一个时钟信号。
2.FIFO用途
首先做一个类比:把FIFO类比为一个蓄水池,该蓄水池有进水口和出水口。当进水量大于出水量时,可以把多余的水存入蓄水池中。当出水量大于进水量时,可以从蓄水池中取出水。由于受到重力的作用,从进水口先进入的水肯定先从出水口出来。但是,你的蓄水池有容量的限制,不可能无止境地装水,当装满时就不能再进水了;同时,蓄水池里面的水也可能用完,当排空时就不能再出水了,这就是对应的满和空状态。另外,进水和出水要有阀门对其控制。
而FIFO就是数字世界中用于数据缓存的这么一个“蓄水池”。
上面的类比对应的对象如下:
FIFO:蓄水池
进水:写数据
出水:读数据
进水阀门:写使能
出水阀门:读使能
水池装满:FIFO写满
水池排空:FIFO读空
由于存放的是数据,所以就有数据宽度,对应FIFO宽度;同时,把数据放在FIFO的哪个位置,以及从FIFO哪个位置取出数据,这就需要涉及到FIFO的写地址、读地址。因为异步FIFO读写的频率不一样,所以会有对应的读时钟、写时钟。
以上,就是设计一个FIFO需要的所有信号了,接下来我们以一个256位深度,存入数据位宽为8位的FIFO为例进行讲解。
信号定义如下:
输入信号:wr_clk(写时钟),rd_clk(读时钟),rst_n(复位信号),wr_en(写使能),rd_en(读使能),data_in(写入的八位数据)
输出信号:data_out(读出的八位数据),empty(读空信号标志),full(写满信号标志)
3.代码实现

module fifo(
	input  wr_clk,//写时钟
	input  rd_clk,//读时钟
	input  rst_n,//复位信号
	input  wr_en,//写使能
	input  rd_en,//读使能
	input   [7:0]data_in,//写入的数据
	output reg [7:0] data_out,//读出的数据
	output  empty,//读空信号
	output  full//写满信号
	);
	reg [7:0] fifo [255:0];//定义一个位宽为八位,深度为256位的数组,代表fifo
	reg [8:0] wr_addr_ptr;//写地址指针
	reg [8:0] rd_addr_ptr;//读地址指针

	wire [7:0] wr_addr;//写地址
	wire [7:0] rd_addr;//读地址

	assign wr_addr = wr_addr_ptr[7:0];
	assign rd_addr = rd_addr_ptr[7:0];

	wire [8:0] wr_gray;//写地址格雷码
	reg [8:0] wr_gray1;
	reg [8:0] wr_gray2;

	wire [8:0] rd_gray;//读地址格雷码
	reg [8:0] rd_gray1;
	reg [8:0] rd_gray2;
	
	assign wr_gray = (wr_addr_ptr >> 1) ^ wr_addr_ptr;//二进制码转化为格雷码
	assign rd_gray = (rd_addr_ptr >> 1) ^ rd_addr_ptr;

	integer i;
	always@(posedge wr_clk or negedge rst_n)	
		if(!rst_n)begin
			for(i=0;i<256;i=i+1)
				fifo[i] <= 0;
		end
		else if (wr_en && ~full)begin
			fifo[wr_addr] <= data_in;
		end
		else begin
			fifo[wr_addr] <= fifo[wr_addr];
		end
			

	always@(posedge wr_clk or negedge rst_n)
		if(!rst_n)begin
			wr_addr_ptr <= 0;
		end
		else if(wr_en && ~full) begin
			wr_addr_ptr <= wr_addr_ptr + 1'b1;
		end
		else begin
			wr_addr_ptr <= wr_addr_ptr;
		end

	always@(posedge rd_clk or negedge rst_n)
		if (!rst_n) begin
			rd_addr_ptr <= 0;
		end 
		else if (rd_en && ~empty) begin
			rd_addr_ptr <= rd_addr_ptr + 1'b1;
		end
		else begin
			rd_addr_ptr <= rd_addr_ptr;
		end

	always@(posedge rd_clk or negedge rst_n)
		if (!rst_n) begin
			data_out <= 0;
		end 
		else if (rd_en && ~empty) begin
			data_out <= fifo[rd_addr];
		end
		else begin
			data_out <= 0;
		end

	always@(posedge wr_clk or negedge rst_n)//读地址格雷码同步到写时钟域
		if (!rst_n) begin
			rd_gray1 <= 0;
			rd_gray2 <= 0;
		end
		else begin
			rd_gray1 <= rd_gray;
			rd_gray2 <= rd_gray1;
		end

	always@(posedge rd_clk or negedge rst_n)//写地址格雷码同步到读时钟域
		if (!rst_n) begin
			wr_gray1 <= 0;
			wr_gray2 <= 0;
		end
		else begin
			wr_gray1 <= wr_gray;
			wr_gray2 <= wr_gray1;
		end

	assign empty = (rd_gray == wr_gray2)?1:0;
	assign full = (wr_gray[8:7] != rd_gray2[8:7]) &&(wr_gray[6:0] == rd_gray2[6:0]);
endmodule

testbench代码:

`timescale 1ns/1ps
module fifo_tb ();
	reg rst_n;
	reg [7:0] data_in;
	reg wr_clk;
	reg rd_clk;
	reg wr_en;
	reg rd_en;
	wire [7:0]data_out;
	wire full;
	wire empty;

	fifo fifo(
	.wr_clk(wr_clk),//写时钟
	.rd_clk(rd_clk),//读时钟
	.rst_n(rst_n),//复位信号
	.wr_en(wr_en),//写使能
	.rd_en(rd_en),//读使能
	.data_in(data_in),//写入的数据
	.data_out(data_out),//读出的数据
	.empty(empty),//读空信号
	.full(full)//写满信号
	);
	
	initial wr_clk = 0;//产生写时钟
	always #10 wr_clk = ~wr_clk;

	initial rd_clk = 0;//产生读时钟
	always #30 rd_clk = ~rd_clk;

	always@(posedge wr_clk or negedge rst_n)//产生写入的数据
		if(!rst_n)
			data_in <= 0;
		else if (wr_en) begin
			data_in <= data_in + 1'b1;
		end
		else
			data_in <= data_in;
    initial begin
        rst_n = 0;
        wr_en = 0;
        rd_en = 0;
        #200;
        rst_n = 1;
        wr_en = 1;
        #20000;
        wr_en = 0;
        rd_en = 1;
        #20000;
        rd_en = 0;
        $stop; 
    end
endmodule

仿真结果:
在这里插入图片描述
4.代码讲解

	reg [7:0] fifo [255:0];//定义一个位宽为八位,深度为256位的数组,代表fifo
	reg [8:0] wr_addr_ptr;
	reg [8:0] rd_addr_ptr;

	wire [7:0] wr_addr;
	wire [7:0] rd_addr;

首先定义一个8×256的二维数组,用来表示FIFO。FIFO的宽度为8位,表示可以存入的数据位宽为8位,FIFO的深度为256位,表示一共可以存入256个数据。这256的数据在FIFO中的地址依次为0-255。这个地址用于后续把数据写入FIFO以及从FIFO中读出数据。
然后定义了读、写地址指针以及读、写地址,这里注意,指针的位宽要比地址多一位。是因为后面在判断“满”状态时,写指针要领先读指针一圈,因此指针位宽为9位,指针的0-7位就是地址。

	assign wr_addr = wr_addr_ptr[7:0];
	assign rd_addr = rd_addr_ptr[7:0];

	wire [8:0] wr_gray;
	reg [8:0] wr_gray1;
	reg [8:0] wr_gray2;

	wire [8:0] rd_gray;
	reg [8:0] rd_gray1;
	reg [8:0] rd_gray

然后定义了格雷码,格雷码可以减小亚稳态发生的概率。因为格雷码是根据地址指针产生的,因此格雷码位宽也是9位。

	assign wr_gray = (wr_addr_ptr >> 1) ^ wr_addr_ptr;
	assign rd_gray = (rd_addr_ptr >> 1) ^ rd_addr_ptr;

格雷码产生方法:数剧往右移一位然后与它本身异或。

	integer i;
	always@(posedge wr_clk or negedge rst_n)	
		if(!rst_n)begin
			for(i=0;i<256;i=i+1)
				fifo[i] <= 0;
		end
		else if (wr_en && ~full)begin
			fifo[wr_addr] <= data_in;
		end
		else begin
			fifo[wr_addr] <= fifo[wr_addr];
		end
			

	always@(posedge wr_clk or negedge rst_n)
		if(!rst_n)begin
			wr_addr_ptr <= 0;
		end
		else if(wr_en && ~full) begin
			wr_addr_ptr <= wr_addr_ptr + 1;
		end
		else begin
			wr_addr_ptr <= wr_addr_ptr;
		end

这里定义了一个整数i,用于在复位时,清空FIFO中的数据。另外,在有写使能且FIFO没有写满时,每来一个写时钟信号,写地址指针加1,并根据当前的写地址将数据写入FIFO中。

	always@(posedge rd_clk or negedge rst_n)
		if (!rst_n) begin
			rd_addr_ptr <= 0;
		end 
		else if (rd_en && ~empty) begin
			rd_addr_ptr <= rd_addr_ptr + 1;
		end
		else begin
			rd_addr_ptr <= rd_addr_ptr;
		end

	always@(posedge rd_clk or negedge rst_n)
		if (!rst_n) begin
			data_out <= 0;
		end 
		else if (rd_en && ~empty) begin
			data_out <= fifo[rd_addr];
		end
		else begin
			data_out <= 0;
		end

和写数据类似,当有读使能且FIFO没有读空时,每来一个读时钟信号,读地址指针加1,并根据当前的读地址将FIFO中的数据读出。

	always@(posedge wr_clk or negedge rst_n)
		if (!rst_n) begin
			rd_gray1 <= 0;
			rd_gray2 <= 0;
		end
		else begin
			rd_gray1 <= rd_gray;
			rd_gray2 <= rd_gray1;
		end

	always@(posedge rd_clk or negedge rst_n)
		if (!rst_n) begin
			wr_gray1 <= 0;
			wr_gray2 <= 0;
		end
		else begin
			wr_gray1 <= wr_gray;
			wr_gray2 <= wr_gray1;
		end

格雷码打两拍及时钟域同步:因为要将信号同步在同一时钟域。所以,在写时钟域内同步读格雷码;在读时钟域内同步写格雷码。打两拍是为了尽可能降低亚稳态发生的概率。

	assign empty = (rd_gray == wr_gray2)?1:0;
	assign full = (wr_gray[8:7] != rd_gray2[8:7]) &&(wr_gray[6:0] == rd_gray2[6:0]);

空满判断:
empty判断:用的是rd_gray和wr_gray2这两个信号。因为empty状态一定是发生在读时钟域,只有读才能把FIFO读空,所以在判断empty状态时必须用读时钟域的信号。从上面的时钟域同步可以看到,rd_gray是读时钟域的rd_addr_ptr信号生成的。wr_gray2是wr_gray同步到读时钟域的信号。
full判断:用的是wr_gray和rd_gray2这两个信号。和empty判断的逻辑一样,full只可能发生在写时钟域,所以在判断full状态时必须用写时钟域的信号来判断。接下来解释full的判断方法:
在这里插入图片描述
如图所示:绿色代表读指针,红色代表写指针。因为读写时钟不一样,FIFO在写数据的过程中也一直在读数据。所以判断FIFO写满的标志就是:当写指针领先读指针一周的时候,这个时候就是FIFO写满的时候,一周的长度就是FIFO的深度。因此,这也就解释了为什么代码中读写指针的位宽(9位)比地址位宽(8位)多一位的原因。
接下来以一个简单的例子,说明当指针领先一周时,他们对应的格雷码应该是怎么样的。
二进制码转化为格雷码的方法是,先右移一位,然后将得到的数与原来的数做异或。
以读指针为000001110为例,写指针领先它一圈时,应该是100001110。二者对应的格雷码分别是000001001、110001001。可以看出,此时格雷码前两位相反,其他位相同。(其他例子可自行验证)
在这里插入图片描述
对应的full代码为:
assign full = (wr_gray[8:7] != rd_gray2[8:7]) &&(wr_gray[6:0] == rd_gray2[6:0]);
到此,一个FIFO的设计就完成了。
下面解释一下为什么empty信号在第7个数据写入时才拉低,按理说,第一个数据写入时,FIFO就不是空了,就应该把empty拉低。
在这里插入图片描述
因为我们判断empty用的是rd_gray和wr_gray2这两个信号,而wr_gray2是由wr_gray在读时钟域中打两拍之后得到的,所以当data_in为07时,wr_gray2才开始变化,故empty信号也是在这里拉低。

  • 8
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值