范围
本页是有关 FIFOs的五页系列中的第二页。在上一页介绍了 FIFO 的基础知识之后,是时候讨论常见的变体和额外功能了。 FIFO 通常配置为下述选项的组合。
Single clock FIFOs
尽管“baseline FIFO”有两个输入,用于无关的时钟,但往往两边的信号是同步的同一个时钟。将相同的时钟连接到 @wr_clk 和 @rd_clk非常好。但由于两个时钟输入上的时钟相同,因此不需要跨时钟域(clock domain crossing),因此 FIFO 包含不必要的逻辑。更多关于时钟域(clock domains)的信息可以在这里找到。
因此,每 FPGA 供应商都为 FIFO提供两种类别: Dual-clock FIFO 和 single-clock FIFO。经常使用其他名称: Independent Clock FIFO 与 Common Clock FIFO 或异步 FIFO (Asynchronous FIFO)与同步 FIFO(Synchronous FIFO)。上一页介绍的“baseline FIFO”是 dual-clock FIFO。
Single-clock FIFOs 没有任何 synchronization 逻辑,因为所有逻辑都与同一个时钟同步。因此,复位输入(reset input)必须与另一个端口相同的时钟同步。
除了不浪费 FPGA 逻辑之外,使用 single clock FIFOs 的另一个很好的理由是为了清晰。这是一种大声明确表示无意让两个时钟参与其中的方式。
简而言之: 如果 FIFO 不能在两个时钟域(clock domains)之间连接,请选择 single-clock FIFO。
FWFT FIFOs
如上页所述,从“baseline FIFO”读取数据的过程是将 @rd_en 变为高电平,然后在下一个时钟周期(clock cycle)上获取 FIFO的 @dout 输出上的值。这有点违反直觉: 如果数据已经在 FIFO中,我为什么还要索取呢?为什么 FIFO 不能直接放在 @dout 端口上,告诉我用就行了?
所以有一个常见的变体就是这样做的,它被称为 First Word Fall Through FIFO (FWFT,有时也称为 read-ahead、 show-ahead 或 look-ahead)。 FWFT FIFO 的反面通常被称为“标准 FIFO(Standard FIFO)”(有人可以告诉我标准吗?)。
这个想法很简单: 当 FWFT FIFO 不再为空时(因为数据已写入),它会显示 @dout上的第一个字。然后应用逻辑通过将 @rd_en 保持在高电平来读取字。因此,区别仅在于第一个单词。
然而,通过意识到其中两个端口的含义发生了变化,更容易理解 FWFT FIFO : FWFT FIFO 上的 @rd_en 实际上表示“我刚刚消费了 @dout上的数据,可以带下一个”,而 @empty 实际上表示“@dout 无效”。
没有改变的是,如果 @empty 高, @rd_en 不应该高。你不能说你消费了无效数据。因此,出于不同的原因,规则保持不变。
以下波形显示了从 FWFT FIFO 读取的内容:
请注意,当 @rd_en 为低时, @dout 处的第一个有效值出现,并且在有效值出现的同时, @empty 变为低。如前所述, @empty 在 FWFT FIFO上意味着“@dout 无效”,而波形反映了这一点。
另请注意, @rd_en 的第一个脉冲并未从 FIFO读取新值,而是导致 @empty 再次变为高电平。因此, @dout 的价值同时变得未知。实际上,当 @empty 变高时, @dout 通常不会改变,但你不能依赖它。
此后, FIFO 再次对 @dout 赋值并将 @empty 变为低电平。应用逻辑读取三个字,然后将 @rd_en 变为低电平。总而言之,应用逻辑消耗了 FIFO的四五个字。
请注意,波形并没有告诉我们应用逻辑是否也使用了 D4 的值。它可能忽略了第五个词,这意味着它只消耗了四个词。或者它可能使用了第五个单词的值。从波形唯一清楚的是,应用逻辑在四个时钟周期(clock cycles)之后保持 @rd_en 的低位,因此它不允许 FIFO 继续更新 @dout。
还有一点需要注意的是,我们不知道 FIFO的内存中是否还有更多数据。 @empty 在这个波形的末尾为低,仅表示 @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,所以不需要在前面的时钟周期上有 @rd_en 值的寄存器(register)。相反,当 @empty 为低时,可以消耗 @dout 。这个简单的规则有效,因为当 @empty 为低时 @rd_en 为高,所以来自 FIFO 的每个字在 @dout 上对恰好一个时钟周期有效。
我只想用一个无关紧要的观点来结束 FWFT 话题。 “标准” FIFO 和 FWFT FIFO 之间的差异反映了关于逻辑的任意两个模块之间数据流的基本问题: 接收方是否需要索取数据?还是发送端尽快呈现数据,接收端只确认可以继续?当一个模块向另一个模块传递数据时,一定要问自己这个问题,尤其是问问自己这些模块是否同意这个问题。
Asymmetric FIFOs
通常允许为 @din 和 @dout定义不同宽度的 FIFO 。这很有用,例如,如果数据以 32 位字到达 FPGA ,但应用逻辑将它们作为字节处理,即每个字 8 位。在这种情况下,将写入侧的宽度设置为 32 位,将读取侧的宽度设置为 8 位。双方的行为与往常一样,只是需要四个读 cycles (read cycles)才能消耗一个用单个写 cycle(write cycle)插入的单词。
当读取面比写入面宽时,它的行为就像人们期望的那样: 写入 FIFO 的数据在读取端不可用,直到写入的数据填充了一个与读取端大小相同的字。
至于字的排列顺序,好像 FIFOs 都用 Little Endian。例如, FIFO 将 32 位字打包成 8 位字,如下所示: 从 FIFO 读取的第一个字的位范围是 [7:0],然后是 [15:8]、 [23:16] 和 [31:24]。
但是,如果您想使用此功能,请务必查看文档。
组合逻辑(combinatorial logic)对 @empty 和 @full的依赖性
@empty 和 @full 端口有一个共同的缺点: 应用逻辑必须在同一个时钟周期上对它们做出响应。换句话说, @rd_en 必须是依赖于 @empty 的组合函数,以确保这两个信号不会在同一个时钟周期上处于高位(这是被禁止的,正如前面提到的)。同样, @wr_en 必须是依赖于 @full的组合函数(combinatorial function)。
使用组合函数可能会成为实现时序约束(timing constraints)的障碍。当时钟的频率很高(相对于 FPGA的规格)和逻辑函数很复杂时,这可能会成为一个问题。出现问题的主要原因是 @rd_en 和 @wr_en 都经常用在生产或消费数据的逻辑中。尤其是逻辑函数为很多逻辑计算时钟使能(clock enable)可以依赖这些信号。例如,如果有一个长流水线(pipeline)处理来自 FIFO的数据,则当来自 FIFO 的数据流暂时停止时,流水线中的所有逻辑必须冻结。
好吧,为了完全准确,有一种方法可以避免组合函数。例如,假设 @wr_en 被声明为寄存器,而 @want_to_write 是表示应用逻辑在给定时间需要写入的信号。可以这样做:
always @(posedge wr_clk)
wr_en <= want_to_write && !wr_en && !full;
这确保了 @wr_en 和 @full 在同一个时钟周期上永远不会变高,因为 @full 只能在 @wr_en 为高电平后在时钟周期上变为高电平。表达式中的 !wr_en 部分确保 @wr_en 在两个连续的时钟 cycles(clocks cycles)期间永远不会高。所以如果 @full 变高, @wr_en 会因为第一个时钟周期上的 !wr_en 而变低。由于 @full 本身, @wr_en 保持低位。
但是使用此解决方案, @wr_en 必须在一半时间内为低电平。因此,仅使用 FIFO的数据速率的 50% 。这通常是不可接受的。
@rd_en也可以使用相同的解决方案,并且该解决方案在使用一半的数据速率时也存在同样的问题。
该讨论旨在引出下一部分: "almost" 端口。
Almost full、 almost empty 和类似的端口
可以向 FIFO添加两个可选端口: @almost_full 端口和/或 @almost_empty 端口。
@almost_empty 与 @rd_clk同步,与 @empty类似,但略有不同: 当 FIFO 为空时, @almost_empty 为高电平,但当 FIFO正好有一个字要读取时也是如此。
同样, @almost_full 与 @wr_clk同步,并且在 FIFO 已满时为高电平,但在可以向 FIFO写入一个字时也是如此。
这两个输出端口(output ports)的名称取决于 FPGA 供应商及其提供的软件,但总有可能添加具有相同功能的端口。只是有时, FIFO 的某个变体可能不支持这些端口。
这些端口有何帮助?好吧,因为这工作得很好:
always @(posedge wr_clk)
wr_en <= want_to_write && !almost_full;
没有组合逻辑,也不需要跳过一半的写 cycles(write cycles)。当 @almost_full 为高电平时, @wr_en 可能不会在同一个时钟周期上变为低电平,而只会在下一个时钟周期上变为低电平。因此, @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 计数器
应用逻辑经常以块的形式执行操作。例如,逻辑从 FIFO 读取具有恒定长度的数据包,并通过某些物理介质传输这些数据包。由于数据存储在 FIFO上,应用逻辑需要在开始读取之前知道有足够的数据来填充数据包。
同样,应用逻辑通常会产生固定数量的数据以存储在 FIFO中,例如从外部存储器读取 burst 的数据。除非 FIFO 中有足够的空间来完成 burst,否则操作不应开始。
出于这些目的, FIFOs 通常支持 fill 计数器、 programmable empty 端口和 programmable full 端口。 fill 计数器(有时称为数据计数器(data counters))有不同的形式和形状,很大程度上取决于 FPGA 供应商,因此请仔细阅读 FIFO的文档。主要需要注意三个问题:
- 计数器与哪个时钟同步?几乎不用说,它必须是与使用计数器的逻辑相同的时钟。
- 计数器告诉我们什么? “读计数器(Read counters)”通常表示 FIFO中存储的字数,但是“写计数器(write counters)”呢?是存储的字数,还是在 FIFO 写满之前可以写入的字数?
- 计数器保证什么? Fill 计数器通常对其预期目的持悲观态度。例如,通常允许“读计数器”暂时给出低于 FIFO中实际字数的数字。这发生在将字写入 FIFO时,因为这些计数器在响应写入操作时会延迟增加其值,但会在响应读取操作时提前减少其值。这对于它们与控制 @rd_en的逻辑一起使用是有意义的,但是如果控制 @wr_en的逻辑使用这种类型的计数器,则可能会导致溢出(overflow)。为此,有“写计数器”。不过,请阅读文档中的详细信息。
此外还有 programmable empty 和 programmable full,它们是 @almost_empty 和 @almost_full的扩展版本。这个想法是因为 fill 计数器的使用几乎可以肯定是
assign dont_start_reading = (rd_data_count < 64);
为什么不直接提供该信号,并将其称为 prog_empty?再次仔细阅读 FIFO的文档。
再一次,当阅读 FIFO的最后一个字很重要时,一定要问自己,您的逻辑是否真的会这样做。这个问题类似于上面关于 @almost_empty的讨论。
几乎不用说,在配置 FIFO时,如果需要,您必须请求这些额外的端口。
AXI interface
这个主题没有直接关系,但是值得一提以避免混淆,因为这个术语经常出现在 FIFOs的上下文中。
AXI 是由 ARM引入的 AMBA 标准中定义的一组接口。正如人们所预料的那样,带有 AXI 接口的 FIFOs 通常用作 CPU的外围设备。
“baseline FIFO”的接口通常被称为“native”接口,与 AXI 接口相对。
AXI 接口主要有两种类型: “常规” AXI (通常是 AXI3、 AXI4 或 AXI Lite)是总线加上地址(address)和数据(data)。第二种类型 AXI-S (streamed AXI)用于数据流(streams)数据(可能被分成数据包)。
当 FIFO 被配置为 AXI3 / AXI4 或 AXI Lite时,额外的逻辑被添加到它,因此它可以作为外围设备通过该接口连接到 CPU 的地址。我不会进一步详细说明这一点,因为这是一个完全不同的话题。
但由于 streaming 接口与 FIFO的行为有些相似,因此可以将 AXI-S 接口的 handshake 信号转换为“native”接口。请注意, AXI-S 通常还涉及其他需要处理的信号。
因此,给定用于写入 FIFO 的 AXI-S 信号为 @axi_w_valid、 @axi_w_ready 和 @axi_w_data,它们可以连接到“标准” FIFO 的端口
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 的端口
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 。