基于 Verilog 的 FIFO 设计
扫描二维码
随时随地手机看文章
FIFO 设计并不罕见。我们能够找到大量相关信息,包括公开可用的代码。你认为在 2026 年,FIFO 设计仍然重要吗?是的,当然。FIFO(先进先出存储器)在基于现代 FPGA 的解决方案中仍然非常重要,这类解决方案要求在性能和功耗方面实现高效的硬件设计。此外,这也是那些有意成为 FPGA 领域的 RTL(寄存器传输级)/硅设计/工程师的人必须经历的关键设计步骤之一。我收到了很多关于通过 Verilog 编码进行设计的问题,现在正是把这些内容放在这里的绝佳时机。这只是个开始。
FIFO 代表“先进先出”,它是流控系统中一个不为人知但依然重要的组成部分。无论是简单的设计还是高速以太网设计、高性能视频系统,还是其他计算系统,这个小小的组件都能防止流媒体出现故障。
如果您查看各种类型的 FIFO(先进先出存储器),就会发现有同步型和异步型两种。
# 同步——同时钟域——对于在相同时钟域下运行的设计而言非常有用
# 异步——时钟跨域——对于跨越时钟的硬件设计非常有用
在此,我将通过原理理解、编码、行为仿真以及使用 RTL 设计流程进行验证的方式来展示同步 FIFO 的设计。我将使用 Verilog HDL 以及 AMD Vivado 工具来进行设计。
在开始编写 Verilog 代码之前,我们需要先对 FIFO 操作的工作原理有一个基本的了解。
FIFO 用作临时缓冲区,常用于实现行缓冲区。其主要功能是接收来自主设备的有效数据包,并将它们排队存留,直到从设备准备好进行处理。FIFO 有助于防止数据丢失,解决流式传输中的压力回流问题。
AXI4-stream、AXI-MM、以太网以及视频/音频管道在接口之下都设有缓冲区,持续处理数据包流。
OSI 层
FIFO 在数据链路层和物理层中均能发挥作用,用于实现数据在物理介质中的流量控制和传输。
Principal
如果不了解 FIFO 的基本组成部分,我们就无法对其进行设计和验证。接下来我们将探讨 FIFO 的以下关键组成部分。上述图片中已经展示了这些组成部分。
存储单元和 FIFO 指针
这是任何 FIFO 的两个关键组成部分。内存单元其实不过是一个用于存储/缓冲数据的数组元素。数组的长度即为 FIFO 的深度。在大多数情况下,FIFO 的深度是按照 2 的幂次规则设定的,例如 1024、2048、4096、8192 等等。在进行内存单元的综合时,综合器通常会推断出 BRAM,或者您可以直接通过 BRAM 原型模板来实例化 BRAM。然而,如果内存的读写操作不是同步的,并且不符合 BRAM 的读写模式,那么内存单元就会在 LUT 中进行推断。如果不进行适当的逻辑资源规划,这可能会导致大量的 LUT 元件被消耗。
FIFO 指针其实只是数组元素的一个索引。它用于跟踪要读取或写入的内存位置。由于存在读取和写入操作,因此会有单独的读取指针和写入指针,分别用于分别跟踪读取和写入的数据。指针的大小/宽度也是根据 2 的幂次方来定义的,并以“位”为单位表示。例如,如果 FIFO 深度设置为 1024,指针的宽度则确定为 pointerwidth = log2(1024) = 10 位。也就是说,一个指针可以跟踪 FIFO 中多达 1024 个数据。
一旦指针的值达到最大深度,指针应回滚/重置回初始值,并同时显示指针已“绕回”的情况。这就是为什么读取或写入指针在最高位部分多了一个额外的位。例如,
对于 1024 个 FIFO 存储单元的深度,
每当指针在达到最大 FIFO 深度值后回滚至初始值时,必须对该 MSB 进行切换。这对于正确检测 FIFO 满和空的状态至关重要。
先进先出读写使能
这是 FIFO 的其他组成部分。这些信号负责决定是否启动或停止 FIFO 的读取或写入操作。有两个独立的使能信号,即“读使能”和“写使能”,分别用于控制读取和写入操作。如果“使能”信号被置高,则读取或写入操作开始;否则则停止。同时,在“使能”信号为高电平时,读取或写入指针也会相应递增,以跟踪存储单元的索引位置。
“FIFO 全满与空闲”
这些是 FIFO 的关键组成部分,它们在实际控制中起着重要作用。在设计任何 FIFO 时,这些部分绝不能被遗漏或遗忘。正如您可能已经注意到的,FIFO 是一个临时缓冲区。显然,在操作过程中,该缓冲区随时可能处于“满”或“空”的状态。FIFO 应该能够报告其缓冲区的状态。状态信息由 FIFO 的主设备和从设备利用来精确控制数据流。
FIFO(先进先出)的满溢和空置状态检测是任何 FIFO 设计中颇具特色的部分。这些状态可以通过读写指针的当前值来确定。
这里的“回绕位”这一术语具有重要意义。否则,就会出现 FIFO 的模糊性问题。回绕位用于跟踪读写指针的回绕情况。这些回绕位能够准确地帮助确定 FIFO 的满溢和空闲状态。
在满载和空载两种情况下,读取指针和写入指针都是相等的,但循环标志位在准确判断这些条件方面起着重要作用。
在这种情况下(读指针 == 写指针),只要读指针和写指针的“翻转位”值相同,那么就处于 FIFO 空闲状态;反之,如果翻转位值不同,则处于 FIFO 满位状态。
我们可以通过两种情况来方便地理解这些条件:
案例 1:没有读操作,只有写操作,将位 0 进行置位。
在初始状态下,读指针和写指针的值均为 0。存在一个 FIFO 空置状态,因为读指针和写指针的地址相同,并且其“循环位”值也相同。
当写操作开始时,指针值会增加。此时,读指针和写指针的值并不相等。这就形成了“FIFO 未满”和“FIFO 未满置满”的条件。同时,接下来写指针和循环标志的值也会被计算出来。
每当写指针到达最大 FIFO 深度值时。
比如说,
读取指针 = 0
读取指针环绕位 = 0
写指针 = 1023
写指针包装位 = 0 ,
这些结果,
下一个写指针 = 0(回滚至初始值)
“wrap bit = 1”(表示写指针已回滚或回退至初始值)。
在此我们注意到,读指针和接下来即将进行写入操作的指针值变得相同,而读指针和写指针之间的循环位也有所不同。这就构成了 FIFO 满的条件。如果写入操作继续进行到这种状态之后,数据将会被覆盖。因此,必须利用满的条件来阻止进一步的写入操作。
案例 2:读操作与写操作一同进行(即写操作嵌套在读操作之中)
在这种情况下,我们假定写操作会伴随着“队列已满”的条件而进行,并且会执行读取操作。
此刻,
读取指针 = 0
读取指针环绕位 = 0
写指针值 = 0
写指针包装位 = 1(表示正在进行写操作的封装)
在读取操作中,读取指针会一直递增,直至达到最大 FIFO 深度值,然后指针会重新从零开始计数。
我们将具备以下条件,
读取指针 = 0
读指针翻转位 = 1(读操作已进行翻转)
写指针值 = 0
写指针包装位 = 1(表示正在进行写操作的封装)
在此,读取和写入操作的指针值均为零,并且其循环位值相同。这是 FIFO 空置状态的条件。读取操作不应继续进行。否则,会出现未知的数据读取情况。因此,从机必须利用空置状态来防止在此状态之后进行进一步的数据读取。
其他情况:
在上述案例中,我们先考虑写操作,然后再考虑读操作,以便清晰地说明 FIFO 的满和空状态。在实际情况中,读写操作是动态发生的。然而,在任何情况下,检测 FIFO 的满和空状态都是相同的。
时钟与复位
由于我们正在设计一个同步 FIFO,因此仅使用一个时钟来同步执行读取和写入操作。所以,我们无需使用冲突检测电路(CDC)逻辑。同样,同步复位也被用于复位 FIFO 状态。
编程
一旦我们理解了先进先出的工作机制,我们就可以按照同样的原则编写 Verilog 代码,明确地定义同步 FIFO 的功能行为。
下面的图片展示了写入和读取操作的简化先进先出状态。
#1 港口声明
我们从 RTL 端口声明开始创建一个 FIFO 模块,如下面所附所示。由于我们设计的是同步 FIFO,因此会有单个时钟和复位引脚。读写操作都在同一个时钟域内进行。
#2 前进式 FIFO 存储单元是通过创建一维数组来定义的。数组的大小由“fifo_depth”值决定。
#3 前进式写指针声明。正如我们在主要部分中所讨论的,我们通过增加一个额外的位来设置指针值,以用于指示循环情况。
#4 前进式写指针递增。我们通过定义时序逻辑来计算每次写操作启用时的当前和下一个写指针值。同时,我们还必须确保在写操作期间 FIFO 未满,否则会导致数据丢失。
#5 先进先出写入操作。每当写入操作被启用时,我们将接收到的数据写入到由写入指针标识的内存单元中。请注意,我们仅使用写入指针的值(不包括循环位)。
#6 同样地,我们可以为读取操作编写 Verilog 代码,其方式与写入操作类似。
#7 先进先出状态。正如我们在主要部分中所讨论的那样,我们通过以下方式检测 FIFO 的满和空状态。利用组合逻辑来快速比较指针状态。
#8 我们将上述代码整合为一个单一的 Verilog 代码片段,创建一个 Vivado 项目,并开始进行仿真和验证。
模拟
正如我们之前所讨论的那样,现在我们将通过行为模拟来评估不同的 FIFO 情况。
案例 1:仅写入操作
您可以在下方查看模拟波形的前一部分。我们最初仅设置了“wr_en”信号。它控制着写入操作。这可以在以下波形中进行监测。wr_ptr 递增,数据正被写入存储单元。由于我们将 FIFO 深度值设为 8,写入操作将持续 8 个时钟周期,直至达到 fifo_full 状态。
案例 2:写操作完成后变为只读状态
在这种情况下,我们假设一旦满足全部条件,就不会再进行写操作,然后我们仅针对这种情况来激活“rd_en”信号。该信号驱动读操作,即递增读指针并从存储单元中读取数据。该读操作会持续到 FIFO 空为止。这些在下面的仿真波形的第二部分中得到了清晰的展示。
案例 3:现实情况
在以往的案例中,我们仅考虑了写操作和读操作以作说明之用。然而,在实际情况中,这些操作是由从属设备和主设备主动控制的,会考虑到满载和空载两种情况。大多数高速接口和流水线都有一个 FIFO(先进先出缓冲区),用于主动控制数据包/数据的流动,从而避免数据丢失。
例如,在 AXI4Stream 协议中,主设备的 TVALID 信号负责驱动 FIFO 的写入操作。同时,TREADY 信号对主设备而言也非常重要,因为它用于有效数据的传输。此信号将 FIFO 的完整信息传递给主设备。如果 FIFO 已满,TREADY 信号将被解除激活,主设备则会停止传输。
从从属 AXI4Stream 协议的角度来看,该 FIFO 自身成为了后续流水线的主设备。空闲状态的 FIFO 负责驱动 TVLAID 信号,而从属设备的 TREADY 则负责驱动 FIFO 的读取操作。因此,只有当 Tvalid 和 Tready 信号都处于高电平状态时,有效的数据传输才会发生。这意味着通过 TREADY 信号,从属设备正在驱动 FIFO 的读取操作。同时,通过 TVALID,它表明 FIFO 不为空。可供读取操作使用的数据已准备好。
示威活动
在演示中,我采用了基于赛灵思 TPG 的仿真设计,将一个 TPG 帧传入同步 FIFO 中,然后对其进行捕获。
通过同步 FIFO 抓取 TPG 帧后,得到了以下帧图。
这证实了我们的同步 FIFO 正在正常运行。
本文编译自hackster.io





