跨时钟域之异步FIFO

参考:https://www.cnblogs.com/aslmer/p/6114216.html
文章:Simulation and Synthesis Techniques for Asynchronous

Asynchronous FIFO Design


异步FIFO的读写指针


写指针
  写指针指向当前将要写入数据的位置,复位之后,读写指针被置零。
执行写操作的时候,向写指针指向的存储区写入数据,之后写指针加1,指向接下来要被写入数据的位置。
On a FIFO-write operation, the memory location that is pointed to by the write pointer is written, and then the write pointer is incremented to point to the next location to be written.
读指针:
读指针指向当前要被读取数据的位置,复位时,读写指针被置零,FIFO为空读指针指向一个无效的数据(FIFO为空,empty信号有效——拉高)。当第一个有效数据被写入FIFO之后,写指针增加,empty flag信号被拉低,且读指针一直指向FIFO第一个数据的存储区域。接收逻辑没必要使用两个时钟周期读取数据,这样会使得效率很低。
FIFO空标志:
当读写指针是相等的时候:分两种情况
  1.当读写指针执行复位操作的时候。
  2.当读指针赶上写指针的时候,最后一笔数据从FIFO读出后FIFO为空
FIFO满标志:
  读写指针相等,当FIFO里面的写指针写满一圈之后又转回到和读指针同样的位置。有个问题,读写指针相等的时候怎么判断FIFO是empty还是full?
设计的时候增加一位bit去辅助判断FIFO是空还是满。当写指针超过FIFO的最大寻址范围时,写指针将使辅助位zhi高.
FIFO满的时候:读写指针的低位(n-1位bit)相等,高位(第n位bit)不同。
FIFO空的时候,读写指针的低位和高位都相等。(针对二进制)
但是二进制FIFO指针综合电路复杂,一般采用**格雷码**,文章中采用二进制转换格雷码的方法,判断FIFO的空满标志4位二进制格雷码,有效地址位为三位。
二进制转换为格雷码的算法:rgraynext = (rbinnext>>1) ^ rbinnext; 
采用格列码判断FIFO空满:
**
当最高位和次高位相同,其余位相同认为是读空
当最高位和次高位不同,其余位相同认为是写满
**

采用双寄存器同步后也不能直接判断空满
因为二进制编码判断空满的时候会有较多位电平的同时跳转,容易产生亚稳态现象,而格雷码相邻码组之间只有一位码元发生变化,故可以有效避免这个问题,此时及时产生相邻码元之间电平不跳转的现象,也不会产生危害(最多是让空标志在FIFO不是真正空的时候产生,而不会出现空读的情形)。防止亚稳态带来的危害。
采用双寄存器同步方法判断空满标志会使得电路额外消耗两个时钟周期,从而会使得空满信号的判断有所延迟,这种延迟会有危害吗?

  • 读空标志的判断:现将写指针通过读时钟信号同步到读时钟域,然后将同步后的写指针同读指针比较。如果此时读时钟快,写时钟慢,同步消耗了两个时钟周期,使得同步后的读指针落后于当前的实际的读指针,从而使得满标志会提前产生,这样为FIFO满预留了一定的容限空间,只有稍微影响一下FIFO资源利用,对实际功能没有影响;如果读时钟慢,写时钟快,会造成同步的写指针不是当前时刻的写指针或者说会漏掉一部分写指针,使得FIFO判断是否空的时候,实际并不是空,同样为FIFO带来了一定的空容限
  • 写满标志的判断:现将读时钟同步到写时钟域,然后将同步后的读时钟和写时钟进行比较

1.顶层模块fifo:例化各个子模块


//顶层模块   实例化各个子模块
module fifo
#(
  parameter DSIZE = 8,  //读写数据位宽均设置为8位      
  parameter ASIZE = 4   // 存储地址位宽设置
 ) 
 (  
     output [DSIZE-1:0] rdata,  
     output             wfull,  
     output             rempty,  
     input  [DSIZE-1:0] wdata,  
     input              winc, wclk, wrst_n, 
     input              rinc, rclk, rrst_n
 );

  wire   [ASIZE-1:0] waddr, raddr;  
  wire   [ASIZE:0]   wptr, rptr, wq2_rptr, rq2_wptr;// 内部线网
  
// synchronize the read pointer into the write-clock domain
  sync_r2w  sync_r2w
  (
                    .wq2_rptr    (wq2_rptr),
                    .rptr        (rptr    ),                          
                    .wclk        (wclk    ), 
                    .wrst_n      (wrst_n  )  
 );

// synchronize the write pointer into the read-clock domain
  sync_w2r  sync_w2r 
  (
                   .rq2_wptr(rq2_wptr), 
                   .wptr(wptr),                          
                   .rclk(rclk),
                   .rrst_n(rrst_n)
 );

//this is the FIFO memory buffer that is accessed by both the write and read clock domains.
//This buffer is most likely an instantiated, synchronous dual-port RAM. 
//Other memory styles can be adapted to function as the FIFO buffer. 
  fifomem 
  #(DSIZE, ASIZE)
  fifomem                        
  (
      .rdata(rdata), 
      .wdata(wdata),                           
      .waddr(waddr),
      .raddr(raddr),                           
      .wclken(winc),
      .wfull(wfull),                           
      .wclk(wclk)
  );

//this module is completely synchronous to the read-clock domain and contains the FIFO read pointer and empty-flag logic.  
  rptr_empty
  #(ASIZE)    
  rptr_empty                          
  (
      .rempty(rempty),                          
      .raddr(raddr),                          
      .rptr(rptr),
      .rq2_wptr(rq2_wptr),                          
      .rinc(rinc),
      .rclk(rclk),                          
      .rrst_n(rrst_n)
  );

//this module is completely synchronous to the write-clock domain and contains the FIFO write pointer and full-flag logic
  wptr_full 
  #(ASIZE)    
  wptr_full                         
  (
      .wfull(wfull),
      .waddr(waddr),  
      .wptr(wptr),
      .wq2_rptr(wq2_rptr),    
      .winc(winc),
      .wclk(wclk),        
      .wrst_n(wrst_n)
  );
  endmodule

2.时钟域同步模块sync_r2w:读指针同步到写时钟域wclk


// 采用两级寄存器同步读指针到写时钟域
module sync_r2w
#(
    parameter ADDRSIZE = 4
)
(
    output reg [ADDRSIZE:0] wq2_rptr,   //读指针同步到写时钟域
    input      [ADDRSIZE:0] rptr,       // 格雷码形式的读指针,格雷码的好处后面会细说 
    input                   wclk, wrst_n
);
 
reg [ADDRSIZE:0] wq1_rptr;
 
  always @(posedge wclk or negedge wrst_n)   
      if (!wrst_n) begin
          wq1_rptr <= 0;          
          wq2_rptr <= 0;
      end           
      else begin        
          wq1_rptr<= rptr;
          wq2_rptr<=wq1_rptr;
      end          
  endmodule

原理图

3.时钟域同步模块sync_w2r:写指针同步到读时钟域rclk

//采用两级寄存器同步写指针到读时钟域
module sync_w2r
#(parameter ADDRSIZE = 4)
(
    output reg [ADDRSIZE:0] rq2_wptr, //写指针同步到读时钟域
    input      [ADDRSIZE:0] wptr,     //格雷码形式的写指针
    input                   rclk, rrst_n
);
 
reg [ADDRSIZE:0] rq1_wptr;
 
  always @(posedge rclk or negedge rrst_n)   
      if (!rrst_n)begin
          rq1_wptr <= 0;
          rq2_wptr <= 0;
      end 
      else begin
          rq1_wptr <= wptr;
          rq2_wptr <= rq1_wptr;
      end
        
endmodule

RTL原理图

4.存储模块

//存储模块
module fifomem
#(
    parameter  DATASIZE = 8, // Memory data word width               
    parameter  ADDRSIZE = 4  // 深度为8即地址为3位即可,这里多定义一位的原因是用来判断是空还是满,详细在后文讲到
) // Number of mem address bits
(
    output [DATASIZE-1:0] rdata, 
    input  [DATASIZE-1:0] wdata, 
    input  [ADDRSIZE-1:0] waddr, raddr, 
    input                 wclken, wfull, wclk
);

 ////////////////////////////////这部分没用到,可以单独写一个模块来调用//////////////
`ifdef RAM   //可以调用一个RAM IP核
// instantiation of a vendor's dual-port RAM 
my_ram  mem
      (
          .dout(rdata),
          .din(wdata),     
          .waddr(waddr),
          .raddr(raddr),   
          .wclken(wclken), 
          .wclken_n(wfull),
          .clk(wclk)
      );
//////////////////////////这部分没用到,可以单独写一个模块来调用//////////////////

  `else  //用数组生成存储体
 // RTL Verilog memory model
localparam DEPTH = 1<<ADDRSIZE;   // 左移相当于乘法,2^4 将1左移4位
reg [DATASIZE-1:0] mem [0:DEPTH-1]; //生成2^4个位宽位8的数组
assign rdata = mem[raddr];
always @(posedge wclk)  //当写使能有效且还未写满的时候将数据写入存储实体中,注意这里是与wclk同步的
    if (wclken && !wfull)
        mem[waddr] <= wdata;
 `endif
 endmodule

原理图

5. rptr_empty模块:产生rempty和raddr信号

//产生empty信号和raddar信号的模块
module rptr_empty
#(
    parameter ADDRSIZE = 4
)
(
    output reg                rempty, 
    output     [ADDRSIZE-1:0] raddr,  //二进制形式的读指针
    output reg [ADDRSIZE  :0] rptr,  //格雷码形式的读指针
    input      [ADDRSIZE  :0] rq2_wptr, //同步后的写指针  同步到读时钟域
    input                     rinc, rclk, rrst_n
);
  reg  [ADDRSIZE:0] rbin;
  wire [ADDRSIZE:0] rgraynext, rbinnext;
 // GRAYSTYLE2 pointer
 //将二进制的读指针与格雷码进制的读指针同步
  always @(posedge rclk or negedge rrst_n) 
      if (!rrst_n) begin
          rbin <= 0;
          rptr <= 0;
      end  
      else begin        
          rbin<=rbinnext; //直接作为存储实体的地址
          rptr<=rgraynext;//输出到 sync_r2w.v模块,被同步到 wrclk 时钟域
      end
  // Memory read-address pointer (okay to use binary to address memory)
  assign raddr     = rbin[ADDRSIZE-1:0]; //直接作为存储实体的地址,比如连接到RAM存储实体的读地址端。
  assign rbinnext  = rbin + (rinc & ~rempty); //不空且有读请求的时候读指针加1,//否则输出原先地址的数据waq
  assign rgraynext = (rbinnext>>1) ^ rbinnext; //将二进制的读指针转为格雷码  先右移一位然后与原二进制数异或
  // FIFO empty when the next rptr == synchronized wptr or on reset 
  assign rempty_val = (rgraynext == rq2_wptr); //当读指针等于同步后的写指针,则为空。
  always @(posedge rclk or negedge rrst_n) 
      if (!rrst_n)
          rempty <= 1'b0; 
      else     
          rempty <= rempty_val;
 
endmodule

RTL原理图

6.wfull和waddr信号产生的模块

//产生写满信号(wptr_full)以及写地址的逻辑部分
module wptr_full
#(
    parameter ADDRSIZE = 4
) 
(
    output reg                wfull,   
    output     [ADDRSIZE-1:0] waddr,
    output reg [ADDRSIZE  :0] wptr, 
    //同步后的读指针,注意是多了一个位,用作判断是否fifo满(已经在rptr_empty模块将二进制读地址转化为格雷码形式,经过写时钟域同步后为wq2_rptr)
    input      [ADDRSIZE  :0] wq2_rptr,
    input                     winc, wclk, wrst_n //
);
  reg  [ADDRSIZE:0] wbin;
  wire [ADDRSIZE:0] wgraynext, wbinnext;
  // GRAYSTYLE2 pointer
  always @(posedge wclk or negedge wrst_n)   
      if (!wrst_n)
          {wbin, wptr} <= 0;   
      else         
          {wbin, wptr} <= {wbinnext, wgraynext};// wptr 被同步到sync_w2r.v读时钟域rclk
  // Memory write-address pointer (okay to use binary to address memory) 
  assign waddr = wbin[ADDRSIZE-1:0];
  assign wbinnext  = wbin + (winc & ~wfull);// winc为1的时候数据才能写入对应的地址中,否则即使wdata=0,数据是没有被写入地址的!
  assign wgraynext = (wbinnext>>1) ^ wbinnext; //二进制转为格雷码   
  //-----------------------------------------------------------------
  assign wfull_val = (wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]}); //当最高位和次高位不同其余位相同时则写指针超前于读指针一圈,即写满。
//   assign wfull_val = ( (wgraynext[ADDRSIZE]      !=wq2_rptr[ADDRSIZE])&&
//                        (wgraynext[ADDRSIZE-1]    !=wq2_rptr[ADDRSIZE-1])&&
//                        (wgraynext[ADDRSIZE-2]    ==wq2_rptr[ADDRSIZE-2]) );
//写满举例:假设读地址为fifo起始位置:00000(格雷码)
//当写地址由(01111)b增加1后变为(10000)b,此时要先判断存储区域是否写满,然后才会考虑是否写入数据
//转换为格雷码01000^10000=11000 
//此时对比最高位和次高位可知,FIFO已满,实际上写指针和读指针此时刚好跨越一圈重合 
  always @(posedge wclk or negedge wrst_n)
      if (!wrst_n)
          wfull  <= 1'b0;   
      else     
          wfull  <= wfull_val;
 
  endmodule

RTL原理图

testbench文件


`timescale 1ns /1ns 

module test();
reg  [7:0] wdata;
reg           winc, wclk, wrst_n; 
reg           rinc, rclk, rrst_n;
wire [7:0] rdata;  
wire           wfull;  
wire          rempty;  

fifo

u_fifo (
               .rdata(rdata),  
               .wfull(wfull),  
               .rempty(rempty),  
               .wdata (wdata),  
               .winc  (winc), 
               .wclk  (wclk), 
               .wrst_n(wrst_n), 
               .rinc(rinc), 
               .rclk(rclk), 
               .rrst_n(rrst_n)
 );
localparam CYCLE = 20;
localparam CYCLE1 = 40;



        //时钟周期,单位为ns,可在此修改时钟周期。
     
            //生成本地时钟50M (写时钟50M)
            initial begin
                wclk = 0;
                forever
                #(CYCLE/2)
                wclk=~wclk;
            end
            //  读时钟25M
            initial begin
                rclk = 0;
                forever
                #(CYCLE1/2)
                rclk=~rclk;
            end

            //产生复位信号
            initial begin
                wrst_n = 1;
                #2;
                wrst_n = 0;
                #(CYCLE*3);
                wrst_n = 1;
            end
            
             initial begin
                rrst_n = 1;
                #2;
                rrst_n = 0;
                #(CYCLE*3);
                rrst_n = 1;
            end

            always  @(posedge wclk or negedge wrst_n)begin
                if(wrst_n==1'b0)begin
                    winc <= 0;
                    // rinc <= 0;
                end
                else begin
                    winc <= $random;
                    $display ("winc=%h", winc);
                    // rinc <= $random;
                    // $display ("winc=%h", winc);
                end
            end

            always  @(posedge rclk or negedge rrst_n)begin
                if(rrst_n==1'b0)begin                  
                    rinc <= 0;
                end
                else begin                
                    rinc <= $random;
                    $display ("rinc=%h", rinc);
                end
            end
always@(*)begin
  if(winc == 1)
    wdata= $random ;
  else
    wdata = 0;
end  
endmodule

仿真图:

原理图:

posted @ 2021-09-14 15:18  冰峰漫步  阅读(1840)  评论(0编辑  收藏  举报