FIFO的原理和设计

描述

FIFO(First In First Out)是异步数据传输时经常使用的存储器。该存储器的特点是数据先进先出(后进后出)。其实,多位宽数据的异步传输问题,无论是从快时钟到慢时钟域,还是从慢时钟到快时钟域,都可以使用 FIFO 处理。

FIFO 原理

工作流程

(1) 复位之后,在写时钟和非满状态信号的控制下,数据可以写入 FIFO 中。RAM 的写地址从 0 开始,每写一次数据写地址指针加一,指向下一个存储单元。当 FIFO 写满后,数据将不能再写入,否则数据会因覆盖而丢失。

(2) FIFO 数据为非空、或满状态时,在读时钟和非空状态信号的控制下,数据可以从 FIFO 中读出。RAM 的读地址从 0 开始,每读一次数据读地址指针加一,即指向下一个存储单元。当 FIFO 读空后,就不能再读数据,否则读出的数据将是错误的。

(3) FIFO 的存储结构为双口 RAM,允许读写同时进行。FIFO 的读写指针是循环计数的,即 读写指针对应的 RAM 地址超过 FIFO 深度时,会溢出归零,重新计数。

典型异步 FIFO 结构图如下所示。相关信号及空满状态的原理将在下面一一说明。

计数器

读写时刻

(1) 关于写时刻,只要 FIFO 中数据为非满状态,就可以进行写操作;如果 FIFO 为满状态,则禁止再写数据。

(2) 关于读时刻,只要 FIFO 中数据为非空状态,就可以进行读操作;如果 FIFO 为空状态,则禁止再读数据。

(3) 总之,如果一段时间内不间断的对 FIFO 同时进行读写操作,则要求写 FIFO 速率不能大于读 FIFO 速率。

读空状态

(1) 复位时,FIFO 中没有数据,空状态信号拉高。当 FIFO 被写入数据后,空状态信号拉低,表示非空状态。当读数据地址追赶上写地址,即读写地址都相等时,FIFO 为空状态。

(2) 因为 FIFO 是异步的,所以读写地址进行比较时,需要同步打拍逻辑,就需要耗费一定的时间。因此,空状态的指示信号不是实时的,会有一定的延时。如果在这段延迟时间内又有新的数据写入 FIFO,就会出现空状态指示信号有效,但实际上 FIFO 中存在数据的现象。

(3) 严格来讲该空状态指示是错误的。但是产生空状态的意义在于防止读操作对空状态的 FIFO 进行数据读取。产生空状态信号时,实际 FIFO 中有数据,相当于提前判断了空状态信号,此时不再进行读 FIFO 操作也是安全的。所以,该设计从应用上来说是没有问题的。

(4) 牢记,读空状态信号,是在读时钟域产生的。

写满状态

(1) 复位时,FIFO 中没有数据,满信号是拉低的,表示 FIFO 中的数据没有写满 (其实 FIFO 是空的 )。当 FIFO 开始写数据且读操作不进行或读速率相对较慢时,只要写数据地址超过读数据地址的 FIFO 深度时,便会产生满状态信号。此时写地址和读地址也是相等的,但是意义是不一样的。

计数器

(2) 此时经常使用多余的 1bit 分别当做读写地址的拓展位,来区分读写地址相同的时候,FIFO 的状态是空还是满状态。当读写地址与拓展位均相同的时候,表明读写数据的数量是一致的,则此时 FIFO 是空状态。如果读写地址相同,拓展位相反,表明写数据的数量已经超过读数据数量的一个 FIFO 深度了,此时 FIFO 是满状态。当然,此条件成立的前提是空状态禁止读操作、满状态禁止写操作。

(3) 同理,由于异步延迟逻辑的存在,满状态信号也不是实时的。但是也相当于提前判断了满状态信号,此时不再进行写 FIFO 操作也不会影响应用的正确性。

(4) 牢记,写满状态信号,是在写时钟域产生的。

格雷码

(1) 当读写时钟都是同一个时钟时,此时 FIFO 是同步的,直接对读写指针进行比对,产生空、满信号即可。

(2) 当读写时钟是异步的时候,因为读时钟域产生读空信号,写时钟域产生写满信号,所以产生空逻辑信号时,需要将写指针同步到读时钟域,再与读指针进行比较;产生满逻辑信号时,需要将读指针同步到写时钟域,再与写指针进行比较。

(3) 因为读写指针的信号宽度一般都是大于 1bit 的,所以同步处理时不能直接对多位宽的读写指针进行延迟打拍,需要借助格雷码对读写指针进行转换,保证每一个周期内地址指针只有 1bit 变化,然后再进行延迟打拍的同步处理。

(4) 4bit 的二进制码与格雷码之间的变化关系如下所示,其中 ⊕ 表示异或操作符。由图可知,二进制码对应的十六进制码递增时,二进制码对应的相邻的两个格雷码之间只有 1bit 数据有变化。当多位宽信号每次只有 1bit 数据变化时,可以使用延迟打拍的方法对其进行同步处理。

计数器

(5) 下面对空逻辑的产生进行举例说明:

5.1) 首先需要对写指针 waddr 进行组合逻辑的格雷码变换 waddr_gray。

5.2) 为了保证 waddr_gary 在读时钟域每次被采集时只有 1bit 数据变化,则 waddr_gray 需要在其源时钟域即写时钟域进行一拍缓存 waddr_gray_d。因为 waddr 到 waddr_gray 的组合逻辑变换时,每次两者之间不只是有 1bit 变化的。

5.3) 在读时钟域对 waddr_gray_d 进行打拍同步,得到读时钟域同步后的写指针为 waddr_gray_rclk。

5.4) 根据格雷码变换规则,空信号有效时二进制码相等的读写指针,变为格雷码之后仍然相等。所以直接使用 waddr_gray_rclk 与读指针进行组合逻辑变换后的格雷码进行相等比较,即可产生读空信号逻辑。

5.5) 需要说明的是,满信号有效时,带有拓展位的读写指针高 1bit 相反、低位相同。所以变为格雷码之后,写满信号产生的条件,则是读写指针高 2bit 相反、低位相同 (请读者思考一下为什么?)。

FIFO 设计

设计要求

为设计应用于各种场景的 FIFO,这里对设计提出如下要求:

(1) FIFO 是异步的,即读写控制信号来自不同的时钟域。

(2) FIFO 深度、宽度参数化,输出空、满状态信号,并输出一个可配置的满状态信号。当 FIFO 内部数据达到设置的参数数量时,该信号拉高,此时需要对格雷码进行反解码。

(3) 输入数据和输出数据位宽可以不一致,但要保证写数据、写地址位宽与读数据、读地址位宽的一致性。例如写数据位宽 8bit,写地址位宽为 6bit(64 个数据)。如果输出数据位宽要求 32bit,则输出地址位宽应该为 4bit(16 个数据)。

双口 RAM 设计

RAM 地址位宽、数据位宽等端口参数可配置,读写位宽一致。实际中 RAMDP(Dual Port) 是需要使用 Memory IP 的,这里创建的 RAM 并没有考虑到异步问题。

Verilog 描述如下。

module  ramdp
  #(  parameter       AW     = 5 ,
      parameter       DW     = 16
   )
   (
    input                   CLK_WR , //写时钟
    input [DW-1:0]          D ,      //写数据
    input                   WR_EN ,  //写使能
    input [AW-1:0]          ADDR_WR ,//写地址
    input                   CLK_RD , //读时钟
    input                   RD_EN ,  //读使能
    input [AW-1:0]          ADDR_RD ,//读地址
    output reg [DW-1:0]     Q        //读数据
    );

   reg [DW-1:0]                 mem [(1<

计数器设计

计数器用于产生读写地址信息,位宽可配置,不需要设置结束值,让其溢出后自动重新计数即可。同时该计数器还具有格雷码转换与缓存的功能。

Verilog 描述如下。

module  ccnt_gray
  #(parameter W = 32'd8
    )
   (
    input              rstn ,
    input              clk ,
    input              en ,
    output [W-1:0]     cnt ,
    output [W-1:0]     cnt_gray ,
    output [W-1:0]     cnt_gray_d
    );


   reg [W-1:0]          cnt_r ;
   always @(posedge clk or negedge rstn) begin
      if (!rstn) begin
         cnt_r        <= 'b0 ;
      end
      else if (en) begin
         cnt_r        <= cnt_r + 1'b1 ;
      end
   end
   assign cnt = cnt_r ;
   assign cnt_gray      = cnt ^ (cnt>>1);


   reg [W-1:0]          cnt_gray_buf ;
   always @(posedge clk or negedge rstn) begin
      if (!rstn) begin
         cnt_gray_buf   <= 'b0 ;
      end
      else begin
         cnt_gray_buf   <= cnt_gray ;
      end
   end
   assign cnt_gray_d = cnt_gray_buf ;


endmodule

多位宽数据同步设计

读写指针进行格雷码变换并缓存后,每一个计数周期内地址指针只有 1bit 变化,所以可以直接使用延迟打拍的方法进行同步。数据宽度、同步级数均可配置。

Verilog 描述如下。

module  data1c_sync
  #(parameter DW    = 8,
    parameter STAGE = 3
    )
   (
    input              rstn ,
    input              clk ,
    input [DW-1:0]     data_in ,
    output [DW-1:0]    data_out
    );


   reg [DW-1: 0]       data_r [STAGE-1: 0];
   integer             i ;
   always @(posedge clk or negedge rstn) begin
      if (!rstn) begin
         for (i=0; i

格雷码反解码

因为该 FIFO 还存在一个可配置的满状态信号输出,所以需要对格雷码同步后的读指针进行反解码,然后在写时钟域与写指针进行比较,以判读当前 FIFO 中数据的具体个数。

module  gray_decode
  #(parameter W = 32'd8
    )
   (
    input [W-1:0]      gray ,
    output [W-1:0]     gray_decode
    );


   integer             i ;
   reg [W-1:0] gray_decode_r ;
   always @(*) begin
      gray_decode_r[W-1]        = gray[W-1];
      for (i=W-2; i>=0; i=i-1) begin
         gray_decode_r[i]     = gray_decode_r[i+1] ^ gray[i];
      end
   end


   assign gray_decode = gray_decode_r ;
endmodule

FIFO 设计

该模块为 FIFO 的主体部分,产生读写控制逻辑,包括读写指针、读写有效时刻以及空、满、可编程满状态的逻辑。

实际上此模块已经是典型的 FIFO 设计,有需要的读者可以直接使用该层次的 FIFO 代码进行测试,甚至应用到自己的设计之中。

module  fifo
    #(  parameter       DW        = 16 ,
        parameter       DEPTH     = 32 ,
        parameter       PROG_DEPTH = 16) //可设置深度
    (
        input                   rstn,  //读写使用一个复位
        input                   wclk,  //写时钟
        input                   wren,  //写使能
        input [DW-1: 0]         wdata, //写数据
        output                  wfull,    //写满标志
        output                  prog_full, //可编程满标志
        input                   rclk,  //读时钟
        input                   rden,  //读使能
        output [DW-1 : 0]       rdata, //读数据
        output                  rempty   //读空标志
     );


   localparam AW = log2b(DEPTH);
   //==================== push/wr counter ===============
   //wptr/waddr using one more bit to indict new-loop
   wire [AW:0]          waddr ;
   wire [AW:0]          waddr_gray ;
   wire [AW:0]          waddr_gray_d ;
   ccnt_gray        #(.W(AW+1))
   u_push_cnt(
      .rstn             (rstn),
      .clk              (wclk),
      .en               (wren && !wfull), //full 时禁止写
      .cnt              (waddr),
      .cnt_gray         (waddr_gray),
      .cnt_gray_d       (waddr_gray_d)
      );


   // sync: wptr from wclk to rclk
   wire [AW:0]          waddr_gray_rclk ;
   data1c_sync   #(.DW(AW+1), .STAGE(3))
   u_waddr_to_rclk
   (
      .rstn        (rstn),
      .clk         (rclk),
      .data_in     (waddr_gray_d),
      .data_out    (waddr_gray_rclk)
    );
   //============== pop/rd counter ===================
   wire [AW:0]          raddr ;
   wire [AW:0]          raddr_gray ;
   wire [AW:0]          raddr_gray_d ;
   ccnt_gray        #(.W(AW+1))
   u_pop_cnt(
      .rstn             (rstn),
      .clk              (rclk),
      .en               (rden && !rempty), //full 时禁止写
      .cnt              (raddr),
      .cnt_gray         (raddr_gray),
      .cnt_gray_d       (raddr_gray_d)
      );
   // sync: rdtr from rclk to wclk
   wire [AW:0]          raddr_gray_wclk ;
   data1c_sync   #(.DW(AW+1), .STAGE(3) )
   u_raddr_to_wclk
   (
       .rstn        (rstn),
       .clk         (wclk),
       .data_in     (raddr_gray_d),
       .data_out    (raddr_gray_wclk)
    );
   //============== full/empty logic ===================
   //(1) empty logic
   assign rempty = (raddr_gray == waddr_gray_rclk);




   //(2) full logic
   assign wfull  = (waddr_gray[AW:AW-1] == ~raddr_gray_wclk[AW:AW-1]) &&
                   (waddr_gray[AW-2:0] == raddr_gray_wclk[AW-2:0]) ;


   //(3) porgrammable full
   //waddr gray decode
   wire [AW:0]          raddr_degray_wclk ;
   gray_decode  #(.W(AW+1))
   u_waddr_degray_rclk (
       .gray            (raddr_gray_wclk),
       .gray_decode     (raddr_degray_wclk)
     );
   //prog full
   wire [AW:0]  waddr_delta = waddr >= raddr_degray_wclk ?
                              (waddr - raddr_degray_wclk) :
                              ((1<<(AW+1)) + waddr - raddr_degray_wclk) ;
   assign       prog_full   = waddr_delta >= PROG_DEPTH ;


   //双口 ram 例化
   ramdp     #(.AW(AW), .DW (DW))
   u_ramdp
     (
      .CLK_WR          (wclk),
      .WR_EN           (wren & !wfull), //写满时禁止写
      .ADDR_WR         (waddr[AW-1:0]),
      .D               (wdata[DW-1:0]),
      .CLK_RD          (rclk),
      .RD_EN           (rden & !rempty), //读空时禁止读
      .ADDR_RD         (raddr[AW-1:0]),
      .Q               (rdata[DW-1:0])
      );


   function  integer log2b ;
      input     integer depth ;
      for (log2b=0; (1<

FIFO 调用

下面可以调用设计的 FIFO,完成多位宽数据传输的异步处理。

写数据位宽为 4bit,写深度为 32。

读数据位宽为 16bit,读深度为 8,可配置 full 深度为 16。

该模块只是 FIFO 的一个具体应用,用于数据的异步传输、缓存与整合。

//ensure write rate < read rate
module  fifo_buf
  #(  parameter       DWI        = 4 , //width 4
      parameter       AWI        = 5 , //depth 32
      parameter       DWO        = 16 ,
      parameter       AWO        = 3 ,
      parameter       PROG_DEPTH = 16
   )
   (
      input                     rstn,  //读写使用一个复位
      //data in
      input                     din_clk,  //写时钟
      input                     din_en,  //写使能
      input [DWI-1: 0]          din, //写数据
      //data out
      input                     dout_clk,  //读时钟
      output                    dout_valid,  //读使能
      output [DWO-1 : 0]        dout //读数据
    );


   wire                         wfull ;    //写满标志
   wire                         prog_full ; //可编程满标志
   wire                         rempty ;   //读空标志
   wire [DWI-1:0]               rdata_fifo ;
   wire                         rden_fifo ;
   fifo  #(.DW(DWI), .DEPTH(1<

testbench

testbench 描述如下,用于测试空、满逻辑信号,以及读写操作。测试中只列举了输入数据位宽小于输出数据位宽的情景。

`timescale 1ns/1ns
`define SMALL2BIG


module test ;
`ifdef SMALL2BIG
   reg          rstn ;
   reg          clk_slow, clk_fast ;
   reg [3:0]    din ;
   reg          din_en ;
   wire [15:0]  dout ;
   wire         dout_valid ;


   //reset
   initial begin
      clk_slow  = 0 ;
      clk_fast  = 0 ;
      rstn      = 0 ;
      #50 rstn  = 1 ;
   end


   //读时钟 clock_slow 较快于写时钟 clk_fast 的 1/4
   //保证读数据稍快于写数据
   parameter CYCLE_WR = 40 ;
   always #(CYCLE_WR/2/4) clk_fast = ~clk_fast ;
   always #(CYCLE_WR/2-1) clk_slow = ~clk_slow ;


   //data generate
   initial begin
      din       = 16'h4321 ;
      din_en    = 0 ;
      wait (rstn) ;
      //(1) 测试 full、prog_full、empyt 信号
      force test.u_data_buf.u_fifo.rden = 1'b0 ;
      repeat(32) begin
         @(negedge clk_fast) ;
         din_en = 1'b1 ;
         din    = {$random()} % 16;
      end
      @(negedge clk_fast) din_en = 1'b0 ;


      //(2) 测试数据读写
      #500 ;
      rstn = 0 ;
      #10 rstn = 1 ;
      release test.u_data_buf.u_fifo.rden ;
      repeat(60) begin
         @(negedge clk_fast) ;
         if (!test.u_data_buf.u_fifo.wfull) begin
            din_en = 1'b1 ;
            din    = {$random()} % 16;
         end
         else begin
            din_en = 1'b0 ;
         end
      end


      //(3) 停止读取再一次测试 empyt、full、prog_full 信号
      #800 ;
      force test.u_data_buf.u_fifo.rden = 1'b0 ;
      repeat(18) begin
         @(negedge clk_fast) ;
         din_en = 1'b1 ;
         din    = {$random()} % 16;
      end
   end


   fifo_buf #(.DWI(4), .AWI(5), .DWO(16), .AWO(3), .PROG_DEPTH(16))
     u_data_buf(
        .rstn           (rstn),
        .din_clk        (clk_fast),
        .din            (din),
        .din_en         (din_en),
        .dout_clk       (clk_slow),
        .dout           (dout),
        .dout_valid     (dout_valid));


`else // !`ifdef SMALL2BIG
`endif


   //stop sim
   initial begin
      forever begin
         #100;
         if ($time >= 5000)  $finish ;
      end
   end


endmodule

仿真分析

根据 testbench 中的 3 步测试激励,分析如下:

测试 (1) : FIFO 端口及一些内部信号时序结果如下。

由图可知,FIFO 内部开始写数据,空状态信号 rempty 拉低(红色 M1 ) 之前有一段时间延迟,这是写地址同步延时导致的。

由于此时没有进行读 FIFO 操作,所以 prog_full 与 full 拉高 (黄色 M2 与绿色 M3) 几乎没有延迟。

计数器

测试 (2) : FIFO 同时进行读写时,fifo 与 fifo_buf 模块的端口信号如下所示。

由图可知,数据开始传输时,fifo 模块的等位宽数据输出、fifo_buf 整合之后的数据输出,均与输入数据对应,没有传输错误。

计数器

测试 (3) :整个 FIFO 读写行为及读停止的时序仿真图如下所示。

由图可知,读写操作同时进行时,wfull 信号会不断翻转 (M4 时刻之前),这是因为此时 fifo 的使用方法是非满即写,非空即读。

M4 时刻禁止写操作后(din_en 恒为 0),由于读一致存在,所以 full 信号会拉低并保持,表示 fifo 中数据一直未满。而 prog_full 也会相继拉低 (M5 时刻),表示 FIFO 中的数据已经低于配置的数目。

当恢复写操作后 (din_en 恒为 1,对应 M6 时刻),prog_full 与 full 信号相继拉高。

计数器

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分