网络知识 娱乐 关于异步FIFO设计,这7点你必须要搞清楚

关于异步FIFO设计,这7点你必须要搞清楚

写在前面

        在这篇文章,默认您已经对异步FIFO的设计方法有了基本的了解。

        如果不了解异步FIFO设计的基本方法,可参考:异步FIFO的Verilg实现方法


1、什么是格雷码(Gray Code)?

        格雷码是美国学者Frank Gray于1947年提出的一种二进制编码方式,后面这种编码方式就以他的名字命名。实际上,格雷码是有多种编码形式的。根据定义,在一组数的编码中,若任意两个相邻的代码只有一位二进制数不同,则称这种编码为格雷码

        下表是不同形式的格雷码:

        表中典型格雷码具有代表性。若不作特别说明,格雷码就是指典型格雷码(后文将简称格雷码),它可从自然二进制码转换而来。

        好了,现在我问你,根据格雷码的性质你能默写出16个4bit宽度的格雷码来吗?我想一般是比较难的,因为单靠上述性质很难推导出来。比如说,0(0000)可以写出来,1(0001)也可以写出来,2(0011)也可以写出来,但是3就不好写了,因为根据只能变化一位的性质,可以是0111,也可以是0010,那么到底是哪个呢?如何不是经常使用的话,我想是很难判断的。

        所以接下来就要介绍格雷码的第二个性质了:当第N位从0变到1的时候,之后的数的N-1位会关于前半段轴对称,而比N位高的位是相同的。

        我们看一下4bit格雷码的前四位的例子。示意图如下:

  • 当0001跳转到下一位时,毋庸置疑的是,第0位会维持1不变,而第1位会跳转到1,所以可以据此画出对称轴
  • 高2位(第3、2位)这保持不变
  • 低位(该实例中只有第0位)关于对称轴对称

        有了上述三点信息,那么就可以把剩下的2个格雷码写出来了。

         

        4bit格雷码的前8位示意图如下(这我就不啰嗦了,你们肯定看得懂):

        格雷码相关可参考:Verilog实现的格雷码与二进制码的互相转换

2、异步FIFO为什么要使用格雷码?

        异步FIFO设计最关键的点是什么?答案是“空”和“满”的判断。现在回想一下,我们是如何进行状态判断的。很简单,分别构建读指针和写指针。写指针总是指向下一个要写的地址,而读指针永远指向当前要读取的地址。再通过对读、写指针的比较来判断空、满。

        如上图的FIFO深度为8,则其地址位宽应该是3(2的三次方等于8)。当写指针与读指针都指向同一个位置(即相同)时,可能是空状态,但也可能是满状态(写指针超过了读指针一圈)。(这里说句题外话,不可能是读指针超过写指针一圈,因为但读、写指针第一次相等时,就应该输出空状态,然后停止对FIFO进行读取操作。)所以,究竟如何判断FIFO是空还是满呢?答案是无法判断,只能通过在高位增加一位的方式来判断,当除了最高位MSB外的其他位都相同,最高位不同时则表明此时写指针超过了读指针一圈,FIFO被写满了;当除了最高位MSB外的其他位都相同,最高位相同时则表明此时写指针等于读指针,FIFO被读空了。

        这种方法用来对同步FIFO进行判断是没有问题的,因为同步FIFO的读、写指针是同一时钟域下的信号,可以直接对比。但是异步FIFO的读、写指针是不同时钟域下的信号,如果直接对比则会有亚稳态的问题。为了对异步FIFO的读、写指针进行判断,我们首先需要将其同步到统一的时钟域下,而这就引发出了新的问题----读、写指针在大多数情况下都不是个单bit信号,而是个多bit信号。如果读、写指针直接使用2进制的形式进行同步,则难以避免同步过程中会出现的多个bit信号同时变化的问题。如7(0111)跳转到8(1000),此时有4个bit信号都发生了变化,如果直接同步,则由于不同信号之间的延迟(skew),可能导致亚稳态、错采、漏采等等问题。

        此时不妨回想下格雷码的性质:每相邻位之间只有一个bit的变化。FIFO的指针是递增的,这使得在传输递增的多bit信号时,格雷码具有天然的优势。还是从7到8的例子,若使用格雷码,则应该是7(0100)--8(1100),这样就只有1个bit的变化了(最高位),这样就将多bit信号的跨时钟域转变成了单bit信号的跨时钟域,而单个bit的跨时钟域同步是很好实现的。

3、读指针、写指针应该被同步到哪个时钟域?

        异步FIFO的读、写指针是不同时钟域的信号,那么就不能直接对比,而是需要将其同步到同一时钟域才能进行对比。现在问题来了?指针应该被同步到哪个时钟域?可选项有第三方时钟域、读时钟域和写时钟域。接下来不妨逐个分析。

        首先需要说明的是,这说的同步都是指使用2个(或者3个,但此类情况不多)FF(触发器)来进行同步(俗称“打两拍”),这种同步方式是有延迟的(时序开销,可以看做是两个目同步时钟周期)。

        第三方时钟域不难知道一个信号从一个时钟域同步到另一个时钟域(被同步时钟域)是需要时间的(这里仅考虑从满到快,也就是暂时不考虑漏采的问题),需要的时间取决于被同步时钟域的周期以及需要同步的个数。假设这个时间是T,那么经过T时间后,由于读写时钟不一致,原来的读写时针增加(也可能不变)的量是不一致。比如说实际上读写时针都指向4(且最高位相同),那么这种情况实际上是出现了读空的情况。但是同步到第三方时钟域后,可能写指针成了6,而读指针变成了8(读时钟比写时钟快),那么在这种情况下FIFO就不会报“读空”,从而造成功能错乱。所以该种方法不可取。

        同步到写时钟域读指针同步到写时钟域需要时间T,在经过T时间后,可能原来的读指针会增加或者不变,也就是说同步后的读指针一定是小于等于原来的读指针的。写指针也可能发生变化,但是写指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的写指针就是真实的写指针。

                现在来进行写满的判断:也就是写指针超过了同步后的读指针一圈。但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候写指针其实是没有超过读指针一圈的,也就是说这种情况是“假写满”。那么“假写满”是一种错误的设计吗?答案是NO。前面我们说过异步FIFO设计的关键点是产生合适的“写满”和“读空”信号,那么何谓“合适”?该报的时候没报算合适吗?当然不算合适。不该报的时候报了算不算合适?答案是算。可以想象一下,假设一个深度为100的FIFO,在写到第98个数据的时候就报了“写满”,会引起什么后果?答案是不会造成功能错误,只会造成性能损失(2%),大不了FIFO的深度我少用一点点就是的。事实上这还可以算是某种程度上的保守设计(安全)。

                接着进行读空的判断:也就是同步后的读指针追上了写指针。但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候读指针实际上是超过了写指针。这种情况意味着已经发生了“读空”,却仍然有错误数据读出。所以这种情况就造成了FIFO的功能错误。

        同步到读时钟域 写指针同步到读时钟域需要时间T,在经过T时间后,可能原来的读指针会增加或者不变,也就是说同步后的写指针一定是小于等于原来的写指针的。读指针也可能发生变化,但是读指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的读指针就是真实的读指针。

                现在来进行写满的判断:也就是同步后的写指针超过了读指针一圈。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候写指针已经超过了读指针不止一圈,这种情况意味着已经发生了“写满”,却仍然数据被覆盖写入。所以这种情况就造成了FIFO的功能错误。

                接着进行读空的判断:也就是读指针追上了同步后的指针。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候读指针其实还没有追上写指针,也就是说这种情况是“假读空”。那么“假读空”是一种错误的设计吗?答案是NO。前面我们说过异步FIFO设计的关键点是产生合适的“写满”和“读空”信号,那么何谓“合适”?该报的时候没报算合适吗?当然不算合适。不该报的时候报了算不算合适?答案是算。可以想象一下,假设某个FIFO,在读到还剩2个数据的时候就报了“读空”,会引起什么后果?答案是不会造成功能错误,只会造成性能损失(2%),大不了我先不读了,等数据多了再读就是的。事实上这还可以算是某种程度上的保守设计(安全)。

        现在我们可以总结一下:

  • “写满”的判断:需要将读指针同步到写时钟域,再与写指针判断
  • “读空”的判断:需要将写指针同步到读时钟域,再与读指针判断

        假读空示意如下:

        假写满示意如下:

4、如何判断异步FIFO的空和满?

        在同步FIFO的设计中,我们把读、写指针的位宽拓宽了1bit,目的是区分原来的读、写指针相等时判断空、满的问题。同步FIFO的指针使用的是2进制码的形式,而异步FIFO为了减少多bit信号跨时钟域传输的亚稳态问题,采用的是格雷码形式的指针,那么格雷码形式的指针应该如何对比来判断空和满?

        首先要说明的是,将格雷码转换成2进制再进行对比是一种很好的办法,但是会消耗多的组合逻辑资源,所以我们暂时不讨论。

        先观察上图,假定我们要设计的FIFO深度为8,那么指针宽度是4,因为宽度为3时无法区分空、满。

        空的判断很好判断,只要读写指针所有位全部一致,则说明此时是空状态。

        满的判断复杂一点,假设采用同步FIFO的判断方法----最高位不同,其他位一致。我们用实际的数值来看看是什么情况:当读指针的值为0100,则说明此时读指针指向最高的空间7,那么若是FIFO满了,则写指针应该是1100,那么1100对应的二进制是多少呢?是8。那么读写指针、一个指向7,另一个却指向8,这是满?显然不是。

        如果真的是满的话,写指针应该是多少?应该是15,也就是1000,此时写指针是超过读指针8,也就是一圈的。那么0100和1000有什么联系?很显然,高两位相反,低位相同。这种规律的形成原因是我们之前提到的,格雷码的变化都关于某个对称轴对称。

        总结:

  • 当最高位和次高位相同,其余位相同认为是读空
  • 当最高位和次高位不同,其余位相同认为是写满

5、空和满的判断是准确的吗?

        在第4点我们知道了----将读指针同步到写时钟域来判断满;将写指针同步到读时钟域来判断空。既然是异步FIFO,那么读写时钟域的信号是不一致的,其中一个的频率快,另一个的频率这慢。那么在两次同步过程中,一定是一次慢时钟采快时钟和一次快时钟采慢时钟。快时钟采慢时钟是不会有问题的,因为这符合采样定理。但是慢时钟采快时钟则会有问题,因为采样过程不符合采样定理。

        那么会造成什么问题?答案是漏采。某些数值可能会被漏掉。例如原本是连续的0--1--2---3的信号,从快时钟同步到慢时钟后,就变成了离散的0--3,其中的1、2被漏掉了。那么这样一种现象会导致空、满的判断是准确的吗?答案是不准确,但没关系。

        设想读慢写快与读快写慢两种情况:

        读慢写快:

                进行写满判断的时候需要将读指针同步到写时钟域,因为读慢写快,所以不会有读指针遗漏,同步消耗时钟周期,所以同步后的读指针滞后(小于等于)当前读地址,所以可能写满会提前产生,并非真写满。

                进行读空判断的时候需要将写指针同步到读指针 ,因为读慢写快,所以当读时钟同步写指针的时候,必然会漏掉一部分写指针,我们不用关心那到底会漏掉哪些写指针,我们在乎的是漏掉的指针会对FIFO的读空产生影响吗?比如写指针从0写到10,期间读时钟域只同步捕捉到了3、5、8这三个写指针而漏掉了其他指针。当同步到8这个写指针时,真实的写指针可能已经写到10 ,相当于在读时钟域还没来得及觉察的情况下,写时钟域可能写了数据到FIFO去,这样在判断它是不是空的时候会出现不是真正空的情况,漏掉的指针也没有对FIFO的逻辑操作产生影响。

        读快写慢:

                进行读空判断的时候需要将写指针同步到读指针 ,因为读快写慢,所以不会有写指针遗漏,同步消耗时钟周期,所以同步后的写指针滞后(小于等于)当前写地址,所以可能读空会提前产生,并非真读空。

                进行写满判断的时候需要将读指针同步到写时钟域,因为读快写慢,所以当写时钟同步读指针的时候,必然会漏掉一部分读指针,我们不用关心那到底会漏掉哪些读指针,我们在乎的是漏掉的指针会对FIFO的写满产生影响吗?比如读指针从0读到10,期间写时钟域只同步捕捉到了3、5、8这三个读指针而漏掉了其他指针。当同步到8这个读指针时,真实的读指针可能已经读到10 ,相当于在写时钟域还没来得及觉察的情况下,读时钟域可能从FIFO读了数据出来,这样在判断它是不是满的时候会出现不是真正满的情况,漏掉的指针也没有对FIFO的逻辑操作产生影响。

        现在我们会发现,所谓的空满信号实际上是不准确的,在还没有空、满的时钟就已经输出了空满信号,这样的空满信号一般称为假空、假满。假空、假满信号本质上是一种保守设计,想象一下,一个深度为16的异步FIFO,在其写入14个数据时,即输出了写满(假满)标志,这会对我们的设计造成影响吗?会,会削弱我们的效率,我们实际使用的FIFO深度成了14,但是会使得我们的设计产生错误吗?显然不会。同样的,在FIFO深度仍有2时即输出了读空(假空)标志,也不会使得我们的设计出错,但是会降低效率,因为我们使用的FIFO深度又少了2。

6、既然有假满、假空,那么有没有真满、真空?

        还真有,但是没意义。既然我们可以将读指针同步到写时钟域来判断假满;将写指针同步到读时钟域来判断假空。那么对应地,可以读指针同步到写时钟域来判断空;将写指针同步到读时钟域来判断满。

        在写时钟域判断空:

                读指针被同步过来的信号(同步后读指针)是滞后于真实读指针的信号,当同步后读指针等于写指针时,真实读指针实际上早就等于写指针了,也就是说此时的空一定是空,甚至已经空了一段时间了。这样的空标志显然是没有使用意义的,因为会造成对FIFO的过读操作,你来来回回读个空FIFO有什么意义呢?也就是说真空能实现,但是没实际使用意义。

        在读时钟域判断满:

                写指针被同步过来的信号(同步后写指针)是滞后于真实写指针的信号,但同步后写指针超过读指针一圈时,真实写指针实际上早就超过读指针一圈了,也就是说此时的满一定是满,甚至已经满了一段时间了。这样的满标志显然是没有使用意义的,因为会造成对FIFO的过写操作,你来来回回写个满FIFO有什么意义呢?也就是说真满也能实现,但是同样没实际使用意义。

7、非2次幂深度的FIFO如何设计?

        在第1点关于格雷码的性质中,我们阐述了:

  • 在一组数的编码中,若任意两个相邻的代码只有一位二进制数不同,则称这种编码为格雷码
  • 当第N位从0变到1的时候,之后的数的N-1位会关于前半段轴对称,而比N位高的位是相同的。

        由这两点,我们发现格雷码都可以关于某条对称轴对称。所以只有当FIFO深度为2的幂次方时,才能做到格雷码绕一圈后,回到初始位置仍然只有1bit变化,如15(1000)----0(0000)。当FIFO深度不为2的幂次方时,显然从最尾端跳转到开头端,变化的就不止一个bit了。比如FIFO深度为7,显然,从13(1011)----0(0000),变化了可不止1bit。这样的话在这一次跳变就失去了格雷码存在的意义了,所以得想点其他办法解决。

        前面说过,格雷码相邻每位只变化1bit,而且关于中轴对称的。那么我们可以这样编码:针对深度为7的FIFO,从1(0001)开始,一直到(14个数表示7深度,高位区分状态)14(1001),1(0001)与14(1001)是关于中轴对称的(高位为变化位),所以也只有1bit的跳变。同样的如果深度为6的FIFO,就从2(0011)开始,到13(1011),同样只跳变1bit。

        空标志只用判断读、写指针是否全部相等即可。但是满标志就不能用“高两位相反,其他位相同来判断了”,需要找其他规律了。从这里可以看出,格雷码作为一种无权码,在比较和运算等方面不如有权码二进制灵活。所以,咱要不还是转回二进制再比较吧。

        多啰嗦一句,实际上不管你设计FIFO用RAM还是直接调用IP也好,最终实现都是用的Block RAM资源,其生成的位宽肯定是2的幂次。所以啊,不用也是浪费啊。