异步FIFO的设计和验证

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

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

一、原理

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

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

img

二、读写指针满空状态

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

1. 写满时:

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

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

写满判断:

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

2. 读空时:

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

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

读空判断:

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

三、使用格雷码

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

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

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

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

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

序号 二进制 格雷码 序号 二进制 格雷码
0 0000 0000 8 1000 1100
1 0001 0001 9 1001 1101
2 0010 0011 10 1010 1111
3 0011 0010 11 1011 1110
4 0100 0110 12 1100 1010
5 0101 0111 13 1101 1011
6 0110 0101 14 1110 1001
7 0111 0100 15 1111 1000

四、跨时钟域出现的问题

  1. 读慢写快 w2r
  2. 读快写慢 r2w

解决方法:两级寄存器(打两拍,对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

异步FIFO1

五、代码

异步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

本文由博客一文多发平台 OpenWrite 发布!

你可能感兴趣的:(异步FIFO的设计和验证)