• 1、设计思路
  • 2、设计过程中遇到的一些问题
  • 3、verilog代码和仿真文件

1、设计思路
关键的点:读写控制信号的生成、读写地址、状态产生。

(1)读控制(ren)、写控制(wen)的生成:当外部输入的wr_en=1且full=0时,也就是外部让你写且你的fifo现在没有写满的情况下,你就可以对fifo进行写操作。用verilog表示即为
wen=!full&&wr_en;
读控制同理:ren=(!empty)&&rd_en;
(2)读地址(raddr)、写地址(waddr)
这块比较简单,时钟上升沿到来了且读使能(ren)或者写使能(wen)有效,读地址(读指针)+1或者写地址(写指针)+1;
对应到verilog代码为:
always@(posedge clk)
if(reset)
waddr<=5’b0;
else if(wen)
waddr<=waddr+1;

always@(posedge clk)
if(reset)
raddr<=5’b0;
else if(ren)
raddr<=raddr+1;

(3)状态判断
要对状态判断就需要计算出读写地址的差值,因为是异步的,所以需要进行跨时钟域。即计算wr_gap的时候需要将读地址同步到写时钟域来,具体怎么将读地址同步过来呢?
因为读地址是自加1的,这个特性就可以使用格雷码,即将二进制转换成格雷码,这样相邻两位就只有1bit发生变化。
二进制转换成格雷码的方法:将二进制右移移位后,和右移前的数值进行异或即可。

//读地址从二进制转变成格雷码
always @(posedge rd_clk or posedge rd_reset)
if(rd_reset)
	raddr_gray<=0;
else raddr_gray<=raddr^(raddr>>1);

转换成格雷码后,就可以通过打两拍的方法进行同步。但是计算wr_gap的时候因为waddr是二进制,所以将同步过来的读地址的格雷码再转换为二进制,具体怎么将格雷码转化为二进制呢?
将格雷码的最高位作为二进制的最高位,然后将格雷码的次高位和二进制的最高位按位异或获得二进制的次高位,依次类推,具体见下图:

//将从读时钟域同步到写时钟域的格雷码读地址转变成二进制读地址
always@(*)
	raddr_gray2bin={raddr_gray_syn2w1[4],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2]^raddr_gray_syn2w1[1],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2]^raddr_gray_syn2w1[1]^raddr_gray_syn2w1[0]};

关于间隔计算见下图:需要提醒读者的是,wr_gap是计算还有几个空格子,rd_gap计算还有几个数据。因此可以这样理解(可能不是很准确只是一种记忆方法)wr_gap是用读地址减去写地址;rd_gap是用写地址减去读地址。具体见下图:

相关代码如下:

//wr_gap间隔计算
always@(*)
	if(raddr_gray2bin[4]^waddr[4])
		wr_gap=raddr_gray2bin[3:0]-waddr[3:0];
	else wr_gap=FIFO_DEEP+raddr_gray2bin-waddr;
//计算rd_gap
always@(*)
	rd_gap=waddr_gray2bin-raddr;

在此处我其实踩了一个坑,导致我搞了一个上午现象都不太对,在这里先买个关子,具体现象和错误原因我再下一个部分说。

2、设计过程中思考的一些问题
(1)先说说第二部分我踩的坑。
复位后,我的almost_full也有效,而且持续了一段时间为0,但是我写的代码理论上这里应该一直是15呀,然后almost_full应该是0才对。

always@(*)
	if(raddr_gray2bin[4]^waddr[4])
		wr_gap=raddr_gray2bin[3:0]-waddr[3:0];
	else wr_gap=FIFO_DEEP+raddr_gray2bin-waddr;


其实问题很智障,就是我的FIFO_DEEP是16,但是我的wr_gap的位宽是3:0;这样当我把FIFO_DEEP赋值给wr_gap的还是只会取低4位,就全是0。比较推荐大家看明白后自己动手敲一遍代码,这样印象会更加深刻。一会儿会把完整的代码贴出来。
(2)在计算wr_gap的时候需要将读时钟的地址同步过来,打两拍的话,同步过来的读地址会延迟两拍,如果要和当时的写地址进行计算,需要对同步过来的地址加2吗?

解析: 这里会涉及到一个问题:就是假空和假满的问题,体现了异步fifo的保守性,这样操作既不会引起上溢也不会引起下溢。 具体分析如下:计算wr_gap的时候,使用raddr_gray2bin来计算,raddr从读时钟域同步过来因为打了两拍会有延时,但是我们在计算的这一时刻是使用同步之前的读地址值(假设现在最高位为0,后面的数值为8)和当前时刻写地址值(假设最高位为1,后面的数值为6),如果这个值算出来是满足了almost_full(假设为3)的标准,我们就可以不继续往fifo里写数据。即是此时其实raddr在读时钟域已经更新为10了,真是的wr_gap应该是4,不满足almost_full的标准,但是前者也只是将almost_full提早有效了,并不会造成什么影响啊,不会产生上溢也不会产生下溢。(这段话可能看起来有点绕,但是如果你在写代码的时候也有这个疑惑,可以认真读一读),我是读这段话才想明白的。

(3)发现一个问题:将地址转变成格雷码的形式后采用打两拍的方法意味着是慢到快时钟域。意味着写时钟的频率比读时钟的频率低?那么如果写时钟的频率比读时钟的频率高应该怎么办呢?
这样的话就是只就是慢时钟域能采到那一时刻写时钟域(快时钟域)的变化,不会影响什么,对fifo状态的产生不会有影响,也就是不会产生上溢和下溢。

(4)为什么选择格雷码?
选择格雷码的原因:可以将多比特跨时钟域转换成单比特,降低亚稳态发生的概率(这里有一个隐含的条件,就是读写地址转换成格雷码进行时钟域的前提是它的地址是相邻两个数之间只变化1,因此对于多bit总线传输不能使用格雷码,因为不能保证是相邻两位只变化1)

(5)如何设计depth不是2的幂次的异步FIFO?比如深度为6(这个问题是我看了公众号IC加油站才进行思考的,大家可以去关注一下,这个人写的东西都很透彻,写了一系列关于跨时钟域的文章)
利用格雷码的对称性:

3、代码
(1)设计代码

`timescale 1ns / 1ps
//
// Company: 
// Engineer: 
// 
// Create Date: 2021/08/03 20:55:16
// Design Name: 
// Module Name: asy_fifo
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 简单梳理一下fifo,同步fifo和异步fifo的差别在于计算读写地址的差距的时候,
//同步fifo是可以直接进行计算;异步fifo是需要垮时钟域再进行的,垮时钟域选择格雷码。
//


module asy_fifo(
//fifo write
input wr_clk,
input wr_en,
input wr_reset,
input [15:0]wr_data,
output reg full,
output reg almost_full,
//fifo read
input rd_clk,
input rd_reset,
input rd_en,
output [15:0]rd_data,
output reg empty,
output reg almost_empty
    );
parameter FIFO_DEEP=16;
parameter ALMOST_EMPTY_GAP=3;
parameter ALMOST_FULL_GAP=3;
wire [3:0]rd_addr;
wire [3:0]wr_addr;
reg [4:0]wr_gap;
reg [4:0]rd_gap;
//reg [15:0]data_out_temp;
reg [4:0]waddr;	//拓展一位,最高位用来判断读写指针是否在同一轮
reg [4:0]raddr;
reg [4:0]waddr_gray;
reg [4:0]waddr_gray_syn2r;
reg [4:0]waddr_gray_syn2r1;
reg [4:0]raddr_gray;
reg [4:0]raddr_gray_syn2w;
reg [4:0]raddr_gray_syn2w1;
reg [4:0]raddr_gray2bin;
reg [4:0]waddr_gray2bin;
wire wen;
wire ren;
//写控制逻辑
assign wen=!full&&wr_en;
always@(posedge wr_clk)
if(wr_reset)
waddr<=5'b0;
else if(wen)
	waddr<=waddr+1;


//地址
/读地址变成格雷码并且同步到写时钟域来
//读地址从二进制转变成格雷码
always @(posedge rd_clk)
if(rd_reset)
	raddr_gray<=0;
else raddr_gray<=raddr^(raddr>>1);
//将读地址的格雷码同步到写时钟域来(打两拍)
always@(posedge wr_clk)
if(wr_reset)begin
		raddr_gray_syn2w <=5'b0;
		raddr_gray_syn2w1<=5'b0;
	end
else begin
		raddr_gray_syn2w<=raddr_gray;
		raddr_gray_syn2w1<=raddr_gray_syn2w;
end
//将从读时钟域同步到写时钟域的格雷码读地址转变成二进制读地址
always@(*)
	raddr_gray2bin={raddr_gray_syn2w1[4],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2]^raddr_gray_syn2w1[1],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2]^raddr_gray_syn2w1[1]^raddr_gray_syn2w1[0]};
//wr_gap间隔计算
/*  always@(raddr_gray2bin or waddr or rd_reset or wr_reset)
	if(~rd_reset && ~wr_reset)begin
		if(raddr_gray2bin[4]^waddr[4])
			wr_gap=raddr_gray2bin[3:0]-waddr[3:0];
		else wr_gap=FIFO_DEEP+raddr_gray2bin-waddr; 
	end
	else begin
		wr_gap <= 4'hf;	
	end */
	
/* assign wr_gap = ((raddr_gray2bin[4]^waddr[4]) ? (raddr_gray2bin[3:0]-waddr[3:0]) : (16 + raddr_gray2bin-waddr)); */
// always@(posedge wr_clk)
// if(wr_reset)
	// wr_gap<=4'd15;
// else if(raddr_gray2bin[4]^waddr[4])
		// wr_gap<=raddr_gray2bin[3:0]-waddr[3:0];
	// else wr_gap<=FIFO_DEEP+raddr_gray2bin-waddr;	 
	
	//间隔计算
always@(*)
	if(raddr_gray2bin[4]^waddr[4])
		wr_gap=raddr_gray2bin[3:0]-waddr[3:0];
	else wr_gap=FIFO_DEEP+raddr_gray2bin-waddr;

	
	
// assign wr_gap=(raddr_gray_syn2w1[4]^waddr[4])?(raddr_gray_syn2w1[3:0]-waddr[3:0]):(FIFO_DEEP+raddr_gray_syn2w1-waddr);
//状态判断
//almost_full产生
always@(posedge wr_clk)
if(wr_reset)
	almost_full<=1'b0;
else if(wr_gap<ALMOST_FULL_GAP)
	almost_full<=1'b1;
else almost_full<=1'b0;
//full信号产生
always@(posedge wr_clk)
if(wr_reset)
	full<=1'b0;
else if(wr_gap==1&&wen)
	full<=1'b1;
else full<=1'b0;



///读时钟域//
//1、读控制
assign ren=(!empty)&&rd_en;

always@(posedge rd_clk)
if(rd_reset)
	raddr<=5'b0;
else if(ren)
	raddr<=raddr+1;

//判断状态需要对读写地址进行比较,因为是异步的,所以需要对地址进行垮时钟域处理
//将写地址同步到读时钟域

always@(posedge wr_clk)
if(wr_reset)
	waddr_gray<=5'b0;
else waddr_gray<=waddr^{1'b0,waddr[4:1]};

always@(posedge rd_clk)
if(rd_reset)begin
	waddr_gray_syn2r<=0;
	waddr_gray_syn2r1<=0;
end
else begin
waddr_gray_syn2r<=waddr_gray;
waddr_gray_syn2r1<=waddr_gray_syn2r;

end
//将同步过来的写地址转换为二进制
always@(*)
	waddr_gray2bin={
	waddr_gray_syn2r1[4],
	waddr_gray_syn2r1[4]^waddr_gray_syn2r1[3],
	waddr_gray_syn2r1[4]^waddr_gray_syn2r1[3]^waddr_gray_syn2r1[2],
	waddr_gray_syn2r1[4]^waddr_gray_syn2r1[3]^waddr_gray_syn2r1[2]^waddr_gray_syn2r1[1],
	waddr_gray_syn2r1[4]^waddr_gray_syn2r1[3]^waddr_gray_syn2r1[2]^waddr_gray_syn2r1[1]^waddr_gray_syn2r1[0]	
	};
//计算rd_gap
always@(*)
	rd_gap=waddr_gray2bin-raddr;
	
	
//状态产生
//almost_empty
always@(posedge rd_clk)
	if(rd_reset)
		almost_empty<=1'b0;
	else if(rd_gap<ALMOST_EMPTY_GAP)
	//else if(rd_gap==3||rd_gap==2||rd_gap==1)
		almost_empty<=1'b1;
	else almost_empty<=1'b0;
	
//empty 
always@(posedge rd_clk)
if(rd_reset)
	empty<=1'b0;
else if(rd_gap==1&&ren)
	empty<=1'b1;
else empty<=1'b0;
assign wr_addr=waddr[3:0];
assign rd_addr=raddr[3:0];
ram ram (
  .clka(wr_clk),    // input wire clka
  .wea(wen),      // input wire [0 : 0] wea
  .addra(wr_addr),  // input wire [3 : 0] addra
  .dina(wr_data),    // input wire [15 : 0] dina
  .douta(),  // output wire [15 : 0] douta
  .clkb(rd_clk),    // input wire clkb
  .web(),      // input wire [0 : 0] web
  .addrb(rd_addr),  // input wire [3 : 0] addrb
  .dinb(),    // input wire [15 : 0] dinb
  .doutb(rd_data)  // output wire [15 : 0] doutb
);
/* ram your_instance_name (
  .clka(clka),    // input wire clka
  .wea(wea),      // input wire [0 : 0] wea
  .addra(addra),  // input wire [3 : 0] addra
  .dina(dina),    // input wire [15 : 0] dina
  .douta(douta),  // output wire [15 : 0] douta
  .clkb(clkb),    // input wire clkb
  .web(web),      // input wire [0 : 0] web
  .addrb(addrb),  // input wire [3 : 0] addrb
  .dinb(dinb),    // input wire [15 : 0] dinb
  .doutb(doutb)  // output wire [15 : 0] doutb
); */
	
	
endmodule

2、仿真文件

`timescale 1ns / 1ps
//
// Company: 
// Engineer: 
// 
// Create Date: 2021/08/03 23:45:06
// Design Name: 
// Module Name: asy_fifo_tst
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//


module asy_fifo_tst();
reg        wr_clk      ;
reg        wr_en       ;
reg        wr_reset    ;
reg [15:0] wr_data     ;
reg        rd_clk      ;
reg        rd_reset    ;
reg        rd_en       ;
wire [15:0]rd_data     ;
wire       empty       ;
wire       almost_empty;
wire       full        ;
wire       almost_full ;

asy_fifo U_asy_fifo(
.wr_clk         (wr_clk       ), 
.wr_en          (wr_en        ),
.wr_reset       (wr_reset     ),
.wr_data        (wr_data      ),
.rd_clk         (rd_clk       ),
.rd_reset       (rd_reset     ),
.rd_en          (rd_en        ),
.rd_data        (rd_data      ),
.empty          (empty        ),
.almost_empty   (almost_empty ),
.full           (full         ),
.almost_full    (almost_full  )
);


initial
begin
wr_clk=1;
rd_clk=1;
rd_reset=1;
wr_reset=1;
wr_data=0;
wr_en=0;
rd_en=0;
# 40 
rd_reset=0;
wr_reset=0;
#40 wr_en=1;
#40 wr_data=1;//写使能后需要隔至少一个时钟周期再给数据,如果直接给数据,最开始的数据是读不出来的
#40 wr_data=1;
#40 wr_data=2;
#40 wr_data=3;
#40 wr_data=4;
#40 wr_data=5;
#40 wr_data=6;
#40 wr_data=7;
#40 wr_data=8;
#40 wr_data=9;
#40 wr_data=10;
#40 wr_data=11;
#40 wr_data=12;
#40 wr_data=13;
rd_en=1;
#40 wr_data=10;
#40 wr_en=0;
#100 rd_en=0;
end

always #20 wr_clk=~wr_clk;
always #10 rd_clk=~rd_clk;
endmodule

需要注意的地方:写使能后需要隔至少一个时钟周期再给数据,如果直接给数据,最开始的数据是读不出来的

(3)仿真现象

这篇文章的代码是参考《FPGA深度解析》。
大家在阅读的过程中,如果发现问题,欢迎批评指正。