快时钟域相比慢时钟域采样速度更快,也就是说从慢时钟域来到快时钟域的信号一定可以被采集到。既然快时钟一定可以采集到慢时钟分发的数据,那么考虑的问题就只剩下如何保证采样到的信号质量!最常用的同步方法是双级触发器缓存法,俗称延迟打拍法。信号从一个时钟域进入另一个时钟域之前,将该信号用两级触发器连续缓存两次,可有效降低因为时序不满足而导致的亚稳态问题。
具体如下图所示:来自慢时钟clk1的信号在clk2被多次采样(信号在clk1持续一个时钟周期,在clk2持续三个时钟周期),如果只需要在clk2持续一个时钟周期,可以采用边沿检测即可得到signal4;
慢时钟域相比快时钟域采样速度更慢,也就是说从快时钟域来到慢时钟域的信号极有可能被漏采。一般要求在接收时钟域中采样信号要保持三个时钟边沿的时间(也就是1.5倍的采样时钟周期)才会避免出现漏采。也就是快到慢跨时钟域的核心是如何延长信号长度!
对于电平信号而言(一般电平信号持续时间足够长),信号长度可以得到保证,所以正常采用两级同步器采样即可。
对于脉冲信号而言(一般脉冲信号持续时间很短),长度难以得到保证,需要对信号进行延长。目前,常用延长方法有两种:
然而,除了“握手协议”以外其他两种方法都是有缺陷、有限制的,具体如下图所示:
可以看到无论是电平还是脉冲信号使用起来都是有限制的,因为它们采用的都是无反馈的开环设计(详细可查看博文跨时钟传输——单比特)。采用闭环的反馈设计可以避免这些问题,具体流程如下:
verilog代码
//单比特快到慢“握手协议”
module cdc_sbit_handshake(
input aclk, //快时钟
input arst_n, //快时钟域复位信号
input signal_a,//快时钟域信号
input bclk, //慢时钟
input brst_n, //慢时钟域复位信号
output signal_b//慢时钟域输出信号
);
//慢时钟域信号展宽直至反馈信号回来再恢复
reg req;//寄存慢时钟域展宽信号
reg ack_r0;//反馈信号
always@(posedge aclk or negedge arst_n) begin
if(!arst_n) begin
req <= 1'b0;
end
else if(signal_a) begin
req <= 1'b1; //信号展宽
end
else if(ack_r0) begin
req <= 1'b0; //反馈信号到来时恢复
end
end
//展宽信号跨时钟同步至慢时钟域
reg req_r0;
reg req_r1;
reg req_r2;
always@(posedge bclk or negedge brst_n) begin
if(!brst_n)begin
{req_r2,req_r1,req_r0} <= 3'b0;
end
else begin
{req_r2,req_r1,req_r0} <= {req_r1,req_r0,req};
end
end
//生成反馈信号并同步至快时钟域
reg ack;
always@(posedge aclk or negedge arst_n) begin
if(!arst_n) begin
{ack_r0,ack} <= 2'b0;
end
else begin
{ack_r0,ack} <= {ack,req_r1};
end
end
//信号上升沿检测,让输出持续一个慢时钟周期
assign signal_b = ~req_r2 & req_r1;
endmodule
Testbench
`timescale 1ns/1ps //仿真时间单位1ns 仿真时间精度1ps
module cdc_sbit_handshake_tb;
//信号申明
reg aclk;
reg arst_n;
reg signal_a;
reg bclk;
reg brst_n;
wire signal_b;
//例化
cdc_sbit_handshake u_cdc_sbit_handshake(
.aclk (aclk),
.bclk (bclk),
.arst_n (arst_n),
.brst_n (brst_n),
.signal_a (signal_a),
.signal_b (signal_b)
);
//快时钟域慢时钟生成
always #5 aclk =~ aclk;
always #15 bclk =~ bclk;
//初始信号赋值与激励
initial begin
signal_a = 0;
aclk = 0;
bclk = 0;
arst_n = 1;
brst_n = 1;
#15;
arst_n = 0;
brst_n = 0;
#15;
arst_n = 1;
brst_n = 1;
signal_a = 1;
#10;
signal_a = 0;
end
endmodule
仿真结果
先给结论:多比特信号不能用二级同步器跨时钟传输,哪怕使用格雷码大部分情况也不行,只有在格雷码自增或自减顺序变化才可以跨时钟传输。对于多比特数据,在进行传输时候会因为时序问题导致所有寄存器不会同时翻转(不是不翻转,是不同时翻转!),所以容易在跨时钟传输的时候出现中间态。使用格雷码可以避免这种现象,但是当格雷码不是按计数顺序变化(非顺序变化相当于每次变化不止一位),这同样是不允许的,因为格雷码每次只有一位发生变化的前提是,数据是递增或递减的。比如异步FIFO中格雷码可以通过二级同步器进行CDC传输。
慢到快这种情况在快时钟接收端是一定能够采样得到的,但是根据上文可知,多比特不适合采用二级同步器直接传输采样,因为在传输过程中有多位同时变化,那么有什么解决办法呢?解决办法是在传输过程中不变化!所以必须在写入使能信号有效时传输!
传输非同步数据到接收时钟域时配上一个同步的控制信号,数据和控制信号被同时发送到接收时钟域,同时控制信号在接收时钟域使用两级寄存器同步到接收时钟域,使用此同步后的控制信号来加载数据,这样数据就可以在目的寄存器被安全加载。
具体代码可参考链接:Verilog 跨时钟域传输:慢到快
verilog代码
//同步模块工作时钟为 100MHz 的模块
//异步数据对来自工作时钟为 20MHz 的模块
module delay_sample(
input rstn,
input clk1,
input [31:0] din,
input din_en,
input clk2,
output [31:0] dout,
output dout_en);
//sync din_en
reg [2:0] din_en_r ;
always @(posedge clk2 or negedge rstn) begin
if (!rstn) din_en_r <= 3'b0 ;
else din_en_r <= {din_en_r[1:0], din_en} ;
end
wire din_en_pos = din_en_r[1] && !din_en_r[2] ;
//sync data
reg [31:0] dout_r ;
reg dout_en_r ;
always @(posedge clk2 or negedge rstn) begin
if (!rstn)
dout_r <= 'b0 ;
else if (din_en_pos)
dout_r <= din ;
end
//dout_en delay
always @(posedge clk2 or negedge rstn) begin
if (!rstn) dout_en_r <= 1'b0 ;
else dout_en_r <= din_en_pos ;
end
assign dout = dout_r ;
assign dout_en = dout_en_r ;
endmodule
时序结构如下图所示:
但如果慢时钟域没有数据使能信号 din_en, 或数据使能信号一直有效,此时在快时钟域对数据使能信号进行上升沿检测的方法将会失效。因为数据使能信号一直有效,除了第一个数据,快时钟域将无法检测到后继数据的传输时刻。
解决方法就是,在快时钟域对慢时钟信号的边沿进行检测。
快到慢必然会伴随着漏采的风险,根据单比特CDC传输的方法可以知道避免的方法就是延长信号的长度,所以需要带写入的使能信号对信号进行延长。此处任采用握手的方式,完全握手具体原理如下图所示:
优点:可以解决快时钟域向慢时钟域过渡的问题,且其适用的范围很广。
缺点:实现较为复杂,特别是其效率不高,在对设计性能要求较高的场合应该慎用。
这一部分具体可以查看链接:FPGA学习笔记——跨时钟域(CDC)设计之多bit信号同步
verilog代码
module data_driver(
input clk_a, //发送端时钟信号
input rst_n, //复位信号,低电平有效
input data_ack, //数据接收确人信号
input clk_b, //接收端时钟信号
input rst_n, //复位信号,低电平有效
input [3:0] data, //接收数据
input data_req, //请求接收信号
output reg data_ack//数据接收确人信号
);
/********************** 发送端 **********************/
reg [3:0] data; //发送数据
reg data_req ; //请求接收信号
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
/********************** 接收端 **********************/
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
关于异步FIFO具体可以看看这篇:异步FIFO设计原理与设计方法以及重要问题汇总(包含verilog代码|Testbench|仿真结果),对异步FIFO介绍很详细并且总结了若干重要问题。
FIFO 是一种“先进先出队列”,数据从一头写入,从另一头读出,读出顺序和写入顺序一模一样。因为队列空间有限,因此一般把队列设计为环形。对于队列来说,最重要的事情是不能在队空的时候读数、不能在队满的时候写数。一般通过比较读写指针来获得“队空”和“队满”信息。异步FIFO常常用在高速数据跨时钟域的场景上。
异步FIFO主要由五部分组成:RAM、写控制端、读控制端、两个时钟同步端
双端口RAM:此处为伪双端口RAM进行数据存储与读出,有两组数据线、地址线、时钟线。
写控制端:写指针与满信号产生器,用于判断是否可以写入数据,写操作时,写使能有效且FIFO未满。
读控制端:读指针与空信号产生器,用于判断是否可以读取数据,读操作时,读使能有效且FIFO未空。
两个时钟同步端:读指针同步到写指针域进行“写满”判断,写指针同步到读指针域进行“读空”判断。
verilog代码
//深度为8,数据位宽为8的异步FIFO
module async_fifo #(
parameter DATA_DEPTH = 8, //深度为8
parameter DATA_WIDTH = 8, //数据位宽为8
parameter PTR_WIDTH = 3 //读写指针位宽为3
)(
input [DATA_WIDTH - 1 : 0] wr_data, //写数据
input wr_clk, //写时钟
input wr_rst_n, //写时钟复位
input wr_en, //写使能
input rd_clk, //读数据
input rd_rst_n, //读时钟复位
input rd_en, //读使能
output reg fifo_full, //“满”标志位
output reg fifo_empty, //“空”标志位
output reg [DATA_WIDTH - 1 : 0] rd_data //写时钟
);
/*-----------------------------------------------------------------
-----------------------------伪双口RAM模块--------------------------
------------------------------------------------------------------*/
//定义一个宽度为8,深度为DEPTH的8的RAM_FIFO
reg [DATA_WIDTH - 1 : 0] ram_fifo [DATA_DEPTH - 1 : 0];
//写指针计数
reg [PTR_WIDTH : 0] wr_ptr; //信息位+地址位所以指针位宽为4
always@ (posedge wr_clk or negedge wr_rst_n) begin
if(!wr_rst_n) begin
wr_ptr <= 0;
end
else if(wr_en && !fifo_full) begin
wr_ptr <= wr_ptr + 1;
end
else begin
wr_ptr <= wr_ptr;
end
end
//RAM写入数据
wire [PTR_WIDTH -1 : 0] wr_addr;
assign wr_addr = wr_ptr[PTR_WIDTH -1 : 0]; //RAM写数据只需要地址位不需要信息位,所以寻址地址位宽为3
always@ (posedge wr_clk or negedge wr_rst_n) begin
if(!wr_rst_n) begin
ram_fifo[wr_addr] <= 0; //复位
end
else if(wr_en && !fifo_full) begin
ram_fifo[wr_addr] <= wr_data; //数据写入
end
else begin
ram_fifo[wr_addr] <= ram_fifo[wr_addr]; //保持不变
end
end
//读指针计数
reg [PTR_WIDTH : 0] rd_ptr;
always@ (posedge rd_clk or negedge rd_rst_n) begin
if(!rd_rst_n) begin
rd_ptr <= 0;
end
else if(rd_en && !fifo_empty) begin
rd_ptr <= rd_ptr + 1;
end
else begin
rd_ptr <= rd_ptr;
end
end
//RAM读出数据
wire [PTR_WIDTH -1 : 0] rd_addr;
assign rd_addr = rd_ptr[PTR_WIDTH -1 : 0];//RAM读数据只需要地址位不需要信息位,所以寻址地址位宽为3
always@ (posedge rd_clk or negedge rd_rst_n) begin
if(!rd_rst_n) begin
rd_data <= 0; //复位
end
else if(rd_en && !fifo_empty) begin
rd_data <= ram_fifo[rd_addr]; //读数据
end
else begin
rd_data <= rd_data; //保持不变
end
end
/*--------------------------------------------------------------------
------------------------读写指针(格雷码)转换与跨时钟域同步模块------
---------------------------------------------------------------------------------------*/
//读写指针转换成格雷码
wire [PTR_WIDTH : 0] wr_ptr_gray;
wire [PTR_WIDTH : 0] rd_ptr_gray;
assign wr_ptr_gray = wr_ptr ^ (wr_ptr >> 1);
assign rd_ptr_gray = rd_ptr ^ (rd_ptr >> 1);
//写指针同步到读时钟域
//打两拍
reg [PTR_WIDTH : 0] wr_ptr_gray_r1;
reg [PTR_WIDTH : 0] wr_ptr_gray_r2;
always@ (posedge rd_clk or negedge rd_rst_n) begin
if(!rd_rst_n) begin
wr_ptr_gray_r1 <= 0;
wr_ptr_gray_r2 <= 0;
end
else begin
wr_ptr_gray_r1 <= wr_ptr_gray;
wr_ptr_gray_r2 <= wr_ptr_gray_r1;
end
end
//读指针同步到写时钟域
//打两拍
reg [PTR_WIDTH : 0] rd_ptr_gray_r1;
reg [PTR_WIDTH : 0] rd_ptr_gray_r2;
always@ (posedge wr_clk or negedge wr_rst_n) begin
if(!wr_rst_n) begin
rd_ptr_gray_r1 <= 0;
rd_ptr_gray_r2 <= 0;
end
else begin
rd_ptr_gray_r1 <= rd_ptr_gray;
rd_ptr_gray_r2 <= rd_ptr_gray_r1;
end
end
/*--------------------------------------------------------------------------------------
--------------------------------------空满信号判断模块-----------------------------------
---------------------------------------------------------------------------------------*/
//组合逻辑判断写满
always@ (*) begin
if(!wr_rst_n) begin
fifo_full <= 0;
end
else if( wr_ptr_gray == { ~rd_ptr_gray_r2[PTR_WIDTH : PTR_WIDTH - 1],
rd_ptr_gray_r2[PTR_WIDTH - 2 : 0] }) begin
fifo_full <= 1;
end
else begin
fifo_full <= 0;
end
end
//组合逻辑判断读空
always@ (*) begin
if(!rd_rst_n) begin
fifo_empty <= 0;
end
else if(rd_ptr_gray == wr_ptr_gray_r2) begin
fifo_empty <= 1;
end
else begin
fifo_empty <= 0;
end
end
endmodule
Testbench
`timescale 1ns/1ps;//仿真时间单位1ns 仿真时间精度1ps
module async_fifo_tb #(
parameter DATA_DEPTH = 8,
parameter DATA_WIDTH = 8,
parameter PTR_WIDTH = 3
);
//信号申明
reg [DATA_WIDTH - 1 : 0] wr_data;
reg wr_clk;
reg wr_rst_n;
reg wr_en;
reg rd_clk;
reg rd_rst_n;
reg rd_en;
wire fifo_full;
wire fifo_empty;
wire [DATA_WIDTH - 1 : 0] rd_data;
//例化
async_fifo u_async_fifo (
.wr_clk (wr_clk),
.rd_clk (rd_clk),
.wr_rst_n (wr_rst_n),
.rd_rst_n (rd_rst_n),
.wr_en (wr_en),
.rd_en (rd_en),
.wr_data (wr_data),
.rd_data (rd_data),
.fifo_empty (fifo_empty),
.fifo_full (fifo_full)
);
//读写时钟信号生成
always #10 rd_clk = ~rd_clk;
always #5 wr_clk = ~wr_clk;
//信号初始化和赋值
initial begin
wr_clk = 0;
wr_rst_n = 1;
wr_en = 0;
rd_clk = 0;
rd_rst_n = 1;
rd_en = 0;
#10;
wr_rst_n = 0;
rd_rst_n = 0;
#10;
wr_rst_n = 1;
rd_rst_n = 1;
//only write
wr_en = 1;
rd_en = 0;
repeat(10) begin
@(negedge wr_clk) begin
wr_data = {$random}%30;
end
end
//only read
wr_en = 0;
rd_en = 1;
repeat(10) begin
@(negedge rd_clk);
end
rd_en =0;
//read and write
wr_en = 0;
rd_en = 0;
#80;
wr_en = 1;
rd_en = 1;
repeat(20) begin
@(negedge wr_clk) begin
wr_data = {$random}%30;
end
end
end
endmodule
仿真结果
先给结论:多比特信号不能用二级同步器跨时钟传输,哪怕使用格雷码大部分情况也不行,只有在格雷码自增或自减顺序变化才可以跨时钟传输。对于多比特数据,在进行传输时候会因为时序问题导致所有寄存器不会同时翻转(不是不翻转,是不同时翻转!),所以容易在跨时钟传输的时候出现中间态。使用格雷码可以避免这种现象,但是当格雷码不是按计数顺序变化(非顺序变化相当于每次变化不止一位),这同样是不允许的,因为格雷码每次只有一位发生变化的前提是,数据是递增或递减的。比如异步FIFO中格雷码可以通过二级同步器进行CDC传输。
将b_load和b_en信号在b_clk时钟域中合并成一个信号b_lden,然后同步至a_clk中。若果不能合并,比如译码信息则加入一个控制信号,等两个信号稳定了再采样!
多比特:
慢到快:只考虑亚稳态问题,采用延迟打拍法。为需要传输的数据配上一个同步的控制使能信号,数据和控制信号被同时发送到接收时钟域,使用此同步后的控制信号来加载数据(控制信号有效表示数据稳定不变化从而避免传输出错),这样数据就可以在目的寄存器被安全加载。这种方法我们称为MUX同步器法/多周期路径同步法(意思都差不多)。
快到慢:因为考虑时钟采样速度,所以需要延长(使能信号)信号长度。最常用的还是“握手协议”,将使能信号同步后再加载多比特数据。
处理多比特数据跨时钟传输,最常用还是异步FIFO,
一来异步FIFO同时适用快到慢和慢到快两种CDC传输;
二来也能更好地满足数据流具有较快的传输速度要求。
更多可查看个人主页链接
软件版本:Modelsim 10.6c
不定期纠错补充,欢迎随时交流
最后修改日期:2023.6.8
下面几篇文章介绍的很详细,推荐详细阅读
异步FIFO设计原理与设计方法以及重要问题汇总(包含verilog代码|Testbench|仿真结果)
IC学习笔记8——单比特信号的跨时钟域处理方法之“握手信号”
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。