跨时钟域设计
参考文献
- https://www.zhihu.com/people/li-hong-jiang-54/posts(非常感谢这位博主)
- 《Clock Domain Crossing (CDC) Design & Verification Techniques Using SystemVerilog》
- 《Simulation and Synthesis Techniques for Asynchronous FIFO Design》
1. 背景介绍
1.1 同步电路和异步电路
无法判定两个时钟间相位时,则可以称这两个时钟为异步时钟(asynchronous clocks)。两个来自不同晶振的时钟,一定是异步时钟。通常情况下设计中不同的主时钟肯定是异步时钟,因此可以将这两个主时钟及其衍生时钟约束成不同的时钟组。经过一个 PLL 产生相位不同,但相位固定的两个时钟,他们依旧是同步时钟。(PLL通过比较外部信号的相位和由压控晶振(VCXO)的相位来实现同步的,在比较的过程中, 锁相环电路不断根据外部信号的相位来调整本地晶振的时钟相位,直到两个信号的相位同步。)
同步电路是由时钟驱动存储元件的电路,也就是说存储元件的状态只在时钟沿到来的时候才能发生变化。因为组合逻辑电路在输入变化时输出可能出现毛刺(glitch),而存储元件因为只会在时钟沿到来时才会更新状态,那么在时钟沿之间的时间状态是稳定的,这样的同步电路可以消除组合电路中的毛刺,如下图所示:
相应的,时钟周期的大小取决于最长的传输延时。同步电路好处是时序清晰,电路中的存储元件例如触发器都是依照一个固定的节拍来工作,便于EDA工具来进行延时的分析和计算。
而异步电路没有时钟的概念,存储元件所存的状态跟随了输入信号的变化立刻发生变化。信号之间的传递通常通过握手(handshake)来完成,因为没有时钟的约束,每一级存储元件之间的逻辑电路都是各自独立的,可以各自进行优化,这样可以达到很好的性能。但是这既是优势,也是劣势。劣势就在于EDA工具没有满足每一级都单独优化的计算能力,而且由于相邻的级之间互相影响,使得计算总的时序时变得异常复杂,所以异步电路的规模通常无法做大,进而也限制了它的用途。
1.2 建立时间、保持时间和亚稳态
Setup Time: 时钟沿到来之前输入信号D必须保持稳定的最小时间
Hold Time: 时钟沿到来之后输入信号D必须保持稳定的最小时间
Clk-to-q Time: 输入D满足setup/hold time要求,从时钟沿到来时刻到输出端Q变化至稳定的时间
那么当输入信号D无法满足setup time 或者hold time的要求,我们称之为产生了setup time / hold time violation, Flop Q的输出这个时候是0还是1是不确定的,需要一定的时间才能够稳定在0或者1。所以如果当Q端在clk-to-q时间之后才变得稳定的话,我们就说这个触发器产生了亚稳态Metastability。
1.3 跨时钟域亚稳态现象
当只有一个时钟存在时,只要保证setup/hold time就好了,那么当有多个时钟存在的时候会发生什么呢?如下图所示,aclk时钟域的信号需要传输到bclk时钟域去。在各自时钟域内,EDA工具可以保证触发器不会产生metastable,但是当aclk和bclk异步的时候,我们是无法保证aclk和bclk之间的关系的,也就是说adata相对于bclk的沿来说,可能在任何时候发生变化,这样bdata这个flop就可能产生亚稳态。导致后级电路无法sample到一个确定的正确的值,进而导致运算逻辑错误。
亚稳态的出现并导致逻辑错误并且芯片失效是一个概率事件,而不是一个100%会发生的确定性事件。这一点可能有点难以理解,举例来说明,很有可能bdata这个flop的后面组合逻辑的delay很小,而这个flop在发生亚稳态之后所需要稳定的时间也很短,这样即使flop发生了亚稳态,而后级的flop的setup time/hold time也可能可以得到满足,这样的话在实际芯片工作中,我们可能观察不到产生错误输出的情况。但也正是这样的原因,很多看似正常工作的芯片内部可能其实有跨时钟域设计上的问题,却从来没有暴露出来。这种情况其实非常危险,因为这种问题一旦出现,则会非常难以debug,因为出现的概率很低,看起来很随机。或者很可能同样的设计换一个工艺,以前可以工作在新的工艺上突然产生问题,造成很严重的后果。所以芯片在设计的时候需要尽量在流片前发现并解决所有的CDC问题。
那么我们能够完全消除亚稳态吗?答案是否定的。其实我们关心的并不是亚稳态,而是说能否避免由于亚稳态而造成的逻辑问题。在这里要引入一个MTBF的概念。MTBF-- mean time between failure. 意思是两次失效之间的平均时间,其与频率成反比。简单来说,就是这个芯片或者这个IP或者这个电路发生两次发生错误之间的间隔。对于不同的系统和应用场景,MTBF的要求也不同。比如说对于我们的手机,没有人拿一个手机用二三十年吧?那么如果能够保证MTBF大于30年,那么也等效于在整个手机的使用寿命中,这个逻辑错误不会发生2次,那么针对这个错误来说,这样的MTBF是可以接受的。但是对于有些应用场景,比如通讯卫星,一个通讯卫星的寿命可能超过二三十年,那么这种情况下MTBF 如果只有30年,那么就无法接受了。一个产品是由许多小的单元组成的,整个产品不发生失效的概率是所有部分不发生失效概率的乘积。所以越是小的单元,越要保证MTBF越高,这样才能不会导致整个产品的MTBF 有显著下降。
2. Single-bit level 信号的跨时钟域
2.1 Double-flop Synchronizer
之前已经介绍了跨时钟域传输单bit信号的亚稳态问题,解决的办法就是之后再加一个flop,也就是用两级的flop来同步source domain的signal。俗称“打两拍”。
原理:第一级flop产生Metastable的原因是flop里面没有及时锁住该锁的值,所以我们无法直接使用第一级flop的Q来直接用于bclk时钟域。但是要注意,之前说过,第一级flop的Q会最终稳定下来的,而且在绝大多数时候,可以在一个bclk周期内稳定下来,这样第二级flop的D输入就是一个稳定的值,进而第二级flop的Q是满足clk-to-q的,没有亚稳态的产生。如上图所示,尽管bdata0产生了metastable,但是bdata是stable的。
问题一:之前不是说第一级的flop的输出可能需要很长时间才能稳定吗?凭什么说在第二个时钟沿到来的时候bdata0就能稳定呢?如果还是不稳定,让第二级flop产生了setup/hold time violation,那第二级flop不还是有可能产生metastable么?
的确,double flop synchronizer不能完全消除亚稳态!但是很多有经验的工程师会告诉你,用个double flop synchronizer就够了,那是因为double flop会使得metastable产生的概率显著降低,这就回到了MTBF的概念。在使用double flop的时候,由于给了第一级flop一个周期的时间去稳定,使得两级发生metastable的概率大大降低。MTBF有一个计算公式,对于一个100MHz时钟的例子,MTBF是957亿年。随着sample clock frequency的提升,以及工艺节点越来越小,有些时候打两拍已经不太够了,那么就简单粗暴来一个打三拍,就能够保证了。需要注意的是,即使通过“打两拍”的方法消除了亚稳态,也并不意味着第二级Flop所采到的值就是正确的,而只能保证其是稳定的。(亚稳态输出在稳定下来之前可能是毛刺、振荡、固定的某一电压值,因此亚稳态除了导致逻辑误判之外,输出0~1之间的中间电压值还会使下一级产生亚稳态,即导致亚稳态的传播。 逻辑误判有可能通过电路的特殊设计减轻危害(如异步FIFO中Gray码计数器的作用),而亚稳态的传播则扩大了故障面,难以处理。)
问题二:现已知打两拍基本可以解决亚稳态传输问题,但如果bclk变化很慢以至于完全检测不到aclk时钟域的信号adata该如何做?
所以double-flop同步有一个条件,要求source data必须保证稳定不变至少碰见destination clock 3个连续的沿(超过1.5倍周期甚至两倍)。
这样才能保证bclk时钟域一定可以检测到aclk时钟域的信号。需要注意的是利用double flop,bdata发生变化可能是在adata翻转之后1个周期,也可能是2个周期,这是由于第一级flop的metastable可能会resolve在不同的值。如果第一级flop 稳定在和adata相同的值,那么就只需要1个周期就能看到bdata翻转(下图①)。而如果第一级flop 稳定在和adata相反的值,那么则需要再多一个周期(下图②)。总结就是3edge条件可以保证不同时钟域的信号可以被检测到(保证数据的正确性),而打两拍可以保证亚稳态(即下图uncertain部分)不会在电路中传播。
问题三:组合逻辑的单bit信号跨时钟域时输出可不可以用double flop呢?
不可以。组合逻辑的输出可能会有毛刺,这些毛刺会增大第一级flop产生metastable的概率,进而影响整个synchronizer的MTBF,更严重的问题是由于第一级flop可能稳定在和输入adata不同的值,会导致bdata出现一个不该出现的值。组合逻辑的输出,在跨时钟域之前一定要先寄存(flop),只有flop的输出才能经过synchronizer. 下面的图就是不flop的情形。
2.2 Pulse Synchronizer
当aclk频率比bclk频率高的时候,a时钟域的信号被b时钟域检测到,该信号需要至少稳定b时钟周期的三倍,但有时候adata就只是a时钟域的一个pulse信号。比如:有一个counter在不停地计数,每个周期加一或者减一,当counter的值等于一个特定的值的时候,我们就输出一个周期的pulse,用这个pulse来作为使能信号(enable)来做其他的事情。反过来,也有可能用这个信号去作为某个counter增加1或者减去1的条件。再比如说,需要对memory进行读操作或者写操作,CE和WE同时为1的周期表示要对memory进行写操作,CE为1但WE为0的表示要对memory进行读操作。再比如对于一个FIFO,push为高一个周期就表示给FIFO加入一个数,pop为高一个周期就是表示给FIFO减去一个数。以上这些例子,都说明了当这些信号为高时,就要有相应的操作发生,为高一个周期,就操作一次,再次为高时,就需要再操作一次,这是和另外一些状态信号(status signal)的差别。对于那些状态信号,它们为高或低只表示一种状态,而与它们为高为低经过了多少个时钟周期没有关系。在2.1节说到的用2flop来同步的单bit信号,几乎都是针对的那些状态信号。而对于active时需要进行相应操作的信号来说,很显然由于2flop synchronizer的限制,adata同步到bclk时钟域就无法保证持续相应的周期数,自然不能用2flop synchronizer了。
解决方案一(脉宽展宽):
- 将aclk时钟域pulse信号“记录下来”,转成一个level信号(比如:在pulse来临后,拉高一个信号,等下一个pulse来之后,再拉低)
- 用2-flop synchronizer同步这个level信号
- 在bclk时钟域将同步过来的level信号转化为pulse
要求转化成为的level的信号Tq要足够长。如果Tq不满足bclk的3edge要求,那么这个level信号我们就无法同步过去。而Tq每次变化是由于aclk来了一个新的pulse,这也就是要求aclk的连续两个pulse之间的间隔要足够大,要满足bclk的3edge要求。
解决方案二(脉冲同步器):
- 对信号取反
- 然后两排同步并进行边沿检测
但如果不知道下一个pulse是什么时间来怎么办呢?(握手)
解决方案三(握手机制):
发送一个使能控制限号,将它同步到新的时钟域,然后通过另一个同步器将同步信号作为确认信号作为确认信号传回发送时钟域。
这种技术比较安全,但在允许控制信号改变之前,两个方向上同步控制信号有相当大的延迟。也就是说,在应答信号到来之前,是不允许源信号改变的。有没有更快的办法呢?
解决方案四(异步FIFO):
利用异步FIFO,无论是从快到慢,还是从慢到快,都可解决跨时钟域问题。FIFO宽度设为1,当FIFO不是无限长时,尽管两边的时钟速率不同,能够保持一一对应的条件是两边push和pop的平均速率是一样的。如果FIFO告诉aclk说FIFO满了,那么aclk域就得停止产生pulse,如果不满,就可以继续产生pulse,最后我们把FIFO里面的元素完全弹出,就可以做到一一对应了。
3 Multi-bit level 信号的跨时钟域
多bit信号(比如一个数),在绝大多数情况下,不能直接利用2flop synchronizer来同步。因为2flop synchronizer的delay有随机性,可能是一个周期之后同步,也可能需要两个周期。
解决方案一:DMUX
DMUX就是带使能端的触发器,可以在aclk时钟域产生一个load_aclk信号,load_aclk为1’b1时代表多bit data信号data_aclk稳定。load_aclk信号本身利用double flop同步到bclk时钟域得到load_bclk。bclk时钟域可以直接利用flop来load bus信号。
由于在load_bclk为高期间,data_aclk相对于bclk时钟域已经稳定,所以可以直接采用1flop进行采样。但这种开环的方法不知道什么时候可以更新下一个数据,而且对数据保持时间有一定要求。所以更一般的方法是,在此基础上引入握手机制,形成闭环。
解决方案二:数据保持触发器 + Handshake
此时,load_aclk与data都可以是一个pulse,经过第一级触发器后保持稳定,load_aclk利用Pulse Sync在bclk时钟域产生一个pulse,在此pulse下采样data_aclk,还可以将bclk下的pulse同步回aclk,告诉aclk数据load完成,可以更新下一个数据。
以上解决方案只适用于非高速传输的场合,即在source 时钟域的多bit信号可以保持稳定一段时间,而不是时刻都在变化,可以有一个明确的load窗口。
解决方案三:
利用异步FIFO。
4. 异步FIFO
背景介绍:
同步FIFO & 异步FIFO
同步FIFO:读写在同一个始终域,需要两个指针:write pointer和read pointer。对于write pointer和read pointer我们一般用2进制,写入操作(Push)使得write pointer + 1,读出操作(Pop)使得read pointer + 1。就像是两个人在一个环形跑道上赛跑。当write pointer领先了read pointer一圈之后,也就是说FIFO里面所有的存储单元都存了数据,FIFO没有空余的存储单元了,我们就说FIFO满了。反过来,当read pointer追上了write pointer,所有的存储单元都空闲了,我们就说FIFO空了。
异步FIFO:读写分别在不同的时钟域,真正的满和空状态是很难判定,我们需要在读的这一侧判断FIFO是否空,在写的这一侧来判断FIFO是否满。当然还是要有read pointer和write pointer,在pop这一侧更新read pointer,在push这一侧更新write pointer。判断空满需要比较read pointer和write pointer,而当我们要把pointer同步到另外的时钟域去比较时,就会遇到multi-bit 同步的问题,即binary counter不能直接利用double flop来同步。如果采用方案一那种握手机制会降低效率,比如push这一侧要等到反馈信号回来之后才能继续下一个push,哪怕FIFO里面还有很多空闲的单元。pop这一侧也一样。这样对于FIFO的整体性能影响太大。解决方法就是格雷码。
格雷码
·每相邻的两个编码之间有且只有一位不同
·当第N位从0变到1的时候,之后的数的N-1位会关于前半段轴对称,而比N位高的位是相同的。
比如:看前4个数,在4’b0001和4’b0011之间画一条对称轴,第2、3位是相同的,第0位则是轴对称的,从0-1到1-0。
Gray Code与bin code关系:
设计思路:
异步FIFO主要问题就是如何把pointer高效地同步到另一个时钟域,解决方案就是利用Gray Code。Gray code有一个特点:相邻两个编码之间有且只有1位不同。我们说multi-bit如果在一个时钟沿有多个bit同时翻转,在另外一个时钟域采到的时候由于2flop 稳定需要1个或2个周期,所以可能会出现错误的值。Gray code这种编码,从根本上就没有这个问题,因为以Gray code编码作为计数器,每个时钟沿来的时候只会有1个bit发生了翻转,其余所有bit都是稳定的!这样即使这一个bit在用2flop synchronizer同步到另外一个时钟域时,可能需要1个周期发生变化,或者2个周期,但另一个域的值在变化之前就是之前的稳定值,变化后就是新的值,而不会出现其他不该出现的值。
判断空满的时候可以将格雷码转成二进制码,也可以直接用格雷码判断:
和同步FIFO一样,我们对于2^N个entry的FIFO, 需要N+1个bit来表示address和gray code。假设FIFO有8个entry,我们用4位来表示。FIFO空比较好判断,write pointer == read pointer,用binary或者gray code都行,要求每位都相同。FIFO满比较复杂,举例来说,假设FIFO一开始一直写,不读,写满8个entry后write pointer 的binary变成4’b1000, gray code是4‘b1100, 而read pointer的gray code是4’b0000,可以看到高两位是相反的,之后的低位是相同的。再举个例子,假设write pointer 的gray code到了4'b1011, 而这个时候read pointer如果是4'b0111,那么也是8个entry满了。所以归纳出,利用gray code判断满的条件为:
Asynchronous FIFO 设计细节:
Framework
一共五个部分:dpram、write domain ctrl、read domain ctrl、sync_r2w、sync_w2r
wptr和rptr都是gray code。而用来读写实际的memory address必须是binary的,在FIFO write control和FIFO read control 里面进行binary to gray code的转换。
Almost full/empty
将满和将空可以将格雷码转换为二进制码比较即可,只需要记住空的时候是rptr赶上wptr,即两个Pointer低位全等,最高位不等;满的时候wptr比rptr多一个Mem_size,即两个Pointer低位全等,最高位也相等;
Fake full/empty & True full/empty
假设wclk速度比rclk快,那么当raddr+1,再同步到wclk后,如果这期间有了push操作,那会不会使得wptr超过了rptr,造成FIFO overflow呢?
其实当我们判断满信号的时候,我们用的是WR_Pointer和同步过来的RD_Pointer_syn做的比较。RD_Pointer_syn要比真正的RD_Pointer要滞后,导致判满的逻辑并不完全准确。但是rptr在传过去之前,如果wptr已经追上了rptr-1,那么wfull已经是1了,从而阻止对FIFO继续写入。(比如一个深度8的FIFO,还没有读数据,在写地址到4’b0111时,wptr已经到4’b1000,拉高full信号,下一个push则不会发生,若此时有pop,此时pop的wptr若还没同步到wclk时钟域,在写时钟域看还是满的,但实际上已经有一个free entry,这就是假满状态,相应的,FIFO的empty为1时,也可能FIFO此时有个push操作,导致FIFO为假空),假空和假满并不会影响FIFO的正确性。只是在性能上略微有一些损失。
怎么得到真满空?
之前是在写时钟域判断full信号,在读时钟域判断empty信号,得到了假满空。如果我们在写时钟域判断empty信号,在读时钟域判断full信号,得到的就是真满空!假如说,在写时钟域,通过滞后的read_pointer_syn都得到了空信号,那说明实际的read pointer必然真的赶上了write pointer,所以FIFO此刻绝对空了。在读时钟域,通过滞后的write_pointer_syn都得到了满信号,那说明实际的write pointer必然真的超过了read pointer一圈,所以FIFO此刻绝对满了。
Async FIFO (Depth = 1)
只有1个entry,则只需要1位的address。因为只有一个entry,所以也不需要pointer,当一次push,FIFO就满了,一次pop,FIFO就空了。用1个bit用来表示满和空即可。
Async FIFO (Depth ≠ 2^N)
如果FIFO深度不是2的幂次,比如7,那我们怎么样来利用Gray code呢?直接从4'b0000到4'b0101肯定是不行的,因为4'b0101变到4'b0000有两个bit发生了变化,这样我们就没法利用2flop synchronizer来同步了。
可以利用gray code的第二个性质:gray code每一位是有个对称轴的。我们可以这样编码,addr==0的时候gray code不从4'b0000开始,而是从4‘b0001开始,直到4’b1001来wrap around,这样从4'b1001->4'b0001依然只有一个bit翻转。同理,如果是depth=6,那么我们继续往里收缩1位,只利用gray code关于对称轴两侧的部分编码,从4'b0011到4'b1011,可以看到,这样的编码依然可以保证相邻两个码之间只会有1位变化。
相应的,FIFO的满判断逻辑就不是高两位取反,低位相同了,可以将Gray转为bin比较。或者若无特定需求,也可将FIFO深度设置为2次幂,浪费一些存储空间,来简化控制电路的复杂度。
读写时钟快慢对信号同步的影响:
- 由于同步有时间,所以会造成假满空状态,虽然性能有些损失,但是不会出错
- 读写频率相差很大,对空满信号的判断是否有影响?
假设wclk是500M,rclk是100M,full信号是在写时钟域判断,比较同步过来的读信号,属于快采慢,没有问题;但是判断空的时候,是慢采快,采到的可能是离散值,但是采样到的写指针必定小于写时钟域的写指针,所以判断出的空只是假空,在实际应用中没有问题。