01signal.com

通过添加 registers在 FIFOs 上改进 timing

概述

本页是 FIFOs系列的最后一篇,展示了如何将现有的 FIFO 修改为另一个。首先,如何将“standard FIFO”变成 FWFT FIFO,然后反过来。接下来是一些更高级的方法来改进 FIFO的 timing,即让它在更高的频率下工作。

从实际的角度来看,除非您遇到与 FIFO 相关的问题并实现 timing constraints,否则阅读此页面是毫无意义的。这个页面的内容比较难,对于常用的 FIFO 来说不是必需的。尽管如此,为了训练您在设计 logic(尤其是处理数据的 logic )时将使用的肌肉,将其作为一项练习可能是值得的。

没有直接关系,还有另一页展示了如何在外部存储器的帮助下创建一个非常深的 FIFO (通常是 DDR memory,但任何用 AXI 接口包装的东西都可以)。这个技巧的好处是,即使这个巨大的 FIFO 可以和它使用的外部存储器一样深,所有这些对 application logic都是透明的: 它具有与 baseline FIFO相同的接口。

应该提一下,我多年前在这个页面上写了 Verilog 代码,所以编码风格与今天略有不同。

Standard FIFO 至 FWFT FIFO

只是为了从前面的页面快速回顾一下 FWFT FIFOs : 对于“standard FIFO”, @empty port 低表示在 rising clock edge上的 @rd_en 为高电平后,有效数据将显示在 FIFO的 output 上。 FWFT FIFO 一有数据就呈现 output 上的数据,所以 @empty 信号低表示 output 上的数据有效

@rd_en 的含义也不同: 对于“standard FIFO”,它的意思是“给我带来数据”。在 FWFT FIFO 上,它类似于“我刚刚使用了数据,如果你有下一个数据,请给我带来”。

这就是将“standard FIFO”变成 FWFT FIFO的 module 。毫不奇怪,它只是操纵 @rd_en 和 @empty。其余的信号只是通过。

module basic_fwft_fifo(rst,
                       rd_clk, rd_en, dout, empty,
                       wr_clk, wr_en, din, full);

   parameter width = 8;

   input                 rst;
   input                 rd_clk;
   input                 rd_en;
   input                 wr_clk;
   input                 wr_en;
   input [(width-1):0]   din;
   output                empty;
   output                full;
   output [(width-1):0]  dout;

   reg                   dout_valid;
   wire                  fifo_rd_en, fifo_empty;

   // orig_fifo is just a normal (non-FWFT) synchronous or asynchronous FIFO
   fifo orig_fifo
      (
       .rst(rst),
       .rd_clk(rd_clk),
       .rd_en(fifo_rd_en),
       .dout(dout),
       .empty(fifo_empty),
       .wr_clk(wr_clk),
       .wr_en(wr_en),
       .din(din),
       .full(full)
       );

   assign fifo_rd_en = !fifo_empty && (!dout_valid || rd_en);
   assign empty = !dout_valid;

   always @(posedge rd_clk or posedge rst)
      if (rst)
         dout_valid <= 0;
      else
         begin
            if (fifo_rd_en)
               dout_valid <= 1;
            else if (rd_en)
               dout_valid <= 0;
         end
endmodule

我通常会在这里解释代码,但这只会重复上一页对 FWFT FIFO 的解释。

FWFT FIFO 至 standard FIFO

这真的很简单。由于来自 FWFT FIFO 的低 @empty 意味着 output port上存在数据,因此创建一个 register 在 @rd_en 高时采样此数据。

所以就是这样:

module standard_fifo(rst,
                     rd_clk, rd_en, dout, empty,
                     wr_clk, wr_en, din, full);

   parameter width = 8;

   input                 rst;
   input                 rd_clk;
   input                 rd_en;
   input                 wr_clk;
   input                 wr_en;
   input [(width-1):0]   din;
   output                empty;
   output                full;
   output [(width-1):0]  dout;

   reg [(width-1):0]     dout;
   wire [(width-1):0]    dout_w;

   always @(posedge rd_clk)
     if (rd_en && !empty)
       dout <= dout_w;

   fwft_fifo wrapper
     (
      .wr_clk(wr_clk),
      .rd_clk(rd_clk),
      .rst(rst),
      .din(din),
      .wr_en(wr_en),
      .rd_en(rd_en && !empty),
      .dout(dout_w),
      .full(full),
      .empty(empty)
      );
endmodule

请注意,仅对 @dout 进行了操作。 @empty 按原样通过: 如果为高,则 @dout_w 无效,因此 @dout 无法从中采样值。

改进 timing的技巧

欢迎来到 FIFOs上这四页的总决赛。这绝对是最难阅读的部分。

所以时不时地,当试图弄清楚为什么 FPGA design 没有达到 timing constraints (即达到所需的 clock frequency)时,事实证明 critical path 开始和/或结束于 FIFO。让我们先回顾一下容易解决的案例,并以硬螺母完成。

当 @empty 和/或 @full 在 critical path中时

@empty 信号和 @full 信号可能出现在 critical path中,特别是如果 @wr_en 和 @rd_en 是其中的 combinatorial functions 。这主要是因为这些信号通常不仅用于请求来自 FIFO 的写入操作或读取操作,而且它们还充当 enable signals 的 enable signals 用于消费或产生数据的 application logic : 如果数据没有流动, logic 也会冻结。

因此,往往有很多 logic equations 依赖 @wr_en 和 @rd_en,而 logic functions 往往相当复杂。结果是高 fanout。所有这一切都归结为有问题的 propagation delay。

在任何正确编写的 FIFO中,@empty 和 @full 都是 flip-flops 的 outputs ,因此没有太多改进之处。但是因为 FPGA的软件经常将 FIFO 作为 synthesized netlist提供,所以为了减少它们的 fanout而复制这些 registers 是不可能的(或者至少是困难的)。此外,在 FPGA的 logic fabric 上,这些 registers 和使用其输出值的 application logic 之间可能存在很大的物理距离。在大型 FPGAs上,这可以对 paths的 delay做出至关重要的贡献。

在讨论 @almost_empty 和 @almost_full时,此页面上已经给出了此问题的修复。通过使用这些端口, @wr_en 和 @rd_en 的 outputs 可以是 registers。这解决了 combinatorial function的问题,也允许控制这些信号的 fanout 。最重要的是,这还有助于工具将这些 registers 放置在更接近消耗其值的 logic 的位置,因此有助于减少 propagation delay。

当 @wr_en 和/或 @din 在 critical path中时

这种情况绝对是最容易解决的。只需添加一层 registers。就像是

always @(posedge wr_clk)
  begin
    wr_en_reg <= wr_en;
    din_reg <= din_reg;
  end

然后将 @wr_en_reg 和 @din_reg 连接到 FIFO 。为防止 FIFO 与 overflow发生冲突,应使用 @almost_full 而不是 @full。或者更一般地说,填充 FIFO 的阈值应该减一。

当 @rd_en 和/或 @dout 在 critical path中时

现在我们变得严肃起来。这不仅是一个相对难以解决的问题,而且也是最有可能发生的问题。有几个原因:

@dout:

所以目标是消除 @rd_en 和 FIFO的 logic之间的 combinatorial path ,对 @dout做同样的事情。

仅分离 @dout的 combinatorial path

我并不是真的打算将此作为解决方案提出,但讨论可能有助于将其理解为为掌握下一步做准备。如果这只是让您感到困惑,请跳过本节。

所以假设我们只想分离 @dout的 combinatorial path。请注意,将 FWFT FIFO 转换为“standard FIFO”(如上所示)的 wrapper module 正是这样做的: 它添加了一个 register,通过这样做,它结束了 @dout的 combinatorial path。但这需要 FWFT FIFO 作为起点。

但是有 wrapper module 将“standard FIFO”转换为 FWFT FIFO。那么也许来回转换 FIFO ?或者编写一个等效的 module ?无论哪种方式,这种形式的解决方案都会使 @rd_en的情况恶化。

然而,这个解决方案值得仔细研究: 转换为 FWFT FIFO 仅包括跟踪 wrapped FIFO的 @dout 何时有效,并在 @dout 无效时(和/或外部 @rd_en 为高电平时)保持 @fifo_rd_en 为高电平。

转换回“standard FIFO”是通过在 @rd_en 为高时将 wrapped FIFO的 @dout 的值复制到 register 来完成的。

所以总而言之,第一种机制尽可能保持 wrapped FIFO的 @dout 有效,第二种机制在外部 @rd_en 请求时将 @dout 复制到另一个 register 。

但这并不能解决 @rd_en的 combinatorial path的问题: 为了允许连续读取,必须在外部 @rd_en 为高电平的每个 clock 上从原始 FIFO 读取一个字。否则 FWFT的 @dout 将变为无效,因为它已被消耗但未更新。因此,这个内部 FIFO的 @rd_en 必须是外部 @rd_en的 combinatorial function 。如果我们想改变这一点,需要在 @dout的 path中添加另一个 register ,如下图所示。

将 combinatorial paths 与 reg_fifo分离

不用多说,这就是 reg_fifo module,它为 @rd_en 和 @dout分离了 combinatorial paths :

module reg_fifo(rst,
                rd_clk, rd_en, dout, empty,
                wr_clk, wr_en, din, full);

   parameter width = 8;

   input                 rst;
   input                 rd_clk;
   input                 rd_en;
   input                 wr_clk;
   input                 wr_en;
   input [(width-1):0]   din;
   output                empty;
   output                full;
   output [(width-1):0]  dout;

   reg                   fifo_valid, middle_valid;
   reg [(width-1):0]     dout, middle_dout;

   wire [(width-1):0]    fifo_dout;
   wire                  fifo_empty, fifo_rd_en;
   wire                  will_update_middle, will_update_dout;

   // orig_fifo is "standard" (non-FWFT) FIFO
   fifo orig_fifo
      (
       .rst(rst),
       .rd_clk(rd_clk),
       .rd_en(fifo_rd_en),
       .dout(fifo_dout),
       .empty(fifo_empty),
       .wr_clk(wr_clk),
       .wr_en(wr_en),
       .din(din),
       .full(full)
       );

   assign will_update_middle = fifo_valid && (middle_valid == will_update_dout);
   assign will_update_dout = rd_en && !empty;
   assign fifo_rd_en = !fifo_empty && !(middle_valid && fifo_valid);
   assign empty = !(fifo_valid || middle_valid);

   always @(posedge rd_clk)
      if (rst)
         begin
            fifo_valid <= 0;
            middle_valid <= 0;
            dout <= 0;
            middle_dout <= 0;
         end
      else
         begin
            if (will_update_middle)
               middle_dout <= fifo_dout;

            if (will_update_dout)
               dout <= middle_valid ? middle_dout : fifo_dout;

            if (fifo_rd_en)
               fifo_valid <= 1;
            else if (will_update_middle || will_update_dout)
               fifo_valid <= 0;

            if (will_update_middle)
               middle_valid <= 1;
            else if (will_update_dout)
               middle_valid <= 0;
         end
endmodule

首先要注意的是, @dout 是在此 module中定义的 register ,而 @rd_en 会导致此 register的更新。重要的是不要将这两个与连接到内部 FIFO的类似信号(即 @fifo_dout 和 @fifo_rd_en)混淆。

现在来看看这个 module 是如何工作的。

了解 pipeline

就像 FWFT FIFO的转换器一样,有一个普通的 FIFO、 orig_fifo的 instantiation 。当 @fifo_dout 不包含有效值时, reg_fifo module 中的 logic 尝试通过从 orig_fifo 读取一个字来保持 @fifo_dout的值有效。但除此之外,还有第二个 register,称为 @middle_dout。 logic 尝试通过尽可能取 @fifo_dout的值来保持此 register 有效。

因此,可以将 @fifo_dout、 @middle_dout 和 @dout 视为将 orig_fifo 中的数据向前移动的 pipeline 。

有两个 registers 会跟踪这些 pipeline stages 何时有效: @fifo_dout 有效时 @fifo_valid 为高电平, @middle_dout 有效时 @middle_valid 为高电平。

这个 pipeline 的目的是能够绕过它的中间阶段: 当 @rd_en 高(而 @empty 低)时, @dout 会从 @middle_dout 或 @fifo_dout中获取新值,但它总是更喜欢 @middle_dout。也就是说,如果 @middle_dout 有效,则 @dout 使用 @middle_dout,否则使用 @fifo_dout 。这是如何分离 @rd_en的 combinatorial path 的关键将在后面解释。

所以首先,让我们看一下实现的细节。从 FIFO的 data output 到 fifo_reg的 output register有两条独立的路径。在此图中,它们分别显示在左侧和右侧:

Data flow with extra registers

如果两个 pipeline stages (@fifo_dout 和 @middle_dout)均无效,则 @empty 为高电平表示无处可取数据:

assign empty = !(fifo_valid || middle_valid);

保持这些 pipeline stages 有效的尝试反映在

assign fifo_rd_en = !fifo_empty && !(middle_valid && fifo_valid);

这表示如果两个 pipeline stages 中的任何一个无效,请尽可能从 orig_fifo读取。如果 @fifo_dout 已经有效,则在更新 @fifo_dout 的同时将其值复制到 @middle_dout 中(更多信息见下文)。

现在让我们看一下 @will_update_* 对的定义:

assign will_update_middle = fifo_valid && (middle_valid == will_update_dout);
assign will_update_dout = rd_en && !empty;

首先注意 @will_update_dout 等于 @rd_en 加上一个安全防护,当 @empty 为高电平时,防止从 reg_fifo 读取它。

接下来我们有 @will_update_middle,它控制着 @middle_out的更新,如下:

always @(posedge rd_clk)
  if (will_update_middle)
     middle_dout <= fifo_dout;

看上面 @will_update_middle的定义,更新 @middle_dout有两个条件: 一是 @fifo_dout 的值是有效的,这很明显,然后是 (middle_valid == will_update_dout)的表达。让我们将这个表达式分解为四个可能的选项,因为它解释了整个机器是如何工作的。请记住,所有这些仅在 @fifo_dout 有效时才起作用:

请注意,当 @middle_valid 和 @fifo_valid 同时为高时, @fifo_rd_en 为低。因此,当最后两种情况发生时,不会从 orig_fifo 中获取任何数据。

特别是当两个 pipeline stages 都有效且 @rd_en 为高电平时, @fifo_dout的值被复制到 @middle_dout中。并且由于 @fifo_rd_en 为低电平, @fifo_valid 会在后面的 clock cycle上变为低电平。这很好,因为 @middle_valid 将保持高电平,因此如果需要,它可以提供有关以下 clock cycle 的数据。在 clock cycle 之后 @fifo_valid 将再次变高(如果 orig_fifo中有数据)。

那么为什么没有定义 @fifo_rd_en 来保持 @fifo_dout 在这种特定情况下有效呢?因为那时 @fifo_rd_en 必须是 @rd_en的 combinatorial function ,这正是 dual-stage pipeline 旨在避免的。

有了这个,是时候看看 @dout 是如何定义的了。除 reset外,定义为:

always @(posedge rd_clk)
  if (will_update_dout)
    dout <= middle_valid ? middle_dout : fifo_dout;

用它的定义代替 @will_update_dout ,它变成:

always @(posedge rd_clk)
  if (rd_en && !empty)
    dout <= middle_valid ? middle_dout : fifo_dout;

这类似于从 FWFT FIFO 到“standard FIFO”的转换,只有两个来源可供选择: 如果 @middle_dout 包含一个有效值,它就会被采用。否则, @fifo_dout。如果两者都无效,则 @empty 为高电平,因此无论如何都不会发生任何事情。

为什么这有帮助?至于 output timing, @dout 显然是 register。关于 @rd_en,注意 @fifo_rd_en 只依赖于 @middle_valid 和 @fifo_valid,都是 registers。加上 @fifo_empty,也就是 orig_fifo 本身的 output (这个 combinatorial path 是必然的)。因此, @fifo_rd_en 不依赖于外部 @rd_en,因此从 @rd_en 到 orig_fifo没有 combinatorial path 。

跟踪 pipeline stages的 registers的有效性

只是为了完成图片: 两个 *_valid 标志告诉我们相关的 register 是否包含有效数据。关于 @fifo_valid:

if (fifo_rd_en)
   fifo_valid <= 1;
 else if (will_update_middle || will_update_dout)
   fifo_valid <= 0;

这就像上面从“standard FIFO”到 FWFT FIFO 的转换中对 @dout_valid 的定义: 当 rising edge上的 @fifo_rd_en 为高时, @fifo_valid 会因此而变高。这是有道理的,因为如果我们从 orig_fifo读取数据,那么这个 FIFO的 output 就被认为是有效的。但是如果 @fifo_rd_en 较低,并且数据被复制到 @middle_dout 或 @dout之一,我们不再认为 @fifo_dout 有效: FIFO的 output 刚刚用过, FIFO 没有换新数据。

@middle_valid 遵循相同的逻辑:

if (will_update_middle)
   middle_valid <= 1;
 else if (will_update_dout)
   middle_valid <= 0;

当 @will_update_middle 为高电平时,数据被复制到 @middle_dout,因此 @middle_valid 也变为高电平。否则,如果 @middle_dout 中的数据复制到 @dout, @middle_valid 变为低。 @will_update_dout 是这一点的充分条件,因为从上面回想一下, @dout 在可能的情况下更喜欢从 @middle_dout复制。

它真的有效吗?

这个 module 非常复杂,需要一个几乎正式的证明来证明它可以工作。所以回答这个问题的一种方法是询问两个 pipelines stages、 @fifo_dout 和 @middle_dout中有多少是有效的。这个值在 reg_fifo module中没有定义,但它可以定义为

wire [1:0] valid_count;
assign valid_count = fifo_valid + middle_valid;

这个虚构的 @valid_count 显然可以取 0、 1 或 2的值。它向上或向下计数如下:

看看 logic equations,并说服自己这三个陈述是正确的。

那么让我们看看当 orig_fifo 中有数据并且 application logic 想要连续读取时会发生什么:

reg_fifo的 logic 试图通过读取 orig_fifo将 @valid_count 推向 2。另一方面,当 @valid_count 不为零时, @empty 为低电平,因此一旦 @valid_count 为 1, @rd_en 就被允许变高。因此,当 @valid_count 为 1 时, @fifo_rd_en 将为高电平,因为 @valid_count 不是 2。

但是 @valid_count 不会达到 2 的值,因为 @rd_en 通过保持高来阻止它。因此, @fifo_rd_en 和 @rd_en 都保持高电平, @valid_count 保持为 1 的数据流。除了开头,数据从 @fifo_dout 复制到 @dout。

当 orig_fifo 变空时,这个盈亏平衡被打破: 在这种情况下, @valid_count 变为零,因为 @fifo_rd_en 不再允许为高电平。另一个决胜局是当 FIFO 不为空时, @rd_en 变低是因为 application logic 不想阅读更多内容: 在这种情况下, @valid_count 上升到 2,并保持在那里。

但后来,当 @rd_en 再次变高时, @valid_count 下降到 1,只有在那个时候 @fifo_rd_en 才变高(除非 orig_fifo 为空)。

再一次, @valid_count 只是 module中未实现的理论信号。希望这个解释有助于理解为什么两个额外的 pipeline stages 保证数据的连续流动。

使用说明

此 module 可用作其包装的“standard FIFO”的替代品。从功能的角度来看,没有任何变化。然而, orig_fifo 会看到 @rd_en、 @dout 和 @empty的行为略有变化,但只要 orig_fifo 的行为与 FIFO 一样正确(这是一个安全的假设),这并不重要。与写入数据相关的 ports 是通过原封不动的,因此这些没有任何变化。

由于 module 增加了几个 pipelines stages, orig_fifo的 fill counters 可能呈现比 reg_fifo 中存储的总字数(即 orig_fifo 中存储的字数以及 pipeline stages 一起计算的字数)更低的值。因此,如果在 orig_fifo上启用 @almost_empty 或类似的 ports ,它们可能会呈现出悲观的画面。

reg_fifo 的一个小缺点是它的 @empty output 不是 register,而是两个 registers的 combinatorial function 。这不是 timing的最佳选择,但在大多数用例中影响很小。这可以通过以与 @next_words_in_ram相同的精神定义 combinatorial registers 来解决,如 @next_fifo_valid 和 @next_middle_valid ,如本页所示。这里没有实现,主要是因为 reg_fifo 已经足够复杂了。

带有改进的 timing的 FWFT FIFO

为了结束这个话题,下面是 module ,它与 reg_fifo做同样的事情,但给 application logic 一个 FWFT FIFO 。请注意,它基于 standard FIFO,而不是 FWFT FIFO。所以不要混淆这个...

从 reg_fifo 到 module 的过渡在很大程度上与我已经讨论过的有关 FWFT FIFOs的情况相同。

module fwft_reg_fifo(rst,
                     rd_clk, rd_en, dout, empty,
                     wr_clk, wr_en, din, full);

   parameter width = 8;

   input                 rst;
   input                 rd_clk;
   input                 rd_en;
   input                 wr_clk;
   input                 wr_en;
   input [(width-1):0]   din;
   output                empty;
   output                full;
   output [(width-1):0]  dout;

   reg                   middle_valid, dout_valid;
   reg [(width-1):0]     dout, middle_dout;

   wire [(width-1):0]    fifo_dout;
   wire                  fifo_empty, fifo_rd_en;
   wire                  will_update_middle, will_update_dout;

   // orig_fifo is "standard" (non-FWFT) FIFO
   fifo orig_fifo
      (
       .rst(rst),
       .rd_clk(rd_clk),
       .rd_en(fifo_rd_en),
       .dout(fifo_dout),
       .empty(fifo_empty),
       .wr_clk(wr_clk),
       .wr_en(wr_en),
       .din(din),
       .full(full)
       );

   assign will_update_middle = !fifo_empty && (middle_valid == will_update_dout);
   assign will_update_dout = (middle_valid || !fifo_empty) && (rd_en || !dout_valid);
   assign fifo_rd_en = !fifo_empty && !(middle_valid && dout_valid);
   assign empty = !dout_valid;

   always @(posedge rd_clk)
      if (rst)
         begin
            middle_valid <= 0;
            dout_valid <= 0;
            dout <= 0;
            middle_dout <= 0;
         end
      else
         begin
            if (will_update_middle)
               middle_dout <= fifo_dout;

            if (will_update_dout)
               dout <= middle_valid ? middle_dout : fifo_dout;

            if (will_update_middle)
               middle_valid <= 1;
            else if (will_update_dout)
               middle_valid <= 0;

            if (will_update_dout)
               dout_valid <= 1;
            else if (rd_en)
               dout_valid <= 0;
         end
endmodule

关于 FIFOs的系列到此结束。

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