CDC跨时钟域信号处理

  • 跨时钟阈值问题
  • 时钟域就是指时钟的管辖范围,在我的管辖范围之内的逻辑由我来提供时钟。逻辑设计中将所有同步元件(如触发器和RAM等)使用相同时钟信号的部分称为时钟域。跨时钟域就是数据在不同时钟域之间的交互!从一个时钟域到另一个时钟域,环境(始终快慢、相位)可能不同,这样就导致一系列的问题,轻者亚稳态(水土不服生病),重则影子都没了(对方检测不到)
  • 各大FPGA厂商的FPGA工具在逻辑综合及实现之后会出一个时序报告,里面就包含跨时钟域的报告,在里面可以看到有哪些信号进行了跨时钟域。如Vivado中的报告:
  • 对于这些跨时钟域的情况,一般在逻辑设计时就解决,在FPGA工具中也要对其进行约束,一般可以设置为false path等,即让综合工具不要自作聪明的去通过布局布线让时序满足要求,因为这样会拖慢编译时间,时序也不会成功。设置为false path或者时钟组之后,工具默认不对其进行时序分析,因为设计中已经解决了跨时钟域的问题,这也就是为什么说跨时钟域问题是设计解决的,而不是约束解决的。
  • 跨时钟阈(CDC,Clock Domain Crossing)容易产生亚稳态(Metastablity),即一种不太稳定的状态,触发器会在短时间内高低震荡徘徊,这本来是触发器的一个固有属性,正常采样时也会经历亚稳态到稳态的过程,但是在两个时钟域中进行数据传输时很容易出现一个时钟域的输出在另一个时钟域中的时钟上升沿到来时发生改变,而跨时钟阈容易导致建立时间和保持时间的违背。触发器的输入电压采样时间过短,触发器需要花很长时间来实现输出逻辑达到标准电平,这就使得中间的亚稳态时间过长,电路反应迟钝,功能受到影响。

  • 如上图所示,在时钟域A中生成了一个使能信号En_Out去触发时钟域B内的算法,总之时钟域B内的逻辑需要时钟域A中生成的使能控制信号。
  • 注意:时钟域A中的使能信号En_Out有效状态至少持续一个时钟周期的时间,否则称不上脉冲,只能叫做毛刺。

 

跨时钟阈传输一般分以下几种情况:

跨时钟阈传输

单bit信号传输

(一般为控制信号)

慢时钟域到快时钟域 ①两级寄存器组成的同步器对异步信号进行同步(俗称打两拍)



异步FIFO

 

快时钟域到慢时钟域 开环方案:① 电平同步器(长电平信号,不用扩展直接打拍)②脉冲信号展开(扩1.5倍+)+打拍同步  闭环方案:①握手/反馈机制

多bit信号传输

(一般为数据信号)

慢时钟域到快时钟域 MUX同步器(写入使能信号,同步后再加载数据)
快时钟域到慢时钟域 ①握手/反馈(需写入使能信号,扩展使能信号+同步到慢时钟域后再加载数据,然后反馈使能信号恢复)
      慢到快时钟域只需要考虑亚稳态问题,因为快时钟域一定可以采样到慢时钟域的信号快到慢时钟域需要同时考虑亚稳态和漏采的问题,核心思想是信号展宽握手协议适用性广,单/多比特均适合,但是实现稍复杂,效率不高

 

  • 异步FIFO使用慢到快和快到慢的单/多比特信号的跨时钟域传输,数据流传输速率快、连续性好
  • 所谓同步器是一种对异步信号进行采样并输出与跳变同步到本地或采样时钟的信号版本的设备。数据同步时分两种情况:

    1. 在跨时钟域时允许丢失一些信号采样,但是要保证采集到的数据都是正确的。
    2. 跨时钟域时传递的信号必须都要被采样到。

    情况1对应的比如异步FIFO,格雷码转换时不需要从另外一个时钟域对每个数据都采样,只需要在最后空满判断时采集到正确数据即可。

         情况2时就需要对每个数据进行传输之前,都需要进行正确识别,或者通过握手的方式进行传输,保证数据的正确性。

         握手应该是最稳定的数据传输手段,尤其是全握手,其数据安全性要比单比特数据打拍传输更安全,但也要平衡性能与安全性,这也许是异步FIFO更多采用格雷码方式跨时钟域而不是全握手的原因。


  • 单bit信号,从慢时钟到快时钟
  • 对于单bit信号,从慢时钟到快时钟,可以通过两级寄存器组成的同步器对异步信号进行同步。我们默认单比特脉冲信号在源时钟域内已经被本地时钟控制的寄存器同步,经过时钟同步后信号不仅与时钟保持同步,而且有利于时序优化(时序路径为两个时钟元件之间的数据路径,使用触发器同步,无疑将长的数据路径截短,有利于时序通过),这也是我们推荐的一种设计习惯;快时钟域的时钟一定能采集到异步的输入信号,因为异步的输入信号至少保持一个慢时钟周期,而快时钟周期比慢时钟周期更短,采样频率更高,所以异步信号结束前一定会被快时钟采到。两块寄存器组成同步器,。如果同步器的第一级触发器产生亚稳态输出,这个亚稳态在第二级触发器取样前稳定,增加多级触发器,可以进一步降低亚稳态出现的可能性(但会带来的问题是增加了整体电路的时延)一般两级触发器就可以将数据采样的正确性提升到99%了,所以一般打两拍;

     使用寄存器级联构成的同步器需满足:

  • 级联的寄存器必须使用同一个采样时钟,且要用同一种时钟沿触发,如果使用了不同的边沿进行触发会导致在同一个时钟周期内的上升(寄存器1)和下降(寄存器2)边沿进行处理,第二个寄存器处理时间不够。
  • 发送端时钟域寄存器输出和接收端异步时钟域级联寄存器输入之间不能有任何其他组合逻辑,否则容易产生毛刺(组合逻辑中信号状态改变引起的竞争与冒险导致),使同步器很可能采集到不需要的数据。
  • 在跨时钟域时,用寄存器的形式输出信号,而不是组合逻辑电路直接输出,因为组合逻辑输出不稳定,有毛刺和震荡,会增加采样时钟采样到不稳定信号的概率,导致亚稳态信号生成产生的概率。
  • 同步器中级联的寄存器除了最后一个寄存器外,中间所有的寄存器只能有一个扇出,即只能驱动下一级寄存器的输入。
pluse_slow2fast_cdc.v
//clk_a是慢时钟域时钟,clk_b是快时钟域时钟,data_in为输入信号,data_out为输出信号
module pluse_slow2fast_cdc(
	input clk_a,
	input clk_b,
	input rst_n,
	input data_in,
	output wire data_out	
);

reg data_in_reg;
reg [1:0] data_out_reg;

//将输入data_in信号寄存
always@(posedge clk_a or negedge rst_n)begin
	if(!rst_n)
		data_in_reg <= 1'b0;
	else
		data_in_reg <= data_in;
end

//将要输出的信号寄存打拍
always@(posedge clk_b or negedge rst_n)begin
	if(!rst_n)
		data_out_reg <= 2'b0;
	else
		data_out_reg <= { data_out_reg[0] , data_in_reg } ;
end

//给输出赋值
assign data_out = data_out_reg[1];

慢时钟域到快时钟域的同步有可能带来信号的冗余,即在快时钟下多次采样到异步信号,从而使同步后的信号保持较长的时间,这个问题可以通过对其进行上升沿检测输出标志信号解决,这样就只会采集到一个目标信号。


  • 单bit信号从快时钟域到慢时钟域
  • 对于单bit信号从快时钟域到慢时钟域,慢时钟域相比快时钟域采样速度更慢,从快时钟域来到慢时钟域的信号极有可能被漏采:

  • 一般要求在接收时钟域中采样信号要保持三个时钟边沿的时间,也就是1.5倍的采样时钟周期(慢时钟)才会避免出现漏采。即快到慢跨时钟域的核心是如何延长信号长度。
  • 对于电平信号而言,一般它的持续时间足够长,长度可以得到保证,所以正常采用两级同步器采样即可,但是也不能完全精准的保证采样后的电平信号持续的时钟周期和采样前快时钟域中持续的时钟周期相同。

常用的方法:①开环(无反馈)设计,即信号展宽+边沿检测 ②闭环(有反馈)设计,即握手/反馈机制,信号延长的恢复位置由反馈信号决定,实质为通过相互握手的方式对窄脉冲信号进行脉冲扩展,

  • 第一种方法信号展宽+边沿检测法:
pulse_detect.v
module pulse_detect(
    input              clk_fast    , 
    input              clk_slow    ,   
    input              rst_n       ,
    input              data_in     ,
    output             dataout      
);
reg data_in_fast;
reg [2:0] data_slow;
//将脉冲信号在快时钟域展平为电平信号。即展宽脉冲信号。在两次脉冲信号之间为电平信号。
always@(posedge clk_fast or negedge rst_n)begin
    if(!rst_n)
        data_in_fast<= 0;
    else
        data_in_fast<= data_in ? (~data_in_fast) : data_in_fast;
end
//将展宽的脉冲信号在慢时钟域打两拍,检测上升沿沿并只取边沿后一个时钟周期的单脉冲即为跨时钟域转换后的脉冲。
always@(posedge clk_slow or negedge rst_n)begin
    if(!rst_n)
        data_slow <= 3'b0;
    else
        data_slow <= {data_slow[1:0],data_in_fast};
end   

assign dataout = data_slow[2] ^ data_slow[1];//取边沿(上升/下降)后一个时钟周期的单脉冲即为跨时钟域转换后的脉冲
//上升沿检测:~data_slow[2] & data_slow[1]     下降沿检测:data_slow[2] & ~data_slow[1]    
//双边检测(上升或者下降都为高):~data_slow[2] & data_slow[1] || data_slow[2] & ~data_slow[1]  
//这不就是异或的与或非表达形式吗,因此直接可写成 data_slow[2] ^ data_slow[1]
endmodule

贴一个激励文件,可以直接仿真看到波形

tb_pulse_detect.v
`include "../rtl/pulse_detect.v"
`timescale 1ns/1ns
module tb_pulse_detect;
reg clk_fast            ;
reg clk_slow            ;
reg rst_n               ;
reg data_in             ;
wire dataout            ;

initial begin
    clk_fast = 1'b1;
    clk_slow = 1'b1;
    rst_n <= 1'b0;
    data_in <= 1'b0;
    #20 rst_n <= 1'b1;
end

always #10 clk_fast = ~clk_fast;
always #50 clk_slow = ~clk_slow;

initial begin
    data_in_task();
    #1000 $finish;
end

initial begin
    $dumpfile("tb_pulse_detect.vcd");
    $dumpvars();
end

task data_in_task;
begin
    #100
    @(posedge clk_fast)begin
        data_in <= 1'b1;
    end
    @(posedge clk_fast)begin
        data_in <= 1'b0;
    end
    #200
    @(posedge clk_fast)begin
        data_in <= 1'b1;
    end
    @(posedge clk_fast)begin
        data_in <= 1'b0;
    end
    #200
    @(posedge clk_fast)begin
        data_in <= 1'b1;
    end
    @(posedge clk_fast)begin
        data_in <= 1'b0;
    end
end
endtask

pulse_detect pulse_detect_inst(
    .clk_fast               (clk_fast           ),
    .clk_slow               (clk_slow           ),
    .rst_n                  (rst_n              ),
    .data_in                (data_in            ),
    .dataout                (dataout            )
);
    
endmodule

pulse_fast2slow_cdc.v
module pulse_fast2slow_cdc
(
    input              clk_fast         , 
    input              clk_slow         ,   
    input              rst_n            ,
    input              pulse_clk1       ,

    output             pulse_sync_clk2  
);
//input            pulse_clk1
reg             pulse_wide_clk1     ;//对输入的clk1时钟域下的pulse_clk1信号进行脉冲扩宽

reg             pulse_wide_clk2     ;//将脉冲扩宽后的结果pulse_wide_clk1同步到clk2时钟域下
reg        reg1_pulse_wide_clk2     ;//将pulse_wide_clk2打一拍形成两级同步,该信号可以作为反馈信号反馈回clk1

reg        reg1_pulse_wide_clk2_clk1;//将反馈信号同步回clk1
reg   reg1_reg1_pulse_wide_clk2_clk1;//上述信号打一拍形成两级同步,可以作为脉冲扩宽的结束条件
//为什么它可以作为反馈信号去结束脉冲扩宽呢?因为pulse_wide_clk2拉高时说明clk2已经采样到clk1输入的脉冲了,可以结束展宽了

//生成脉冲展宽信号
always@(posedge clk_fast or negedge rst_n)begin
    if(!rst_n)
        pulse_wide_clk1 <= 1'b0;
    else if(pulse_clk1)//在pulse_clk1脉冲信号拉高的下一个时钟周期,拉高pulse_wide_clk1
        pulse_wide_clk1 <= 1'b1;
    else if(reg1_reg1_pulse_wide_clk2_clk1)//当pulse_wide_clk2_clk1拉高时 拉低pulse_wide_clk1   脉冲扩宽结束
        pulse_wide_clk1 <= 1'b0;
    else
        pulse_wide_clk1 <= pulse_wide_clk1;
end
//在目的时钟域内采样展宽后的信号
always@(posedge clk_slow or negedge rst_n)begin
    if(!rst_n)
        pulse_wide_clk2 <= 1'b0;
    else
        pulse_wide_clk2 <= pulse_wide_clk1;
end
//展宽后的信号再打一拍 形成两级同步
always@(posedge clk_slow or negedge rst_n)begin
    if(!rst_n)
        reg1_pulse_wide_clk2 <= 1'b0;
    else
        reg1_pulse_wide_clk2 <= pulse_wide_clk2;
end
//将目的时钟域采样并打拍后的信号 作为反馈信号同步到clk1
always@(posedge clk_fast or negedge rst_n)begin
    if(!rst_n)
        reg1_pulse_wide_clk2_clk1 <= 1'b0;
    else
        reg1_pulse_wide_clk2_clk1 <= reg1_pulse_wide_clk2;
end
//将上面的信号再打一拍形成二级同步
always@(posedge clk_fast or negedge rst_n)begin
    if(!rst_n)
        reg1_reg1_pulse_wide_clk2_clk1 <= 1'b0;
    else
        reg1_reg1_pulse_wide_clk2_clk1 <= reg1_pulse_wide_clk2_clk1;
end

assign pulse_sync_clk2 =  pulse_wide_clk2 & ~reg1_pulse_wide_clk2;

endmodule
tb_pulse_fast2slow_cdc.v
`include "../rtl/pulse_fast2slow_cdc.v"
`timescale 1ns/1ns
module tb_pulse_fast2slow_cdc;
reg clk_fast            ;
reg clk_slow            ;
reg rst_n               ;
reg pulse_clk1          ;
wire pulse_sync_clk2    ;

initial begin
    clk_fast = 1'b1;
    clk_slow = 1'b1;
    rst_n <= 1'b0;
    pulse_clk1 <= 1'b0;
    #20 rst_n <= 1'b1;
end

always #10 clk_fast = ~clk_fast;
always #50 clk_slow = ~clk_slow;

initial begin
    data_in_task();
    #1000 $finish;
end

initial begin
    $dumpfile("tb_pulse_fast2slow_cdc1.vcd");
    $dumpvars();
end

task data_in_task;
begin
    #100
    @(posedge clk_fast)begin
        pulse_clk1 <= 1'b1;
    end
    @(posedge clk_fast)begin
        pulse_clk1 <= 1'b0;
    end
    #500
    @(posedge clk_fast)begin
        pulse_clk1 <= 1'b1;
    end
    @(posedge clk_fast)begin
        pulse_clk1 <= 1'b0;
    end
    #500
    @(posedge clk_fast)begin
        pulse_clk1 <= 1'b1;
    end
    @(posedge clk_fast)begin
        pulse_clk1 <= 1'b0;
    end
end
endtask

pulse_fast2slow_cdc pulse_fast2slow_cdc_inst(
    .clk_fast               (clk_fast           ),
    .clk_slow               (clk_slow           ),
    .rst_n                  (rst_n              ),
    .pulse_clk1             (pulse_clk1         ),
    .pulse_sync_clk2        (pulse_sync_clk2    )
);
    
endmodule

仿真结果如图所示,实现了pulse_clk1脉冲到pulse_sync_clk2脉冲的快到慢跨时钟域的同步,pulse_clk1在快时钟clk1上升沿拉高,持续一个时钟周期脉冲,pulse_sync_clk2在慢时钟clk2的上升沿拉高,也持续一个时钟周期的脉冲。但是有个问题要注意,如果连续输入多个pulse_clk1脉冲,这些脉冲的距离不能太近,下一个脉冲不能落在上一个脉冲的展宽范围内,否则会只识别到第一个脉冲,而识别不到后面的脉冲,导致最后输出也只会输出一个脉冲,当输入脉冲距离足够远时可实现完美同步。

  • 多比特信号跨时钟域传输
  • 为什么多bit传输不能使用二级同步器呢?使用格雷码也不行?什么情况下可以使用同步器和格雷码跨时钟传输?因为每个寄存器位置不同,布局布线会导致每bit数据到达下一级寄存器的延时不同,数据的所有寄存器可能不会同时翻转,这样一来容易在跨时钟域传输过程中出现中间态。格雷码的话是指数据是单位递增递减的前提下才会每次只有一位发生变化,这种数据才能用来解决跨时钟域问题,异步FIFO中就用格雷码通过二级同步对读写指针进行了跨时钟域传输。

 

  • 多比特慢到快:
  • 这种情况下快时钟接收端一定能采样到慢时钟的数据,但是由于在传输过程中很可能出现多位同时变化的情况,所以同时输入与数据同步的写使能信号,写使能信号在这个过程中一直为高不会发生变化,然后将写使能信号同步到快时钟域,二级同步后作为数据信号的稳定传输条件,此时再将数据信号安全同步到快时钟域。

  • 如果想要将data从aclk转到bclk(无所谓谁的频率快慢),我们通过data的valid信号(从属于aclkA)两级同步到bclk,并且将同步后的信号作为MUX选通信号,为1时将data第一级寄存器的q值输出到后级寄存器的d;为0时将后级寄存器的q输出到d。由于data的时序和data_valid的时序是同时的,跨到bclk后data_valid稳定也就等价于穿到bclk DFF的data也已经稳定,可以拿来锁入最后一级DFF,最后一级DFF的Q输出bdata就是data跨到bclk的稳定信号。

  • 假设aclk是慢时钟,数据仅持续一个时钟即可被同步到bclk:

多bit慢到快时钟域mux同步器设计,输出包扩同步后的data以及使能信号

mux_synchronizer.v
module mux_synchronizer(
    input               aclk                ,
    input               bclk                ,
    input               rst_n               ,
    input       [7:0]   adata_in            ,
    input               adata_in_valid      ,
    output reg  [7:0]   bdata_out           ,
    output              bdata_out_valid     
);

//wire     [7:0]      adata_in_mux;
reg      [7:0]      reg1_adata_in;

reg                 reg1_adata_in_valid;
reg                 reg1_adata_in_valid_bclk;
reg            reg1_reg1_adata_in_valid_bclk;
reg                      bdata_out_flag;
//时钟域a下同步本地数据及其有效标志信号,改善时序,不清楚输入情况,打一拍准没错
always @(posedge aclk or negedge rst_n) begin
    if(!rst_n)
        reg1_adata_in <= 8'd0;
    else
        reg1_adata_in <= adata_in;
end
always @(posedge aclk or negedge rst_n) begin
    if(!rst_n)
        reg1_adata_in_valid <= 1'b0;
    else
        reg1_adata_in_valid <= adata_in_valid;
end

//将时钟域a下的有效标志信号展宽同步到时钟域b下
always @(posedge bclk or negedge rst_n) begin
    if(!rst_n)
        reg1_adata_in_valid_bclk <= 1'b0;
    else
        reg1_adata_in_valid_bclk <= reg1_adata_in_valid;
end
//再打一拍形成二级同步消除亚稳态
always @(posedge bclk or negedge rst_n) begin
    if(!rst_n)
        reg1_reg1_adata_in_valid_bclk <= 1'b0;
    else
        reg1_reg1_adata_in_valid_bclk <= reg1_adata_in_valid_bclk;
end
//mux 根据bclk下同步后的valid信号 选择要输入到最后一级寄存器D的值 (同步到b时钟域)
//对该控制信号做做处理
//assign adata_in_mux = reg1_reg1_adata_in_valid_bclk?reg1_adata_in:bdata_out;

//将reg1_adata_in同步到b时钟域
always @(posedge bclk or negedge rst_n) begin
    if(!rst_n)
        bdata_out <= 8'd0;
    else 
        bdata_out <= reg1_reg1_adata_in_valid_bclk?reg1_adata_in:bdata_out;
end
//将bdata_out_valid与bdata_out同步输出,reg1_reg1_adata_in_valid_bclk打一拍后与之对齐
always @(posedge bclk or negedge rst_n) begin
    if(!rst_n)
        bdata_out_flag<= 1'b0;
    else 
        bdata_out_flag <= reg1_reg1_adata_in_valid_bclk;
end

assign bdata_out_valid = bdata_out_flag & reg1_adata_in_valid_bclk;

endmodule
tb_mux_synchronizer.v
`include "../rtl/mux_synchronizer.v"
`timescale 1ns/1ns
module tb_mux_synchronizer;

reg          aclk              ;
reg          bclk              ;
reg          rst_n             ;
reg [7:0]    adata_in          ;
reg          adata_in_valid    ;

wire [7:0]   bdata_out         ; 
wire         bdata_out_valid   ;    
           
initial begin
    aclk = 1'b1;
    bclk = 1'b1;
    rst_n <= 1'b0;
    adata_in <= 8'd0;
    adata_in_valid <= 1'b0;
    #20 rst_n <= 1'b1;
end

always #11.1 aclk = ~aclk;//#50
always #33.2 bclk = ~bclk;//#10

initial begin
    data_in_task();
    #100 $finish;//$finish;//
end

initial begin
    $dumpfile("tb_mux_synchronizer.vcd");
    $dumpvars();
end

task data_in_task;
begin
    #500 byte_in('d1);
    #500 byte_in('d9);
    #500 byte_in('d9);
    #500 byte_in('d6);
end
endtask

task byte_in
(
    input [7:0] data
);
begin
    @(posedge aclk)begin
        adata_in <= data;
        adata_in_valid <= 1'b1;
    end
    repeat(7) @(posedge aclk);

    @(posedge aclk)begin
        adata_in <= 8'd0;
        adata_in_valid <= 1'b0;
    end
end
endtask

mux_synchronizer mux_synchronizer_inst(
    .aclk                   (aclk               ),
    .bclk                   (bclk               ),
    .rst_n                  (rst_n              ),
    .adata_in               (adata_in           ),
    .adata_in_valid         (adata_in_valid     ),

    .bdata_out              (bdata_out          ),
    .bdata_out_valid        (bdata_out_valid    )     
);
    
endmodule

多bit快到慢mux同步器使用上述方法是有问题的,我暂时没有想到该怎么设计,总是会出各种错误,网上基本上也全是慢到快的,基本上没有快到慢MUX的例子。

  • 握手反馈同步方式
  • 解决总线同步问题最基本的方式是同步器和握手信号,同步器用于数据锁存及采样,握手信号指示接收域中的电路何时可以对总线进行采样,以及发送域电路何时可以更新同步器当前的内容。握手协议是一种闭环的数据同步方法,更像是基于MUX同步器的应答机制的扩展。在这种同步方案中,无论源时钟和目的时钟之间的时钟周期关系如何,都采用请求和确认机制来保证正确的数据采样到目的时钟域,适用于不规则变化的数据。

从图中时序图,我们提炼出输入信号:

    • clk_a, clk_b
    • rst(这个图中没有,但是很有必要)
    • a_en,data_a(这两者作为输入,可以当成一种协议,检测到a_en有效(下降沿)的时候,输入数据就更新,更新数据一直持续到a_en的下一个下降沿)

输出信号:

    • b_en,这是对a_en的同步
    • data_b_out,这是对data_in从clka到clk_b的同步
    • ack_a,它是B时钟域的响应信号ack,同步到a时钟域后的信号,通知a时钟域(下降沿通知),可以发送下一个数据了。事实上,a_en就是该信号的下降沿;
handshake_cdc
 module handshake_cdc(
    input   wire       clk_a        ,
    input   wire       clk_b        ,
    input   wire       rst          ,
    input   wire       a_en         , //来自于外部的使能信号,脉冲持续一个时钟周期
    input   wire [3:0] data_a_in    , //外部输入信号

    output  reg  [3:0] data_b_out   ,
    output  wire       b_en         ,
    output  wire       ack_a

    );

    //生成a_en的下降沿
    reg     a_en_d1     ;
    reg     a_en_d2     ;
    reg     a_en_neg    ;
    always@(posedge clk_a or posedge rst) begin
        if(rst) begin
            a_en_d1 <= 1'd0;
            a_en_d2 <= 1'd0;
            a_en_neg <= 1'd0;
        end
        else begin
            a_en_d1 <= a_en;
            a_en_d2 <= a_en_d1;
            // a_en_neg <= ~a_en_d1 && a_en_d2;
            a_en_neg <= ~a_en && a_en_d1;
        end
    end

    //生成请求信号
    reg     a_req   ; 
    always@(posedge clk_a or posedge rst) begin
        if(rst) begin
            a_req <= 1'd0;
        end
        else if(a_en_neg) begin//下降沿到来时 拉高请求行a_req
            a_req <= 1'd1;
        end
        else if(a2_q_pos) begin //a_req 拉低条件
            a_req <= 1'd0;
        end
        else begin
            a_req <= a_req;
        end
        
    end

    //请求信号a_req跨时钟域处理 到clk_b时钟域并打拍形成二级同步
    reg     b1_q, b2_q;
    always@(posedge clk_b or posedge rst) begin
        if(rst) begin
            b1_q <= 1'd0;
            b2_q <= 1'd0;
        end
        else begin
            b1_q <= a_req;
            b2_q <= b1_q;
        end
    end

    //b时钟域对a时钟域的请求信号产生响应 生成时钟域b内的数据使能信号,生成条件是b2_q信号的上升沿
    reg     b2_q_d1;
    reg     b2_q_d2;
    reg     b2_q_d3;
    //wire     b_en;
    reg     ack_b;//回ack_b信号
    always@(posedge clk_b or posedge rst) begin
        if(rst) begin
            b2_q_d1 <= 1'd0;
            b2_q_d2 <= 1'd0;
            b2_q_d3 <= 1'd0;
            ack_b <= 1'd0;            
        end
        else begin
            b2_q_d1 <= b2_q;
            b2_q_d2 <= b2_q_d1;
            b2_q_d3 <= b2_q_d2;
            ack_b <= b2_q_d2;//b2_q_d2是a_req打拍同步到clk_b时钟域再打拍稳定形成的,后面要传回clk_a  与data_b_out同步,开始传数据
        end
    end
    assign b_en = ~b2_q_d3 && b2_q_d2; //b_en信号生成

    //b_en有效,则表示数据有效,可以采样a时钟域的数据了
    always@(posedge clk_b or posedge rst) begin
        if(rst) begin
            data_b_out <= 4'd0;
        end
        else if(b_en) begin
            data_b_out <= data_a_in;
        end
    end

    //响应信号同步到a时钟域
    //a2_q上升沿作为a_req拉低的条件
    reg a1_q, a2_q;
    reg a3_q ;
    wire a2_q_pos;
    always@(posedge clk_a or posedge rst) begin
        if(rst) begin
            a1_q <= 1'd0;
            a2_q <= 1'd0;
            a3_q <= 1'd0;
        end
        else begin
            a1_q <= ack_b;//将ack_b回到clk_a时钟域  再打两拍 
            a2_q <= a1_q;//该信号说明clk_b时钟域已经开始稳定采样信号了
            a3_q <= a2_q;
        end
    end
    assign a2_q_pos = ~a3_q && a2_q;//取下降沿

    assign ack_a = a2_q; //此信号作为a_en的反馈信号,a_en取此信号的下降沿


endmodule
tb_handshake_cdc
 `include "../rtl/handshake_cdc.v"
module tb_handshake_cdc;

    reg       clk_a        ;
    reg       clk_b        ;
    reg       rst          ;
    reg       a_en         ; //来自于外部的使能信号,脉冲持续一个时钟周期
    reg [3:0] data_a_in    ; //外部输入信号
    wire [3:0] data_b_out  ;
    wire       b_en        ;
    wire       ack_a       ;

    initial begin
        clk_a = 1'd0;
        forever begin
            #2.2 clk_a = ~clk_a;
        end
    end

    initial begin
        clk_b = 1'd0;
        forever begin
            #3.7 clk_b = ~clk_b;
        end
    end

    initial begins
        rst = 1'd1;
        #15
        @(negedge clk_a);
        rst = 1'd0;


    end

    reg ack_a_d1;
initial begin
    #1000 $finish;
end
initial begin
    $dumpfile("tb_handshake_cdc.vcd");
    $dumpvars();
end
    always@(posedge clk_a or posedge rst) begin
        if(rst) begin
            a_en <= 1'b0;
            ack_a_d1 <= 1'd0;
        end
        else begin
            ack_a_d1 <= ack_a;
            a_en <= ~ack_a && ack_a_d1;
        end
    end

    always@(posedge clk_a or posedge rst) begin
        if(rst) begin
            data_a_in <= 4'd0;
        end
        else if(a_en) begin
            data_a_in <= $random;
        end
    end

    handshake_cdc handshake_cdc_inst(
        .clk_a       ( clk_a       ),
        .clk_b       ( clk_b       ),
        .rst         ( rst         ),
        .a_en        ( a_en        ),
        .data_a_in   ( data_a_in   ),

        .data_b_out  ( data_b_out  ),
        .b_en        ( b_en        ),
        .ack_a       ( ack_a       )
    );
endmodule

 

异步FIFO

  • 异步FIFO是处理多bit信号跨时钟域最常用的方法,可以完美解决慢到快或者是快到慢时钟域的多bit信号传输问题。
  • 异步FIFO是双口RAM的一个封装,其存储器本质上还是一个RAM,只不过对其添加了某些控制,使其能实现先进先出的功能,该功能十分强大,可以广泛应用。
  • 真双口RAM可以实现在一端存储,另一端读取的功能,两端的时钟可以不同步,将数据存入一个容器,再取出来,这个过程再双口RAM的两端完全不存在亚稳态问题。
  • 由于异步FIFO的实现中也存在数据的存取问题,和双口RAM类似,只要处理好空满信号的控制及它们的跨时钟域的问题,就可以使用FIFO解决多比特信号的跨时钟域问题。

关于异步FIFO,在另外一篇有讲,在此不再赘述。

posted @   million_yh  阅读(589)  评论(4编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示