4. 控制信号同步:单比特信号同步、边沿检测同步器、脉冲同步器
时钟域处理里,控制信号的同步是最基础、也最容易出问题的地方。我这些年做DFT,见过太多因为控制信号没处理好导致的芯片功能失效。说白了,跨时钟域传递控制信号,核心就三个字:防亚稳态。
你想想看,一个信号从时钟域A跳到时钟域B,如果刚好在时钟B的采样窗口边缘变化,那采样结果可能既不是0也不是1,而是介于两者之间的电压值。这个不确定状态如果传播下去,整个逻辑就乱了。嗯,这就是亚稳态。
那怎么解决?三种经典方案:单比特同步器、边沿检测同步器、脉冲同步器。咱们一个一个说。
4.1 单比特信号同步器
这是最基础的同步结构。说白了就是两级触发器串联。第一级采样可能采到亚稳态,但第二级采样时,第一级的输出已经稳定了。这样亚稳态就被「憋」在了第一级,不会传到后面的逻辑。
我个人习惯,代码里这样写:
module sync_2ff (
input wire clk_b,
input wire rst_n,
input wire data_in, // 来自时钟域A
output wire data_out // 同步到时钟域B
);
reg sync_reg1, sync_reg2;
always @(posedge clk_b or negedge rst_n) begin
if (!rst_n) begin
sync_reg1 <= 1'b0;
sync_reg2 <= 1'b0;
end else begin
sync_reg1 <= data_in;
sync_reg2 <= sync_reg1;
end
end
assign data_out = sync_reg2;
endmodule
这里有个细节要注意:第一级触发器的时序约束。我在项目中遇到过,后端工具默认会对所有路径做时序检查。但跨时钟域路径,你根本没法保证满足建立时间。所以必须用 set_false_path 或 set_clock_groups 把这条路径设为异步路径。否则工具会报一堆时序违例,你看着头疼,它改着也头疼。
4.2 边沿检测同步器
有时候我们不只要同步电平,还要检测信号的变化。比如某个控制信号从0变1,表示开始测试。这时候就需要边沿检测同步器。
结构很简单:在两级同步器后面再加一级触发器,然后用组合逻辑比较最后两级的值。
module edge_detect_sync (
input wire clk_b,
input wire rst_n,
input wire data_in,
output wire rising_edge,
output wire falling_edge
);
reg sync_reg1, sync_reg2, sync_reg3;
always @(posedge clk_b or negedge rst_n) begin
if (!rst_n) begin
{sync_reg3, sync_reg2, sync_reg1} <= 3'b0;
end else begin
sync_reg1 <= data_in;
sync_reg2 <= sync_reg1;
sync_reg3 <= sync_reg2;
end
end
assign rising_edge = sync_reg2 & ~sync_reg3;
assign falling_edge = ~sync_reg2 & sync_reg3;
endmodule
你看,rising_edge 在检测到上升沿时只拉高一个时钟周期。这个脉冲宽度正好是时钟域B的一个周期。我曾经在ATE测试时遇到一个问题:ATE给的测试使能信号宽度只有5ns,但我的同步器时钟周期是10ns。结果边沿检测根本没抓到。后来我加了一个展宽电路才解决。
4.3 脉冲同步器
脉冲同步器是专门处理「窄脉冲」跨时钟域问题的。什么叫窄脉冲?就是宽度小于目标时钟周期的脉冲。比如源时钟域100MHz,目标时钟域10MHz,源时钟域的一个脉冲宽度只有10ns,目标时钟域采样周期是100ns。你直接用两级同步器,大概率采不到。
脉冲同步器的思路是:把脉冲转换成电平变化,同步后再还原成脉冲。具体做法是用一个Toggle触发器,每次来脉冲就翻转一次。这样电平变化至少保持一个源时钟周期,目标时钟域肯定能采到。
module pulse_sync (
input wire clk_a,
input wire clk_b,
input wire rst_n,
input wire pulse_in, // 时钟域A的脉冲
output wire pulse_out // 同步到时钟域B的脉冲
);
// 时钟域A:脉冲转电平
reg toggle_a;
always @(posedge clk_a or negedge rst_n) begin
if (!rst_n)
toggle_a <= 1'b0;
else if (pulse_in)
toggle_a <= ~toggle_a;
end
// 两级同步器同步电平
reg sync1, sync2;
always @(posedge clk_b or negedge rst_n) begin
if (!rst_n) begin
sync1 <= 1'b0;
sync2 <= 1'b0;
end else begin
sync1 <= toggle_a;
sync2 <= sync1;
end
end
// 时钟域B:电平还原成脉冲
reg sync2_delay;
always @(posedge clk_b or negedge rst_n) begin
if (!rst_n)
sync2_delay <= 1'b0;
else
sync2_delay <= sync2;
end
assign pulse_out = sync2 ^ sync2_delay;
endmodule
这个结构有个限制:两个脉冲之间的间隔必须大于同步器的延迟。否则第二个脉冲来的时候,第一个脉冲的电平还没同步完,Toggle会连续翻转两次,导致目标时钟域只看到一个脉冲。我记得在某个DFT项目中,测试向量里连续给了两个紧挨着的扫描使能脉冲,结果同步器漏掉了第二个。查了三天才找到原因。
- 单比特同步器:适用于慢变信号,两级防亚稳态
- 边沿检测同步器:需要检测信号跳变时使用,注意脉冲宽度要求
- 脉冲同步器:窄脉冲跨时钟域专用,注意最小脉冲间隔
4.4 三种同步器的选择指南
实际项目中怎么选?我一般这样判断:
| 信号类型 | 推荐方案 | 典型应用场景 |
|---|---|---|
| 电平信号(如复位、模式选择) | 单比特同步器 | DFT测试模式切换、复位同步 |
| 边沿触发信号(如开始、结束标志) | 边沿检测同步器 | 测试开始信号、状态机跳转条件 |
| 窄脉冲信号(如单周期脉冲) | 脉冲同步器 | 扫描使能脉冲、测试时钟门控信号 |
嗯,这里要特别提醒:千万不要把脉冲同步器用在电平信号上。我曾经见过有人把测试模式选择信号用脉冲同步器处理,结果每次模式切换都产生一个脉冲,而不是保持电平。那后果就是测试模式一直在跳,根本没法正常工作。
另外,如果你用的是Synopsys的DesignWare或Cadence的Inferface IP,它们都有现成的同步器单元。我建议直接例化这些库单元,比自己手写的RTL更可靠。毕竟人家是经过硅验证的,亚稳态MTBF(平均故障间隔时间)都算得清清楚楚。
好了,控制信号同步就讲到这里。下一章咱们聊聊数据总线的同步,那个更复杂,涉及到握手协议和FIFO。到时候再细说。