备战秋招[五]-异步FIFO
欢迎关注个人公众号摸鱼范式
异步FIFO
参考资料为Clifford E. Cummings的论文。
1.0 摘要
异步FIFO是一种FIFO设计,数据从一个时钟域进入到FIFO,在另一个时钟域读取数据,并且两个时钟域是异步。
异步FIFO的用于将数据从一个时钟域安全准确地传递到另一个时钟域。
设计异步FIFO的方法有很多,其中也有很多错误的设计方法。这些不正确的设计方法,在90%的时间内都能够正确运行,而大多数正确的设计方法能够保证在99%的时间中能够正确运行。不幸的是,在99%以上的时间中都能正常工作的FIFO也具有设计缺陷,这些缺陷通常最难检测和调试(如果你很幸运地在产品出厂前就注意到了此错误),或者诊断和召回的代价很高(直到产品由不满意的客户掌握之前才发现)。
本文讨论了一种FIFO设计风格,以及进行异步FIFO设计时必须考虑的重要细节。
下文将“异步FIFO”简称为“ FIFO”
2.0 传递多个异步信号
将多个信号从一个时钟域同步到另一个时钟域,并确保所有的信号都同步到新时钟域中的同一时钟周期这是一个关键问题。 FIFO在设计中用于将多位数据从一个时钟域安全地传递到另一个时钟域。 通过一个时钟域中的控制信号将数据存入FIFO缓存中,并通过来自第二时钟域的控制信号将数据从同一FIFO缓存的另一个端口中读取并删除。 看上去,设计FIFO似乎很容易。
FIFO设计的难点在于生成FIFO指针以及FIFO上的满和空状态确定。
2.1 同步FIFO指针
对于同步FIFO设计(在同一时钟域中执行FIFO读写操作),一种实现方式是对FIFO的写入和读取次数进行计数,递增(以FIFO写入但不读取) ),递减(在FIFO读取但无写入时)或保持(无写入和读或同时进行写入和读取操作)FIFO计数值。 当FIFO计数器达到预定的最大值时,FIFO为满,而当FIFO计数器为零时,FIFO为空。
不幸的是,对于异步FIFO设计,不能使用增减FIFO填充计数器,因为将需要两个不同的异步时钟来控制计数器。 异步FIFO设计的满状态和空状态,必须通过比较写指针和读指针确定。
2.2 异步FIFO指针
为了理解FIFO设计,需要了解FIFO指针是如何工作的。写指针总是指向下一个要写的字;因此,在复位时,两个指针都设置为零,这也恰好是下一个要写入的FIFO字位置。在FIFO写入操作中,将数据写入指针所指向的位置,然后将写指针递增以指向要写入的下一个位置。同样,读取指针始终指向要读取的当前FIFO字。再次复位时,两个指针均复位为零,FIFO为空,而读指针指向无效数据(因为FIFO为空且声明了空标志)。一旦第一个数据被写入FIFO,写入指针就会递增,清空标志,并且在寻址第一个FIFO存储字内容的读指针会立即将第一个有效数据驱动到FIFO数据输出端口,由接收端读取。读指针始终指向要读取的下一个FIFO字意味着,接收端不必使用两个时钟周期来读取数据。如果接收端在读取FIFO数据之前必须先递增读指针,则接收端必须先用一个周期等FIFO输出数据字,再用一个周期将数据字捕获到接收端,这回浪费一倍的时间。
当读写指针相等时,FIFO为空。当两个指针在复位操作期间都复位为零时,或者在从FIFO读取了最后一个字的情况下,读指针赶上了写指针,就会发生这种情况。
当指针再次相等时,也就是说,当写指针跑了一圈并追上读指针时,FIFO为满。那么问题来了,当指针相等时,FIFO到底为空还是满?
区分是满还是空可以为每个指针添加一个额外的位。当写指针增加到最终FIFO地址之后,写指针将使未使用的MSB标志位递增,同时将其余位置回零,如下图所示(FIFO使用了带MSB标志位的指针)。读取指针也是如此。如果两个指针的MSB不同,则意味着写指针比读指针多跑了一圈。如果两个指针的MSB相同,则意味着两个指针的圈数一致相同。
使用n位指针(其中n-1是访问整个FIFO缓存所需的地址位数),当两个指针(包括MSB)相等时,FIFO为空。当两个指针(MSB除外)相等时,FIFO已满。
本文中的FIFO设计将n位指针用于具有2(n-1)个可写位置的FIFO,以帮助处理满和空条件。 后续章节中包含了有关满逻辑和空逻辑的更多设计详细信息。
2.3 二进制FIFO指针注意事项
将二进制计数值从一个时钟域同步到另一个时钟域存在一些问题,因为n位计数器的每个位都可以同时更改(例如,二进制数7-> 8的值为0111-> 1000,所有位都更改了)。解决该问题的一种方法是使用握手机制将周期二进制计数值采样并保存在保持寄存器中,然后将同步请求信号传递到新的时钟域。识别请求信号后,接收时钟域将同步的应答信号发送回发送时钟域。在从接收时钟域接收到应答信号之前,不得更改采样指针。使用此技术,可以将具有多个更改位的计数值安全地传输到新的时钟域。收到应答信号后,发送时钟域有权清除就绪信号并重新采样二进制计数值。
使用此技术,将定期采样二进制计数器值,并且并非所有二进制计数器值都可以传递到新的时钟域。问题是,我们是否需要关注二进制计数器可能继续递增和溢出的情况?还是在采样的计数器值之间使FIFO下溢?答案是否定的。
- 当写指针追上同步和采样的读指针时,FIFO满。同步和采样的读取指针可能无法反映实际读取指针的当前值,但写入指针将不会尝试计数超过同步读取指针值的值。因此不会发生溢出。
- 当读指针赶上同步采样的写指针时,FIFO空。同步和采样的写指针也可能无法反映实际写指针的当前值,但读指针将不会尝试计数超过同步写指针值的值。因此不会发生下溢[8]。
在讨论了同步格雷码指针之后,将在7.0节中详细介绍有关对具有同步握手信号对的二进制指针进行采样的技术。
FIFO计数器指针的另一种常见方法是使用格雷码计数器。格雷码在每次时钟转换时只允许改变一位,从而消除了与试图在同一时钟沿同步多个变化信号相关的问题。
2.4 FIFO测试的问题
完备的FIFO测试几乎是不可能的,这与功能仿真不同,涉及到时序信息。问题根源在于就算RTL仿真中的FIFO指针表现理想,在实际设计中使用它们也会导致灾难性的故障。
在RTL仿真中,如果设计中包含二进制计数FIFO指针,则所有FIFO指针位将同时变化,没有机会观察到同步和比较问题。在没有反标延迟的门级仿真中,如果门上升沿和下降沿信号的门延迟不同,观察到问题的机会将会很小,即使能够观察到,也必须需要具有正确的时序能够触发这种情况。数据在时钟上升沿之前和之后变化,对于更高速度的设计,上升沿信号和下降沿信号之间的延迟差减小,发现问题的可能性也会减小。找到实际的FIFO设计问题对于带有反标延迟的门级设计来说是更加容易一些,但是即使进行这种类型的仿真,也很难做到发现问题,而且随着信号传播延迟的减少,观察到设计问题的几率也会降低。
如果要解决这一问题,就必须认识到存在潜在的FIFO设计问题,并且从一开始就进行正确地设计。
我有时用于测试FIFO设计的行为模型是FIFO模型,它易于编码,对于行为测试来说是准确的,但是如果用作RTL综合模型,则很难调试。只建议将此FIFO模型用于FIFO测试平台。该模型可准确地确定何时应设置FIFO满和空状态位,并可用于确定应存储在工作FIFO中的数据值。再重复一遍,此FIFO模型不能安全地进行综合!
module beh_fifo (rdata, wfull, rempty, wdata, winc, wclk, wrst_n, rinc, rclk, rrst_n);
parameter DSIZE = 8;
parameter ASIZE = 4;
output [DSIZE-1:0] rdata;
output wfull ;
output rempty;
input [DSIZE-1:0] wdata;
input winc,wclk,wrst_ n;
input rinc,rc1k,rrst_ n;
reg [ASIZE: 0] wptr,wrptr1,wrptr2,wrptr3;
reg [ASIZE: 0] rptr,rwptr1,rwptr2,rwptr3;
parameter MEMDEPTH = 1<<ASIZE;
reg [DSIZE-1:0] ex_ mem [0 : MEMDEPTH-1];
always @ (posedge wclk or negedge wrst_ n)
if (!wrst_ n) wptr <= 0
else if (winc && !wfu11) begin
ex_ mem [wptr [ASIZE-1:0]] <= wdata;
wptr <= wptr+1;
end
always @ (posedge wc1k or negedge wrst_ n)
if (!wrst_ n) {wrptr3,wrptr2,wrptr1} <= 0;
else {wrptr3,wrptr2,wrptr1} <= {wrptr2,wrptr1,rptr};
always @ (posedge rc1k or negedge rrst_ n)
if (!rrst_ n) rptr <= 0;
else if (rinc & !rempty) rptr <= rptr+1;
always @ (posedge rclk or negedge rrst_ n)
if (!rrst_ n) {rwptr3,rwptr2,rwptr1} <= 0;
else {rwptr3,rwptr2,rwptr1} <= {rwptr2,rwptr1,wptr};
assign rdata = ex_ mem [rptr [ASIZE-1:0]] ;
assign rempty = (rptr == rwptr3) ;
assign wfu11 = ((wptr [ASIZE-1:0] == wrptr3 [ASIZE-1:0]) &&
(wptr [ASIZE]!= wrptr3 [ASIZE]));
endmodule
在上面的行为模型中,使用二进制计数指针,一个Verilog数组来表示FIFO缓存,同一模块中的多个异步时钟以及未注册的输出。 该模型不适合综合!(希望在本节中使用了足够的警示,以阻止任何人尝试综合此模型!)
模块中的两个Always模块(带有连接的Always模块),以行为模型的方式表示实际RTL FIFO设计中所需的同步。 它们对于通过FIFO进行的数据传输测试并不重要,但对于在FIFO模型中正确生成的满标志和空标志的测试至关重要。 行为模型中所需的同步阶段的确切数量取决于FIFO设计。 该模型可用于帮助测试本文所述的FIFO设计。
3.0格雷码计数器(风格1)
格雷码以1953年最初为该代码申请专利的人的名字命名,弗兰克·格雷。 格雷码计数器有很多种设计方法。 本节详细介绍了一种简单而直接的设计方法。 本文描述的技术仅使用一组触发器实现格雷码计数器。 下一节将详细介绍使用两组触发器实现更高速度的第二种方法。
3.1 格雷码模式
出于后面的原因,我们希望同时设计n位格雷码计数器和(n-1)位格雷码计数器。 分别设计两个计数器很容易,但是设计一个通用的n位格雷码计数器然后修改第二个MSB以形成一个共享LSB的(n-1)位格雷码计数器也很容易且有效。在本文中,这将被称为“对偶n-bit位格雷码计数器”。
为了更好地理解将n位格雷码转换为(n-1)位格雷码的问题,请考虑设计像图2中的双4位和3位格雷码计数器。
如上图所示,最常见的格雷码是一种反射码,其中除MSB以外的任何列中的位均关于序列中点对称[6]。这意味着4位格雷码的后半部分是MSB反转的前半部分的镜像。
要将4位转换为3位格雷码,我们不希望4位序列的后半部分的LSB是前半部分的LSB的镜像,而是希望将4位序列的后半部分的LSB镜像。下半部分重复上半部分的4位LSB序列。
经过仔细检查,很明显将4位格雷码后半部分的第二个MSB取反将在4位序列的三个LSB中产生所需的3位格雷码序列。唯一的另一个问题是带有额外MSB的3位格雷码不再是真正的格雷码,因为当序列从7(格雷码0100)变为8(〜格雷码1000),然后又从15(〜格雷码1100)变为0(格雷码0000),两位在变化,而不是一位。真正的格雷码只会在计数之间改变一位。
3.2格雷码计数器基础
关于格雷码,任何两个相邻数之间的编码距离仅为1(从一个格雷计数到下一个格雷计数,只能改变一位)。其次,最有用的格雷码计数器的深度必须是2的幂。可以使格雷码计数器计数偶数个序列,但是这些序列之间的转换通常不像标准格雷码那样简单。另外请注意,由于没有奇数长度的格雷码序列,因此无法制作23深度的格雷码。这意味着本文描述的技术用于制作2^n深度的FIFO。
图3是风格1的对偶n-bit位格雷码计数器的框图。风格1的格雷码计数器假定寄存器位的输出是格雷码值本身(ptr,wptr或rptr)。然后,将格雷码输出传递到格雷码-二进制转换程序(bin),将其传递给条件二进制值增量器,以生成下一个二进制计数值(bnext),并将其传递给二进制-格雷码转换器,生成下一个格雷码计数值(gnext),该值传递到寄存器输入。图3方框图的上半部分显示了所描述的逻辑流程,而下半部分则表示了与第二格雷码计数器相关的逻辑,如下一节所述。
3.3 对偶n-bit格雷码计数器
对偶n-bit格雷码计数器是一个格雷码计数器,它生成n位格雷码序列和一个(n-1)位格雷码序列。
通过对n位格雷码的两个MSB进行异或运算,即可生成(n-1)位格雷码的MSB,从而简单地生成(n-1)位格雷码。 它与n位格雷码计数器的(n-2)个LSB组合在一起,形成(n-1)位格雷码计数器。
3.4 其他格雷码计数器注意事项
- 如图3所示,应该对二进制值增量器进行“不满”或“不空”检查,以确保FIFO指针在FIFO满或FIFO空的情况下不会递增,最终导致FIFO缓存上溢或下溢。
- 如果能够保证在FIFO满状态时不向FIFO发送数据,则可以通过从FIFO写指针中删除满状态检查逻辑(full-testing logic)来简化FIFO设计。
- FIFO指针本身并不能保护FIFO缓存不被覆盖,但是可以将其他条件逻辑添加到FIFO缓存中,以确保在FIFO满状态下不能激活write_enable信号。
- 可以将额外的“粘性”状态位ovf(上溢)或unf(下溢)添加到指针设计中,以指示在满状态时发生了另外的FIFO写操作,或者在空状态时发生了另外的FIFO读操作,而标志位错误情况只能在重置期间清除。
一个安全的通用FIFO设计需要包括上述保护措施,但代价是实现起来消耗会稍大一些,甚至可能导致速度变慢。但是这是一个好的设计,因为将来的同事不需要考虑那么多细节就能够直接复用模块。
4.0 格雷码计数器(风格2)
从本文的1.2版开始,FIFO设计将使用格雷码计数器风格2,这种风格使用两组寄存器来消除将格雷指针值转换为二进制值的需求。 第二组寄存器(二进制寄存器)也可用于直接寻址FIFO存储器,而无需将存储器地址转换为格雷码。 但是仍然需要n位格雷码指针将指针同步到相反的时钟域,但是n-1位二进制指针可用于直接寻址内存。 如果需要,二进制指针还使运行计算更容易以生成“接近空”和“接近空”的位(本文未显示)。
FIFO(风格1)
下图是第一种风格的FIFO
为了进行风格1FIFO设计的静态时序分析,该设计已划分为以下六个具有以下功能和时钟域的Verilog模块:
- fifo1.v-(请参见6.1节中的示例2)-此为顶层的包装器模块,包括所有时钟域。顶部模块仅用作包装,以实例化设计中使用的所有其他FIFO模块。如果将此FIFO用作较大的ASIC或FPGA设计的一部分,则可能会舍弃此顶级包装程序,以允许将其他FIFO模块分组到各自的时钟域中,以改进综合和静态时序分析。
- fifomem.v-(请参见第6.2节中的示例3)-这是FIFO缓存,可通过写入和读取时钟域进行访问。该缓冲区很可能是实例化的同步双端口RAM。可以修改其他存储器样式以用作FIFO缓冲区。
- sync_r2w.v-(请参阅6.3节中的示例4)-这是一个同步器模块,用于将读指针同步到写时钟域中。wptr_full模块将使用同步的读指针来生成FIFO满状态。该模块仅包含与写时钟同步的触发器。此模块中没有其他逻辑。
- sync_w2r.v-(请参阅6.4节中的示例5)-这是一个同步器模块,用于将写指针同步到读时钟域中。rptr_empty模块将使用同步的写指针来生成FIFO空状态。该模块仅包含与读取时钟同步的触发器。此模块中没有其他逻辑。
- rptr_empty.v-(请参见6.5节中的示例6)-此模块与读取时钟域完全同步,并包含FIFO读取指针和空标志逻辑。
- wptr_full.v-(请参见6.6节中的示例7)-此模块与写时钟域完全同步,并且包含FIFO写指针和全标志逻辑。
为了使用此FIFO风格执行FIFO满和FIFO空测试,必须将读取和写入指针传递到相反的时钟域以进行指针比较。
与其他FIFO设计一样,由于两个指针是从两个不同的时钟域生成的,因此需要将这些指针“安全地”传递到相对的时钟域。本文使用的技术是同步格雷码指针,以确保一次只能更改一个指针位。
5.0 处理空和满标志位
到底如何实现FIFO满标志和FIFO空标志取决于设计方法。
本文中的FIFO设计假定将在读时钟域中生成空标志,以确保当FIFO缓冲区为空时(即,读指针赶上写操作的瞬间)能够立即检测到空标志指针(包括指针的MSB)。
本文中的FIFO设计假定将在写时钟域中生成满标志,以确保当FIFO缓冲区已满时(即,写指针赶上读取的瞬间)能够立即检测到满标志指针(不同的指针的MSB除外)。
5.1 生成空标志位
如2.2中所述,当读指针和同步写指针相等时,FIFO为空。
空标志位很容易生成。使用指针比寻址FIFO缓存所需的指针大一位。如果两个指针的额外位(指针的MSB)相等,则指针的换行次数相同,并且如果其余读取指针等于写指针,则FIFO为空。
格雷码写指针必须通过sync_w2r模块中的一对同步器寄存器同步到读时钟域中。由于使用格雷码指针一次只更改一位,因此在时钟域之间同步多位转换没有问题。
为了有效地输出空标志位,实际上会将写指针与rgraynext(rptr的下一个格雷码值)进行比较。下面是空标志位检查的代码,来自示例6中的rptr_empty.v:
assign rempty_val = (rgraynext == rq2_wptr);
always @(posedge rclk or negedge rrst_n)
if (!rrst_n) rempty <= 1'b1;
else rempty <= rempty_val;
5.2 生成满标志位
由于需要通过在写时钟域中比较写指针和读指针来生成满标志,因此要在进行指针比较之前将读指针同步到写时钟域中。
生成满标志位不像空标志位那么简单。 比寻址FIFO缓存所需的地址大一点的指针仍用于比较,但是仅使用带有额外位的格雷码计数器进行比较并不准确。 问题在于,格雷码除MSB之外是对称码。
考虑上图所示的深度为8的FIFO。在这个例子中,使用3位格雷码指针寻址存储器,并添加了一个额外的位(4位格雷码的MSB)来判断满和空状态。如果允许FIFO填充前七个位置(字0-6),然后如果通过回读相同的七个字来清空FIFO,则两个指针将相等并指向地址Gray-7(FIFO为空) )。在下一次写操作中,写指针将使4位格雷码指针递增(请记住,只有3个LSB用于地址存储器),使MSB在和读指针不一样,但其余部分和读指针一致,判定FIFO满状态。这是错误的!不仅FIFO未满,而且3个LSB均未更改,这意味着寻址的缓存位置会覆盖最后写入的FIFO缓存。这也是错的!
这就是为什么使用第4.0节的对偶n-bit位格雷码计数器的原因之一。
通过将rptr同步到wclk域中,进行比较,然后需要满足以下三个条件才能判定为满状态:
(1)wptr和rptr的MSB不相等(因为wptr必须比rptr多跑一圈)。
(2)wptr和rptr的次MSB不相等(参见上面的图)。
(3)所有其他wptr和同步rptr位必须相等。
为了有效地判定满标志,实际上需要把同步读指针与wgnext(wptr的下一个格雷码)进行比较。如下所示,这是从示例7的wptr_full.v代码中提取的代码块
assign wfull_val = ((wgnext[ADDRSIZE] !=wq2_rptr[ADDRSIZE] ) &&
(wgnext[ADDRSIZE-1] !=wq2_rptr[ADDRSIZE-1]) &&
(wgnext[ADDRSIZE-2:0]==wq2_rptr[ADDRSIZE-2:0]));
always @(posedge wclk or negedge wrst_n)
if (!wrst_n) wfull <= 1'b0;
else wfull <= wfull_val;
上述代码中的assign可以进一步简化为
assign wfull_val = (wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1], wq2_rptr[ADDRSIZE-2:0]});
5.3 不同的时钟速度
由于异步FIFO是从两个不同的时钟域提供时钟,因此显然时钟运行在不同的速度。将较快的时钟同步到较慢的时钟域时,由于较快的时钟将在较慢的时钟沿之间半周期地递增两次,因此会跳过一些计数值。这引发了对以下两个问题的讨论:
第一个问题。快时钟是慢时钟的两倍,那么快域地址变化两次,慢时钟域采样一次,前后采样值变化了两次,会产生多位同步的问题吗?
不会。快时钟域第一次改变一位,比如从A到B,慢时钟域没有采样,当快时钟域改变第二次B到C之后,慢时钟域才采样,虽然这期间快时钟域的地址从A到C变了两次,但是慢时钟域只看到第二次B到C,只跳变了一位,因此不会产生多位同步问题。
第二个问题。快时钟域是否会引起full+1的情况——写溢出,或者empty+1——读溢出?
在本文的设计下,答案是不。首先考虑FIFO已满。当写指针追上同步的读指针并且在写时钟域中检测到FIFO满状态时,FIFO充满。如果wclk域快于rclk域,则写指针最终将追上同步的读指针,FIFO达到满状态,wfull置1,并且FIFO将停止写操作,直到同步的读指针前进才会继续。也就是说写指针不能超过wclk域中的同步读指针。
对空标志的检查也是类似的,当读取指针追上同步写入指针并且在读取时钟域中检测到FIFO空状态时,FIFO变空。如果rclk域快于wclk域,则读指针最终将追上同步写指针,FIFO将为空,rempty位将置位,并且FIFO会暂停读取,直到同步写指针前进才会继续。读指针不能超过rclk域中的同步写指针。
使用这种设计,FIFO的空满状态判定总是及时、准确地。对清楚空满状态是悲观的。
5.4 悲观的空与满
本文使用的FIFO使用“悲观”方法实现了满清除和空清楚。也就是说,“full”和“empty”都能准确判断,但是清除得很晚。
由于在写时钟生成FIFO-full状态,且当写指针赶上同步读指针时发生成FIFO-full,因此满检测是“准确的”和即时的。删除“满”状态是悲观是因为“满”比较是用同步读指针完成的。当读指针增加时,FIFO不再满,但是直到两个wclk边沿将更新后的rptr同步到wclk域中,整个生成逻辑才会检测到这种变化。这并不是一个问题,因为这意味着数据发送硬件被“阻止”了,或者FIFO标志位仍然是满的,有两个额外的wclk周期延迟。因此,能够保证FIFO不会溢出。向数据发送方发出信号,让它不要发送更多的数据,腾出两个额外的wclk周期,给FIFO留出时间来接收更多的数据。
类似地,由于用读时钟生成FIFO-empty状态,并且当读指针赶上同步的写指针时产生FIFO-empty,所以空检测是准确的和及时的。清除“空”状态是悲观的,因为“空”比较是用同步写指针完成的。当写指针增加时,FIFO不再为空,但是直到两个上升的rclk上升沿才能将新的wptr同步到rclk域中,空生成逻辑才会检测到这种变化。这并不是问题,因为这意味着数据接收逻辑被“阻止”了,或者通知FIFO仍然是空的,因为有两个额外的rclk边沿。重要的是确保FIFO不会下溢。向数据接收器发出信号,让它停止从FIFO中读取数据,腾出两个额外的rclk周期,这只是为FIFO填充更多的数据提供了时间。
5.4.1 精确的空和满
注意,如果两个指针同时递增,则设置满标志或空标志可能不太准确。 例如,如果写指针赶上了同步的读指针,拉高满标志,但是如果读指针与写指针同时增加,FIFO过早的置为满状态。 由于读操作与“写到满”操作同时发生,因此未真正写满,但读指针尚未同步到写时钟域中。 因此满标志的生成有点太早,有些悲观。 但这不是设计问题。
5.5 多bit异步复位
我们已经非常确定FIFO指针一次仅改变一位。 问题是是否存在与异步复位相关的问题,是否会导致多个指针位同时更改?
答案是不。 复位表示FIFO也已复位,并且FIFO中没有有效数据。 复位生效后,所有同步寄存器,wclk域逻辑(包括已生成的满标志)和rclk域逻辑均同时异步复位。 生成的空标志也同时复位。 更重要的问题是如何有序去除复位信号。
请注意,本文包含的设计对wclk和rclk域使用了不同的复位信号。 FIFO指针的异步复位不是问题。
5.6 几乎已满,几乎已空
许多设计要求通过生成“几乎已满”和“几乎为空”状态位来通知未决的已满或已空状态。有很多方法可以实现这两个状态位,每种实现都取决于指定的设计要求。
某些FIFO设计要求可编程的FIFO满和FIFO空差值,以便当两个指针之间的差小于编程差时,会声明相应的几乎满或几乎为空的位。有些设计会要求在固定差值的条件下,生成几乎满或空的内容。当FIFO指针的MSB关闭时,松散地产生几乎满和空的状态,可以满足某些FIFO的要求。而有些设计可能只需要知道FIFO何时满或不足一半即可。
当wptr赶上同步rptr时FIFO已满,几乎满的情况可以描述为(wptr + 4)赶上同步rptr时的情况。
本文的代码不包括almost_full和almost_empty标志位的生成。
6.0 风格1FIFO的RTL代码
6.1 fifo1.v FIFO的顶层模块
module fifo1 #(parameter DSIZE = 8,
parameter ASIZE = 4)
(output [DSIZE-1:0] rdata,
output wfull,
output rempty,
input [DSIZE-1:0] wdata,
input winc, wclk, wrst_n,
input rinc, rclk, rrst_n);
wire [ASIZE-1:0] waddr, raddr;
wire [ASIZE:0] wptr, rptr, wq2_rptr, rq2_wptr;
sync_r2w sync_r2w (.wq2_rptr(wq2_rptr), .rptr(rptr),
.wclk(wclk), .wrst_n(wrst_n));
sync_w2r sync_w2r (.rq2_wptr(rq2_wptr), .wptr(wptr),
.rclk(rclk), .rrst_n(rrst_n));
fifomem #(DSIZE, ASIZE) fifomem
(.rdata(rdata), .wdata(wdata),
.waddr(waddr), .raddr(raddr),
.wclken(winc), .wfull(wfull),
.wclk(wclk));
rptr_empty #(ASIZE) rptr_empty
(.rempty(rempty),
.raddr(raddr),
.rptr(rptr), .rq2_wptr(rq2_wptr),
.rinc(rinc), .rclk(rclk),
.rrst_n(rrst_n));
wptr_full #(ASIZE) wptr_full
(.wfull(wfull), .waddr(waddr),
.wptr(wptr), .wq2_rptr(wq2_rptr),
.winc(winc), .wclk(wclk),
.wrst_n(wrst_n));
endmodule
6.2 fifomem.v FIFO缓存
读数据是直接从mem中读,不需要时钟。读地址A指示的是下一次要读的地址——此时FIFO不为空,读A地址是有效的。当前rdata信号上保存的是下一次要读的数据。如果外部读时钟域打算读数据,那么给一个读有效rinc,在rclk上升沿就可以直接把rdata取走,在rinc上升沿FIFO内部会根据A+1地址判断是否empty。(可以参考3.2节)。
写数据winc有效时,下一个写时钟沿wclk要写入数据。满标志表示如果在下一个时钟沿写数据,就写到读地址处(读写地址相同)。如果满,则下一个时钟沿不能写。所以在下一个写时钟沿到来时要判断满标志,如果满了,则不能写。
同样,读地址表示下一个读时钟沿要读的数据,空表示下一个时钟沿是否可以读。对于读数据的设备,它需要在读的时候判断是否空了,至于FIFO的rdata输出端则不需要进行empty判断,FIFO将下一次要读的数据放在rdata处,如果读数据的设备要读,就在rclk时钟沿读就行了。
module fifomem #(parameter DATASIZE = 8, // Memory data word width
parameter ADDRSIZE = 4) // Number of mem address bits
(output [DATASIZE-1:0] rdata,
input [DATASIZE-1:0] wdata,
input [ADDRSIZE-1:0] waddr, raddr,
input wclken, wfull, wclk);
`ifdef VENDORRAM
// instantiation of a vendor's dual-port RAM
vendor_ram mem (.dout(rdata), .din(wdata), //这一块不用管
.waddr(waddr), .raddr(raddr),
.wclken(wclken),
.wclken_n(wfull), .clk(wclk));
`else
// RTL Verilog memory model
localparam DEPTH = 1<<ADDRSIZE; //将地址位数转化成FIFO深度
reg [DATASIZE-1:0] mem [0:DEPTH-1];
assign rdata = mem[raddr]; //直接读,empty标志位影响raddr的累加
always @(posedge wclk)
if (wclken && !wfull) mem[waddr] <= wdata;//写需要判断full,并且在时钟沿处写
`endif
endmodule
6.3 sync_r2w.v 读时钟域到写时钟域同步器
用写时钟打两拍
module sync_r2w #(parameter ADDRSIZE = 4)
(output reg [ADDRSIZE:0] wq2_rptr,
input [ADDRSIZE:0] rptr,
input wclk, wrst_n);
reg [ADDRSIZE:0] wq1_rptr;
always @(posedge wclk or negedge wrst_n)
if (!wrst_n) {wq2_rptr,wq1_rptr} <= 0;
else {wq2_rptr,wq1_rptr} <= {wq1_rptr,rptr};
endmodule
6.4 sync_w2r.v 写时钟域到读时钟域同步器
用读时钟打两拍
module sync_w2r #(parameter ADDRSIZE = 4)
(output reg [ADDRSIZE:0] rq2_wptr,
input [ADDRSIZE:0] wptr,
input rclk, rrst_n);
reg [ADDRSIZE:0] rq1_wptr;
always @(posedge rclk or negedge rrst_n)
if (!rrst_n) {rq2_wptr,rq1_wptr} <= 0;
else {rq2_wptr,rq1_wptr} <= {rq1_wptr,wptr};
endmodule
6.5 rptr_empty.v 读指针和空标志位生成逻辑
产生空标志位、n-1位当前的二进制读地址、n位格雷码读地址
module rptr_empty #(parameter ADDRSIZE = 4)
(output reg rempty,
output [ADDRSIZE-1:0] raddr,
output reg [ADDRSIZE :0] rptr,
input [ADDRSIZE :0] rq2_wptr,
input rinc, rclk, rrst_n);
reg [ADDRSIZE:0] rbin;
wire [ADDRSIZE:0] rgraynext, rbinnext;
wire rempty_val;
//-------------------
// GRAYSTYLE2 pointer
//-------------------
always @(posedge rclk or negedge rrst_n) //格雷码计数器的第二种写法
if (!rrst_n) {rbin, rptr} <= 0;
else {rbin, rptr} <= {rbinnext, rgraynext};
// Memory read-address pointer (okay to use binary to address memory)
assign raddr = rbin[ADDRSIZE-1:0];//给到memory模块
assign rbinnext = rbin + (rinc & ~rempty);
assign rgraynext = (rbinnext>>1) ^ rbinnext;
//---------------------------------------------------------------
// FIFO empty when the next rptr == synchronized wptr or on reset
//---------------------------------------------------------------
assign rempty_val = (rgraynext == rq2_wptr);//空判断是根据下一次要读的地址来判断的
always @(posedge rclk or negedge rrst_n)
if (!rrst_n) rempty <= 1'b1;
else rempty <= rempty_val;
endmodule
6.6 wptr_full.v 写指针和满标志生成逻辑
产生满标志、n-1位当前写地址、n位格雷码写地址
module wptr_full #(parameter ADDRSIZE = 4)
(output reg wfull,
output [ADDRSIZE-1:0] waddr,
output reg [ADDRSIZE :0] wptr,
input [ADDRSIZE :0] wq2_rptr,
input winc, wclk, wrst_n);
reg [ADDRSIZE:0] wbin;
wire [ADDRSIZE:0] wgraynext, wbinnext;
wire wfull_val;
// GRAYSTYLE2 pointer
always @(posedge wclk or negedge wrst_n)
if (!wrst_n) {wbin, wptr} <= 0;
else {wbin, wptr} <= {wbinnext, wgraynext};
// Memory write-address pointer (okay to use binary to address memory)
assign waddr = wbin[ADDRSIZE-1:0];
assign wbinnext = wbin + (winc & ~wfull);
assign wgraynext = (wbinnext>>1) ^ wbinnext;
//------------------------------------------------------------------
// Simplified version of the three necessary full-tests:
// assign wfull_val=((wgnext[ADDRSIZE] !=wq2_rptr[ADDRSIZE] ) &&
// (wgnext[ADDRSIZE-1] !=wq2_rptr[ADDRSIZE-1]) &&
// (wgnext[ADDRSIZE-2:0]==wq2_rptr[ADDRSIZE-2:0]));
//------------------------------------------------------------------
assign wfull_val = (wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]});
// 空判断是根据下一次要写的地址判断的。
always @(posedge wclk or negedge wrst_n)
if (!wrst_n) wfull <= 1'b0;
else wfull <= wfull_val;
endmodule
7.0 对比格雷码指针和二进制指针
如第2.3节所述,如果对指针进行了采样并且在两个时钟域之间使用了握手控制信号来安全地传递采样的二进制计数值,也可以可以使用二进制指针进行FIFO设计。与格雷码指针相比,使用二进制指针的一些优点:
-
将多位值采样到保持寄存器中并使用同步握手控制信号将多位值传递到新的时钟域中的色痕迹可用于跨时钟域传递任何任意多位值。 这种方法可用于传递FIFO指针或任何多位值。
-
每个同步格雷码指针需要2n个触发器(每个指针位2个)。采样的多位寄存器需要2n + 4个触发器(由于握手机制)。两种指针样式产生亚稳性的可能并没有差异。
-
采样的多位二进制指针允许任意更改指针。格雷码指针只能递增和递减。
-
采样的多位二进制指针允许任意FIFO深度;格雷码指针需要2的幂的FIFO深度。如果设计需要至少132个字的FIFO深度,则使用标准格雷码指针将采用256个字的FIFO深度。由于大多数实例化的双端口RAM块深度为2的幂,因此这并不是什么大问题。
-
使用二进制指针使生成“几乎为空”和“几乎为满”状态位变得容易。
与格雷码指针相比,使用二进制指针的一个小缺点是:
- 采样并持有二进制FIFO指针,然后在时钟边界上握手会有延迟。
- 至少从接收时钟域两个时钟沿捕获新采样,从发送时钟域至少两个时钟沿捕获新采样。等待时间通常不是问题,但通常会在判别full和empty时会更加悲观,并且可能需要额外的FIFO深度来补偿所增加的悲观度。由于大多数FIFO通常都是用较大的深度指定的,因此不太可能需要额外的寄存器或更大的双端口FIFO缓存大小。
设计FIFO时,上述关键点值得考虑。
8.0 结论
异步FIFO设计需要仔细注意从指针生成到空满生成的细节。对重要细节的无知通常会导致设计易于验证但也是错误的。要发现FIFO设计错误,通常需要对FIFO设计进行门级仿真,并需要带有实际延迟的反标和很多运气!
使用格雷码指针可以安全地将FIFO指针同步到另一个的时钟域中。
产生FIFO已满状态可能是FIFO设计中最难的部分。对偶n位格雷码计数器对于将n位指针同步到相反的时钟域并使用(n-1)位指针进行满标志生成比较非常有用。使用第7.0节中介绍的技术同步二进制FIFO指针是进行FIFO设计时要使用的另一项有价值的技术。
通过将n位读指针与同步的n位写指针进行比较,可以轻松实现生成FIFO空状态。本文的技术对于速度差大和小的异步时钟都是适用的。
沿着时钟域边界对FIFO模块进行仔细的划分,并记录所有输出,对两个异步时钟域内的综合和静态时序分析有很大帮助。