范围
本页是有关 FIFOs的系列五页中的第三页,展示了 baseline single clock FIFO在 Verilog 中的实现。这对于编写 portable 代码很有用,但本页的重点是重申 FIFO 的工作原理。因此,我将展示“标准 FIFO(standard FIFO)”和 FWFT FIFO的实现。但首先我们需要一 dual port RAM ,两 FIFOs 都会使用。
dual port RAM
这就是凭借 inference实现 RAM 的 Verilog 模块。很可能任何综合工具(synthesizer)都能做到这一点,但它可能会生成不希望的 RAM (block RAM 与 distributed RAM),因此可能有必要添加综合工具指令(synthesizer directives)。或者,如果效果最好,也可以使用 FPGA 供应商为 dual port RAM提供的 IP 。
所以这里是模块:
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
“标准 FIFO”
现在我们准备看看实现“标准 FIFO”(即不是 FWFT FIFO)的模块:
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。
这个模块的操作非常简单: @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 都变为高电平,但当复位释放时, @full 将返回低电平。
关于编码风格的说明: 当 @rst 为高时, if statement的 begin-end 子句中的赋值会覆盖之前可能进行的赋值,因此 @rst 确实将所有寄存器重置为其初始值。这不是最常见的编码风格,但当不是所有寄存器都被重置时,它具有明显的优势。这个具体案例并没有展示这种优势,但请参阅此页面。
最后,还有 dual port RAM的例化(instantiation)。
FWFT FIFO
如本页所示,将“标准 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
首先注意我们有一个新的寄存器,也就是 @has_more_words。将其定义与上面的“标准 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 的逻辑已更改为
always @(posedge clk)
     if (fetch_data)
       empty <= 0;
     else if (rd_en)
       empty <= 1;
这简单地说,如果从内存阵列中读取一个字, @empty 会在下一个时钟周期(clock cycle)上变为低电平,因为现在显然有新的有效数据。但是,如果这没有发生,并且 @rd_en 仍然启用,那么应用逻辑刚刚使用了最后一个可用的单词,因此将 @empty 更改为高电平。请注意,如果 @rd_en 为高而 @fetch_data 为低,则 @has_more_words 肯定为低(请参阅上面 @fetch_data 的分配)。这就是为什么这个条件相当于读取 FIFO中的最后一个字。
关于 FIFOs的本系列的第三页到此结束。下一页显示如何调整“标准 FIFO”以在数据采集(data acquisition)应用程序中使用。