跨时钟域传递多比特信号的问题是,在同步多个信号到一个时钟域时将可能偶发数据变化歪斜(Skew),这种数据歪斜最终会在第二个时钟域的不同时钟上升沿上被采集。即便能够完美地控制和匹配这些多比特信号的走线长度,随着芯片衬底工艺不同,上升和下降的时间也会不一样,这些因素都会产生足够的歪斜导致在精心匹配的多条信号上采样失败。
为了避免这种多比特跨时钟域信号上的采样歪斜,需要掌握一些不一样的方法。这些方法大致可以分为以下几种:
一般适用于多个控制信号且控制信号之间有一定逻辑关系,可以合并成单比特信号。
例如接收域有一个寄存器,它需要一个加载(Load)信号和一个使能(Enable)信号来加载一个数值到寄存器。如果加载和使能信号在发送时钟域的同一个时钟沿被驱动有效(即两个控制信号需要同时有效),那么这两个控制信号之间就有可能存在产生一个小歪斜的机会,这就可能导致在接收时钟域中这两个信号被同步到不同的时钟周期。在这种情况下,数据是不能被加载到寄存器的。
解决上述问题的方法非常简单,就是将加载和使能两个控制信号融合成一个单比特控制信号(这两个控制信号本身相同,且同时有效),如图下图所示。
在发送时钟域先后有两个使能信号(注意不同于之前的同时有效),同步到接收时钟域用于控制两级流水寄存器寄存数据。问题是在第一个时钟域中,B_en1控制信号正好稍微在B_en2有效前结束有效,这就导致在接收时钟域时钟上升沿采集B_en1和B_en2脉冲时产生一个细微的缝隙。如图下图所示,
同步后两个使能控制信号间隔了两个时钟周期,而不是流水一个时钟周期。这样导致数据a2没有及时加载到第二个寄存器。这里需要注意的是,a3会在Aq2_en2有效的时候加载到第二个寄存器,但是在设计要求a1,a2,a3的更新符合一个周期的流水,所以设计出现了问题。
首先是在发送时钟域将两个使能控制信号融合为一个控制信号,其次是要增加一个额外的寄存器将同步后的使能控制信号寄存一拍,这样数据和控制信号形成匹配的流水,如下图所示:
MUX同步器为了同步数据,当数据在源触发器准备就绪时,在源时钟域中产生控制脉冲。然后,根据源域和目标域之间的时钟比,使用两个触发器同步器或脉冲同步器(触发或握手)来同步控制脉冲。同步控制脉冲用于在目的域内对总线上的数据进行采样。以下两种情况时可以使用MUX同步器:
以牛客网练习题为例:
描述:在data_en为高期间,data_in将保持不变,data_en为高至少保持3个B时钟周期。表明,当data_en为高时,可将数据进行同步。本题中data_in端数据变化频率很低,相邻两个数据间的变化,至少间隔10个B时钟周期。电路的接口如下图所示。端口说明如下表所示:
实现电路图如下:A时钟域异步复位和B时钟域异步复位未在图中标注
代码实现:
module mux_sync(clk_a, clk_b, arst_n, brst_n, data_in, data_out, data_en);
/********************参数定义********************/
/*********************IO 说明********************/
input wire clk_a ;//A时钟域时钟(发送域)
input wire clk_b ;//B时钟域时钟(接收域)
input wire arst_n ;//A时钟异步复位
input wire brst_n ;//B时钟异步复位
input wire data_en ;//A时钟域有效信号,高电平有效
input wire [3:0] data_in ;//A时钟域数据输入
output reg [3:0] data_out;//B时钟域数据输出
/********************** 内部信号声明 **********************/
reg [3:0] data_in_reg; //输入数据暂存寄存器
reg a_data_en;
reg b_data_en;
reg b_date_en_reg;
/*************************功能定义*************************/
/*数据暂存*/
always@(posedge clk_a or negedge arst_n)
begin
if(!arst_n)
data_in_reg <= 1'b0;
else
data_in_reg <= data_in;
end
//使能信号在A时钟域用一个D触发器暂存
always@(posedge clk_a or negedge arst_n)
begin
if(!arst_n)
a_data_en <= 1'b0;
else
a_data_en <= data_en;
end
//使能信号在B时钟域打两拍(两级电平同步器)
always@(posedge clk_b or negedge brst_n)
begin
if(!brst_n)
begin
b_date_en_reg<= 1'b0;
b_data_en <= 1'b0;
end
else
begin
b_date_en_reg <= a_data_en;
b_data_en <= b_date_en_reg;
end
end
/*根据同步到B时钟域的使能信号b_data_en,更新输出。*/
always@(posedge clk_b or negedge brst_n)
begin
if(!brst_n)
data_out <= 4'b0;
else
begin
data_out <= b_data_en ? data_in_reg : data_out;
end
end
endmodule
RTL视图:
若上题中为B时钟域(接收域)的时钟频率时A时钟域(发送域)的时钟频率的几十倍,甚至上百倍。且data_en为脉冲信号时,data_en在在快时钟域打完几拍的时间相对于慢时钟域是非常短暂的,此时慢时钟域中的多bit数据信号可能还处于冒险中间态,则此时选通进入快时钟域的数据就是“毛刺”。
可以在接收域使用边沿同步器,检测data_en的下降沿,以保证此时的多比特数据一定是稳定的。
实现电路图如下:A时钟域异步复位和B时钟域异步复位未在图中标注
代码实现
module mux_sync(clk_a, clk_b, arst_n, brst_n, data_in, data_out, data_en);
/********************参数定义********************/
/*********************IO 说明********************/
input wire clk_a ;//A时钟域时钟(发送域)
input wire clk_b ;//B时钟域时钟(接收域)
input wire arst_n ;//A时钟异步复位
input wire brst_n ;//B时钟异步复位
input wire data_en ;//A时钟域有效信号,高电平有效
input wire [3:0] data_in ;//A时钟域数据输入
output reg [3:0] data_out;//B时钟域数据输出
/********************** 内部信号声明 **********************/
reg [3:0] data_in_reg; //输入数据暂存寄存器
reg a_data_en;
reg b_data_en;
reg b_date_en_reg;
reg edge_reg;
wire edge_flag;
/*************************功能定义*************************/
/*数据暂存*/
always@(posedge clk_a or negedge arst_n)
begin
if(!arst_n)
data_in_reg <= 1'b0;
else
data_in_reg <= data_in;
end
//使能信号在A时钟域用一个D触发器暂存
always@(posedge clk_a or negedge arst_n)
begin
if(!arst_n)
a_data_en <= 1'b0;
else
a_data_en <= data_en;
end
//使能信号在B时钟域打两拍(两级电平同步器)
always@(posedge clk_b or negedge brst_n)
begin
if(!brst_n)
begin
b_date_en_reg<= 1'b0;
b_data_en <= 1'b0;
end
else
begin
b_date_en_reg <= a_data_en;
b_data_en <= b_date_en_reg;
end
end
//下降沿检测
always@(posedge clk_b or negedge brst_n)
begin
if(!brst_n)
edge_reg <= 1'b0;
else
edge_reg <= b_data_en;
end
assign edge_flag = ~b_data_en & edge_reg;
/*根据同步到B时钟域的使能信号b_data_en,更新输出。*/
always@(posedge clk_b or negedge brst_n)
begin
if(!brst_n)
data_out <= 4'b0;
else
data_out <= edge_flag ? data_in_reg : data_out;
end
endmodule
多周期路径规划是一种通用的安全传递多比特跨时钟域信号技术。多周期路径规划是指在传输非同步数据到接收时钟域时配上一个同步的控制信号,数据和控制信号被同时发送到接收时钟域,同时控制信号在接收时钟域使用两级寄存器同步到接收时钟域,使用此同步后的控制信号来加载数据,这样数据就可以在目的寄存器被安全加载。 使用这种技术有以下两个好处:
个人感觉多周期路径同步法包含MUX同步器和握手同步方式。其中MUX同步器相当于开环的多周期路径同步。而完全握手方式相当于带确认反馈的多周期路径同步,部分握手方式相当于带反馈的闭环多周期路径同步。
解决总线同步问题最基本的方法是保持寄存器和握手信号。保持寄存器用于进行数据锁存和采样,握手信号指示接收域中的电路何时可以对总线进行采样,以及发送域电路何时可以更新保持寄存器当前的内容。握手协议是一种闭环的数据同步方法,更像是基于MUX同步器的应答机制的扩展,其主要结构如下图所示,
握手同步方式分为完全握手和部分握手两种基本类型。 每种类型的握手都使用上述的结构,但每种握手都有自己的设计权衡。握手信号指示接收域何时可以对总线数据进行采样,以及发送域何时可以更新当前数据锁存器中保存的内容。数据路径主要由发送时钟域的数据锁存模块以及接收时钟域的数据采集模块组成。
完全握手的特点是发送域和接收域两端在发送请求或者终止请求之前都需要等待对端的回应。完全握手在收发两侧都使用的是电平同步器。当接收域需要通知发送域它正在积极处理请求时,通常会采用完全握手。完全握手要求发送域推迟它的下一个请求,直到它检测到无效的确认信号。以下是完全握手的流程和时序:
用前缀t_表示发送域,用前缀r_表示接收域,发送时钟用t_clk表示,接收时钟用r_clk表示。数据由发送域向接收域传输;以下是完全握手的工作步骤:
部分握手协议则可以缩短握手的过程,提高数据传输的效率,但使用部分握手信号时,收发双方不等对方响应就中止各自的请求,并继续执行握手命令序列。部分握手类型比全握手类型在可靠性方面稍弱,因为握手信号并不指示各自电路的状态,收发侧都必须保存状态信息。但是,由于无需等待其他电路的响应,完整的时间周期花费时间较少。有两种部分握手方案,区别在于握手所采用的信号类型。一种所用的部分握手方法在发送侧使用电平信号来发送请求,在接收侧使用脉冲信号来发送响应;另一种所用的部分握手方法在发送侧与接收侧都使用脉冲信号。
在第一种部分握手方式中,接收域对请求信号使用电平同步器,发送域对确认信号使用脉冲同步器。在此握手协议中,应答脉冲仅在接收域检测到请求信号时出现。这允许发送域通过控制其请求信号的时序来控制脉冲进入同步器的间隔。为了确保部分握手同步方式成立,发送域中止请求信号至少持续发送域1个时钟周期长,否则接收域就不能区别前一个请求和新的请求。以下是部分握手1的流程和时序:
用前缀t_表示发送域,用前缀r_表示接收域,发送时钟用t_clk表示,接收时钟用r_clk表示。数据由发送域向接收域传输;以下是部分握手1的工作步骤:
完整的部分握手1在发送域中最多需要3个周期,在接收域中最多需要5个周期。部分握手1在发送域中使用的时钟周期比完全握手少2个时钟周期,在接收域中使用的时钟周期少1个时钟周期。
在第二种部分握手方式中,发送域用一个单时钟宽度脉冲发出它的请求,而接收域也用一个单时钟宽度脉冲响应这个请求。这种情况下,发送域和接受域都需要保存状态,以指示请求正待处理。这种握手类型使用的是脉冲同步器,但如果其中一个电路时钟比另一个电路时钟快两倍,则可以用边沿检测同步器来代替。完整的时序是:发送侧时钟域最多2个时钟周期,接收侧最多3个时钟周期。以下是部分握手2的流程和时序:
用前缀t_表示发送域,用前缀r_表示接收域,发送时钟用t_clk表示,接收时钟用r_clk表示。数据由发送域向接收域传输;以下是部分握手1的工作步骤:
分别编写一个数据发送模块和一个数据接收模块,模块的时钟信号分别为clk_a,clk_b。两个时钟的频率不相同。数据发送模块循环发送0-7,在每个数据传输完成之后,间隔5个时钟,发送下一个数据。请在两个模块之间添加必要的握手信号,保证数据传输不丢失。
模块的接口信号图如下:
data_req和data_ack的作用说明:
data_req表示数据请求接受信号。当data_out发出时,该信号拉高,在确认数据被成功接收之前,保持为高,期间data应该保持不变,等待接收端接收数据。
当数据接收端检测到data_req为高,表示该时刻的信号data有效,保存数据,并拉高data_ack。
当数据发送端检测到data_ack,表示上一个发送的数据已经被接收。撤销data_req,然后可以改变数据data。等到下次发送时,再一次拉高data_req。
输入描述:
clk_a:发送端时钟信号
clk_b:接收端时钟信号
rst_n:复位信号,低电平有效
data_ack:数据接收确认信号
输出描述:
data:发送的数据
data_req:请求接收数据
数据发送模块:
module data_driver(
input clk_a, //发送端时钟信号
input rst_n, //复位信号,低电平有效
input data_ack, //数据接收确人信号
output reg [3:0] data, //发送数据
output reg data_req //请求接收信号
);
/********************参数定义********************/
/*********************IO 说明********************/
/********************** 内部信号声明 **********************/
reg [2:0] cnt_reg;
reg data_ack_sync1;
reg data_ack_sync2;
/*************************功能定义*************************/
//计数
always@(posedge clk_a or negedge rst_n)
begin
if(!rst_n)
cnt_reg <= 3'd0;
else if(data_ack_sync1 && !data_ack_sync2 == 1'b1)
cnt_reg <= 3'd0;
else if(data_req == 1'b1)
cnt_reg <= cnt_reg;
else
cnt_reg <= cnt_reg + 1'b1;
end
//data_ack两级同步
always@(posedge clk_a or negedge rst_n)
begin
if(!rst_n)
begin
data_ack_sync1 <= 1'b0;
data_ack_sync2 <= 1'b0;
end
else
begin
data_ack_sync1 <= data_ack;
data_ack_sync2 <= data_ack_sync1;
end
end
//请求接收信号
always@(posedge clk_a or negedge rst_n)
begin
if(!rst_n)
data_req <= 1'b0;
else if(cnt_reg == 3'd4)
data_req <= 1'b1;
else if(data_ack_sync2 == 1'b1)
data_req <= 1'b0;
else
data_req <= data_req;
end
//发送数据
always@(posedge clk_a or negedge rst_n)
begin
if(!rst_n)
data <= 4'd0;
else if(data == 4'd7 && data_ack_sync2 == 1'b1 && data_req == 1'b1 )
data <= 4'd0;
else
begin
if(data_ack_sync2 == 1'b1 && data_req == 1'b1 )
data <= data + 1'b1;
else
data <= data;
end
end
endmodule
数据接收模块
module data_receiver(
input clk_b, //接收端时钟信号
input rst_n, //复位信号,低电平有效
input [3:0] data, //接收数据
input data_req, //请求接收信号
output reg data_ack//数据接收确人信号
);
/********************参数定义********************/
/*********************IO 说明********************/
/********************** 内部信号声明 **********************/
reg data_req_sync1;
reg data_req_sync2;
/*************************功能定义*************************/
//data_req两级同步
always@(posedge clk_b or negedge rst_n)
begin
if(!rst_n)
begin
data_req_sync1 <= 1'b0;
data_req_sync2 <= 1'b0;
end
else
begin
data_req_sync1 <= data_req;
data_req_sync2 <= data_req_sync1;
end
end
//数据接收确人信号
always@(posedge clk_b or negedge rst_n)
begin
if(!rst_n)
data_ack <= 1'b0;
else if(data_req_sync2 == 1'b1)
data_ack <= 1'b1;
else
data_ack <= 1'b0;
end
endmodule
通过编码的方式将多位信号转化为每次只有一位变化的信号,将“多比特”的跨时钟域变换成“单比特”进行处理。格雷码的特点是相邻的两个编码之间只有1位不同,消除了在同一个时钟沿,多比特信号或者数据同时变化所带来的跨时钟域问题。
应用局限:只有在数据在相数值间连续变化的情况下才有用,不适用于大多数信号传输或者数据传输的情况。一般用于跨时钟域的计数器设计,如异步FIFO的读写地址,其他场合基本没有用到。
数据流的传输与指示信号不同在于:数据流大多具有连续性,及背靠背传输;数据流要求信号具有较快的传输速度。跨时钟域传输数据用得最多的方法就是使用先人先出(FIFO)结构。FIFO可以用于在两个异步时钟域之间传输多比特信号。通常看到的FIFO应用包括在两个标准总线之间传输数据,以及从可突发访问的存储器中读出数据或者对其写入数据。
异步FIFO有两个端口,一个端口写人输入数据,另一个端口读出数据。两个端口工作在相互独立的时钟域内,通过各自的指针(地址)来读写数据。由于每个端口工作在相互独立的时钟域内,因此读写操作可以独立实现并且不会出现任何差错。FIFO有满标志和空标志,当FIFO变满时,应停止写操作,直到FIFO中出现空闲空间;同样,当FIFO为空时,应停止读操作,直到有新的数据被写人FIFO中。异步FIFO结构如下图所示
1.Synchronizer techniques for multi-clock domain SoCs & FPGAs
2.Clock Domain Crossing Techniques for FPGA
3.Clock Domain Crossing (CDC) Design & Verification Techniques Using SystemVerilog
4.2021年秋招面经分享·华为【海思·芯片与器件工程师】