跨时钟域的信号分为两类,一类是单比特的信号,一类是多比特的信号。这两类信号无论是快时钟域到慢时钟域还是慢时钟域到快时钟域,无论是流数据还是控制信号,都可以使用异步FIFO进行同步。因此下文分类的不同情景,每一种情景都可以使用异步FIFO进行同步,后文就不作介绍。但需要注意的是,快时钟域到慢时钟域的同步,在使用异步FIFO时,快时钟域平均流量是不能大于慢时钟域的处理速度的,否则数据会丢失,这其实与是否使用FIFO来进行同步无关。因为FIFO的作用本就是在某一段持续时间内,发送方发送的数据大于接收方的处理能力时,暂时作为缓存用的。若发送方的平均流量大于接受方的处理能力,那么除非FIFO无限大,否则随着时间的增加,就会丢失数据。
大部分文章介绍单比特信号的跨时钟域处理时,都是默认该单比特信号为控制信号或者说变化相对较慢的单比特脉冲信号。但实际上单比特信号也存在是流数据的可能,可能实际工程上单比特的流数据信号较少,但是起码理论上存在这种可能,如果不加以区分讨论,往往容易混淆。
慢时钟域的单比特脉冲信号同步至快时钟域,可以采用多级寄存器的方法,也就是将单比特脉冲信号在快时钟域打多拍。一般情况下只需要采用两级寄存器即可,因为更多级的寄存器对性能提升并不明显。前提条件要求从慢时钟域到快时钟域,原因是只有慢时钟域到快时钟域,才能保证慢时钟域的脉冲信号能被快时钟域采样到。
需要注意的是,多级寄存器的主要作用是避免亚稳态的传播(不能完全消除亚稳态,但可以使亚稳态出现的概率大大降低),并不能保证数据稳定后,是正确的值,而是随机的0或1。但是由于慢时钟域的脉冲信号持续时间大于快时钟域的一个周期,因此在快时钟域的下一个上升沿到来时,慢时钟域的脉冲信号仍然持续,此时快时钟域可以采到正确的值。也就是说,出现亚稳态时,快时钟域实际上需要已经对慢时钟域的信号进行第二次采样了。显然地,第二次采样时,需要满足建立时间与保持时间,否则可能会再次出现亚稳态。因此可以看出快时钟域与慢时钟域的关系并不是任意的,两者并不能接近到无法满足第二次采样的建立时间和保持时间。快时钟域域慢时钟域需要满足下面的条件:Tslow>Tfast+Thold+Tsetup,其中Tslow为慢时钟域的时钟周期,Tfast为快时钟域的时钟周期,Thold与Tsetup分别为快时钟域寄存器的保持时间和建立时间。通过上面的讨论我们可以发现,当使用多级寄存器时,如果出现了亚稳态,快时钟域 能够 采到慢时钟域信号所需的时间 比没有出现亚稳态时 可能会多一个周期。如果有彼此关联的两个多比特信号,比如说地址信号,它们从慢时钟域同步至快时钟域时,可能到达快时钟域的时间时不一样的,那么得到的地址就是错误的,这就是多比特信号即使是从慢时钟域到快时钟域,也不能够使用多级寄存器同步的原因。但如果多比特信号彼此无关,从慢时钟域到快时钟域时,是可以使用多级寄存器同步的。另外若多比特数据每次只有一个比特发生变化,也可以采用多级寄存器同步的方法,例如格雷码。
代码如下:
module syn(
input rsta_n,
input clka,
input dataa,
input rstb_n,
input clkb,
output datab
);
reg syn1;
reg syn2;
always@(posedge clkb or negedge rstb_n) begin
if(!rstb_n) begin
syn1<=1'b0;
syn2<=1'b0;
end
else begin
syn1<=level;
syn2<=syn1;
end
end
assign datab=syn2;
endmodule
2、快时钟域到慢时钟域
(1)使用握手信号
快时钟到慢时钟域的单比特信号同步可以使用握手信号。握手信号的使用相对来说耗时较长,如果快时钟域的信号变化较快,是无法使用握手信号来进行同步的,否则慢时钟域可能会漏采快时钟域的信号。
verilog代码如下:
module syn(
input clka,
input rsta_n,
input bit_in,
input clkb,
input rstb_n,
output bit_out
);
reg req;
reg req_f;
reg req_ff;
reg req_fff;
wire ack;
reg ack_f;
reg ack_ff;
//使用req信号对a时钟域数据进行保持
always@(posedge clka or negedge rsta_n) begin
if(!rsta_n)
req<=1'b0;
else if(ack_ff)//ack信号为高时,不接收新的数据
req<= 1'b0;
else if(bit_in)
req<=1'b1;
else
req<=req;
end
//将a时钟域的req信号同步至b时钟域
always@(posedge clkb or negedge rstb_n) begin
if(!rstb_n) begin
req_f <=1'b0;
req_ff<=1'b0;
end
else begin
req_f <=req;
req_ff<=req_f;
end
end
//在b时钟域产生单个数据脉冲
always@(posedge clkb or negedge rstb_n) begin
if(!rstb_n)
req_fff<=1'b0;
else
req_fff<=req_ff;
end
assign bit_out=~req_fff&req_ff;
//将b时钟域的ack信号同步至a时钟域
assign ack=req_ff;
always@(posedge clka or negedge rsta_n) begin
if(!rsta_n) begin
ack_f <= 1'b0;
ack_ff <= 1'b0;
end
else begin
ack_f <= ack;
ack_ff <= ack_f;
end
end
endmodule
testbench:
`timescale 1ns / 1ps
module tb(
);
reg clka,clkb;
reg bit_in;
reg rsta_n,rstb_n;
wire bit_out;
syn test(
.clka(clka),
.rsta_n(rsta_n),
.bit_in(bit_in),
.clkb(clkb),
.rstb_n(rstb_n),
.bit_out(bit_out)
);
initial begin
clka=1'b0;
clkb=1'b1;
rsta_n=1'b0;
rstb_n=1'b0;
bit_in=1'b0;
#28
rsta_n=1'b1;
rstb_n=1'b1;
end
always #2 clka=~clka;
always #7 clkb=~clkb;
initial begin
#98
bit_in =1'b1;
#4
bit_in =1'b0;
#60
bit_in =1'b1;
#4
bit_in =1'b0;
#8
bit_in =1'b1;
#4
bit_in =1'b0;
#300
bit_in =1'b1;
end
endmodule
仿真时序图:
从仿真图可以看出,对于快时钟域的两个脉冲离得比较近的话,慢时钟域是会漏采的,使用握手信号时,对此需要注意。
(2)T触发器 + 多级触发器
对于单比特的脉冲信号,我们也可以使用T触发器 + 多级触发器的方法来进行同步,这种方法相较于使用握手信号所需时间较短,但没有ack信号,无法判断接受方是否接受到了脉冲信号。因此使用时一定要保证 满足使用条件。
T触发器的真值表达式为 Qn+1 =T⊕Qn。总结来说的话,就是每来一个周期的高电平,输出就翻转一次。我们利用这个特性,可以将单比特的信号展宽。就是说在两个脉冲之间的信号是保持不变的,不管保持的是0还是1并不重要,我们只要知道脉冲到来之时,T触发器的输出会翻转就足够了。只要信号发生了变化,我们在进行同步的时钟域多打一拍,并与前一拍的信号进行异或就可以得到一个周期的脉冲,虽然b时钟域采到的并不是脉冲,但是异或之后得到的就是一个脉冲。
这种方法的本质实际上是将信号展宽,只不过展宽的信号可能是0也可能是1。但很显然,a时钟域的两个脉冲间隔要足够大,因为两个脉冲的信号的间隔就是a时钟域的信号持续的时间。如果这个时间太短,在b时钟域是无法采到的。两个脉冲之间的间隔要大于Tb+Thold+Tsetup,其中Tb为b时钟域的时钟周期,Thold与Tsetup分别为b时钟域寄存器的保持时间和建立时间。
verilog代码如下:
module syn(
input rsta_n,
input clka,
input plusea,
input rstb_n,
input clkb,
output pluseb
);
reg level;
reg syn1;
reg syn2;
reg syn2_f;
//将a时钟域的脉冲信号转为电平信号
always@(posedge clka or negedge rsta_n) begin
if(!rsta_n)
level<=1'b0;
else if(plusea)
level<=~level;
else
level<=level;
end
//用两级寄存器同步电平信号
always@(posedge clkb or negedge rstb_n) begin
if(!rstb_n) begin
syn1<=1'b0;
syn2<=1'b0;
end
else begin
syn1<=level;
syn2<=syn1;
end
end
//在b时钟域将同步过来的电平信号转为脉冲信号
always@(posedge clkb or negedge rstb_n) begin
if(!rstb_n)
syn2_f<=1'b0;
else
syn2_f<=syn2;
end
assign pluseb=syn2^syn2_f;
endmodule
对于单比特流数据而言,无论是快时钟域到慢时钟域,还是慢时钟域到快时钟域,如果不使用RAM或者FIFO这类存储空间,想直接将数据通过流的方式进行同步,是无法做到的。这是因为两个时钟域的时钟周期长度不一样,随着时间的积累,一定会发生数据的错位。因此若想同步跨时钟域的流数据,必须要借助存储器空间,否则是无法同步流数据的。需要注意的是,快时钟域到慢时钟域的流数据,是不能一直持续的,否则就需要无限大的存储空间,这在文章开头已经提到了。
(1)方法一:DUUX实现CDC
控制信号tx_sel经两级寄存器同步后作为多路选择器的sel信号,cdc_d为发送时钟域多比特数据。tx_sel信号与cdc_d信号都需要持续一定的时间以保证能被接收时钟域采到。
另外,要求cdc_d在tx_sel被采样时保持稳定,具体地说就是当tx_sel在目的时钟域被采样时,cdc_d这个多比特数据的每个比特都已经“到达”目的时钟域的寄存器D端了。这与异步FIFO设计时,源时钟域的格雷码地址到达目的时钟域时,各个比特之间的skew需要小于目的时钟域的一个周期有点类似。跨时钟域时,多比特数据的各个比特之间,一般是存在一定限制的。
分析方法同单比特的流数据。
---------------------------------------------------------------------------------------------------------------------------------
最后需要说明的一点是,除了异步FIFO,当一个时钟域的信号送入另一个时钟域时,都需要另一个时钟域使用两级寄存器进行打拍,这是为了避免出现亚稳态的传播。但需要注意的是,即使使用多级寄存器进行同步,一个时钟域的信号送入另一个时钟域时,这个信号也必须是寄存器输出的。原因有两个:第一,毛刺会加大亚稳态出现的概率,电路的稳定性会受到影响。第二,也是更重要的一点,毛刺产生的亚稳态会导致出现一个不想要出现的0或1。使用寄存器输出时,若因为信号的跳变造成亚稳态,当数据稳定下来后,最多也就是数据推迟一个周期来到,但这个数据仍是我们需要的数据。但是因毛刺产生的亚稳态稳定之后,产生的0或1是我们不需要的数据,这个不需要的0或1如果出现在后续的电路中,且后续的电路有较强的因果关系时,整个系统都会出现错误,且难以排查。也就是说,使用寄存器输出只会延迟数据的到来时间,而毛刺会产生多余的数据,两者有很大区别。
参考链接:
1.你真的懂2-flop synchronizer吗-- CDC的那些事(2)
2.常见数电面试题Pulse Synchronizer -- CDC的那些事(3)
3.多bit信号跨时钟域怎么办?