异步FIFO的设计和验证

FIFO: First in, First out 是一种算法。 代表先进的数据先出 ,后进的数据后出。

应用领域:读写的时钟域不同,位宽也可以不同。

一、原理

本质是通过在一段地址内,读写指针重复利用这个区域,进行先写后读,数据无限传输。也可以说是读指针不断追加写指针,每次写满就会停下来等待读指针读数据,使得FIFO不满时继续写。

读空状态可以理解为读地址指针追上写地址指针,写满状态可以理解为写地址指针再次追上读地址指针。

img

二、读写指针满空状态

满信号在写时钟域产生,空信号在读时钟域产生。

1. 写满时:

在给定的区域里,写指针比读指针所走了一圈,数据已经写满,如果再写会覆盖掉还没读出的写入数据,造成数据丢失。

数值上写指针比读指针多大一轮,假设数据是四位,那么用五位表示地址,比如,写指针在1 0110,读指针在0 0110,此时处于写满状态。换算到格雷码是最高位和次高位都相反,其余位相同。

写满判断:

读地址(格雷码)通过两级寄存器(会消耗时间)同步到写时钟域,进行是否写满的判断(同步发生在写时钟域)。如果写地址比读地址大一轮(地址二进制只有最高位不同。格雷码的最高位和次高位不同,其他位相同),此时处于写满状态。

2. 读空时:

读指针追上了写指针,将写入的内容全部读出去了,此时读写指针地址相同,处于读空状态。

假设数据是四位,那么用五位表示地址,比如,写指针和读指针都是在1 0110,此时处于读空状态。格雷码也是相同的。

读空判断:

写地址(格雷码)通过两级寄存器(会消耗时间)同步到读时钟域,进行是否读空的判断(同步发生在读时钟域)。如果写地址和读地址相等(格雷码也相等),此时处于读空状态。

三、使用格雷码

在地址比较中,会把不同时钟域的地址放到一个时钟域比较,地址传输使用格雷码,即使出错,也只是会造成空满判断提前一位或落后一位。

二进制的数据右移一位,高位补零与二进制数据进行取反

assign  gray_code = (bin_code>>>1)  ^  bin_code;

最高位是不变的,所以五位格雷码的最高位不变,将剩下四位按照下图变化

通过判断最高位和次高位来判断当前是否写满或者读空

序号二进制格雷码序号二进制格雷码
000000000810001100
100010001910011101
2001000111010101111
3001100101110111110
4010001101211001010
5010101111311011011
6011001011411101001
7011101001511111000

四、跨时钟域出现的问题

  1. 读慢写快
  2. 读快写慢

解决方法:两级寄存器(打两拍,对FIFO的性能有影响,功能无影响)。下方是在两种情况下使用寄存器的分析。

在判断读写不同时钟域的满空状态时,要用两级寄存器将读写地址同步。可以保证FIFO不出现读空继续读,和写满继续写的情况。

在地址传输中可能出现亚稳态的地址数据。如果第一级触发器产生亚稳态输出,这个亚稳态将在第二级触发器取样前稳定,增加更多级触发器,可以进一步降低亚稳态出现的可能性(但会带来的问题是增加了整体电路的时延,使用三级则简单的增加延时,故而使用两级寄存器)。

分析读慢写快情况

**对写满标志的影响:**在读地址同步到写时钟域时,由于读时钟慢,写时钟快,能够很好地锁存读地址,不会出现问题。

**对读空标志的影响:**在写地址同步到读时钟域时,由于读时钟慢,在两拍延迟(快时钟域)里,写时钟会多写几个数据,当判断读空标志产生时,由于多写了数据,实际没有读空,但会停止读数据,不会出现读空继续读的情况。

image-20230320163158809

这是设置每 80ns 产生一个数据,写时钟 30 ns,读时钟 70 ns 同时开始读写的仿真结果,可以看到刚开始由于读写地址相同,判断产生了读空信号。由于写时钟快,读时钟慢,后面不会出现读指针追上写指针的情况,不会产生读空信号,而会产生写满信号。

image-20230320164441959

在采集到读地址格雷码和同步过来的写地址格雷码相等时读空标志置零。

image-20230320164710068

在采集到写地址格雷码 1101 和同步过来的读地址格雷码 0001 最高两位相反,其余位相同时写满标志置一,一直到采集 到1111 和 0010 写满标志置零。

分析读快写慢情况

在写时钟域的两拍中,由于写时钟快,会多写几个数据,导致写满标志提前产生(实际没有写满),但一定不会出现不会出现写满继续写的情况。

image-20230320165919185

这是设置每 80ns 产生一个数据,写时钟 70 ns,读时钟 30 ns 同时开启读写的仿真结果,可以看到由于写时钟快,读时钟慢,不会出现写指针比读指针多走一圈的情况,不会产生写满信号,而会产生读空信号。

五、步骤和思路(图片)

对比着看程序即可,和程序是一样的,下面的思路一个分支是一个 always

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ARdnYqkJ-1679303590686)(https://lemoncos-1300773227.cos.ap-beijing.myqcloud.com/images/%E5%BC%82%E6%AD%A5FIFO1.png)]

五、代码

异步FIFO代码

`timescale 1ns / 1ps

module asy_fifo
#(
    parameter DATA_WIDTH = 32,    //数据总线宽度
    parameter RAM_DEPTH = 8,    //RAM的存储深度,也是2^地址总线宽度
    parameter ADDR = 3            //地址总线宽度  
)
(
    input                       wr_clk,
    input                       wr_en,
    input                       wr_rst_n,
    input [DATA_WIDTH-1:0]      wr_data,
    
    input                       rd_clk,
    input                       rd_rst_n,
    input                       rd_en,
    
    output reg [DATA_WIDTH-1:0] rd_data,
    output reg                  rd_empty,
    output reg                  wr_full
);


//这里的读写地址和读写地址指针不同,指针是本时钟域产生的,地址是另一个时钟域同步过来的,经过了打两拍
    reg [ADDR-1:0] wr_ptr;     //写指针,本时钟域的
    reg [ADDR-1:0] rd_ptr;     //读指针

    reg [ADDR:0] wr_addr;       //写地址,另一个时钟域的
    reg [ADDR:0] rd_addr;       //读地址

    wire [ADDR:0]  wr_addr_gray;
    reg  [ADDR:0]  wr_addr_gray1;
    reg  [ADDR:0]  wr_addr_gray2;   //转化为格雷码并经过了两级寄存器

    wire [ADDR:0]  rd_addr_gray;
    reg  [ADDR:0]  rd_addr_gray1;
    reg  [ADDR:0]  rd_addr_gray2;            

    reg  [DATA_WIDTH-1 : 0] dpram [RAM_DEPTH-1 : 0];         //双端RAM空间,注意这里右边的写法


/****************读写地址的格雷码转换***************/
    assign wr_addr_gray = (wr_addr>>>1) ^ wr_addr;
    assign rd_addr_gray = (rd_addr>>>1) ^ rd_addr;

/****************写满信号和读空信号***********************/

    always @(posedge rd_clk or negedge rd_rst_n) begin
        if(!rd_rst_n)
            rd_empty = 1'b0;
        else if(rd_addr_gray == wr_addr_gray2)
            rd_empty = 1'b1;
        else
            rd_empty = 1'b0;
    end
    always @(posedge wr_clk or negedge wr_rst_n) begin
        if(!wr_rst_n)
            wr_full = 1'b0;
        else if(wr_addr_gray[ADDR]==(~rd_addr_gray2[ADDR]) && wr_addr_gray[ADDR-1]==(~rd_addr_gray2[ADDR-1]) && wr_addr_gray[ADDR-2:0]==rd_addr_gray2[ADDR-2:0])
            wr_full = 1'b1;
        else 
            wr_full = 1'b0;
    end


/****************写地址*****************/

    always @(posedge wr_clk or negedge wr_rst_n) begin
        if (!wr_rst_n)begin  
          wr_addr <= 0;                      
        end
        else if(wr_en && !wr_full) begin
            wr_addr <= wr_addr +1;              
        end
        else
            wr_ptr <= wr_ptr;
    end 
/****************写指针*****************/
        always @(posedge wr_clk or negedge wr_rst_n) begin
        if (!wr_rst_n)begin
          wr_ptr <= 0;                        
        end
        else if(wr_en && !wr_full) begin
            wr_ptr <= wr_ptr + 1;            
        end
        else
            wr_ptr <= wr_ptr;
    end 

/****************写数据*****************/
        always @(posedge wr_clk) begin
        if(wr_en && !wr_full) begin
            dpram[wr_ptr] <= wr_data;       
        end
        else
            dpram[wr_ptr] <= dpram[wr_ptr];
    end 


/****************读地址*****************/

    always @(posedge rd_clk or negedge rd_rst_n) begin
        if (!rd_rst_n)begin
          rd_addr <= 0;
        end
        else if(rd_en && !rd_empty) begin
            rd_addr <= rd_addr + 1;            
        end
        else begin
          rd_addr <= rd_addr;
        end
    end 
/****************读指针*****************/
    always @(posedge rd_clk or negedge rd_rst_n) begin
        if (!rd_rst_n)begin
          rd_ptr <= 0;
        end
        else if(rd_en && !rd_empty) begin
            rd_ptr <= rd_ptr + 1;            
        end
        else begin
          rd_ptr <= rd_ptr;
        end
    end 
/****************读数据*****************/
    always @(posedge rd_clk) begin
        if(rd_en && !rd_empty) begin
            rd_data <= dpram[rd_ptr];         
        end
        else begin
            rd_data <= rd_data;
        end
    end 

/***************写地址同步到读时钟域*******************/
//打两拍发生在读时钟域,防止地址亚稳态问题
    always @(posedge rd_clk or negedge rd_rst_n) begin
        if(!wr_rst_n) begin
            wr_addr_gray1 <= 0;
            wr_addr_gray2 <= 0;
        end
        else begin
            wr_addr_gray1 <= wr_addr_gray;
            wr_addr_gray2 <= wr_addr_gray1;
        end
    end

/***************读地址同步到写时钟域*******************/
    always @(posedge wr_clk or negedge wr_rst_n) begin
        if(!rd_rst_n) begin
            rd_addr_gray1 <= 0;
            rd_addr_gray2 <= 0;
        end
        else begin
            rd_addr_gray1 <= rd_addr_gray;
            rd_addr_gray2 <= rd_addr_gray1;
        end
    end

endmodule

测试代码

`timescale 1ns / 1ps

module asy_fifo_tb();

    parameter DATA_WIDTH = 32;    //数据总线宽度
    parameter RAM_DEPTH = 8;    //RAM的存储深度,也是2^地址总线宽度
    parameter ADDR = 3;

    reg                      wr_clk;
    reg                      wr_en;
    reg                      wr_rst_n;
    reg [DATA_WIDTH-1:0]     wr_data;
    
    reg                       rd_clk;
    reg                       rd_rst_n;
    reg                       rd_en;
    
    wire [DATA_WIDTH-1:0] rd_data;
    wire                  rd_empty;
    wire                  wr_full;    //output型这里用wire,输出型不需要赋值
    
    //localparam wr_perid = 1/30*1000;    //写时钟30MHZ时钟以ns为单位
    //localparam rd_perid = 1/40*1000;

    always #80 wr_data= $random ;
/*****************Enable*******************/
    initial begin
        rd_en = 1'b1;
        wr_en = 1'b1;
    end
/*****************时钟*********************/
    
    initial begin 
        rd_clk = 1'b0;
        wr_clk = 1'b0;
    end
    always #30 wr_clk = ~wr_clk;
    always #40 rd_clk = ~rd_clk;

/***************初始化**************************/
    initial begin
        wr_rst_n = 1'b1;
        rd_rst_n = 1'b1;
        #5  wr_rst_n = 1'b0;
            rd_rst_n = 1'b0;
        #5  wr_rst_n = 1'b1;
            rd_rst_n = 1'b1;
    end 
/*****************例化模块************************/
    asy_fifo myfifo(
         .wr_clk(wr_clk),
         .wr_en(wr_en),
         .wr_rst_n(wr_rst_n),
         .wr_data(wr_data),
    
         .rd_clk(rd_clk),
         .rd_rst_n(rd_rst_n),
         .rd_en(rd_en),
         .rd_data(rd_data),
         .rd_empty(rd_empty),
         .wr_full(wr_full)
 );
 /*
initial begin
	$dumpfile("asy_fifo_tb_y.vcd");  // 指定VCD文件的名字,仿真信息将记录到此文件
	$dumpvars(0, asy_fifo_tb);  // 指定层次数为0,则模块及其下面各层次的所有信号将被记录
    #10000 $finish;
end
*/

endmodule

仿真结果

image-20230320170623422

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
异步FIFO是一种常见的硬件设计模块,用于在不同的时钟之间传输数据。UVM(Universal Verification Methodology)是一种用于验证硬件设计的方法学。在UVM验证中,验证工程师可以使用UVM中提供的各种类和方法来验证异步FIFO的功能和正确性。 下面是一个简单的UVM验证异步FIFO的示例: 首先,我们需要创建一个Transaction类来表示FIFO中的数据项。这个类可以包含需要验证的数据字段。 然后,我们创建一个Agent类来模拟FIFO的发送和接收端。这个Agent类可以包含两个接口,一个用于发送数据到FIFO,另一个用于从FIFO接收数据。Agent类还可以包含一个Monitor来监视FIFO的状态,并将收到的数据转换为Transaction对象。 接下来,我们创建一个Sequencer类来生成数据项并将其发送到FIFO的发送端口。Sequencer类可以使用UVM提供的随机化机制来生成不同的数据项。 然后,我们创建一个Driver类来驱动Sequencer生成的数据项,并将其发送到FIFO的发送端口。 最后,我们可以创建一个Test类来实例化和连接上述组件,并编测试用例来验证异步FIFO的功能和正确性。 在验证过程中,我们可以使用UVM提供的各种断言和功能覆盖率工具来验证异步FIFO的正确性。通过生成不同的测试用例和使用各种场景和边界条件,我们可以尽可能地覆盖所有可能的情况,并验证异步FIFO的正确性。 需要注意的是,上述只是一个简单的UVM验证异步FIFO的示例,实际的验证过程可能更为复杂,需要根据具体的设计和需求进行调整和扩展。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值