学习链接:https://www.cnblogs.com/IClearner/p/6579754.html
跨时钟域数据信号传输,主要方法有握手信号和fifo。
一、握手信号
>数据变化速率比采样时钟域低
所谓握手,即通信双方使用了专用控制信号进行状态指示,这个控制信号既有发送域给接受域的也有接收域给控制域的,有别于单向控制信号方式。
使用握手协议方式处理跨时钟域数据传输时,只需要对双方的握手信号(req 和 ack)分别使用脉冲检测方法进行同步,在具体实现中,假设req ,ack, data,总线在初始化时都处于无效状态,发送域先把数据放入总线,随后发送有效的req信号给接收域;接收域在检测到有效的req信号后锁存数据总线,然后会送一个有效的ack信号表示读取完成应答;发送域在检测到有效ack信号后撤销当前的req信号,接收域在检测到req撤销后也相应撤销ack信号,此时完成一次正常握手通信,此后,发送域可以继续开始下一次握手通信,如此循环,该方式能够使接收到的数据稳定可靠,有效的避免了亚稳态的出现,但是控制信号握手检测会消耗通信双方较多的时间。
数据速率相对于采样时钟域(接收数据的时钟域)来说是慢时钟,可以使用控制信号进行同步,在采样到慢时钟域的控制信号后,接收采样数据,时序图如下所示:
这里只给出了时序图,电路可以按照时序图进行设计。需要注意的是,这个额外的控制信号(wr_en_s)是由前面的逻辑产生的。这与下面的电路不一样:(下面的又没看懂)
在下面的这个电路中,控制信号是由上升沿检测电路产生的,而且是接收时钟驱动的上升沿检测电路(也就是说这个控制信号是由后级的逻辑产生的),检测的是发送时钟的上升沿。电路如下所示:
从时序图中可以看到,可以用上升沿检测电路,检测发送时钟的上升沿,然后这个沿相当于使能信号。上面中,检测到了第二个发送时钟的上升沿,之后就有了使能信号,采样的数据也是第二时钟发送的数据DB,因此对应起来是没有问题。这里由于没有检测到EN1第一个上升沿,所以没有采样到DA也是正常的,这是因为前面的波形没有画出的缘故。
二、FIFO
>数据变化速率比采样时钟快很多(只能用fifo)
>FIFO无论是快到慢,还是慢到快,都可以使用它进行数据的缓冲,可谓是“快慢皆宜”
fifo:first in first out 先进先出的数据缓存器。
他与普通存储器的区别是没有外部读写地址线,这样使用起来非常简单,但缺点就是只能顺序写入数据,顺序的读出数据, 其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。
同步FIFO是指读时钟和写时钟为同一个时钟。在时钟沿来临时同时发生读写操作。异步FIFO是指读写时钟不一致,读写时钟是互相独立的。
fifo原则:full不能写,empty不能读
wire Read_allow=(Read_enable&&!Empty);
wire Write_allow=(Write_enable&&!Full);
关键:full和empty信号如何产生
FIFO的写过程:在复位的时候,FIFO(双口RAM)里面的数据被清零(也就是不存在数据)。复位之后,只能进行写操作,因为什么都没有,读数据会读出错误的值。这个时候,当外部给FIFO写使能信号了,在时钟的驱动下,数据就会被写入FIFO里面的RAM存储单元(存储单元的地址由写指针寄存器的内容确定,写指针寄存器中的内容称为写地址,复位的时候为0),写完数据之后(或者在允许写数据之后),这个写指针寄存器就会自动加一,指向下一个存储单元。当写到一定程度的时候(写指针寄存器到达一定的数值),旧数据还没有被读出的时候,再写入新数据就会把旧数据给覆盖,这个时候称为写满,需要产生写满的状态信号(full,简称满)。在写满的时候,需要禁止继续写数据。
FIFO的读过程:在复位的时候,FIFO里面没有数据,因此这个时候是禁止读数据的。当里面有数据之后,外部读信号到来后,在时钟信号到来的时候,FIFO就会根据读地址(由读指针寄存器的内容确定,读指针寄存器里面的内容称为读地址,复位的时候为0)读出相应的数据,读出数据之后(或者说允许RAM读之后),读指针寄存器自动加一。指向下一个存储单元。当读到一定的程度的时候,也就是FIFO里面没有数据了,这个时候称为读空,需要产生读空的状态信号(empty,简称空)。在读空的时候,需要禁止继续读数据。
首先,FIFO需要存储数据,因此就需要存储器;由于需要读,也需要写,于是乎就需要一个DPRAM(double port RAM,双端口RAM)。
然后,RAM需要读/写地址,它才知道在哪里读/写数据,因此需要读/写地址产生模块,也就是需要读/写地址寄存器。什么时候进行写,什么时候进行读,因此需要读/写控制逻辑和空满状态的信号产生逻辑。
最后,空满信号的产生需要通过对读地址和写地址的比较,由于读写地址在不同的时钟域,因此需要同步电路进行同步。
通过上面的简单介绍,我们就得到了FIFO的大致框图如下(主要是告诉大家为什么会有这么一个框图):
winc:外部输入的写使能信号
rinc:外部输入的读使能信号
wdata :要写进数据,要写进FIFO里面存储的数据。
rdata:读数据,从FIFO里面读取出来的数据。
wdata:要读出的数据,要读出FIFO里面存储的数据
wfull:写满的状态信号
rempty:读空的状态信号
wclken:RAM的允许写信号,在这个信号有效的情况下,RAM才能写得进数据。
rclken:RAM的允许读信号,在这个信号有效的情况下,RAM才能读得出数据。
waddr:RAM的写地址。
raddr:RAM的读地址
wptr:要同步到写时钟域的读指针(读地址)。
rptr:要同步到读时钟域的写指针(写时钟)
wq2_rptr:读地址rptr同步到写时钟域的读地址(格雷码)
rq2_wptr:写地址rptr同步到读时钟域的读地址(格雷码)
syn_r2w:读同步到写触发器链中间信号。syn_w2r:写同步到读触发器链中间信号。
full和empty信号如何产生
①将地址深度拓宽1位当做标志位,回卷一次标志位取反。假设地址是4位,也就是深度是4位,4bit地址拓宽为5bit,那么读地址就是3(由于读地址没有回卷,所以是(0)0011)那里,当写地址回卷之后与读地址相同(由于写地址回卷了,最高位取反,所以是(1)0011),因此这就是写满了。当读地址回卷之后,变成10011,这个时候,就读空了。也就说,虽然DPRAM的深度还是4bit,但是我们在进行设计地址寄存器的时候,增多一位当做状态。然后读写地址全相等的时候,表示是读空;除了标志位外,剩余的地址为全部相等,那么就表示是写满。
②在同步FIFO中,还可以使用计数器的方法。设置一个状态计数器,复位的时候为0。写的时候,计数器加1;读的时候,计数器减1。那么很容易得出,计数器为0的时候,就是读空就有效了;当计数器等于FIFO的深度(2^n - 1)时,就说明写满了。这种方法如果FIFO深度很大的话,就需要很大的计数器了,所以有局限性。
由上述可知,空信号的产生需要把写地址同步到读时钟域,然后进行比较(比较之后产生);满的信号需要把读地址同步到写时钟域,然后进行比较(比较之后产生)。
>>>>使用格雷码的原因:在相邻地址变化中,格雷码只有一位发生变化。二进制码转格雷码:最高位不变,后面的用本位与高一位异或;格雷码转二进制码:最高位不变,生成的高位与自身异或。
>>>>使用格雷码的方法:由于RAM的读写地址都是(传统)二进制编码,有两种方法:
① 将二进制编码转换成格雷码,然后把格雷码同步过去,再把同步过来的格雷码反转换成二进制码,进行二进制地址和二进制地址的比较;
② 将二进制编码转换成格雷码,然后把格雷码同步过去,然后使用格雷码进行比较。
这里使用第一种方式,虽然这种方式比较需要多两块格雷码转二进制的电路,但是我们可以实时比较,能将寻址的二进制马上与同步过来的“延时”二进制进行比较;使用格雷码比较的话,实际值会慢一拍(因为实时方的格雷码需要寄存输出,会慢一拍,如果不寄存输出,就有可能产生毛刺)。
FIFO的深度选择
FIFO的宽度就是RAM的位宽,也是要存入/取出数据的位宽;然后深度就RAM的地址深度,也就是最多可以存多少个数据。例如FIFO的宽度是8bit,那么FIFO每个时钟存入的数据的宽度也是8bit;FIFO的深度是10bit,那么FIFO就最多可以存2^10=1024个8bit的数据。
我们要存储数据,FIFO的深度选小了,在写的时候就很有可能写溢出;深度选大了,就会浪费存储面积。选择一个合适的深度,最主要的就是防止写溢出;因此根据读写速度来选择FIFO深度,写的平均吞吐量要和读的平均吞吐量相等。
举例:设写时钟频率为100M,读时钟为200M;写速度为:100个时钟写入60个数据,读速度为:100个时钟读出30个数据 。
①验证数据吞吐量是否相等:
写的平均吞吐量=100M*60/100=60M个数据/S
读的平均吞吐量=200M*30/100=60M个数据/S
这两个是相等的,不会发生写溢出;如果写大于读,那么FIFO早晚会很快写满溢出;如果读大于写,那么FIFO迟早会读空。因此需要读写吞吐量相同。
②求最低深度
我们知道读写速度之后,就可以判断FIFO要多少深度才合适了,由于FIFO的深度考虑是出于我们要防止写溢出(写满),因此我们考虑写的情况:
写的时候是100个时钟写60个数据,我们不知道它是怎么样子写的,我们从悲观的角度出发,也就是从写得最密集的角度出发:前100个写时钟的最后60个时钟写60个数据,然后后100个写时钟的最前60个时钟写入60个数据,也就是在120个写时钟内写入了120数据。
这120个写时钟的时间是:
在这段时间内,根据读写的速率要求,肯定是要读数据的,读出的数据为:(这个不知道为什么这么算)
因此我们FIFO需要的深读就等于没有读出的数据的个数就是:
120-72 =48
然而由于上面读的方式是一个平均的方式,此外FIFO的深度一般是2的整数次幂,要符合格雷码的编码转换规则,因此我们深度一般不选择48,而是选择比它大的2的整数次幂的数,比如64或者128。FIFO深度的选择过程就如上面所述,(这里参考《FPGA深度解析》)。
FIFO的设计
这里主要是多出了两个状态信号:
wfull_almost:将满信号。为了预防万一,FIFO要满的时候,使这个信号有效,当面的模块时钟(前级电路)检测到这个信号有效后,就把winc变为无效,用来提供给前面电路的指示信号。这个信号要比full信号提前,因为考虑到在判断出满之后,还需要一些动作(延时),才能不写;于是乎我们就用将满信号来补充这些延时,而不是等到满信号才做出反应。
rempty_almost:将空信号。这个也是为了考虑在空信号判断出来之后,到禁止继续读可能有延时,从而设立这个标识,在将空的时候就禁止继续读数据。
将满信号和将空信号的关键因素就是读写地址之间的间隔
对于写时钟域,产生将满信号,这对应的间隔就是看写地址还有多少赶上读地址,求出这两个地址之间的间隔,然后再与预设的间距比较,如果这个间隔小于预设的间距,那么就产生将满信号。求间隔方法:
>>>>当读写状态位相同的时候,如下图所示:
由于最高位相同,所以写需要回卷才能追上读,那么间隔也就是A+B;假设FIFO的深度是D,写地址为waddr,读地址为raddr,那么间隔就是D-C=D-(waddr-raddr)=D+raddr-waddr.(注意,这里的读/写地址不包括状态位)
>>>>当读写状态不同位时,如下图所示:
这时候waddr再有C就追上raddr了,因此间隔就是raddr-waddr。
读区域的间隔的产生:
>>>>状态位一样的时候,也就是没有回卷的时候,如下图所示:
很显然,无论是加不加状态位,间隔都是waddr-raddr,也就是说,还有waddr-raddr的距离,raddr就追上了waddr。
>>>>当状态位不一样时:
需要回卷才能追上写,因此间隔就是:
FIFO的深度-(raddr-waddr)=FIFO+waddr-raddr(这里的读写地址不包括状态位)。
当加上状态位之后,我们发现,间隔是可以用raddr-waddr来表示的,比如raddr=1011,waddr是0011,间隔是8;加上状态位后,Raddr=01011,Waddr=10011,间隔也可以表示为01011-10011=01000=8(借位是会被省略掉的),因此用加上状态位后,间隔可以表示为waddr-raddr。
因此在读时钟域,间隔的表示就是带状态位的waddr-raddr。
说完了将满和将空信号,我们再聊聊上面的框图,整个FIFO可以分成写逻辑模块、读逻辑模块、写/读同步和读/写同步模块。其中
>>>>写逻辑的模块的功能是:根据状态信号和外部的写信号产生对RAM的写控制信号、产生RAM的地址信号、产生空和将空的状态的状态信号。因此写逻辑模块可以分成3个部分:①(RAM)写控制逻辑部分;②RAM写地址产生部分;③状态产生部分。
①RAM写控制逻辑部分的功能就是,产生RAM的写使能信号——wclken有效的条件是:外部信号写使能信号winc来了,而且此时满信号没有效,这个时候就允许往RAM里面写数据了。
②RAM写地址产生部分的功能就是产生RAM的地址和产生格雷码地址(产生的格雷码地址传输给同步模块):复位的时候,RAM的地址waddr为0;此后,在写时钟上升沿检测到RAM的写使能wclken有效之后,waddr自动加一,指向下一个单元,wclken无效waddr则不变。我们使用比RAM地址宽1位的地址寄存器进行递增,地址寄存器的最高位充当空满信号时候的状态比较位。
③状态产生部分的功能就是:将同步模块过来的格雷码转换成二进制,然后跟RAM写地址产生部分传来的地址进行比较,产生将满信号和满信号。(将满信号的产生就是两个地址小于某个间隔时有效,满信号产生则是间隔等于0或者:间隔只有一个地址之差,但是这个时候RAM的写信号还有效)。
>>>>读逻辑也是一样,这里不再详述,具体细节我们在代码后面进行讨论。
>>>>写/读同步读/写模块其实就是双D触发器(链)
代码见原博客地址
能不能先把数据变成格雷码,然后再通过双D触发器同步过去呢?
不能,数据不像FIFO的地址产生那样,是具有相邻性的,也就是只差一个1;因此不能把数据变成格雷码,再传输。