01signal.com

单时钟 FIFO 的 Verilog 实现

范围

本页是关于 FIFO 的五页系列教程中的第三页,演示如何在 Verilog 中实现一个基线单时钟 FIFO。这对于编写可移植代码可能很有用,但本页的重点是再次强调 FIFO 的工作原理。因此,我会给出“标准 FIFO”和 FWFT FIFO 的实现代码。不过,我们先需要一个双端口 RAM(dual-port RAM),因为两种 FIFO 都要用到它。

双端口 RAM

这个 Verilog 模块利用推断(inference)机制来实现一个双端口 RAM。任何综合器(synthesizer)都很可能能正确处理这种写法,但有时它生成的可能不是你想要的那种 RAM(块 RAM(block RAM)或分布式 RAM(distributed RAM)),所以可能需要添加综合器指令(synthesizer directive)。如果使用 FPGA 厂商提供的双端口 RAM IP 核(IP core)效果更好,也可以选择那条路。

下面就是这个模块:

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 计算出来的组合逻辑函数(combinatorial logic function)——注意这里用的是 always @(*) 语句。@next_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);
[ ... ]

请注意,@full(满)和 @empty(空)就是在这里定义的。

与双时钟 FIFO 相比,这个单时钟 FIFO 之所以如此容易实现,是因为我们可以这样定义 @words_in_ram,并在 FIFO 的两侧都使用这同一个寄存器。

再往下看代码,是对 @rd_addr 和 @wr_addr 的更新,然后是 @rst 所对应的 if 分支。请注意,@empty(空)和 @full(满)都会因为复位而变为高电平,但 @full(满)会在复位释放后变回低电平。

关于编码风格的一点说明:当 @rst 为高时,if 语句的 begin-end 块中的赋值会覆盖前面可能已经发生的赋值,所以 @rst 确实会把所有寄存器复位到它们的初始值。这不是最常见的编码风格,但在并非所有寄存器都需要复位的情况下,它有明显的优势。这里的具体例子没能体现出这一优势,不过可以参见这一页。

最后,代码中有一个对双端口 RAM 的实例化(instantiation)。

FWFT FIFO

正如这个页面所示,把“标准 FIFO”转换成 FWFT 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;

回想一下前面介绍的:在 FWFT FIFO 上,@empty(空)表示“@dout 无效”。所以这个赋值语句的意思是,除了 @rd_en 为高的情况之外,如果当前输出无效,而存储阵列中还有数据可读,那就照样去读。这正是第一个数据字“直通”出来的机制。

最后,对 @empty(空)的赋值逻辑变成了下面这样:

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

这段代码的意思很简单:如果从存储阵列中读出了一个数据字,那么 @empty(空)会在下一个时钟周期变为低电平,因为显然此时已经有了新的有效数据。但如果上述情况没有发生,而 @rd_en 仍然有效,那就说明应用逻辑刚刚取走了最后一个可用的数据字,所以要把 @empty(空)置为高。注意,如果 @rd_en 为高而 @fetch_data 为低,那么 @has_more_words 必定为低(参见上面 @fetch_data 的赋值)。这正是为什么这个条件等价于“读出了 FIFO 中的最后一个数据字”。

至此,本系列关于 FIFO 的第三页就结束了。下一页将展示如何把“标准 FIFO”改造用于数据采集(data acquisition)应用中。

本页由机器从英文翻译而来。如有疑问,请参阅原文。
Copyright © 2021-2026. All rights reserved. (dcc38493)