01signal.com

FPGA FIFOs: 不同的功能和变体

范围

本页是有关 FIFOs的五页系列中的第二页。在上一页介绍了 FIFO 的基础知识之后,是时候讨论常见的变体和额外功能了。 FIFO 通常配置为下述选项的组合。

Single clock FIFOs

尽管“baseline FIFO”有两个 inputs ,用于无关的 clocks,但往往两边的信号是同步的同一个 clock。将相同的 clock 连接到 @wr_clk 和 @rd_clk非常好。但由于两个 clock inputs上的 clock 相同,因此不需要 clock domain crossing,因此 FIFO 包含不必要的 logic。更多关于 clock domains 的信息可以在这里找到。

因此,每个 FPGA 供应商都为 FIFO提供两种类别: Dual-clock FIFO 和 single-clock FIFO。经常使用其他名称: Independent Clock FIFO 与 Common Clock FIFO 或 Asynchronous FIFO 与 Synchronous FIFO。上一页介绍的“baseline FIFO”是 dual-clock FIFO。

Single-clock FIFOs 没有任何 synchronization logic,因为所有 logic 都与同一个 clock同步。因此, reset input 必须与另一个 ports相同的 clock 同步。

除了不浪费 FPGA logic之外,使用 single clock FIFOs 的另一个很好的理由是为了清晰。这是一种大声明确表示无意让两个 clocks 参与其中的方式。

简而言之: 如果 FIFO 不能在两个 clock domains之间连接,请选择 single-clock FIFO。

FWFT FIFOs

如上所述,从“baseline FIFO”读取数据的过程是将 @rd_en 变为高电平,然后在下一个 clock cycle上获取 FIFO的 @dout output 上的值。这有点违反直觉: 如果数据已经在 FIFO中,我为什么还要索取呢?为什么 FIFO 不能直接放在 @dout port上,告诉我用就行了?

所以有一个常见的变体就是这样做的,它被称为 First Word Fall Through FIFO (FWFT,有时也称为 read-ahead、 show-ahead 或 look-ahead)。 FWFT FIFO 的反面通常被称为“Standard FIFO”(有人可以告诉我标准吗?)。

这个想法很简单: 当 FWFT FIFO 不再为空时(因为数据已写入),它会显示 @dout上的第一个字。然后 application logic 通过将 @rd_en 保持在高电平来读取字。因此,区别仅在于第一个单词。

然而,通过意识到其中两个 ports 的含义发生了变化,更容易理解 FWFT FIFO : FWFT FIFO 上的 @rd_en 实际上表示“我刚刚消费了 @dout上的数据,可以带下一个”,而 @empty 实际上表示“@dout 无效”。

没有改变的是,如果 @empty 高, @rd_en 不应该高。你不能说你消费了无效数据。因此,出于不同的原因,规则保持不变。

以下 waveform 显示了从 FWFT FIFO 读取的内容:

Example waveform for reading words from a FWFT FIFO

请注意,当 @rd_en 为低时, @dout 处的第一个有效值出现,并且在有效值出现的同时, @empty 变为低。如前所述, @empty 在 FWFT FIFO上意味着“@dout 无效”,而 waveform 反映了这一点。

另请注意, @rd_en 的第一个脉冲并未从 FIFO读取新值,而是导致 @empty 再次变为高电平。因此, @dout 的价值同时变得未知。实际上,当 @empty 变高时, @dout 通常不会改变,但你不能依赖它。

此后, FIFO 再次对 @dout 赋值并将 @empty 变为低电平。 application logic 读取三个字,然后将 @rd_en 变为低电平。总而言之, application logic 消耗了 FIFO的四五个字。

请注意, waveform 并没有告诉我们 application logic 是否也使用了 D4 的值。它可能忽略了第五个词,这意味着它只消耗了四个词。或者它可能使用了第五个单词的值。从 waveform 唯一清楚的是, application logic 在四个 clock cycles之后保持 @rd_en 的低位,因此它不允许 FIFO 继续更新 @dout。

还有一点需要注意的是,我们不知道 FIFO的内存中是否还有更多数据。 @empty 在这个 waveform 的末尾为低,仅表示 @dout 有效。

现在让我们修改上一页中的 Verilog 示例。再一次,这段代码计算出 FIFO的所有东西的累积总和:

assign rd_en = !empty; // If @dout's value is valid, it's consumed.

always @(posedge rd_clk)
  if (!empty) // FIFO is FWFT, so !empty means @dout contains valid data
    sum <= sum + dout; // Don't try this at home: @sum is never reset.

与前面的例子不同的是,这个例子使用的是 FWFT FIFO,所以不需要在前面的 clock cycle上有 @rd_en 值的 register 。相反,当 @empty 为低时,可以消耗 @dout 。这个简单的规则有效,因为当 @empty 为低时 @rd_en 为高,所以来自 FIFO 的每个字在 @dout 上对恰好一个 clock cycle有效。

我只想用一个无关紧要的观点来结束 FWFT 话题。 “标准” FIFO 和 FWFT FIFO 之间的差异反映了关于 logic的任意两个 modules 之间数据流的基本问题: 接收方是否需要索取数据?还是发送端尽快呈现数据,接收端只确认可以继续?当一个 module 向另一个 module 传递数据时,一定要问自己这个问题,尤其是问问自己这些 modules 是否同意这个问题。

Asymmetric FIFOs

通常允许为 @din 和 @dout定义不同宽度的 FIFO 。这很有用,例如,如果数据以 32 位字到达 FPGA ,但 application logic 将它们作为字节处理,即每个字 8 位。在这种情况下,将写入侧的宽度设置为 32 位,将读取侧的宽度设置为 8 位。双方的行为与往常一样,只是需要四个 read cycles 才能消耗一个用单个 write cycle插入的单词。

当读取面比写入面宽时,它的行为就像人们期望的那样: 写入 FIFO 的数据在读取端不可用,直到写入的数据填充了一个与读取端大小相同的字。

至于字的排列顺序,好像 FIFOs 都用 Little Endian。例如, FIFO 将 32 位字打包成 8 位字,如下所示: 从 FIFO 读取的第一个字的位范围是 [7:0],然后是 [15:8]、 [23:16] 和 [31:24]。

但是,如果您想使用此功能,请务必查看文档。

combinatorial logic 对 @empty 和 @full的依赖性

@empty 和 @full 端口有一个共同的缺点: application logic 必须在同一个 clock cycle上响应它们。也就是说, @rd_en 必须是依赖于 @empty 的 combinatorial function ,才能保证这两个 signals 不在同一个 clock cycle 上(这是禁止的,前面已经说过了)。同样的道理, @wr_en 必须是依赖于 @full的 combinatorial function 。

使用 combinatorial functions 可能会成为实现 timing constraints的障碍。当 clock的频率很高(相对于 FPGA的规格)和 logic function 很复杂时,这可能会成为一个问题。出现问题的主要原因是 @rd_en 和 @wr_en 都经常用在生产或消费数据的 logic 中。尤其是 logic function 为很多 logic 计算 clock enable 可以依赖这些信号。例如,如果有一个长 pipeline 处理来自 FIFO的数据,则当来自 FIFO 的数据流暂时停止时, pipeline 中的所有 logic 必须冻结。

好吧,为了完全准确,有一种方法可以避免 combinatorial function。例如,假设 @wr_en 被声明为 register,而 @want_to_write 是表示 application logic在给定时间需要写入的信号。可以这样做:

always @(posedge wr_clk)
  wr_en <= want_to_write && !wr_en && !full;

这确保了 @wr_en 和 @full 在同一个 clock cycle上永远不会变高,因为 @full 只能在 @wr_en 为高电平后在 clock cycle 上变为高电平。表达式中的 !wr_en 部分确保 @wr_en 在两个连续的 clocks cycles期间永远不会高。所以如果 @full 变高, @wr_en 会因为第一个 clock cycle上的 !wr_en 而变低。由于 @full 本身, @wr_en 保持低位。

但是使用此解决方案, @wr_en 必须在一半时间内为低电平。因此,仅使用 FIFO的数据速率的 50% 。这通常是不可接受的。

@rd_en也可以使用相同的解决方案,并且该解决方案在使用一半的数据速率时也存在同样的问题。

该讨论旨在引出下一部分: "almost" ports。

Almost full、 almost empty 和类似的 ports

可以向 FIFO添加两个可选端口: @almost_full port 和/或 @almost_empty port。

@almost_empty 与 @rd_clk同步,与 @empty类似,但略有不同: 当 FIFO 为空时, @almost_empty 为高电平,但当 FIFO正好有一个字要读取时也是如此。

同样, @almost_full 与 @wr_clk同步,并且在 FIFO 已满时为高电平,但在可以向 FIFO写入一个字时也是如此。

这两个 output ports 的名称取决于 FPGA 供应商及其提供的软件,但总有可能添加具有相同功能的 ports 。只是有时, FIFO 的某个变体可能不支持这些 ports。

这些 ports 有何帮助?好吧,因为这工作得很好:

always @(posedge wr_clk)
  wr_en <= want_to_write && !almost_full;

没有 combinatorial logic,也不需要跳过一半的 write cycles。当 @almost_full 为高电平时, @wr_en 可能不会在同一个 clock cycle上变为低电平,而只会在下一个 clock cycle上变为低电平。因此, @almost_full 变为高电平后可以进行一次写操作。但这很好,因为有一个词的地方。

请注意,如果 @want_to_write 在 FIFO 被填充时持续保持高电平,则最后一次写入操作会完全填充 FIFO 。否则, FIFO 可能最终几乎被填满: 如果因为 @want_to_write而 @wr_en 低,并且 FIFO 没有因此而完全充满,那么就没有第二次机会了。只有当对方从 FIFO读取数据时, @almost_full 才会变为低电平,因此 FIFO中有两个或多个字的空间。

这很少有关系,但为了讨论,这确保使用最后一个词:

always @(posedge wr_clk)
  wr_en <= want_to_write && (!almost_full || (!full && !wr_en));

@wr_en 的这个表达式大部分时间都依赖于 @almost_full ,除非可以只写一个单词。只有这样, @wr_en 才依赖于 @full 和 @wr_en,类似于前面使用 !wr_en的表达式。

但是,我严重怀疑 @wr_en 的最后一个表达式是否有用。

@almost_empty 的情况类似,所以可以(但不要将其复制到您的代码中):

always @(posedge rd_clk)
  rd_en <= want_to_read && !almost_empty;

与 @almost_full一样,最后一句话也有问题: 如果 @rd_en 因 @want_to_read而变低,它将失去机会,直到 FIFO 充满更多数据。与 @almost_full的情况不同,这在某些情况下肯定是个问题: 如果 @almost_empty 为高电平但 FIFO 不为空,则表示 FIFO 中存在用于读取的数据,但该数据仍卡在 FIFO中。

所以这是安全的方法:

always @(posedge rd_clk)
  rd_en <= want_to_read && (!almost_empty || (!empty && !rd_en));

Fill counters

Application logic 经常以块的形式执行操作。例如, logic 从 FIFO 读取具有恒定长度的数据包,并通过某些物理介质传输这些数据包。由于数据存储在 FIFO上, application logic 需要在开始读取之前知道有足够的数据来填充数据包。

同样, application logic 通常会产生固定数量的数据以存储在 FIFO中,例如从外部存储器读取 burst 的数据。除非 FIFO 中有足够的空间来完成 burst,否则操作不应开始。

出于这些目的, FIFOs 通常支持 fill counters、 programmable empty port 和 programmable full port。 fill counters (有时称为 data counters)有不同的形式和形状,很大程度上取决于 FPGA 供应商,因此请仔细阅读 FIFO的文档。主要需要注意三个问题:

此外还有 programmable empty 和 programmable full,它们是 @almost_empty 和 @almost_full的扩展版本。这个想法是因为 fill counters 的使用几乎可以肯定是

assign dont_start_reading = (rd_data_count < 64);

为什么不直接提供该信号,并将其称为 prog_empty?再次仔细阅读 FIFO的文档。

再一次,当阅读 FIFO的最后一个字很重要时,一定要问自己,您的 logic 是否真的会这样做。这个问题类似于上面关于 @almost_empty的讨论。

几乎不用说,在配置 FIFO时,如果需要,您必须请求这些额外的 ports 。

AXI interface

这个主题没有直接关系,但是值得一提以避免混淆,因为这个术语经常出现在 FIFOs的上下文中。

AXI 是由 ARM引入的 AMBA 标准中定义的一组接口。正如人们所预料的那样,带有 AXI 接口的 FIFOs 通常用作 CPU的外围设备。

“baseline FIFO”的接口通常被称为“native”接口,与 AXI 接口相对。

AXI 接口主要有两种类型: “常规” AXI (通常是 AXI3、 AXI4 或 AXI Lite)是带有地址和数据的 bus 。第二种类型 AXI-S (streamed AXI) 用于 streams 数据(可能分为数据包)。

当 FIFO 被配置为 AXI3 / AXI4 或 AXI Lite时,额外的 logic 被添加到它,因此它可以作为外围设备通过该接口连接到 CPU 的地址。我不会进一步详细说明这一点,因为这是一个完全不同的话题。

但由于 streaming 接口与 FIFO的行为有些相似,因此可以将 AXI-S 接口的 handshake signals 转换为“native”接口。请注意, AXI-S 通常还涉及其他需要处理的信号。

因此,给定用于写入 FIFO 的 AXI-S 信号为 @axi_w_valid、 @axi_w_ready 和 @axi_w_data,它们可以连接到“标准” FIFO 的 ports

assign axi_w_ready = !full;
assign wr_en = axi_w_valid && axi_w_ready;
assign din = axi_w_data;

同样,用于从 FIFO、 @axi_r_valid、 @axi_r_ready 和 @axi_r_data读取的 AXI-S 信号可以通过以下方式连接到 FWFT FIFO 的 ports

assign axi_r_valid = !empty; // Non-empty means valid with FWFT FIFOs
assign rd_en = axi_r_valid && axi_r_ready;
assign axi_r_data = dout;

再次注意,要使其正常工作, FIFO 必须是 FWFT 变体。

本系列关于 FIFOs的第二页到此结束。下一页显示了如何在 Verilog中实现 single-clock FIFO 。

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