异步FIFO设计(Verilog)

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


FIFO 原理

工作流程

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

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

FIFO 的存储结构为双口 RAM,所以允许读写同时进行。典型异步 FIFO 结构图如下所示。端口及内部信号将在代码编写时进行说明。

读写时刻

关于写时刻,只要 FIFO 中数据为非满状态,就可以进行写操作;如果 FIFO 为满状态,则禁止再写数据。关于读时刻,只要 FIFO 中数据为非空状态,就可以进行读操作;如果 FIFO 为空状态,则禁止再读数据。不管怎样,一段正常读写 FIFO 的时间段,如果读写同时进行,则要求写 FIFO 速率不能大于读速率。

读空状态

开始复位时,FIFO 没有数据,空状态信号是有效的。当 FIFO 中被写入数据后,空状态信号拉低无效。当读数据地址追赶上写地址,即读写地址都相等时,FIFO 为空状态。

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

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

写满状态

开始复位时,FIFO 没有数据,满信号是无效的。当 FIFO 中被写入数据后,此时读操作不进行或读速率相对较慢,只要写数据地址超过读数据地址一个 FIFO 深度时,便会产生满状态信号。此时写地址和读地址也是相等的,但是意义是不一样的。

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

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


FIFO 设计

设计要求

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

  • (1) FIFO 深度、宽度参数化,输出空、满状态信号,并输出一个可配置的满状态信号。当 FIFO 内部数据达到设置的参数数量时,拉高该信号。
  • (2) 输入数据和输出数据位宽可以不一致,但要保证写数据、写地址位宽与读数据、读地址位宽的一致性。例如写数据位宽 8bit,写地址位宽为 6bit(64 个数据)。如果输出数据位宽要求 32bit,则输出地址位宽应该为 4bit(16 个数据)。
  • (3) FIFO 是异步的,即读写控制信号来自不同的时钟域。输出空、满状态信号之前,读写地址信号要用格雷码做同步处理,通过减少多位宽信号的翻转来减少打拍法同步时数据的传输错误。 格雷码与二进制之间的转换如下图所示。

双口 RAM 设计

RAM 端口参数可配置,读写位宽可以不一致。建议 memory 数组定义时,以长位宽地址、短位宽数据的参数为参考,方便数组变量进行选择访问。

Verilog 描述如下。

module  ramdp
    #(  parameter       AWI     = 5 ,
        parameter       AWO     = 7 ,
        parameter       DWI     = 64 ,
        parameter       DWO     = 16
        )
    (
        input                   CLK_WR , //写时钟
        input                   WR_EN ,  //写使能
        input [AWI-1:0]         ADDR_WR ,//写地址
        input [DWI-1:0]         D ,      //写数据
        input                   CLK_RD , //读时钟
        input                   RD_EN ,  //读使能
        input [AWO-1:0]         ADDR_RD ,//读地址
        output reg [DWO-1:0]    Q        //读数据
     );
   //输出位宽大于输入位宽,求取扩大的倍数及对应的位数
   parameter       EXTENT       = DWO/DWI ;
   parameter       EXTENT_BIT   = AWI-AWO > 0 ? AWI-AWO : 'b1 ;
   //输入位宽大于输出位宽,求取缩小的倍数及对应的位数
   parameter       SHRINK       = DWI/DWO ;
   parameter       SHRINK_BIT   = AWO-AWI > 0 ? AWO-AWI : 'b1;

   genvar i ;
   generate
      //数据位宽展宽(地址位宽缩小)
      if (DWO >= DWI) begin
         //写逻辑,每时钟写一次
         reg [DWI-1:0]         mem [(1<<AWI)-1 : 0] ;
         always @(posedge CLK_WR) begin
            if (WR_EN) begin
               mem[ADDR_WR]  <= D ;
            end
         end

         //读逻辑,每时钟读 4 次
         for (i=0; i<EXTENT; i=i+1) begin
            always @(posedge CLK_RD) begin
               if (RD_EN) begin
                  Q[(i+1)*DWI-1: i*DWI]  <= mem[(ADDR_RD*EXTENT) + i ] ;
               end
            end
         end
      end

      //=================================================
      //数据位宽缩小(地址位宽展宽)
      else begin
         //写逻辑,每时钟写 4 次
         reg [DWO-1:0]         mem [(1<<AWO)-1 : 0] ;
         for (i=0; i<SHRINK; i=i+1) begin
            always @(posedge CLK_WR) begin
               if (WR_EN) begin
                  mem[(ADDR_WR*SHRINK)+i]  <= D[(i+1)*DWO -1: i*DWO] ;
               end
            end
         end

         //读逻辑,每时钟读 1 次
         always @(posedge CLK_RD) begin
            if (RD_EN) begin
                Q <= mem[ADDR_RD] ;
            end
         end
      end
   endgenerate

endmodule

计数器设计

计数器用于产生读写地址信息,位宽可配置,不需要设置结束值,让其溢出后自动重新计数即可。Verilg 描述如下。

module  ccnt
  #(parameter W )
   (
    input              rstn ,
    input              clk ,
    input              en ,
    output [W-1:0]     count
    );

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

endmodule

FIFO 设计

该模块为 FIFO 的主体部分,产生读写控制逻辑,并产生空、满、可编程满状态信号。

鉴于篇幅原因,这里只给出读数据位宽大于写数据位宽的逻辑代码。

module  fifo
    #(  parameter       AWI        = 5 ,
        parameter       AWO        = 3 ,
        parameter       DWI        = 4 ,
        parameter       DWO        = 16 ,
        parameter       PROG_DEPTH = 16) //可设置深度
    (
        input                   rstn,  //读写使用一个复位
        input                   wclk,  //写时钟
        input                   winc,  //写使能
        input [DWI-1: 0]        wdata, //写数据

        input                   rclk,  //读时钟
        input                   rinc,  //读使能
        output [DWO-1 : 0]      rdata, //读数据

        output                  wfull,    //写满标志
        output                  rempty,   //读空标志
        output                  prog_full //可编程满标志
     );

   //输出位宽大于输入位宽,求取扩大的倍数及对应的位数
   parameter       EXTENT       = DWO/DWI ;
   parameter       EXTENT_BIT   = AWI-AWO ;
   //输出位宽小于输入位宽,求取缩小的倍数及对应的位数
   parameter       SHRINK       = DWI/DWO ;
   parameter       SHRINK_BIT   = AWO-AWI ;

   //==================== push/wr counter ===============
   wire [AWI-1:0]      waddr ;
   wire                wover_flag ; //多使用一位做写地址拓展
   ccnt         #(.W(AWI+1))            
   u_push_cnt(
      .rstn           (rstn),
      .clk            (wclk),
      .en             (winc && !wfull), //full 时禁止写
      .count          ({wover_flag, waddr})
        );

   //============== pop/rd counter ===================
   wire [AWO-1:0]            raddr ;
   wire                      rover_flag ;  //多使用一位做读地址拓展
   ccnt         #(.W(AWO+1))    
   u_pop_cnt(
      .rstn           (rstn),
      .clk            (rclk),
      .en             (rinc & !rempty), //empyt 时禁止读
      .count          ({rover_flag, raddr})
      );

   //==============================================
   //窄数据进,宽数据出
generate
   if (DWO >= DWI) begin : EXTENT_WIDTH

      //格雷码转换
      wire [AWI:0] wptr    = ({wover_flag, waddr}>>1) ^ ({wover_flag, waddr}) ;
      //将写数据指针同步到读时钟域
      reg [AWI:0]  rq2_wptr_r0 ;
      reg [AWI:0]  rq2_wptr_r1 ;
      always @(posedge rclk or negedge rstn) begin
         if (!rstn) begin
            rq2_wptr_r0     <= 'b0 ;
            rq2_wptr_r1     <= 'b0 ;
         end
         else begin
            rq2_wptr_r0     <= wptr ;
            rq2_wptr_r1     <= rq2_wptr_r0 ;
         end
      end

      //格雷码转换
      wire [AWI-1:0] raddr_ex = raddr << EXTENT_BIT ;
      wire [AWI:0]   rptr     = ({rover_flag, raddr_ex}>>1) ^ ({rover_flag, raddr_ex}) ;
      //将读数据指针同步到写时钟域
      reg [AWI:0]    wq2_rptr_r0 ;
      reg [AWI:0]    wq2_rptr_r1 ;
      always @(posedge wclk or negedge rstn) begin
         if (!rstn) begin
            wq2_rptr_r0     <= 'b0 ;
            wq2_rptr_r1     <= 'b0 ;
         end
         else begin
            wq2_rptr_r0     <= rptr ;
            wq2_rptr_r1     <= wq2_rptr_r0 ;
         end
      end

      //格雷码反解码
      //如果只需要空、满状态信号,则不需要反解码
      //因为可编程满状态信号的存在,地址反解码后便于比较
      reg [AWI:0]       wq2_rptr_decode ;
      reg [AWI:0]       rq2_wptr_decode ;
      integer           i ;
      always @(*) begin
         wq2_rptr_decode[AWI] = wq2_rptr_r1[AWI];
         for (i=AWI-1; i>=0; i=i-1) begin
            wq2_rptr_decode[i] = wq2_rptr_decode[i+1] ^ wq2_rptr_r1[i] ;
         end
      end
      always @(*) begin
         rq2_wptr_decode[AWI] = rq2_wptr_r1[AWI];
         for (i=AWI-1; i>=0; i=i-1) begin
            rq2_wptr_decode[i] = rq2_wptr_decode[i+1] ^ rq2_wptr_r1[i] ;
         end
      end

      //读写地址、拓展位完全相同是,为空状态
      assign rempty    = (rover_flag == rq2_wptr_decode[AWI]) &&
                         (raddr_ex >= rq2_wptr_decode[AWI-1:0]);
      //读写地址相同、拓展位不同,为满状态
      assign wfull     = (wover_flag != wq2_rptr_decode[AWI]) &&
                         (waddr >= wq2_rptr_decode[AWI-1:0]) ;
      //拓展位一样时,写地址必然不小于读地址
      //拓展位不同时,写地址部分比如小于读地址,实际写地址要增加一个FIFO深度
      assign prog_full  = (wover_flag == wq2_rptr_decode[AWI]) ?
                          waddr - wq2_rptr_decode[AWI-1:0] >= PROG_DEPTH-1 :
                          waddr + (1<<AWI) - wq2_rptr_decode[AWI-1:0] >= PROG_DEPTH-1;

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

   end

   //==============================================
   //big in and small out
   /*
   else begin: SHRINK_WIDTH
      ……
   end
   */
 endgenerate
endmodule

FIFO 调用

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

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

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

module  fifo_s2b(
        input                   rstn,
        input [4-1: 0]          din,     //异步写数据
        input                   din_clk, //异步写时钟
        input                   din_en,  //异步写使能

        output [16-1 : 0]       dout,      //同步后数据
        input                   dout_clk,  //同步使用时钟
        input                   dout_en ); //同步数据使能

   wire         fifo_empty, fifo_full, prog_full ;
   wire         rd_en_wir ;
   wire [15:0]  dout_wir ;

   //读空状态时禁止读,否则一直读
   assign rd_en_wir     = fifo_empty ? 1'b0 : 1'b1 ;

   fifo  #(.AWI(5), .AWO(3), .DWI(4), .DWO(16), .PROG_DEPTH(16))
     u_buf_s2b(
        .rstn           (rstn),
        .wclk           (din_clk),
        .winc           (din_en),
        .wdata          (din),

        .rclk           (dout_clk),
        .rinc           (rd_en_wir),
        .rdata          (dout_wir),

        .wfull          (fifo_full),
        .rempty         (fifo_empty),
        .prog_full      (prog_full));

   //缓存同步后的数据和使能
   reg          dout_en_r ;
   always @(posedge dout_clk or negedge rstn) begin
      if (!rstn) begin
         dout_en_r       <= 1'b0 ;
      end
      else begin
         dout_en_r       <= rd_en_wir ;
      end
   end
   assign       dout    = dout_wir ;
   assign       dout_en = dout_en_r ;

endmodule

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_en ;

   //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_buf2.u_buf_s2b.rinc = 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_buf2.u_buf_s2b.rinc;
      repeat(100) begin
         @(negedge clk_fast) ;
         din_en = 1'b1 ;
         din    = {$random()} % 16;
      end

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

   fifo_s2b u_data_buf2(
        .rstn           (rstn),
        .din            (din),
        .din_clk        (clk_fast),
        .din_en         (din_en),

        .dout           (dout),
        .dout_clk       (clk_slow),
        .dout_en        (dout_en));

`else
`endif

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

endmodule

  • 10
    点赞
  • 104
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
在异步FIFO中,计数器用于记录FIFO中的数据数量。在Verilog中实现异步FIFO的计数器可以使用一个整数型的变量来实现。计数器的初始值为0,每当有数据被写入FIFO时,计数器加1;而每当有数据被读取出FIFO时,计数器减1。这样,通过查询计数器的值就可以知道FIFO中的数据数量。同时,还需要考虑到计数器的溢出情况,当计数器达到最大值时(通常为FIFO的容量),需要进行溢出处理。 在Verilog中实现异步FIFO的计数可以按照以下步骤进行: 1. 声明一个整数型的变量作为计数器。 2. 在写入数据时,计数器加1。 3. 在读取数据时,计数器减1。 4. 当计数器的值等于FIFO的容量时,表示FIFO已满。 5. 当计数器的值等于0时,表示FIFO为空。 需要注意的是,由于异步FIFO的读写接口使用了不同的时钟,因此在进行计数操作时需要注意时钟域的同步。这可以通过使用同步触发器或者其他同步逻辑来实现。另外,由于异步FIFO的读写地址比较存在一定的延时,因此空状态的指示信号可能不是实时的,在某些情况下可能存在延迟现象。 综上所述,异步FIFO的计数在Verilog中可以通过一个整数型的变量来实现,通过增减计数器的值来记录FIFO中的数据数量,并注意计数操作的时钟同步和溢出处理。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [异步FIFO设计方法(Verilog)](https://blog.csdn.net/weixin_43167196/article/details/129814717)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [异步FIFO设计Verilog)](https://blog.csdn.net/qq_21842097/article/details/118307227)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沧海一升

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值