在具体的工程项目中,使用单个时钟固然能够减少违例情况的存在,但是基本上很少存在一种时钟存在的情况。
面对多时钟处理问题时遵循两个指导原则,时钟命名法和设计分区;
时钟命名是对不同处理区域的时钟进行不同区域的命名,比如系统时钟命名为SYS_clk;
发送时钟命名为Tx_clk,接收时钟命名为Rx_clk;
某一个时钟域下的信号明明也就遵循这种原则,比如系统时钟下的信号:sys_rom_addr;
sys_rom_data,这样一看就知道是哪个时钟在哪个时钟域~
设计分区,这是有效设计具有多个时钟模块的一种有效的方法,也就是在多个时钟处理的工程中遵循这种有效处理方法。
1.一个模块只能在一个时钟上工作。
2.从一个时钟域到另一个时钟域交叉的所有信号制作同步器模块,执行将一个时钟域的信号传递到另一个时钟域。也就是做同步~
3.同步器模块越小越好,越简单越好,能打两拍解决的就打两拍解决,能在一个时钟下做完处理的也就在该时钟下做完处理,数据流模式就直接用异步FIFO解决。
划分设计区域的尤其有利于STA分析,因为进入该模块的信号或者出去该模块的信号都是在同一个时钟域下工作,同步器不需要做STA分析,但是一定要确保其满足保持时间建立时间要求。不同时钟下的信号组直接写成伪路径,因为根本不存在这种路径下的信息流动~
信息在交叉时钟域的情况也可分为两种情况一种是控制信号的传输,另一种是数据信号的传输。控制信号的传输直接打两拍就可以~
值得一提的是,使用打两拍的方式只是降低了亚稳态的可能性,但并不能使亚稳态取消,第一拍解析亚稳态,第二排进一步降低亚稳态的可能性。
在大多数控制信号的异步传输情况中,两步同步器设置已经能够解决大多数的同步问题,三级同步器在时钟频率非常高的设计中需要~
数据的多时钟域传输:
常用的两个方法:
1.握手采样 。(数据传输较慢,一次握手传一个)
2.异步FIFO采样。(可以快速的传输数据,但是资源消耗比握手要大)
同步时钟域之间的交叉几种情况:
各时钟源自相同时钟,即各个时钟之间的频率和相位存在关系,而根据频率和相位的关系可以分为以下几类时钟交叉情况:
1.具有相同频率和零相位差的时钟。
2.具有相同频率和恒定相位差的时钟。
3.具有不同频率和可变相位差的时钟。
对于相同频率和零相位差的时钟只要保证组合逻辑延迟满足建立时间和保持时间的要求数据就会正确传输,唯一的要求就是STA干净,这回总该情况下不会出现亚稳态,数据丢失或者是数据不连贯的问题。
对于相同频率和有固定相位差的时钟
在这种情况下对组合逻辑的要求更加严格,这个时序裕量太小了,如果存在组合逻辑就会不满足建立时间要求,导致出现违例,所以一定要限制组合逻辑,这些情况下不需要同步器,严格要求组合逻辑保证STA干净即可。
工具在优化的时候针对这种情况,工具为了保证裕量够大,会主动插入buffer增加时钟偏斜,主动的去增加时序裕量~
针对第三种情况在下也存在两个子类的差别,首先第一个是目的时钟和源时钟的频率是整数倍的关系,第二个是目的时钟和源时钟的频率不是整数倍的关系~
当两个时钟频率是整数倍的时候,两个时钟最靠近的时候的相位差就是快时钟的周期,如果在最坏的情况下满足建立时间保持时间要求也就满足了,这个可以整一些小手段,比如数据变换的时候从快时钟传输到慢时钟,我数据数据变换3个时钟周期或者N个时钟周期变换一次,我就必然能然慢时钟满足时间要求,减少亚稳态出现机会~
对于不是整数倍的也存在几种情况:
1.频率不是整数倍的情况下的可能会存在一个最小的相位差,找出最小的相位差,限制组合逻辑,避免由于组合逻辑导致建立时间和保持时间不满足要求,存在违例。
数据从慢时钟到快时钟存在的情况是:数据不会有丢失,但是在快时钟下要保证一点,就是数据只采样一次,防止数据采样多次,但是如果数据从快时钟到慢时钟很有可能导致数据丢失,那么要保证一点,就是数据至少要在目的时钟保持一个时钟周期,在一个周期内保持恒定,通过设置简单的状态机来保证数据在源时钟上保持一个目的时钟的时钟周期,
2.两个时钟有效沿之间非常小的时钟相位差,但这足以引起亚稳态,这时候也就必须设置同步器来避免亚稳态的产生。
但是两个时钟有效沿靠的很近之后的有效时钟沿就靠的比较远了,裕量就大了,那么去通过利用这个时钟之后裕量大的时钟有效沿去捕获信号那么就会避免亚稳态的产生。
3.时钟之间的相位差非常小,并且可以保持好几个周期,这种情况就是两个时钟周期特别接近,一次好几个时钟有效沿特别近,这种情况是最麻烦的,因为很容易产生亚稳态,数据很容易出现错误。
看这个图就已经丢失了数据,这种情况就是使小手段也不好使,最好的办法就是通过握手或者异步FIFO的方式去处理这种跨时钟之间的传输。
握手协议跨时钟域传输
xclk和yclk之间的data传输,通过xreq和yack握手完成传输。
握手顺序:
1.X将数据放到总线上并且传输xreq标志,就是数据有效。
2.Y时钟域先将xreq做同步处理,接收xreq信号。
3.在识别到xreq信号后,将数据锁存。
4.Y完成数据锁存后发送Yack信号标志Y完成锁存,完成接收数据。
5.X将Yack信号做同步处理。
6.X接收同步后的Yack信号后知道上次发的数据已经完成了传输,开始准备下一次数据传输。
源时钟域上,数据和请求信号至少要保证两个时钟有效沿是稳定的,否则比如从快时钟传输到慢时钟域上可能接收不到。
但是缺点也看到了,传输一个数据要经历握手这么麻烦,如果数据流大了,每次都握手严重影响到了数据传输~
对于数据流大的CDC传输还是使用异步FIFO的方式最简单~
网上关于异步FIFO的编写多了去了,建议找一个好好练一下~
以下是异步FIFO的结构图,基本上都是按照这个方式写的
有一点就是要理解为什么异步FIFO的读和写自增采用格雷码实现亚稳态~
附上异步FIFO代码
module brad_asyc_fifo#(
DATA_WIDTH = 32,
DATA_DEPTH = 1024,
ADDR_WIDTH = 10
)
( input rst,
input wrclk,
input rdclk,
input [DATA_WIDTH-1:0] din,
input wr_en,
input rd_en,
output reg [DATA_WIDTH-1:0] dout,
output reg rd_valid,
output empty,
output full
);
reg [DATA_WIDTH-1:0] asy_fifo [DATA_DEPTH-1:0];
reg [ADDR_WIDTH:0] wr_count_ptr;
reg [ADDR_WIDTH:0] rd_count_ptr;
wire [ADDR_WIDTH-1:0] wr_count;
wire [ADDR_WIDTH-1:0] rd_count;
wire [ADDR_WIDTH:0] wr_count_gray;
wire [ADDR_WIDTH:0] rd_count_gray;
reg [ADDR_WIDTH:0] wr_count_gray_r1;
reg [ADDR_WIDTH:0] rd_count_gray_r1;
reg [ADDR_WIDTH:0] wr_count_gray_r2;
reg [ADDR_WIDTH:0] rd_count_gray_r2;
//对fifo进行初始化
genvar i;
generate
for(i=0;i<=ADDR_WIDTH;i=i+1)
begin:fifo_init
//fifo初始化加fifo的写操作
always@(posedge wrclk or posedge rst)
begin
if(rst)
asy_fifo[i] <= 'h0;
else if (wr_en && !full)
asy_fifo[wr_count] <= din;
else
asy_fifo[wr_count] <= asy_fifo[wr_count];
end
end
endgenerate
//对Fifo的读操作
always@(posedge rdclk or posedge rst)
begin
if(rst)
begin
dout <= 'h0;
rd_valid <= 1'b0;
end
else if(rd_en && !empty)
begin
dout <= asy_fifo[rd_count];
rd_valid <= 1'b1;
end
else
begin
rd_valid <= 1'b0;
dout <= dout;
end
end
//计数器最高位用来判断空满,次位开始到最低位为实际值
assign wr_count = wr_count_ptr[ADDR_WIDTH-1:0];
assign rd_count = rd_count_ptr[ADDR_WIDTH-1:0];
//异步fifo最麻烦的操作就是空满判断 下面开始进行空满判断操作
//变为格雷码进行空满判断
assign wr_count_gray = (wr_count_ptr >> 1) ^ wr_count_ptr;
assign rd_count_gray = (rd_count_ptr >> 1) ^ rd_count_ptr;
//跨时钟域进行判断打两拍,降低亚稳态
//判断空满指针到写时钟域判断
always@(posedge wrclk)
begin
rd_count_gray_r1 <= rd_count_gray;
rd_count_gray_r2 <= rd_count_gray_r1;
end
always@(posedge wrclk or posedge rst)
begin
if(rst)
wr_count_ptr <= 'h0;
else if (wr_en && !full)
wr_count_ptr <= wr_count_ptr + 1;
else
wr_count_ptr <= wr_count_ptr;
end
//判断空写指针到读时钟域判
always@(posedge rdclk)
begin
wr_count_gray_r1 <= wr_count_gray;
wr_count_gray_r2 <= wr_count_gray_r1;
end
always@(posedge rdclk or posedge rst)
begin
if(rst)
rd_count_ptr <= 'h0;
else if (rd_en && !empty)
rd_count_ptr <= rd_count_ptr + 1;
else
rd_count_ptr <= rd_count_ptr;
end
//格雷码空标志 每位都相同
assign empty = (wr_count_gray_r2 == rd_count_gray);
//格雷码满标志 高位和次高位不同,其余都相同
assign full = (wr_count_gray == {~rd_count_gray_r2[ADDR_WIDTH:ADDR_WIDTH-1],rd_count_gray_r2[ADDR_WIDTH-2:0]});
endmodule