FPGA之道(48)跨时钟域问题

前言

跨时钟域问题是FPGA以及IC设计中最常见的话题,也几乎是最重要的问题,在笔试面试考察中,这个考点首当其冲,关于这个话题,我之前也总结过很多,但是总觉得比较老套,而且没有完全理解,故借此机会来总结《FPGA之道》系列,一起看下作者是如何看待这个问题的。
注:本文来自《FPGA之道》。

跨时钟域问题

什么是跨时钟域问题

只要FPGA设计中的所有资源不全属于一个时钟域,那么就可能存在跨时钟域问题,因为异步逻辑其实也可以看做一种特殊的跨时钟域问题。发生跨时钟域问题的必要条件是不同时钟域之间存在信息交互,如果一个FPGA设计中存在多个时钟域的话,这几乎是无法避免的,否则各个时钟域互不相关,那么就相当于把原设计分解为多个独立的小设计,这样的话FPGA设计的功能就无法通过协作来扩展。
那么当两个不同时钟域之间进行信息交互的时候,到底会存在什么问题呢,搞得我们如此紧张!还记得在【本篇->编程思路->数字电路中的隐患】章节中,介绍过的寄存器输出的不稳定态和组合逻辑中的竞争和险象吗?如果FPGA设计中仅有一个时钟域,那么我们可以轻松通过时序约束、时序分析来确保这些隐患不会对我们的同步逻辑造成任何影响,但如果是跨时钟域呢?

  • 情况一:如果控制两个时钟域的时钟信号有非常高的关联性,例如一个PLL同频不同相的两个输出,又或者干脆就是一个时钟信号的上升沿或下降沿。那么很幸运,此时时序分析仍能够确保隐患的消除,最多再多指定一些简单的信息,例如时钟信号的占空比。
  • 情况二:如果控制两个时钟域的时钟信号有关联,但是不同频,例如一个PLL不同频的两个输出,那么,如果这两个频率之间没有较大的公约数的话,那么可能需要成百上千个时钟周期才能让它们的相位重新对齐。而在此期间,一个时钟的边沿可以以一个很小的相位步进差划过另一个时钟周期的所有相位,所以几乎可以肯定,一定会有某些边沿采样到了另一个时钟域内数据的不稳定状态。
  • 情况三:如果控制两个时钟域的时钟信号没有关联,例如它们分别是由两个不同频率的晶振产生的,那么它们的相位可能要经过上亿个周期后才能对齐,甚至永远不能对齐,因为不同晶振有着不同的频率偏移和抖动参数。这种情况下,也肯定会出现采样到不稳定态的问题。

针对情况二,还可以通过时序分析加功能仿真确认的方法,虽然比较复杂,但仍能够进行确认是否能消除隐患,但是对于情况三,时序分析已经无能为力了,因为天知道两个晶振起振的相位是多少呢。事实上,除非两个时钟的相关性真的非常高,否则隐患几乎必被激发,那么为了让FPGA设计仍能够有正常的行为,我们必须采取措施!

解决跨时钟域问题的原理

既然跨时钟域问题几乎必然导致隐患成为现实,那么我们该怎么应对呢?思路与单时钟域避免隐患是一样的,躲过不稳定态即可。既然由于时钟频率等的不同,导致总有些时候,一个时钟的有效沿会采样到另一个时钟域信号的不稳定态,那么同时也说明总有些时候,一个时钟的有效沿不会采样到另一个时钟域信号的不稳定态。因此,只要我们采用恰当的方法来确保当我们跨时钟域取信号的时候,当前时钟的有效沿不会采样到另一个时钟域的不稳定态,那么就可以确保FPGA设计的各个时钟域之间能够进行正常的通信,从而也就保证了FPGA设计整体行为的正确性。
那么,究竟有哪些方法可以保证不采样到另一个时钟域的不稳定态呢?接下来我们就来一一介绍。

两级采样法

当一个高频率时钟域需要使用一个非它时钟域内的电平信号时,强烈建议采用两级采样法来确保设计行为的正确性和一致性,若当前时钟域频率小于另一个时钟域频率,则另一个时钟域必须保证其输出信号在一个大于当前时钟域周期的时间内保持不变,否则信号将会被漏采样。两级采样法其实就是异步信号同步化的一个基本方法,它的思路如下图所示:
在这里插入图片描述

为什么要对非本时钟域的信号用本时钟域的时钟进行采样呢?

这是因为,如果一个信号的更新频率与当前时钟域内的信号更新频率不同步的话,那么其和当前时钟域内的信号做任何的运算,都有可能导致结果出现毛刺或者不满足下级寄存器的建立或保持时间。更为头痛的是,所有和它有联系的寄存器的时序分析都会变得十分困难甚至不可分析。因此,为了确保新引入的这个非本时钟域的信号不会对本时钟域造成毁灭性打击,必须对其进行同步化处理,而方法就是使用当前时钟域的时钟对其进行采样。

为什么要采样两次呢?

采样一次,已经完成了非本时钟域信号的同步操作,那么为什么还要强烈建议大家采用两级采样法呢?虽然从逻辑上来看,两级采样法就跟移位寄存器一样,并不能改变信号的逻辑值,而且还会增加信号传递进来的延迟,但是这绝对不是画蛇添足,而是非常必要的,原因如下:请大家思考一下,进行第一级采样的那个寄存器,其建立和保持时间是否能够得到满足?显然在有些情况下,其建立和保持时间是无法得到满足的,因为它和当前时钟域时钟信号的变化并不同步,因此总会碰到问题。例如,当asyn signal从0变化到1时,如果在这前后时间内,clk共经历了3个上升沿,那么unsafe signal的输出可能是001,也可能是011,其中,第二次采样由于建立或者保持时间不满足,所以无法确定其是0还是1,不过值得欣慰的是,无论是001,还是011,都至少正确扑捉到了原始信号的变化。也许你会觉得,异步逻辑本身就不可能在时间上被精确的扑捉到,既然能够正确扑捉到了信号的变化,那不就够了么?没错,从逻辑上来说是够了,但是从驱动能力上来说,也许不够。大家都知道,FPGA内部的工作电压一般为1.5V,这也就是说,理想情况下,逻辑1对应电压1.5V,逻辑0对应电压0V。但是现实是残酷的,逻辑1不可能精确的是1.5V,逻辑0也不可能是精确的0V,事实上,也许业界公认0.5V以上就可以判定为逻辑1,反之则可以被判定为逻辑0,这也是为什么数字信号比模拟信号更能抗干扰的原因。所以,如果现在问你,FPGA内部有两个触发器的输出都是逻辑1,那么它俩的物理电压相等吗?答案显然是不一定。为什么触发器能够给出正确的输出结果的前提是输入信号要满足其建立、保持时间要求?其实原因很简单,就是要给数字电路以充足的时间来进行充电或者放电操作,从而让其输出的逻辑1更接近于1.5V,逻辑0更接近于0V;反之,如果一个逻辑电平1、0对应的物理电平更接近于1.5V或者0V,那么它就更容易在规定时间内对其后级触发器进行充分的充电或放电控制,从而使其后级触发器的输出也更加“强壮”。那么现在,我们在回过头来审视unsafe signal的物理电压,由于输出unsafe signal的触发器,很可能出现建立或保持时间要求不满足的情况,因此,在这种情况下,它的充、放电操作都很不充分,输出的逻辑1或者逻辑0的物理电压都不会太好。例如,如果unsafe signal为逻辑1,那么其物理电压很可能为0.6V,如果当前时钟域中有很多地方都用到了unsafe signal,那么0.6V电压的扇出能力显然会比较差,因此等传递到后续各个用到unsafe signal的地方,物理电压可能就变为0.4V,0.5V,0.55V等等,那么这时unsafe signal就会被不同的触发器认成不同的逻辑电平,于是错误便诞生了。那么为了避免这种情况的发生,我们对unsafe signal再次进行采样。由于unsafe signal和safe signal是同步的,因此对于输出safe signal的触发器来说,建立时间已经远远超出了其的建立时间要求,因此,即使0.6V的电压对其充电比较慢,但由于充电时间足够,充电电流也有保障,所以也能让safe signal达到一个比较健壮的物理电压,例如1.4V。接下来,我们再将safe signal连接到各个需要使用它的地方,其扇出能力就不会再有任何问题了。

握手法

天是晴朗的,不过也许你此时的心情无法像这灿烂的阳光一般,因为当你走向自己的爱车时,发现车牌尼玛不见了,雨刷上夹着一张纸条,狗爬一样歪歪扭扭的写下一行字——“想要车牌,请汇500元到XXXX账户后致电XXXXXXXXXX”。碰到这种情况,报警多半是徒劳的,说时迟那时快,你立马愤怒的汇出了500元到XXXX账户,然后拨打了XXXXXXXXXX,不过一直无人接听。大约过了半个来钟头,你收到一条短信,发信息的正是XXXXXXXXXX,短信的内容为——“请去车右侧的第二个垃圾桶里找!”。怎么办?只有相信了,在体验了一回清洁工的辛苦后,你终于找回了自己的车牌!
喂~喂!写跑偏了吧,赶紧讲握手法吧,扯这些没用的干嘛!呵呵,这些可不是没用的,这个故事正道出了握手法的精髓所在。大家看,整个过程中,小偷与失主完全处于不同的空间,毫无同步性可言,但是整个事情进行的却异常顺利,最后小偷得到了钱,你找回了车牌,奇迹就这么发生了。
现在回过头来再看我们的跨时钟域问题,如果进行信息传递的两个时钟域,能够学学上述故事中的小偷与失主,那么必然也能毫无差错的各取所需。握手法没有固定的形式,不过在这里,为大家介绍一种比较方便的方法来实现数据传递的握手。
如果时钟域A要向时钟域B内传递数据,可以采用以下步骤:

对于时钟域A:

  • step1:先将要传递的数据用一个寄存器dataA寄存,然后保持该寄存器的输出不变。
  • step2:等待至少一个时钟周期后,将寄存器AOK设置为逻辑1,并保持至少相当于1个B时钟域周期的时间长度。
  • step3:释放AOK为逻辑0;
  • step4:等待至少一个周期后,如果有新的值需要传递,更新dataA后重复step2、3、4。

对于时钟域B:

如果AOK为1,则将dataA的输出用本时钟域内的dataB寄存器寄存。
到此为止,时钟域A就完成了向时钟域B内可靠传递数据的工作。对于时钟域A的step2,为什么AOK的逻辑1要保持至少相当于1个B时钟域周期的时间长度呢?就是为了让B时钟域内的时钟肯定能够采样到一次AOK为逻辑1的情况。那么为什么dataA要从AOK为1的前一个周期保持不变到AOK为0后的下一个周期呢?这就是要保证当B时钟域检测到AOK为1的时刻,dataA的输出是稳定的,不存在不定态。
而握手发生的瞬间就是时钟域B读取到AOK为1的时刻。整个通信过程可以形象的理解为:A将“车牌”放入“垃圾桶X”,然后告诉B,B一听到这个消息就去“垃圾桶X”中找,肯定找到了“车牌”,过了一会A宣布“车牌”在“垃圾桶X”中这个命题失效。如果后续A与B还需要进行信息传递,则继续沿用类似的方法即可,这便是处理跨时钟域问题的握手法!如果完成了【时序分析篇】的学习,会发现这其实也多少利用了多周期路径的思路。

异步FIFO法

握手法适用于间歇的、不连续的、频率较低的数据传递,可是如果有大量连续数据需要在不同时钟域间传递,握手法就显得力不从心了。此时就需要使用一种更高效、更可靠的方法,而异步FIFO法就是这类方法中最为典型的。不过相对于握手法来说,它也使用了更多的资源。

蓄水池问题

我们可以通过蓄水池问题来理解异步FIFO法的思路:蓄水池有一个入水口,还有一个出水口。如果单独打开入水口,可以在X小时内将水池蓄满,而单独打开出水口,则可以在Y小时内将一池水放完。对于蓄水池来说,X和Y可以为任意值,因此灌水操作与排水操作是不相关的,也就是异步的。那么,对于蓄水池来说,无论是连续灌水连续排水、间歇灌水连续排水、连续灌水间歇排水、间歇灌水间歇排水等等,只要能够保证蓄水池中的水不溢出,那么所有由入水口流入水池的水,肯定能够一滴不漏的经由出水口流出,这便是异步FIFO传递数据能够做到准确无误,一个不漏的原因。
那么,要怎么才能保证蓄水池不溢出呢?条件有两个:

  • 第一、要保证入水口的平均流入水量WIA等于出水口的平均流出水量WOA。这一进一出必须要相等,即WIA=WOA,水池才能达到一个平衡,从而才能不断地完成从灌水区域到排水区域的一个长期稳定的异步交互。如果平衡打破,那么必然会出现问题!若WIA>WOA,那么由于流入的水多,流出的水少,那么水池的水量必然会不断上升,从而水池很快就会蓄满,然后就会发生溢出,从而造成数据丢失。需要注意的是,不用担心出现WIA<WOA的情况,因为这绝不可能,如果流出的水多,流入的水少,那么水池必然很快枯竭,而当水池中没水的时候,再大的出水口也是摆设,而最终将会使得WIA=WOA。所以,我们一般不刻意去设计出水口的平均流出水量,只要保证出水口的最大排水能力大于等于WIA,那么蓄水池自身会帮助我们来达到WIA=WOA这样一个平衡。
  • 第二、要保证在任意一个时间段T内,总流入的水量WTIS减去总流出的水量WTOS小于水池的蓄水总量。如果说第一个条件是从长期上来保证水池不出现溢出的情况,那么第二条就是从短期上来保证水池不出现溢出的情况。而要保证水池在短期内不出现溢出,就必须要合理设计水池的蓄水总量。在水池的蓄水量为无限的情况下,水池的水位肯定会随着时间不断的变化,若水位线从来没有上升超过H,那么可以设计一个总蓄水量为H的水池来做为灌水操作和排水操作的异步缓冲,并且可以确保水池不会出现短期溢出的情况。
    以上就是蓄水池问题的原理思路,而异步FIFO的道理其实和它是一摸一样的,接下来,我们就来具体介绍一下异步FIFO。

异步FIFO的接口说明

在本小节,我们主要围绕异步FIFO的使用来进行讨论,而异步FIFO的构成原理等其它细节,将在【本篇->编程思路->数据的存储->异步FIFO的HDL描述与用法】章节进行介绍。与蓄水池的入水口和出水口类似,操作FIFO也只需要控制好FIFO的数据写入端和数据读出端即可,那么接下来就针对这两方面进行展开来介绍。

写部分接口

写部分接口中,最重要的要数写时钟(w_clk)、写使能(w_en)、写数据(w_data)。 如果想要完成一次“灌水”操作,就必须在w_clk的有效边沿到来的时刻,保证w_en有效、w_data上稳定的保持有想要写入FIFO的数据。
当然了,要想成功的完成一次“灌水”操作,有时仅仅依靠w_clk、w_en和w_data三者可能还不够,因为FIFO可能有被“灌满”的哪一刻,所以FIFO输出的满(full)信号也是非常重要的。我们可以通过先读取full信号的状态,来判断是否可以开始新的一次“灌水”。有时候我们可能不满足于只“灌水”一次,而想要能够像“刷屏”(BBS术语)一样一次连续写入多个数据,那么full信号也就不能满足我们的需求了,于是FIFO还提供了接近满(almost_full)和若干可编程满(prog_full)信号。例如,我们希望当FIFO内部的“水位”到达四分之三的时候,就给出一个警戒信号,此时就可以利用almost_full或prog_full来实现。
更一般的,我们可以直接阅读FIFO内部的一个位于写时钟域内的计数器——w_data_count,来获得当前FIFO内的近似“水位”,从而更加灵活的来设计我们想要的数据写入方式。
除了上面介绍的这些端口之外,一些FIFO的IP核还会给出更多可供我们使用的写FIFO信号,不过所谓万变不离其宗,往FIFO里写数据,离了谁都可以,唯独不能缺少w_clk、w_en和w_data这三个接口。
不知道大家有没有发现,在FIFO的写部分接口中,并没有提及FIFO的空状态相关信号,这是主要是考虑到:写FIFO的时候,担心的是FIFO是否已经满了,而不在意FIFO是否为空,因为FIFO只能被“写满”和“读空”,而不可能被“写空”或“读满”。因此在对FIFO进行写操作的时候,无需关心FIFO的空状态如何。

读部分接口

读部分接口中,最重要的要数读时钟(r_clk)、读使能(r_en)、读数据(r_data)。如果想要完成一次“排水”操作,就必须在r_clk的有效边沿到来的时刻,保证r_en有效,这样r_data上才会稳定的保持有想要读出FIFO的数据。
当然了,要想成功的完成一次“排水”操作,有时仅仅依靠r_clk、r_en和r_data三者可能还不够,因为FIFO可能有被“排空”的哪一刻,所以FIFO输出的空(empty)信号也是非常重要的。我们可以通过先读取empty信号的状态,来判断是否可以开始新的一次“排水”。有时候我们可能不满足于只“排水”一次,而想要能够一次性连续读出多个数据,那么empty信号也就不能满足我们的需求了,于是FIFO还提供了接近空(almost_empty)和若干可编程空(prog_empty)信号。例如,我们希望当FIFO内部的“水位”减少到四分之一的时候,就给出一个警戒信号,此时就可以利用almost_empty或prog_empty来实现。
更一般的,我们可以直接阅读FIFO内部的一个位于读时钟域内的计数器——r_data_count,来获得当前FIFO内的近似“水位”,从而更加灵活的来设计我们想要的数据读出方式。
除了上面介绍的这些端口之外,一些FIFO的IP核还会给出更多可供我们使用的读FIFO信号,不过所谓万变不离其宗,往FIFO里写数据,离了谁都可以,唯独不能缺少r_clk、r_en和r_data这三个接口。
不知道大家有没有发现,在FIFO的读部分接口中,并没有提及FIFO的满状态相关信号,这是主要是考虑到:读FIFO的时候,担心的是FIFO是否已经空了,而不在意FIFO是否为满,因为FIFO只能被“写满”和“读空”,而不可能被“写空”或“读满”。因此在对FIFO进行读操作的时候,无需关心FIFO的满状态如何。

不准确性问题

虽然FIFO的诸多接口可以划分为写和读两大部分,但是其中有些接口并没有那么纯粹,例如写部分的full相关接口和w_data_count,以及读部分的empty相关接口和r_data_count。为什么说它们不够纯粹,是因为得到它们的准确情况,需要同时从读时钟域和写时钟域获取信息,而这样,就又涉及到了一个跨时钟域的问题。
以“水位”为例,由于“水位”受两个因素影响——写入总量和读出总量,因此,“水位”的实际变化频率就肯定不是单纯的和w_clk或r_clk的频率保持一致。因此按照w_clk还是r_clk的频率得到的“水位”信息,充其量只是实际水位当前、甚至之前某个时刻的一个采样罢了。之所以可能是之前某个时刻的一个采样,是由于要确保跨时钟域传递数据的正确性,需要消耗一定的时间,这也是为什么之前在介绍w_data_count和r_data_count的时候用到了“近似”这个词。
那么现在我们发现,为了解决跨时钟域的问题,我们使用了异步FIFO,可是由于异步FIFO的使用,我们又引入了新的跨时钟域问题,那么必须采用别的方法来稳妥的解决新引入的跨时钟域问题,否则迭迭代代无穷尽也!此时,我们之前介绍的握手法或者后续将要介绍的格雷码法就产生了重要的作用,稍后我们将对格雷码法做详细介绍。
无论是握手法还是格雷码法,在处理跨时钟域问题的时候,都无法保证数据的传递没有延迟,因为不同时钟域之间的寄存器的建立和保持时间无法得到保证。那么,你肯定会担心这样是否会给异步FIFO的操作带来影响,其实不必担心,异步FIFO采用的是“宁可错杀不可放过”的策略,因此可以完全避免错误的发生,分析如下:
初始化的时候,FIFO中肯定是空的,因此empty信号保持有效,当写部分端口通过正确的时序配合写入一个数据到FIFO中后,empty信号并不一定会马上变为无效。为什么?首先,empty信号是位于读时钟域的,因此它不会随着写时钟域的时钟变化而变化。其次,empty信号无效,表示FIFO中有数据可读,但是若empty信号是马上无效的,此时FIFO内部写入的数据储存尚未稳定,就可能被读端口的时钟域访问,那么建立时间必然不一定能够满足要求,因此读出的数据也会有问题。所以,empty一般要延迟若干个读时钟周期后才会无效,这也算是握手法的一种应用。同理,当FIFO中写满了数据后,full信号保持有效,此时若读部分通过正确的时序配合从FIFO中读出一个数据后,full信号也不一定是马上变为无效的,因为若full信号马上无效,表示FIFO可以写入新的数据,此时如果发生写操作,那么由于FIFO内部的数据尚未读干净就被更新,就会造成读时钟域内寄存器的保持时间不满足,从而还是会出问题。所以,full一般要延迟若干个写时钟周期后才会无效,这同样是握手法的一种应用。综上所述,当empty信号无效时,FIFO是真的不空了(虽然之前可能会有那么一小段时间也已经不空了),当full信号无效是,FIFO是真的不满了(虽然之前可能会有那么一小段时间也已经不满了),因此,按照full和empty的指示来对异步FIFO进行操作,不会产生任何问题。
而当FIFO不空也不满的时候,如果我们需要得到“水位”值,那么就需要将输入总量和输出总量都转化为同一个时钟域内的信号,此时,由于计数器的特性,我们可以应用格雷码法来达到比握手法更加高效的传输。不过高效归高效,但是同步后的计数值可能会有误差,不过这种误差就跟full、empty信号一样,不会造成影响。以w_data_count为例,它其实并不一定能准确反映当前FIFO中到底有多少个数据,因为它总是在写时钟的边沿才会进行更新,那么这种误差到底会不会对异步FIFO的操作产生影响,接下来将分三种情况进行讨论:
情况一:w_data_count本次更新前的很长一段时间,异步FIFO都没有发生读操作。那么此次更新时,写时钟域显然可以准确的知道已经往FIFO中写了多少数据,而由于很长时间没有发生读操作,则写时钟域也可以准确的知道已经从FIFO中读了多少数据,那么做差后便可以通过w_data_count真实的反应出FIFO中的数据量。
情况二:w_data_count本次更新前,异步FIFO刚刚发生了读操作。那么此次更新时,写时钟域虽然可以准确的知道已经往FIFO中写了多少数据,但却不一定可以准确的知道已经从FIFO中读了多少数据。由于采用了格雷码的保护措施,所以从读时钟域往写时钟域传递信息是需要消耗时间的(但不会采样到杂乱的数值),因此,写时钟域此时可能并不能感受到最近一次或几次的读操作,故做差后得出的w_data_count可能是要比FIFO中真实的数据量多一些的。
情况三:w_data_count本次更新后、下次更新前,异步FIFO发生了读操作。那么由于在w_data_count两次更新期间,FIFO中又少了一些数,所以其反应的值显然是要比FIFO中真实的数据量多一些的。
由此可见,写时钟域的“水位”指示w_data_count总是大于等于真实的“水位线”的,但由于写FIFO操作只怕写满,并不怕出现短暂的没写满状态,所以这种误差不会对FIFO的操作产生问题。同理可分析r_data_count,它总是小于等于真实的“水位线”的,由于读FIFO只怕读空,并不怕短暂的没读空状态,所以这种误差也不会FIFO的操作产生问题。
综上所述,只要为异步FIFO分配合适的空间,再按照读、写部分端口的意义对其进行操作,我们就能实现跨时钟域的高效传输。

FIFO的使用模式

无论是异步FIFO还是同步FIFO,其实从使用者的角度来看,都没有太大的区别,如果要具体细分的话,按照使用时的侧重点不同,可以归纳为以下几种模式,不过这些模式并不是完全割裂开来的,而是彼此之间都有交集的。

模式一:散模式

散模式也叫无规律模式,即写FIFO部分在full无效时,想写就写,full有效时等待;读FIFO部分在empty无效时,想读就读,empty有效时等待。
一般当传输的数据量没有规律,且读、写双方对传输的实时性都没有特殊需求的话,多采用散模式,例如对SDRAM控制器中命令缓存FIFO的操作。

模式二:帧模式

帧模式也叫包模式,是一种比较规律的模式,通常当我们所处理的数据有固定的帧格式或者数据包格式时,多采用这种模式。因为在这种情况下,写FIFO的时候,常常一次需要连续写入一帧或一包数据;而在读FIFO的时候,常常也需要一次连续读出一帧或一包数据来处理。因此,在这种情况下,empty和full信号的指示意义就不大了,而w_data_count、r_data_count、almost_full、almost_empty、prog_full、prog_empty的用处就凸显了出来。因为为了保证写操作肯定成功,必须保证FIFO中至少还能装的下一帧数据,否则等待;为了保证读操作肯定成功,必须保证FIFO中至少还有一帧的数据量,否则等待。
根据读、写两个端口的特征,帧模式又可分为散写帧读、帧写散度、帧写帧读三种。一般当传输的数据量有一定规律,并且读、写双方对传输的实时性没有很强的要求(除了帧内数据不能断开外),多采用帧模式,例如TS流中各个长度固定的数据包。

模式三:拉模式

拉模式也叫读取阻塞模式,即写入部分的数据写入能力比较强,FIFO可能很快就被写满,但是读取部分为了配合后续的逻辑工作,必须保证输出数据的“细水长流”。因此FIFO中的数据操作总是堵塞在FIFO的读取端,只有读出一些数据,才能允许新写入一些数据,感觉FIFO中读出的数据像是从FIFO的读部分费力拉出来的,因此也叫拉模式。
一般当写部分没有实时性要求,而读部分有特定的时间或实时性要求时,多采用拉模式,例如PC机向PCI插槽上的FPGA板卡写入大量数据时,FPGA端的FIFO即工作在拉模式下。

模式四:推模式

推模式也叫写入阻塞模式,即读取部分的数据读出能力比较强,FIFO可能很快就被读空,但是写入部分由于受限于前端的逻辑工作,只能按照一定规律“有条不紊”的写入数据。因此FIFO中的数据操作总是堵塞在FIFO的写入端,只有写入一些数据,才能允许新读出一些数据,感觉FIFO中读出的数据像是从FIFO的写部分费力推出来的,因此也叫推模式。
一般当读部分没有特定的时间或实时性要求,而写部分有特定的时间或实时性要求时,多采用推模式,例如PCI插槽上的FPGA板卡向PC机写入大量数据时,FPGA端的FIFO即工作在推模式下。

模式五:透明模式

透明模式也叫无阻塞模式,即想写的时候FIFO中必须要有足够的空间,想读的时候FIFO中必须要有足够的数据,因此在这种情况下,任何状态判断都是徒劳的。
一般,当FIFO的读、写双方都有着非常强的实时性或时间性要求时,必须采用这种模式,例如,若写入部分每秒钟发起一次数据写入,每次写入都是连续不可打断的10个数据,而读取部分每10秒钟发起一次数据读取,每次读取都是连续不可打断的100个数据,此时,FIFO的操作就必须为透明模式。

异步RAM法

异步RAM法的主体思路与异步FIFO法是一致的,都是利用一个大的缓冲池来完成两个时钟域之间的握手操作,这里不再赘述。所不同的是,FIFO是一个封装好的存储结构,先进先出,因此使用起来比较简单方便;而RAM是一个较为开放的存储结构,随机存取,其操作方式非常灵活,但是由于要自己控制其读、写地址以及设计好能够安全在读、写时钟域内传递信息的握手机制,因此实现起来较为麻烦。除此以外,为了保证读取数据的稳定性,请一定避免对同一地址小时间间隔的写入、读取操作。
通常来说,异步FIFO法的使用更为广泛,但是对于某些特殊情况,异步RAM往往能够达到更精妙的效果。例如一些基于网络的传输,发送端将一帧数据分成若干个网络数据包,并配合上递增的ID号然后发送到网络中。由于每个数据包的网络路径不同,以及出错重传现象,导致接收端并不是按照递增的ID顺序正确的接收到数据包。此时,如果我们在接收端使用异步RAM法,就根据正确接收数据包的ID号来决定其应该写入RAM中的位置。倘若每一帧数据所分成的包数固定,那么在接收端的写RAM时钟域就可以对正确接收的数据包进行计数,一旦计数等于包数,即可给读RAM时钟域发送一个握手信号,表示整帧数据已经接收完毕,可以放心连续读取。

格雷码法

格雷码是跨时钟域中一种高效且安全的信息传递方法,不过它对传递的数据有一个特殊的要求,那就是必须为递增或递减的计数值,因为只有对递增或递减的计数值应用格雷码公式编码时,才能保证寄存器输出的数据变化时不会存在不稳定态。关于寄存器输出不稳定态的概念,请参见【本篇->编程思路->数字电路中的隐患->寄存器输出的不稳定态】章节,同样在这一小节里面,我们也详细介绍了格雷码的概念、原理以及如何使用格雷码消除寄存器输出的不稳定态。而在本小节,我们将主要分析一下格雷码在跨时钟域设计中的应用。
应用了格雷码后,只能保证用于计数的寄存器输出中不存在不稳定态,但是并不能保证一个时钟域寄存器输出的信号能够满足另一个时钟域寄存器时钟的建立和保持时间要求。那么,当出现时序要求不满足的情况时,会产生什么影响呢?
假设时钟域A中的计数寄存器为regA,输出已经被格雷码化,当前输出相当于自然数N,如果在时钟域B内通过一个寄存器lockB来采样regA的输出,可能碰到如下几种情况:
情况一:lockB的建立时间和保持时间要求都能满足。此时lockB的输出也想当于自然数N。
情况二:lockB的建立时间要求不满足。这种情况通常发生于regA的值刚刚改变,lockB的时钟有效边沿就到来。由于regA应用了格雷码,所以对于lockB来说,其中仅有一个触发器的建立时间要求不满足。由于regA新输出的数据建立时间不够,但是regA旧的数据有着足够的建立时间,因此,此时lockB的输出有很大的几率相当于自然数N-1,有较小的几率相当于自然数N。
情况三:lockB的保持时间要求不满足。这种情况通常发生于lockB的时钟有效边沿刚刚到来之后,regA的输出就发生了变化。由于regA应用了格雷码,所以对于lockB来说,其中仅有一个触发器的保持时间要求不满足。此时由于regA旧的数据已经有着足够的建立时间,因此,此时lockB的输出有很大的几率相当于自然数N-1,有较小的几率相当于自然数N。
由于regA的输出已经格雷码化,所以对于lockB来说,不存在建立时间和保持时间要求都不满足的情况。
综上所述,在应用了格雷码法后,lockB的值非常好的跟随了regA的变化,仅仅当一些极限情况出现时,lockB的值可能会比regA的值小1(如果是递减计数器则大1)。事实上,当极限情况发生时,站在时钟域B的角度上来看,regA本身也是一个说不准的值,因此能够做到仅仅存在误差1的跟随已经非常的好了。
同时还请注意,如果A时钟域的时钟频率比B时钟域的时钟频率高,那么即使受极限情况的影响,也很容易就判断出lockB的输出肯定也是递增或递减的;但是若B时钟域的时钟频率更高一些呢?其实此时也无需担心,因为如果本次lockB输出为N,那么下一次它只可能为N或N+1,而不可能为N-1,因为从regA从N-1到N的变化在regA数据变化的一个完整周期内只可能发生一次,而这一个数据变化的完整周期显然远大于一个B时钟域的时钟周期。即使A、B时钟域的时钟频率完全一样,导致lockB每次都出现极限情况,那么如果在某一时刻,lockB在N-1和N之间抉择,那么下一时刻它肯定在N、N+1之间抉择,所以lockB的输出仍为递增(或递减,当regA递减时)。
综上所述,应用了格雷码法后,lockB不光能够很好的跟随regA,还能够保证完全跟随regA的变化趋势,因此可以在时钟域B内放心的根据lockB的值来判断regA的值,并且做出相应决策。这也是异步FIFO法中,解决如何安全可靠的根据读地址计数器、写地址计数器来得到空、满、水位等信号的原理。虽然格雷码法也会引入一、两个时钟周期的延迟,但是它也正好体现出异步FIFO中“宁可错杀不可放过”的策略,即,如果在写时钟域内,告诉你FIFO中有100个数据,那么这是一个上限,只可能少不可能多;如果在读时钟域内,告诉你FIFO中有100个数据,那么这是一个下限,只可能多不可能少。因此,格雷码法可以保证异步FIFO法的正确应用。

发布了806 篇原创文章 · 获赞 1541 · 访问量 151万+

猜你喜欢

转载自blog.csdn.net/Reborn_Lee/article/details/104351256