数字设计--CDC

异步信号与同步信号

同步信号

频率相同,相位相同的两个信号

频率相同,相位差一定的两个信号

不同频率的两个信号也可能是同步的,比如分频出来的两个信号

异步信号

频率不同的两个信号

频率相同但相位差不定的两个信号

跨时钟域就是要解决信号从异步信号之间传输的问题。

亚稳态

前面我们提到,在跨时钟域时,由于发送数据的时钟域和接收数据的时钟域不同,发送数据的到达时刻与接收时钟异步,就很容易造成接收数据时钟域上的寄存器发生建立时间和保持时间违例,就会导致亚稳态。

亚稳态是没法消除的,但亚稳态却可以避免。

衡量系统的稳定的标准或者发生亚稳态的形况可以用MTBF(Mean Time Netween Failure)来确定。

\[MTBF = \frac{e^{t_{MET}/C_2}}{C_1 \cdot f_{CLK} \cdot f_{DATA}} \]

为了避免亚稳态,我们应该使MTBF越大越好。

必须同步来自发送时钟域的信号!!

在进行跨时钟域之前必须要将传输的信号与源时钟域同步。

image

从图上可以看出,源时钟域信号如果经过组合逻辑可能会出现毛刺抖动的现象,相当于增大了信号反转的频率,减小了MTBF,增大了亚稳态发生的概率。

image

将通过组合逻辑的信号通过一级寄存器,由原时钟域驱动的寄存器相当于滤波器将信号中的毛刺滤除,减小了信号翻转的频率,减小了亚稳态发生的概率。

所以,在使用同步器同步信号时,要求输入信号必须是源时钟域的寄存输出

单比特信号跨时钟域

对于单比特信号跨时钟域,最简单的方法就是采用两级触发器同步器(打两拍)来进行。

image

源时钟域是aclk,目的时钟域是bclk,在目的时钟域有两个寄存器,输出分别是bq1_dat和bq2_dat,bq1_dat因为是直接跨时钟域对adat进行采样,可能出现亚稳态,如图就出现了亚稳态,但亚稳态一般在一个时钟周期内就稳定下来,这样在下一个时钟bq2_dat就会输出稳定的值,不会导致亚稳态的传播。

有人可能会说,如果亚稳态没有在一个时钟周期内稳定下来怎么办呢?

这是一个非常正确的问题,的确,双触发器同步器不能完全消除亚稳态!但是很多有经验的工程师会告诉你,用双触发器锁存器就够了,那是因为双触发器会使得亚稳态产生的概率显著降低

在使用双触发器的时候,由于给了第一级触发器一个周期的时间去稳定,使得两级发生亚稳态的概率大大降低。

下面给了一个100MHz时钟的例子,大家只需要关心一下最终的结果,是957亿年,而地球的年龄是46亿年,太阳的寿命是100亿年,也就是说,直到太阳毁灭,你也碰不到下一次metastable的产生。

\[MTBF= \frac{e^{t_{MET}/C_2}}{C_1 \cdot f_{CLK} \cdot f_{DATA}} \cdot \frac{e^{t_{MET}/C_2}}{C_1 \cdot f_{CLK} } = 9.57 \times 10^{10} years \]

但是注意,随着采样时钟频率的提升,以及工艺节点越来越小,有些时候打两拍已经不太够了,那么就简单粗暴来一个打三拍,就能够保证了。

在使用双触发器来进行单比特信号同步时需要考虑两大类情况:

  1. 信号从快时钟域传输到慢时钟域
  2. 信号从慢时钟域传输到快时钟域

如果不仔细考虑这些情况,那么就会出现下面的情况

image

虽然"打了两拍",但好像这两拍打了个寂寞。两级寄存器好像不起作用。通过仔细观察主要还是因为源时钟域与目的时钟域时钟关系信号宽度的原因。

快时钟域同步到慢时钟域

当信号从快时钟域同步到慢时钟域时就有可能出现两级寄存器不起作用的情况。

如果信号的宽度是一个快时钟域的一个时钟宽度,那么信号拉高时可能在慢时钟域时钟上升沿之后,信号拉低可能在慢时钟域时钟下降沿之前,那么慢时钟就无法采样到信号。

信号扩展

为了使慢时钟域采样到信号,我们可以采用信号扩展的方法。将信号宽度拉长使信号能够覆盖一个慢时钟周期,使慢时钟能够采样到信号。

那么将信号展宽为多长合适呢?

一个想法,信号的宽度是慢时钟周期的一倍或者稍微比一个周期长一些,应该能保证信号可以被采样到。

image

但就像上图那样,信号的宽度是慢时钟周期的一倍,就会出现信号拉高时正好碰上慢时钟的上升沿,信号拉低时正好碰上慢时钟的下一个上升沿,这两次出现了亚稳态,导致这两次上升沿慢时钟都没有采样到信号,信号出现了丢失。

一种保险的方式是信号的宽度能够覆盖慢时钟的三个边沿(上升沿和下降沿)。

image

就像上图那样,信号的宽度覆盖了三个慢时钟的边沿,虽然慢时钟的第一个上升沿与信号的上升沿距离很近,发生了亚稳态,但是在第二个慢时钟的上升沿,信号是稳定的,慢时钟还是能够稳定的采样到信号。

信号展宽的方法可以确保信号同步传输的过程中不发生信号的丢失,但是这个方法的问题在于传输的信号本身。如果将要传输的信号展宽的同时影响到了下一个传输的信号,就会导致同步到慢时钟域的信号出现混乱,比如两个信号展宽后重叠在一起变成了一个信号,那么慢时钟域采样时就会出现一个信号,第二个信号就丢失了。

握手

为了解决上面的问题,我们可以采用闭环握手的方法,通过握手控制信号发送的速度。

image

信号通过两级触发器同步器从快时钟域同步到慢时钟域,注意这里需要传输的信号在拉高后没有立即翻转。同步到慢时钟域的信号再次通过快时钟域的两级寄存器同步器同步到慢时钟域,快时钟域接收到这个信号后再反转信号。

相当于将传输到目的时钟域的信号作为反馈信号反馈到源时钟域,源时钟域根据这个反馈信号才能允许信号发生变化。

这种方式隐含了信号能够覆盖至少三个慢时钟边沿的条件。

详细的握手在后面多数据传输中会讨论

脉冲同步器(开环的结绳法)

上面的方法都控制了发送信号的宽度(将发送信号的宽度展宽),但是大多数情况下我们想要传输的信号都是脉冲的形式。另外上面的方法传输到目的时钟域的信号可能也是展宽信号的形式,我们希望传输到目的时钟域的信号也是脉冲的形式。

要解决的问题:快时钟域的一个脉冲同步到慢时钟域的一个脉冲

脉冲同步器就像字面上的意思,把一个脉冲同步到另一个时钟域

image

一种结构如图所示,这种结构也叫数据驱动的脉冲同步器,将数据脉冲作为寄存器的时钟输入。

在开始每个寄存器的输出都为0(低电平),当数据脉冲到来时,脉冲的上升沿作为第一级寄存器的触发信号,第一级寄存器输出高电平,经过clkB两级寄存器传输第三级寄存器输出高电平,第四级寄存器输出为初始的低电平,取反后为高电平,两个电平相与后输出高电平,下一个时刻,第三级第四级寄存器都输出高电平,第四级寄存器取反后为低电平,相与后为低电平,将输入信号转换为目的时钟域的一个脉冲信号。

当第四级寄存器输出高电平信号时,输出与输入信号取反的结果相与,因为输入信号是一个脉冲,输入信号为0时,取反为高电平,相与为高电平,将四个寄存器异步复位。

目的时钟域(clkB)需要等待三个目的时钟(clkB)才会在最后一个寄存器输出并完成输入端的复位。所以如果Din_clkA变化较快,两个数据之间的时间小于三个clkB,那么Din_clkA的变化将无法采样到(对输入数据的时间间隔有要求)。

这种同步器的好处在于不需要在源时钟域对信号做处理,但是缺点也很明显,因为需要将信号作为时钟,会消耗额外的时钟资源,并且该信号的毛刺会导致不可预知的错误,另外如果复位信号的上升沿靠近源时钟域,还可能导致寄存器出现亚稳态。

module cdc1(
    input clk2,
    input rst2,
    input data,
    output pluse_out
				);
								
//-----------------clk1---
reg pluse_reg1;
reg clk2_r1,clk2_r2,clk2_r3;
wire rst1;
assign rst1 = clk2_r3 & ~data;
always @ (posedge data or posedge rst1)
	if(rst1)
		pluse_reg1 <= 1'b0;
	else 
		pluse_reg1 <= 1'b1;
		
always@(posedge clk2 or posedge rst1 )
	if(rst1)
		{clk2_r1,clk2_r2,clk2_r3}<=  3'b000;
	else
		{clk2_r1,clk2_r2,clk2_r3} <= {pluse_reg1,clk2_r1,clk2_r2};
		
assign pluse_out = clk2_r2 && ~clk2_r3;
 
endmodule

testbench

module sim1(
    );

reg clk2;
reg rst2;
reg data;
wire pluse_out;
initial 
begin
clk2=0;
rst2 = 1;
data =0;
#20
rst2 = 0;
#80;
rst2 = 1;
// data =0;
#100 
data =0;
#200
data =1;
#10
data =0;
#200
data =1;
#10 
data =0;
#80
data =1;
#10 
data =0;
   
end
always #10 clk2 = ~clk2;
cdc1 pluse_data_inst(
        .clk2(clk2),
        .rst2(rst2),
        .data(data),
        .pluse_out(pluse_out)
       );
endmodule

仿真结果

image

因为只是功能仿真,当clk2_r3拉高时立刻将四个寄存器复位,所以没有显示出来。

数据驱动的脉冲同步器还有另一种形式

image

只有第一个寄存器在目的时钟域最后一级寄存器输出信号后被复位,其他寄存器不复位。这种形式第一级寄存器解除复位需要寄存器输出传递到目的寄存器的最后一级,导致这段时间里不能有数据进入,对于输入数据的间隔要求更严格。(一开始以为只有最后一级寄存器输出0才能将第一级的复位解除掉,但是跑仿真发现如果在等解除复位的过程中如果新数据进来,就会解除复位,但是当脉冲降为低电平之后,就会继续复位直到最后一级寄存器输出低电平。)

module cdc1(
    input clk2,
    input rst2,
    input data,
    output pluse_out
				);
								
//-----------------clk1---
reg pluse_reg1;
reg clk2_r1,clk2_r2,clk2_r3;
wire rst1;
assign rst1 = clk2_r3 & ~data;
always @ (posedge data or posedge rst1)
	if(rst1)
		pluse_reg1 <= 1'b0;
	else 
		pluse_reg1 <= 1'b1;
		
always@(posedge clk2 or negedge rst2 )
	if(!rst2)
		{clk2_r1,clk2_r2,clk2_r3}<=  3'b000;
	else
		{clk2_r1,clk2_r2,clk2_r3} <= {pluse_reg1,clk2_r1,clk2_r2};
		
assign pluse_out = clk2_r2 && ~clk2_r3;
 
endmodule

最后的仿真结果为

image

上面虽然在复位的时候有新数据进入解除了复位状态,但是脉冲拉低后还是进入了复位状态,将脉冲转换的电平信号拉低,留给目的时钟域采样的时间不多,可能采不到。

这样还存在一个问题:信号的上升沿先到达寄存器的clk端,解复位因为需要经过组合逻辑,所以要比较晚的到达寄存器,寄存器在时钟沿到达时还在复位,根本就解除不了复位状态。(综合出来vivado给通到时钟端的data信号加了一级buff,导致解除复位比data信号到达clk端要早,不知道为什么...)

另一种结构如图所示

image

这种结构没有把输入信号当作时钟输入也没有把传输信号当作复位信号,所以不会出现毛刺和亚稳态的问题。

脉冲同步器中的寄存器初始状态为0。

当有信号脉冲输入时,当输入信号高电平时,源时钟域的寄存器Tq输出为1,当输入信号低电平时,寄存器的输出Tq仍然为1,Toggle flop的结构将输入脉冲转换为了电平的形式(Toggle flop就是t触发器,当输入为高电平时,输出翻转)。

当又输入一个脉冲时,源时钟域的寄存器输出Tq这时为1(因为上一个脉冲使Tq为1),当输入电平为1时,输出Tq为0,当输入电平为0时,输出电平Tq为0。

Toggle Pulse将脉冲转换为输出电平,输入一个脉冲电平翻转一次。这种操作也叫结绳操作(Pluse to toggle)。

注意和上面两种结构的最后面第二级寄存器与第三级寄存器输出操作结构的不同!!!!!

如下图所示

image

当输出电平Tq为高电平时,经过两个寄存器,输出为高,bclk第三级寄存器输出为低,则bp输出为高电平,下一个时刻bclk第三级寄存器输出为高,则bp输出为低电平。

当输出电平Tq为低电平时,经过两个寄存器,输出为低,bclk第三级寄存器输出为高(上一个脉冲导致寄存器输出为高),则bp输出为高电平,下一个时刻bclk第三级寄存器输出为低,则bp输出为低电平。

可以看出经过一级寄存器后将输出与输入相异或可以实现将电平信号转换为脉冲信号。这种操作也叫做解绳操作(toggle to pluse)

image

看上面的图可能更清晰。其实不仅仅限制于输入信号为脉冲,如果输入信号是展宽后的信号,想要在目的时钟域生成一个脉冲,也可以采用这种结构。

既然这个脉冲同步器中间利用了两级寄存器,那么源时钟域寄存器输出Tq能够覆盖慢时钟三个边沿的要求就必须要满足,换句话说,我们转化成为的电平信号Tq要足够长。如果Tq不满足目的时钟域bclk的三边沿的要求,那么这个电平信号我们就无法同步过去,也就无法产生目的时钟bclk的脉冲了。而Tq每次变化是由于源时钟域aclk来了一个新的脉冲,这也就是要求源时钟域aclk的连续两个脉冲之间的间隔要足够大,要满足目的时钟域bclk的三个时钟沿要求。

源时钟域aclk时钟域最接近的两个脉冲能靠多近呢,显然就是两个脉冲中间只有一个源时钟aclk周期(如果没有一个周期,那么这两个脉冲将合成一个脉冲),这其实就是将源时钟aclk进行了2分频。那么相应的,Tq就是对aclk进行了4分频,每个Tq的电平持续时间是2个源时钟aclk周期,这2个周期需要满足目的时钟域bclk三个边沿的要求,如果满足,那么就可以保证目的时钟bclk域也可以产生每个周期的脉冲的要求。

image

module cdc2(
    input clk1,
    input clk2,
    input rst1_n,
    input rst2_n,
    input data
    );
wire toggle_in_clk1;
reg toggle_1_clk1;
reg reg_1_clk2;
reg reg_2_clk2;
reg reg_3_clk2;
wire out_clk2;

assign toggle_in_clk1 = toggle_1_clk1 ^ data;
always @(posedge clk1 or negedge rst1_n)
    begin
        if(!rst1_n)
            begin
                toggle_1_clk1 <= 1'b0;
            end
        else
            begin
                toggle_1_clk1 <= toggle_in_clk1;
            end
    end    
 
always @(posedge clk2 or negedge rst2_n) 
    begin
        if(!rst2_n)
            begin
                {reg_1_clk2,reg_2_clk2,reg_3_clk2} <= 3'b0;
            end
        else
            begin
                {reg_1_clk2,reg_2_clk2,reg_3_clk2} <= {toggle_1_clk1,reg_1_clk2,reg_2_clk2};
            end
    end
 
 assign out_clk2 = reg_2_clk2 ^ reg_3_clk2;
 
    
endmodule

testbench

module sim2(

    );
reg clk1;
reg clk2;
reg rst1;
reg rst2;
reg data;

initial
    begin
        clk1 = 0;
        forever
            #5 clk1 = ~clk1;
    end
    
initial
    begin
        clk2 = 0;
        forever
            #20 clk2 = ~clk2;
    end    
    
initial
    begin
        rst1 =1;
        rst2 =1;
        #30
        rst1 = 0;
        rst2 = 0;
        #30
        rst1 = 1;
        rst2 = 1;
    end    
    
 initial
    begin
        data = 0;
        #70
        @(posedge clk1)
            data = 1;
        @(posedge clk1)    
            data = 0;
        #100
        @(posedge clk1)
            data = 1;
        @(posedge clk1)    
            data = 0;    
        #40
        @(posedge clk1)
            data = 1;
        @(posedge clk1)    
            data = 0;     
    end   
 cdc2 test1(
    .clk1(clk1),
    .clk2(clk2),
    .rst1_n(rst1),
    .rst2_n(rst2),
    .data(data)
 
 );   
    
    
endmodule

仿真结果

image

从图上可以看出,因为第二个脉冲和第三个脉冲之间间隔没有覆盖到clk2的三个边沿,所以第二个第三个脉冲同步过去的脉冲发生了混叠。

结绳法(闭环)

上面我们讨论时对信号之间的间隔有要求,如果我们发送信号的脉冲之间没有很严格的时序要求,允许我们当前脉冲传输过去之后再传输下一个脉冲,源时钟域可以控制信号发送的间隔,那我们可以采用闭环的结绳法,通过一个反馈信号,允许下一个数据信号的输入。

image

本质思路就是我们不能让源时钟域的脉冲产生得很快,而是要等到脉冲同步到目的时钟域之后才能继续产生下一个脉冲,于是我们要将目的时钟域接收到的信号重新同步回到源时钟域,作为源时钟域放行下一个脉冲的条件。

image

源时钟域产生需要发送的信号脉冲req,脉冲通过源时钟域的寄存器进行打结操作(pluse2toggle)变成电平信号。电平信号被目的时钟域打两拍采样到,通过目的时钟域的最后一级寄存器进行解结操作(toggle2pluse)变成目的时钟域的脉冲。这个脉冲再通过目的时钟域的寄存器通过打结操作(pluse2toggle)转换为电平信号,通过在源时钟域打两拍采样到,通过源时钟域最后一级寄存器进行解结操作(toggle2pluse)转换为脉冲信号,表示源时钟域可以继续发送下一个脉冲。

这种闭环的结构和之前我们说的握手结构一样,只不过将原来单纯的两个寄存器同步器替换成脉冲同步器。

module cdc4(
    input clk1,
    input clk2,
    input rst1_n,
    input rst2_n,
    input data,
    output req
    );
wire pluse_in_clk1;
reg toggle_clk1;
reg data_1_clk2;
reg data_2_clk2;
reg data_3_clk2;
wire pluse_clk2;    
wire pluse_in_clk2;
reg toggle_clk2;
reg data_1_clk1;
reg data_2_clk1;
reg data_3_clk1;
//clk1->clk2
assign pluse_in_clk1 = toggle_clk1 ^ data;
always @(posedge clk1 or negedge rst1_n)   
    begin
        if(!rst1_n)
            toggle_clk1 <= 1'b0;
        else
            toggle_clk1 <= pluse_in_clk1;
    end    
always @(posedge clk2 or negedge rst2_n)
    begin
        if(!rst2_n)
            {data_1_clk2,data_2_clk2,data_3_clk2} <= 3'b0;
        else
            {data_1_clk2,data_2_clk2,data_3_clk2} <= {toggle_clk1,data_1_clk2,data_2_clk2};
    end    
 
 assign pluse_clk2 = data_2_clk2 ^ data_3_clk2;
 
 //clk2->clk1
assign pluse_in_clk2 = pluse_clk2 ^ toggle_clk2;

always @(posedge clk2 or negedge rst2_n)
    begin
        if(!rst2_n)
            toggle_clk2 <= 1'b0;
        else
            toggle_clk2 <= pluse_in_clk2;
    end
    
always @(posedge clk1 or negedge rst1_n)    
    begin
        if(!rst1_n)
            {data_1_clk1,data_2_clk1,data_3_clk1} <= 3'b0;
        else
            {data_1_clk1,data_2_clk1,data_3_clk1} <= {toggle_clk2,data_1_clk1,data_2_clk1};
    end
 
 assign  req = data_2_clk1 ^ data_3_clk1;
    
    
endmodule

testbench

module sim4(

    );
reg clk1;
reg clk2;
reg rst1;
reg rst2;
reg data;
wire req;

initial
    begin
        clk1 = 0;
        forever
            #5 clk1 = ~clk1;
    end
    
initial
    begin
        clk2 = 0;
        forever
            #20 clk2 = ~clk2;
    end    
    
initial
    begin
        rst1 =1;
        rst2 =1;
        #30
        rst1 = 0;
        rst2 = 0;
        #30
        rst1 = 1;
        rst2 = 1;
    end    
    
 initial
    begin
        data = 0;
        #70
        @(posedge clk1)
            data = 1;
        @(posedge clk1)    
            data = 0;
        @(posedge req)
            data = 1;
        @(negedge req)    
            data = 0;    
        @(posedge req)
            data = 1;
        @(negedge req)    
            data = 0;    
    end   
 cdc4 test1(
    .clk1(clk1),
    .clk2(clk2),
    .rst1_n(rst1),
    .rst2_n(rst2),
    .data(data),
    .req(req)
 );       
    
endmodule

仿真结果

image

上面我们讨论的不论是开环的结绳法还是闭环的结绳法,我们发现好像对快时钟域到慢时钟域还是慢时钟域到快时钟域都能用,上面我们主要讨论的是从快时钟域同步到慢时钟域,如果对于慢时钟域同步到快时钟域,也可以采用结绳解绳的操作。

结绳法打适用范围很广,但缺点是实现较为复杂,特别是其效率不高,在对设计性能要求较高的场合应该慎用。

慢时钟域同步到快时钟域

对于慢时钟域同步到快时钟域,如果慢时钟域的脉冲(一个慢时钟域周期)不能覆盖快时钟域的三个边沿,或者快时钟的频率不大于慢时钟的1.5倍,那么就按照快时钟域到慢时钟域的方式来处理。

如果慢时钟域的信号能覆盖快时钟域的三个边沿,那么快时钟能够采样到慢时钟域的信号。

快时钟域采慢时钟域可能会出现慢时钟域信号被快时钟采样了多个周期。

如果我们想要采样到快时钟域的信号是一个脉冲,我们就可以采用上面讨论的电路结构

一种是将第二个寄存器的输出与第三个寄存器的输出取反相与,这样一个脉冲对应一个脉冲,

image

另一种如下图所示,d的一个边沿变化将导致一个脉冲,d的一个脉冲将导致两个脉冲。

image

上面两种方式都可以将对于慢时钟周期比较长的信号转换为快时钟域的脉冲。

咋感觉还是第一种可以将慢时钟周期比较长的信号转换为快时钟域的脉冲。

多比特信号跨时钟域

对于多比特的信号跨时钟域,一种想法是将每个比特信号进行两级寄存器同步,打两拍同步到目的时钟域上。

image

由于多比特信号之间可能由于路径长度的不同存在着偏移,这种偏移可能导致本应该在一个时钟周期内被采样的多比特信号被多个时钟周期采样或者由于亚稳态的发生,最后信号的没有在同一个时钟周期内稳定,导致同步过去的信号发生错位,使传输错误。

所以对于多比特信号的跨时钟域不能简单的打两拍

对于多比特信号跨时钟的问题我们可以从要跨时钟的信号的类型进行讨论。

控制信号和数据信号的跨时钟域有不同的方法去处理。

控制信号跨时钟域

对于控制信号来说,有时候需要将多个控制信号跨时钟域去控制相应的动作。

image

举个例子,如果需要将load信号和使能信号ena跨时钟域去控制目的时钟域的寄存器读写,当load信号和ena信号同时有效时寄存器可以写入数据。

如果将控制信号简单的通过两级寄存器同步器来进行跨时钟域,就可能出现如图所示两个信号之间存在偏移导致两个信号被不同的时钟周期采样,导致最后传递到目的时钟域时两个信号之间差一个时钟。两个信号不能够同时为高,导致寄存器不能写入数据。

因为两个信号同时为高才可以写入数据,其他情况不能写入数据,所以可以在源时钟域将信号相与,之后再将相与的信号跨时钟域,来控制寄存器的写入

image

这种方法将多位控制信号的跨时钟域转换为单比特信号的跨时钟域。

当然还存在这样一种情况,如果源时钟域要控制目的时钟域的两个寄存器,源时钟域要传输两个相邻的使能信号跨时钟域控制寄存器做流水操作。

image

如图所示,由于第二行的寄存器的第一级寄存器发生了亚稳态,导致有效的控制信号在第三个时钟才同步到,导致两个控制信号本应该相邻,但同步过去后之间相差一个时钟,导致两个寄存器不能做打拍操作,导致数据丢失。

一种解决方法,因为要求两个相邻的控制信号在同步到目的时钟域后仍然相邻,所以可以只传输一个信号,在同步一个信号后,在目的寄存器对同步过去的信号打拍得到第二个信号。

image

如上图,就可以通过在目的时钟域打一拍来达到两个相邻信号的传输。

对于多位控制信号,我们可以通过信号之间的关系来将多位信号跨时钟域的问题转换为单比特信号跨时钟域的问题。

数据信号跨时钟域

如果要传输的多比特信号是数据信号,数据信号首先会有多种可能性,数据信号也不能将多比特压缩为单比特去传输,所以不能使用控制信号跨时钟域的方式去传输数据信号。

数据信号跨时钟域的方式可以分为两大类:Multi-Cycle Path (MCP) formulation 多周期路径规划FIFO

对于MCP方法,其实和我们前面讨论的单比特信号传输方式差不多,包含握手,脉冲同步器(开环的结绳法)闭环结绳法

MCP的思想是传输的数据信号之间可能存在偏移,在采样时可能会不稳定,那我们可以再增加一个使能信号,当使能信号传输到目的时钟域时,表明这时的数据信号是稳定的,可以被采样。当使能信号有效时,数据信号不能变化。因为我们想传输一组多比特数据,当使能信号传输到目的时钟域是多个目的时钟周期长的,就会导致寄存器多次读入数据,如果寄存器在使能信号下降沿读入信号,就会导致读入数据出现错误,因为这时数据是不稳定的。所以我们希望使能信号在源时钟域和目的时钟域都是脉冲的形式。

image

那么问题就转换为如果传输同步这个使能信号。(就又转换到了如何传输单比特信号)

握手

一种方法是采用握手的方式,通过跨时钟域的握手信号(req和ack),保证被采样的多比特数据在被采样的过程中保持稳定不变。

这种方式效率较低,一次握手需要多个周期,适合对性能要求不高的数据传输。

握手协议包括两种,2-Phase Handshake和4-Phase Handshake

image

握手指的是发端发送req信号给接收端,表示数据已经稳定,接收端接收到经过同步的req信号后,回传给发送端ack信号,完成一次握手。

2-Phase Handshake与4-Phase Hanshake的区别在于2-Phase Handshake在接收端接收到经过同步的req信号上升沿时就接收信号,同时拉高ack,发送端在接收到经过同步的ack上升沿时就改变发送信号发送下一个数据,同时拉低req,接收端接收到经过同步的req信号下降沿时接收新的数据,同时拉低ack,发送端接收到同步的ack下降沿再次改变发送数据,发送新的数据。2-Phase Handshak的发送端和接收端都根据握手信号req和ack的上升沿和下降沿(信号边沿)来决定发送数据和接收数据。

image

4-Phase Handshake在接收端接收到经过同步的req信号上升沿时就接收信号,同时拉高ack,发送端在接收到经过同步的ack上升沿时拉低req,接收端接收到经过同步的req信号下降沿时拉低ack,发送端接收到同步的ack下降沿改变发送数据,发送新的数据。传输一次数据的过程相较于2-Phase Handshake来说相对较长。

image

4-Phase Handshake的实现

image

从4-Phase Handshake的时序图上可以看出,req与ack的信号波形相同(或者存在一拍的延迟),上面电路中对接收到的req打一拍作为ack回传给发送端,代码里面直接就会传给了发送端。

tx

`timescale 1ns / 1ps
module tx(
    input tclk,
    input trstn,
    input ack,
    output req,
    output [3:0] tx_data
    );
    reg [3:0] cnt;
    reg ack_sync1;
    reg req_reg;
    
    wire ack_sync;
    wire ack_sync_neg;
    
    assign req = req_reg;
    assign tx_data = cnt;
    
    always @(posedge tclk or negedge trstn) begin
        if(~trstn)
            cnt <= 4'd0;
        else if(ack_sync_neg)
            cnt <= cnt + 4'h1;
    end
    
    always @(posedge tclk or negedge trstn) begin
        if(~trstn)
            req_reg <= 1'b0;
        else if(ack_sync)
            req_reg <= 1'b0;
        else
            req_reg <= 1'b1;
     end
     
     cdc_sync u_cdc_sync(
        .clk(tclk),
        .rstn(trstn),
        .in(ack),
        .out(ack_sync)
     );
    
    assign ack_sync_neg = ~ack_sync & ack_sync1;
    
    always @(posedge tclk or negedge trstn) begin
        if(~trstn)
            ack_sync1 <= 4'h0;
        else
            ack_sync1 <= ack_sync; 
    end
   
    
    
    
endmodule

rx

`timescale 1ns / 1ps
module rx(
    input rclk,
    input rrstn,
    input req,
    input [3:0] tx_data,
    output ack
    );
    reg [3:0] rx_data;
    
    wire req_sync;
    assign ack = req_sync;
    always @(posedge rclk or negedge rrstn) begin
        if(~rrstn)
            rx_data <= 4'h0;
        else if(req_sync)
            rx_data <= tx_data;
    end
    
    cdc_sync u_cdc_sync(
        .clk(rclk),
        .rstn(rrstn),
        .in(req),
        .out(req_sync)
    );
    
endmodule

cdc_sync

`timescale 1ns / 1ps
module cdc_sync(
    input clk,
    input rstn,
    input in,
    output out
    );
    
    reg in_d1;
    reg in_d2;
    
    assign out = in_d2;
    
    always @(posedge clk or negedge rstn) begin
        if(~rstn) begin
            in_d1 <= 1'b0;
            in_d2 <= 1'b0;
        end
        else begin
            in_d1 <= in;
            in_d2 <= in_d1;
        end
    end
    
    
endmodule

tb_handshaker

`timescale 1ns / 1ps
module tb_handshaker(

    );
    wire [3:0] tx_data;
    wire req;
    wire ack;
    
    reg tclk;
    reg trstn;
    
    reg rclk;
    reg rrstn;
    
   initial
    begin
    tclk = 1'b0;
    trstn = 1'b0;
    #5
    trstn = 1'b1;
    end 
    
    initial
        begin
            rclk = 1'b0;
            rrstn = 1'b0;
            #11
            rrstn = 1'b1;
            #2000
            $finish;      
        end
    always  #3 tclk = ~tclk;
    always  #10 rclk = ~rclk;
    
    tx inst1(
        .tclk(tclk),
        .trstn(trstn),
        .ack(ack),
        .req(req),
        .tx_data(tx_data)
        );
    
     rx inst2(
        .rclk(rclk),
        .rrstn(rrstn),
        .req(req),
        .tx_data(tx_data),
        .ack(ack)
            );
    
    
endmodule

image

发端req拉高,导致收端ack拉高,导致发端的req拉低,再导致收端的ack拉低,这样一个握手环节结束,发端检测到ack的下降沿,开始下一轮的数据传输,req拉高。

拉高可以通过判断信号高电平直接检测,下降沿可以通过下降沿检测来进行。

2-phase代码

tx_2

`timescale 1ns/1ps


module tx_2 (
	input tclk,
	input trstn,
	input ack,
	output req,
	output [3:0] tx_data
);

reg [3:0] cnt;
reg req_reg;

wire ack_pose_edge;
wire ack_nege_edge;

wire ack_sync;
reg ack_sync_1;


assign req = req_reg;

always @(posedge tclk or negedge trstn)
	if(!trstn)
		cnt <= 4'd0;
	else if(ack_pose_edge || ack_nege_edge)
		cnt <= cnt + 4'd1;
	
assign tx_data = cnt;

always @(posedge tclk or negedge trstn)
	if(!trstn)
		req_reg <= 1'b0;
	else if(ack_sync)
		req_reg <= 1'b0;
	else
		req_reg <= 1'b1;
		
 cdc_sync u_cdc_sync(
    .clk(tclk),
    .rstn(trstn),
    .in(ack),
    .out(ack_sync)
 );

always @(posedge tclk or negedge trstn)
	if(!trstn)
		ack_sync_1 <= 1'b0;
	else
		ack_sync_1 <= ack_sync;

assign ack_pose_edge = ack_sync & ~ack_sync_1;
assign ack_nege_edge = ~ack_sync & ack_sync_1;

endmodule

rx_2代码

`timescale 1ns/1ps

module rx_2(
	input rclk,
	input rrstn,
	input req,
	input [3:0] tx_data,
	output ack
);

wire req_sync;
reg req_sync_1;
wire req_pose_edge,req_nege_edge;
reg [3:0] rx_data;

always @(posedge rclk or negedge rrstn)
	if(!rrstn)
		rx_data <= 4'd0;
	else if(req_pose_edge || req_nege_edge)
		rx_data <= tx_data;

cdc_sync u_cdc_sync(
    .clk(rclk),
    .rstn(rrstn),
    .in(req),
    .out(req_sync)
);


assign ack = req_sync;

always @(posedge rclk or negedge rrstn)
	if(!rrstn)
		req_sync_1 <= 1'b0;
	else
		req_sync_1 <= req_sync;

assign req_pose_edge = req_sync & ~req_sync_1;
assign req_nege_edge = ~req_sync & req_sync_1;
			
endmodule

cdc_sync

`timescale 1ns / 1ps

module cdc_sync(
    input clk,
    input rstn,
    input in,
    output out
    );
    
    reg in_d1;
    reg in_d2;
    
    assign out = in_d2;
    
    always @(posedge clk or negedge rstn) begin
        if(~rstn) begin
            in_d1 <= 1'b0;
            in_d2 <= 1'b0;
        end
        else begin
            in_d1 <= in;
            in_d2 <= in_d1;
        end
    end
    
endmodule

tb_handshaker

`timescale 1ns / 1ps
module tb_handshaker(

    );
wire [3:0] tx_data;
wire req;
wire ack;

reg tclk;
reg trstn;

reg rclk;
reg rrstn;

initial
begin
tclk = 1'b0;
trstn = 1'b0;
#5
trstn = 1'b1;
end 

initial
    begin
        rclk = 1'b0;
        rrstn = 1'b0;
        #11
        rrstn = 1'b1;
        #2000
        $finish;      
    end
always  #3 tclk = ~tclk;
always  #10 rclk = ~rclk;

tx_2 inst1(
    .tclk(tclk),
    .trstn(trstn),
    .ack(ack),
    .req(req),
    .tx_data(tx_data)
    );

rx_2 inst2(
    .rclk(rclk),
    .rrstn(rrstn),
    .req(req),
    .tx_data(tx_data),
    .ack(ack)
        );
    
//dump fsdb 
initial begin 
    $fsdbDumpfile("test.fsdb");
    $fsdbDumpvars(0);
end    
endmodule

image

发端req拉高,导致收端ack拉高,再导致发端req拉低,再导致收端ack拉低,2-phase与4-phase在握手顺序上是一样的,从波形上看ack与同步过来的req相同,所以收端可以直接将接收到的req回传给发端。

2-phase与4-phase不同的在于,2-phase检测到ack的边沿变化就改变发送数据,检测到req的边沿变化就接收数据,所以不管是发端还是收端都需要检测边沿变化,根据边沿的变化来接收或者发送数据。4-phase,发端只在ack下降沿的时候改变数据,收端只要检测到req为高就接收数据(与检测req上升沿相同)

结绳法

我们可以使用脉冲同步器,这是一种开环的结绳法。

image

通过脉冲同步器,将使能信号脉冲转换为电平信号,供目的时钟域采样。

image

这种方式正如我们分析单比特信号跨时钟域时所说,他对输入信号的变化周期有要求,输入信号的变化导致的转换电平的长度必须包含目标时钟的三个边沿,否则将导致数据不能被同步到目的时钟域。

如果我们不能保证输入信号之间的间隔,但我们可以在源时钟域控制数据的输入,那对于使能信号的传输我们还可以采用闭环的结绳法

闭环的结绳法从目的时钟域传输一个反馈信号给源时钟域表明数据可以继续发送,这种方式保证了输入数据之间的间隔能够满足数据的跨时钟域需要。

image

这种方式的一个问题在于我们认为数据传输到目的时钟域后能够被寄存器立刻处理,这时数据发端一直发送数据将没有影响。

如果目的时钟域处理数据需要一定的时间,就需要对源时钟域进行反馈,来保证每一个传输的数据都能被处理。

image

之前我们的反馈信号是目的时钟域接收到信号后立刻将信号传输到源时钟域,但现在我们需要判断数据是否处理完,或者数据是否能够继续传输,就需要引入额外的使能位,这里的使能位是bload。本质上就是我们不立刻反馈使能信号,而是在判断数据可以继续发送时再反馈使能信号。

FIFO

对于多比特信号传输可以通过异步FIFO来进行跨时钟域。


芯片设计进阶之路——跨时钟信号处理方法 - 知乎 (zhihu.com)
跨时钟域处理方法总结--最终详尽版 - love小酒窝 - 博客园 (cnblogs.com)
跟老李一起学习芯片设计-- CDC的那些事(1) - 知乎 (zhihu.com)
(13条消息) 结绳法:文章详细解读(异步时钟设计的同步策略)(五)_u011412586的专栏-CSDN博客_结绳法同步
跨时钟域信号免做CDC处理的一种方法 - 知乎 (zhihu.com)
(10条消息) 异步时钟处理之结绳法1_Mr.zhang_FPGA的博客-CSDN博客_fpga 结绳法
(10条消息) 异步时钟处理之结绳法2_Mr.zhang_FPGA的博客-CSDN博客_verilog结绳法
【设计开发】 典型异步电路设计-脉冲同步(1) - digital-world - 博客园 (cnblogs.com)
【设计开发】 典型异步电路设计-脉冲同步(2) - digital-world - 博客园 (cnblogs.com)
(3条消息) 跨时钟域电路设计——结绳法_沧海一升的博客-CSDN博客
(3条消息) 跨时钟域设计(结绳法,脉冲展宽法)_dxz44444的博客-CSDN博客

posted @ 2024-07-06 18:39  孤独野猪骑士  阅读(59)  评论(0编辑  收藏  举报