01signal.com

Verilog中 single clock FIFOs 的实现

范围

这个页面是关于 FIFOs的系列文章中的第四个页面,展示了 baseline single clock FIFO在 Verilog 中的实现。这对于编写 portable code很有用,但本页的重点是重申 FIFO 的工作原理。因此,我将展示“standard FIFO”和 FWFT FIFO的实现。但首先我们需要一个 FIFOs 都可以使用的 dual port RAM 。

dual port RAM

这就是凭借 inference实现 RAM 的 Verilog module 。很可能任何 synthesizer 都能做到这一点,但它可能会生成不希望的 RAM (block RAM 与 distributed RAM),因此可能有必要添加 synthesizer directives。或者,如果效果最好,也可以使用 FPGA 供应商为 dual port RAM提供的 IP 。

所以这里是 module:

module dualport_ram #(parameter depth = 64,
                      log2_depth = 6,
                      width = 8
                     )
   (
    input                    clk,
    input [(log2_depth-1):0] wr_addr,
    input [(log2_depth-1):0] rd_addr,
    output reg [(width-1):0] rd_data,
    input [(width-1):0]      wr_data,
    input                    rd_en,
    input                    wr_en
    );

   reg [(width-1):0] inferred_ram[0:(depth-1)];

   always @(posedge clk)
     begin
        if (wr_en)
          inferred_ram[wr_addr] <= wr_data;

        if (rd_en)
          rd_data <= inferred_ram[rd_addr];
     end
endmodule

“standard FIFO”

现在我们准备看看实现“standard FIFO”(即不是 FWFT FIFO)的 module :

module fifo
  #(parameter depth = 64, // Must equal 2^log2_depth exactly
    log2_depth = 6,
    width = 8
    )
   (
    input  clk,
    input  rst,

    input  wr_en,
    input [(width-1):0] din,

    input  rd_en,
    output [(width-1):0] dout,

    output reg full,
    output reg empty
    );

   reg [log2_depth:0]         next_words_in_ram; // Combinatorial
   reg [log2_depth:0]         words_in_ram;
   reg [(log2_depth-1):0]     rd_addr;
   reg [(log2_depth-1):0]     wr_addr;
   wire                       fetch_data, commit_data;

   assign fetch_data = rd_en && !empty;
   assign commit_data = wr_en && !full;

   always @(*)
     if (commit_data && !fetch_data)
       next_words_in_ram <= words_in_ram + 1;
     else if (!commit_data && fetch_data)
       next_words_in_ram <= words_in_ram - 1;
     else
       next_words_in_ram <= words_in_ram;

   always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        empty <= (next_words_in_ram == 0);

        if (fetch_data)
          rd_addr <= rd_addr + 1;

        if (commit_data)
          wr_addr <= wr_addr + 1;

        if (rst)
          begin
             empty <= 1;
             full <= 1;
             words_in_ram <= 0;
             rd_addr <= 0;
             wr_addr <= 0;
          end
     end

   dualport_ram
     #(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
       (.clk(clk), .wr_addr(wr_addr),
        .rd_addr(rd_addr),
        .wr_en(commit_data),
        .rd_en(fetch_data),
        .wr_data(din),
        .rd_data(dout)
        );
endmodule

关于“depth”和“log2_depth”参数的唯一重要用法说明在顶部的注释中: depth 必须等于 2log2_depth

这个 module 的操作非常简单: @fetch_data 类似于 @rd_en,但它考虑了 @empty 。因此, @fetch_data 是 @rd_en的安全版本,即使 @rd_en 和 @empty 同时为高电平(这是非法的),也不会发生任何不好的事情,因为在这种情况下 @rd_en 会被忽略。

@commit_data 是 @wr_en 的安全版本,同样考虑到 @full 。

@words_in_ram的下一个值 @next_words_in_ram根据 @fetch_data 和 @commit_data (注意 always @(*) 语句)计算为 combinatorial function。 @next_words_in_ram 用于通过以下代码片段生成多个 registers 的值:

always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        empty <= (next_words_in_ram == 0);
[ ... ]

请注意,此处定义了 @full 和 @empty 。

与 dual-clock FIFO相比, FIFO 如此易于实现的原因在于能够像这样定义 @words_in_ram ,并在 FIFO的两侧使用相同的 register 。

接下来在代码中,我们更新了 @rd_addr 和 @wr_addr,然后是 @rst的子句。请注意, reset会导致 @empty 和 @full 都变为高电平,但当 reset 释放时, @full 将返回低电平。

关于编码风格的说明: 当 @rst 为高时, if statement的 begin-end 子句中的赋值会覆盖之前可能进行的赋值,因此 @rst 确实将所有 registers 重置为其初始值。这不是最常见的编码风格,但当不是所有 registers 都被重置时,它具有明显的优势。这个具体案例并没有展示这种优势,但请参阅此页面

最后,还有 dual port RAM的 instantiation 。

FWFT FIFO

本页所示,将“standard FIFO”转换为 FWFT FIFO非常容易。但它的直接实现允许讨论一些有趣的点,所以这里是:

module fwft_fifo
  #(parameter depth = 64, // Must equal 2^log2_depth exactly
    log2_depth = 6,
    width = 8
    )
   (
    input  clk,
    input  rst,

    input  wr_en,
    input [(width-1):0] din,

    input  rd_en,
    output [(width-1):0] dout,

    output reg full,
    output reg empty
    );

   reg [log2_depth:0]         next_words_in_ram; // Combinatorial
   reg [log2_depth:0]         words_in_ram;
   reg [(log2_depth-1):0]     rd_addr;
   reg [(log2_depth-1):0]     wr_addr;
   reg                        has_more_words;
   wire                       fetch_data, commit_data;

   assign fetch_data = (rd_en || empty) && has_more_words;
   assign commit_data = wr_en && !full;

   always @(*)
     if (commit_data && !fetch_data)
       next_words_in_ram <= words_in_ram + 1;
     else if (!commit_data && fetch_data)
       next_words_in_ram <= words_in_ram - 1;
     else
       next_words_in_ram <= words_in_ram;

   always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        has_more_words <= (next_words_in_ram != 0);

        if (fetch_data)
          rd_addr <= rd_addr + 1;

        if (commit_data)
          wr_addr <= wr_addr + 1;

        if (fetch_data)
          empty <= 0;
        else if (rd_en)
          empty <= 1;

        if (rst)
          begin
             empty <= 1;
             full <= 1;
             words_in_ram <= 0;
             has_more_words <= 0;
             rd_addr <= 0;
             wr_addr <= 0;
          end
     end

   dualport_ram
     #(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
       (.clk(clk), .wr_addr(wr_addr),
        .rd_addr(rd_addr),
        .wr_en(commit_data),
        .rd_en(fetch_data),
        .wr_data(din),
        .rd_data(dout)
        );
endmodule

首先注意我们有一个新的 register,也就是 @has_more_words。将其定义与上面的“standard FIFO”进行比较,并说服自己 @has_more_words 是 @empty的 logical NOT 。

接下来,请注意 @fetch_data 的定义已更改。它现在由 @has_more_words 保护(不足为奇),所以它说

assign fetch_data = (rd_en || empty) && has_more_words;

回想一下, @empty FWFT FIFO上意味着“@dout 无效”。所以这个赋值意味着除了 @rd_en之外,如果 output 无效,并且内存阵列中有数据可以读取,请继续执行此操作。那就是 first word的 falling through 。

最后,用于分配 @empty 的 logic 已更改为

always @(posedge clk)
     if (fetch_data)
       empty <= 0;
     else if (rd_en)
       empty <= 1;

这简单地说,如果从内存阵列中读取一个字, @empty 会在下一个 clock cycle上变为低电平,因为现在显然有新的有效数据。但是,如果这没有发生,并且 @rd_en 仍然启用,那么 application logic 刚刚使用了最后一个可用的单词,因此将 @empty 更改为高电平。请注意,如果 @rd_en 为高而 @fetch_data 为低,则 @has_more_words 肯定为低(请参阅上面 @fetch_data 的分配)。这就是为什么这个条件相当于读取 FIFO中的最后一个字。

本系列关于 FIFOs的第三页到此结束。下一页显示如何将“standard FIFO”变成 FWFT FIFO ,反之亦然,以及如何改进 timing。

此页面由英文自动翻译。 如果有不清楚的地方,请参考原始页面
Copyright © 2021-2023. All rights reserved. (4c701b97)