电子说
前面的文章我们讲了亚稳态的产生,是由于触发器在工作过程中存在数据的建立时间和保持时间。在上升沿触发电路中,建立时间就是在时钟上升沿到来之前,触发器数据保持稳定的最小时间;而保持时间就是在时钟上升沿到来之后,触发器数据端数据还应该保持的最小时间。如果数据在时钟上升沿前后的这个窗口内发生改变,即违反了建立保持时间,会使触发器工作在一个不稳定的状态,影响下一级触发器,发生连锁反应使整个系统工作失常。
异步FIFO是一种重要的异步时钟域的数据同步手段,在实际应用中非常常见,面试时也作为常考题型出现在视野中。但是如何设计一个高可靠性、高速的异步FIFO也是一个难点。
UART项目内部虽然只使用了同步FIFO实现数据的缓存,但咱们趁热打铁,将异步FIFO的原理和设计中需要关注的地方一并讲透,可以说干货满满,建议收藏~
异步FIFO架构
异步FIFO主要是由 双端口存储器 、 写指针产生逻辑 、读指针产生逻辑及空满标志产生逻辑4部分组成。读写操作是由两个完全不同时钟域的时钟所控制。在写时钟域部分,由写指针产生逻辑生成写端口所需要的写地址和写控制信号;在读时钟域部分,由读指针产生逻辑生成读端口所需要的读地址和读控制信号;在空满标志产生部分,通常是把写指针与读指针相互比较产生空满标志。读写时钟属于不同的时钟域,如何同步异步信号,使触发器不产生亚稳态及如何正确地设计空、满信号的控制电路,使FIFO不会溢出,造成数据丢失是异步FIFO设计的两个难点。
以写指针为例。在写请求有效时,写指针在时钟上升沿来时递增。同样在读请求有效时,读指针在读时钟沿来时递增。在产生空满信号时,需要比较读写指针,由于两个指针分属于不同的时钟域,彼此之间异步,在使用二进制计数器实现指针时, 可能会导致用于比较的指针采样错误 。
比如,二进制的值会从1111变为0000,这时所有位都会发生改变。虽然同步以后可以有效避免亚稳态,但是仍然可能得到错误的采样值。
从1111到0000转换可能的中间值:
1111 —> 0000
1111 —> 0001
1111 —> 0010
1111 —> 0011
1111 —> 0100
1111 —> 0101
1111 —> 0110
1111 —> 0111
1111 —> 1000
1111 —> 1001
1111 —> 1010
1111 —> 1011
1111 —> 1100
1111 —> 1101
1111 —> 1110
1111 —> 1111
如果同步时钟沿在1111向0000转换的中间某个位置到来,就可能将四位2进制的任何值采样同步到新的时钟域中。而FIFO空满信号产生使用错误的指针将产生误标志,从而使FIFO满时没有正确产生满标志,导致数据丢失;FIFO空时没有正确产生空标志,导致读出垃圾数据。
所以采用二进制方式实现指针不是最终的方案,建议尽量避免使用二进制计数产生指针。
** 使用格雷码方式实现指针
格雷码(gray)相对于二进制的优势在于:格雷码 从一个数变为下一个数时只有一位发生变化 。
所以格雷码在转换时最多只会出现一位错误。比如从1000变为0000时,两级同步器采样要么为1000(旧值),要么为0000(新值),而不会出现其他的值。这样就可以避免产生错误的空满标志。
在格雷码实现FIFO指针时需要注意, 对于写逻辑部分,产生写满信号需要对读指针(格雷码)进行同步;对于读逻辑部分,产生读空信号需要对写指针(格雷码)进行同步 。
有人要问了,使用两级同步器对指针同步时,不可避免的会产生两个时钟的延迟,这个延迟会不会对读写数据产生?别急,我会掰开来讲清楚的。我们先讲讲二进制和格雷码的相互转换。
上面我们已经讲了格雷码这种编码方式可以有效的避免绝大部分错误。那怎么产生格雷码编码方式的读写指针呢?别看格雷码计数器看起来复杂,实现起来其实很简单。这里有两种方案产生读写指针。
方案A:
方案A
步骤1:将格雷码转换为二进制值;
步骤2:根据条件递增二进制值;
步骤3:将二进制值转换为格雷码;
步骤4:将计数器的最终格雷码值保存到寄存器中。方案B:
方案B
步骤1:根据条件递增二进制值;
步骤2:将二进制值保存到寄存器中,同时将二进制值转换为格雷码;
步骤3:将计数器的最终格雷码值保存到寄存器中。这两种方案有什么区别?
可以发现,方案A的读写指针产生必须经过相对复杂的二进制和格雷码的相互转换逻辑和递增逻辑,这条组合逻辑极有可能 限制FIFO的最高工作频率 ,成为关键路径。
而方案B将二进制值的结果保存在一组额外的寄存器中,虽然增加了电路的面积(这点面积基本没有影响),但避免了复杂的组合逻辑,可以 提高系统工作频率 。
格雷码转换为二进制的公式为:
对于n位计数器来说,i
例如当n=4时,对应的格雷码到二进制的转换为:
bin[0] = gray[0] ^ gray[1] ^ gray[2] ^ gray[3]
bin[1] = gray[1] ^ gray[2] ^ gray[3]
bin[2] = gray[2] ^ gray[3]
bin[3] = gray[3]
可以看出,bin[3]是通过将格雷值右移3位得到;bin[2]是通过将格雷值右移2位得到;bin[1]是将格雷值右移1位得到;bin[0]是将格雷值右移0位得到。(右移后需按位异或)
下面是格雷码到二进制转换的Verilog代码。
module gray_to_bin(bin,gray);
parameter SIZE=4;
input [SIZE-1:0] gray;
output [SIZE-1:0] bin;
reg [SIZE-1:0] bin;
integer i;
always @(*) begin
for(i=0;i<=SIZE;i=i+1)
bin = ^(gray > >i);
end
endmodule
二进制到格雷码的转换公式为:
同样,对于n位计数器来说,i
例如当n=4时,对应的二进制到格雷码的转换为:
gray[0] = bin[0] ^ bin[1]
gray[1] = bin[1] ^ bin[2]
gray[2] = bin[2] ^ bin[3]
gray[3] = bin[3]
可以看出,可以通过逐位异或,或者将二进制码右移后与自身异或的操作,计算出对应的格雷码。
下面是二进制到格雷码转换的Verilog代码:
module bin_to_gray(bin,gray);
parameter SIZE=4;
input [SIZE-1:0] bin;
output [SIZE-1:0] gray;
assign gray = (bin > >1) ^ bin;
endmodule
本格雷码计数器采用方案B实现,可有效提高FIFO的最大操作频率。所以不会涉及到格雷码到二进制的转换。
根据上文中格雷码和二进制码的特点,细心的同学可以发现,格雷码和二进制码的最高位是相同的,所以只需对低三位进行格雷码的转换。以写指针为例,具体实现方式如下:
parameter SIZE=4;
reg [SIZE:0] wbin,wbnext;
reg [SIZE:0] wptr,wgnext;
always @(posedge wclk ornegedge rst_n) begin
if(!rst_n) begin
wbin <= 'h0;
wptr[SIZE-1:0] <= 'h0;
end
elsebegin
wbin <= wbnext;
wptr[SIZE-1:0] <= wgnext[SIZE-1:0];
end
end
always @(*) wptr[SIZE] = wbin[SIZE];
assign wbnext = !wfull ? wbin+winc : wbin;
assign wgnext[SIZE-1:0] = (wbnext[SIZE-1:0] > >1) ^ wbnext[SIZE-1:0];
在FIFO中,FIFO写满时不应该再向FIFO中写数据,避免造成FIFO溢出,导致数据丢失。所以在写时钟域,需要将写指针和同步后的读指针进行比较,产生写满标志。下面我将举例说明将读指针同步到写时钟域后对写满标志和写数据的影响。
在最初的t0时刻,读写指针都为0。随着后续数据写入FIFO,写指针递增。当到达t5时刻时,FIFO写满,读写指针相等,写满信号wfull由0变为1。
如果在t6时刻发生了读操作,由于两级同步器包含两个触发器,将读指针同步到写时钟域会导致读指针在两个写时钟后出现,写满信号wfull在t6和t7时刻都为1,t7时刻后变为0。这虽然增加了阻止数据写入的周期(2 wclk cycle),但是对数据的准确性无任何影响。
空满逻辑同步后效果
类似的,在FIFO空时也会阻止FIFO的读操作。
FIFO的读空信号产生是把写指针同步到读时钟域和读指针进行比较。
在t10时刻,写指针等于读指针,此时FIFO为空。若t11时刻时开始向FIFO写入数据,t11,t12时刻虽然FIFO不为空,但是由于两级同步器的延时,此时读空信号rempty仍然为1,这会阻止FIFO的读操作,但这是无害的。在t12时刻后,rempty释放,此时FIFO可以开始读操作。
在将满时通知写的一边FIFO已满,在将空时通知读的一边FIFO已空都是可以的,即使同步后的指针存在延时,阻止写/读的影响会使FIFO挂起一段时间,但不会导致任何错误。
空满标志的产生是异步FIFO设计的核心。如何正确设计此部分的逻辑,会直接影响到FIFO的性能。空满标志的产生原则是: 写满不溢出,读空不多读 。
最直接的做法是采用读写指针相比较来产生空满标志。当读写指针的差值等于一个预设值时,空满信号被置位。这种实现方式逻辑简单,但是它的减法器形成了一个比较大的组合逻辑。限制了FIFO的速度。
所以一般采用相等或是不相等的比较逻辑,避免使用减法器。采用直接比较的方法必须区分当读写地址相等的时候是空还是满,所以必须增加额外的1位控制信号来区分空满标志。
以16x16异步FIFO为例,写指针wptr[4:0],读指针rptr[4:0],其中低三位为真正的读写地址,最高位用来区分空满。
若读写指针完全相等,表示读地址追上了写地址,此时FIFO为空;若最高位相反但其余位相同,表示写地址领先读地址一个周期,此时FIFO为满。
读写指针比较产生空满标志rempty和wfull,将写指针同步到读时钟域,产生读空信号rempty控制异步FIFO的读操作;将读指针同步到写时钟域,产生写满信号wfull控制异步FIFO的写操作。
空满信号的Verilog实现如下:
wire wfull;
wire rempty;
reg [SIZE:0] wptr_reg1;
reg [SIZE:0] wptr_reg2;
reg [SIZE:0] rptr_reg1;
reg [SIZE:0] rptr_reg2;
// 写指针在读时钟域的两级同步
always @(posedge rclk ornegedge rst_n) begin
if(!rst_n) begin
wptr_reg1 <= 'h0;
wptr_reg2 <= 'h0;
end
elsebegin
wptr_reg1 <= wptr;
wptr_reg2 <= wptr_reg1;
end
end
// 读指针在写时钟域的两级同步
always @(posedge wclk ornegedge rst_n) begin
if(!rst_n) begin
rptr_reg1 <= 'h0;
rptr_reg2 <= 'h0;
end
elsebegin
rptr_reg1 <= rptr;
rptr_reg2 <= rptr_reg1;
end
end
//空满标志产生
assign wfull = ({!wptr[SIZE],wptr[SIZE-1:0]}==rptr_reg2)? 1'b1 : 1'b0;
assign rempty = (wptr_reg2==rptr)? 1'b1:1'b0;
好了,异步FIFO到这里讲完了,大家可以试试自己写一个异步FIFO,调整读写时钟的频率和比例,进行前后仿真看看性能怎样呢~
全部0条评论
快来发表一下你的评论吧 !