数字IC知识点:处理多个时钟
1. 多时钟域
对于工程师来说,开发含多个时钟(见图1)的设计是一种挑战。
这样的设计中可能有以下任何一个,或者全部类型的时钟关系:
- 时钟的频率不同
- 时钟频率相同,但相位不同
以上两种关系如图2所示。
2. 多时钟域设计的难题
2.1 违背建立时间和保持时间
建立时间:在时钟脉冲到来前,输人数据需要保持稳定的时间。
保持时间:在时钟脉冲到达后,输入数据仍需保持稳定的时间。
图3解释了相对于时钟上升沿的建立时间和保持时间。
建立时间要求数据必须在时钟上升沿到来前保持稳定,保持时间要求在时钟上升沿到来后数据必须仍然保持稳定。对于单时钟域,这样的要求很容易满足。但是,在多时钟域情况下,很容易出现一个时钟域的输出在另一个时钟域中的时钟上升沿到来时发生改变的现象。这将会引起第二个时钟域中触发器的输出处于亚稳态,由此导致一系列错误的结果。
如图4所示,xclk_outputl
(属于xclk
时钟域)在yclk
的上升沿附近发生了变化。即xclk_output1
信号在变化期间被yclk
时钟域采样。这会导致相对于yclk
的建立时间和保持时间违背现象的发生。因此,在yclk
时钟域中,依赖于xclk_output1
的信号将进人亚稳态并产生错误的结果。
然而,xclk_output2
(属于xclk
时钟域)在yclk
的上升沿处是稳定的。所以不会出现建立时间和保持时间违背的问题。因此,对于yclk
时钟域中的信号,如果依赖xclk_outpu2
信号,将会产生正确的输出。
2.2 亚稳态
亚稳态是由于违背了触发器的建立和保持时间而产生的。设计中任何一个触发器都有特定的建立和保持时间,在时钟上升沿前后的这段时间窗口内,数据输入信号必须保持稳定。如果信号在这段时期发生了变化,那么输出将是未知的或者称为“亚稳的“。这种有害状态的传播就叫做亚稳态。触发器的输出会因此而产生毛刺,或者暂时保持在不稳定状态而且需要较长时间才能回到稳定状态。
如图5所示,当触发器处在亚稳态时,输出会在高低电平之间波动,这会导致延迟输出转换过程,并超出所规定的时钟到输出的延迟值(\(t_{co}\))。亚稳态输出恢复到稳定状态所需的超出\(t_{co}\)的额外时间部分称为稳定时间(\(t_{MET}\))。并非所有不满足建立和保持时间的输人变化都会导致亚稳态输出。触发器是否进入亚稳态和返回稳态所需时间取决于生产器件的工艺技术与外界环境。一般来说,触发器都会在一个或者两个时钟周期内返回稳态。
所以,简单地说,当信号在一个时钟域(src_data_out
)里变化,在另一个时钟域(dest_date_in
)内采样时,就会导致输出变成亚稳态。这就是所谓的同步失败(见图6)。
3. 多时钟设计的处理技术
3.1 时钟命名法
为了保证综合脚本可以更轻松地处理所有的时钟信号,有必要对整个设计使用一个确定的命名步骤。
例如,系统时钟可以命名为sys_clk
,发送时钟可以命名为tx_clk
,接收时钟可以命名为rx_ck
等。这样就可以在脚本中使用通配符来对所有时钟进行操作。
同理,属于同一个时钟域的信号,也应该在命名时使用同样的前缀。例如,由系统时钟驱动的信号,可以用类似于sys_rom_addr
、sys_rom_data
这样的方式作为起始。
3.2 分块化设计
这是设计含多时钟的模块时常用的另一种有效技术。如下所述:
- 每个模块只应当在单个时钟下工作。
- 在信号跨时钟域传输时,使用同步器模块(该模块的作用是将信号从一个时钟域转递到另一个时钟域),以使所有信号在进入某个时钟域内的模块时,与该模块的时钟保持同步。
- 同步器模块的规模应尽可能小。
将整个设计分割成模块的优点在于使得静态时序分析变得很简单,因为所有输出或输入某时钟域的信号都与使用该时钟的模块保持同步。所以,设计就成为完全同步的。另外,对于同步模块是不需要做静态时序分析的。但是,要保证满足保持时间的要求。
如图7所示,整个逻辑分成了三个时钟域,分别是Clock1
时钟域、Clock2
时钟域和Clock3
时钟域。根据3.1节中所述的命名规则对各时钟域中信号进行命名。所有跨时钟域传输的信号都要经过一个额外的同步模块。该同步模块的作用是将信号从原来所在的时钟域传化到将使用该信号的时钟域。因此,如图7所示,“从1同步到2”模块将信号从Clock1
时钟域转化到Clock2
时钟域。
3.3 跨时钟域
跨时钟域信号的传输可以分为两类,分别是:
- 控制信号的传输
- 数据信号的传输
3.3.1 控制信号的传输(同步化)
在设计中,如果将一个异步信号直接送给若干个并行工作的触发器就会大大增加亚稳态事件发生的概率,因为有可能有多个触发器进入亚稳态。为了避免形成这种情况下的亚稳态,我们常常使用同步触发器的输出信号来取代异步信号。
为了减少亚稳态的影响,设计者最常用的方式是使用多级同步器,即将两个或多个触发器串联起来组成的同步电路,如图8所示。
如果同步器的第一级触发器产生亚稳态输出,那么这个亚稳态会在同步器的第二个触发器取样前进人稳态。这种方法无法保证第二级触发器的输出一定不会出现亚稳态,但是它确实降低出现亚稳态的可能性。同理如果为同步器增加更多级触发器,就会进一步降低出现亚稳态的可能性。
这种方法的一个缺点,也可以看做同步器电路不可避免的开销,就是增加了电路的整体延时。
上述同步电路的时序如图9所示。
有些情况下,第一级同步器触发器的输出信号从亚稳态进入稳定状态所需的时间可能需要不止一个时钟周期,这就意味着第二级同步触发器的输出仍然亚稳定。这时为安全起见,应当使用三级同步器电路。
图10是一个三级同步器电路的例子。
三级同步器电路由三个串联的触发器组成。第二级触发器可能输出的亚稳态信号在第三级触发器采样前会进入稳定状态。
三级同步器电路的时序如图11所示。
尽管如此,在大多数多时钟域设计中,使用两级同步电路就足以避免亚稳态的出现了。所以,只有在时钟频率非常高的设计中才要求使用三级同步器电路。
3.3.2 数据信号的传输
在多时钟域设计中,数据经常会从一个时钟域传输到另一个时钟域。以下是保证数据正常地在不同时钟域间传输的两种方法:
- 使用握手信号的方式
- 使用异步FIFO
4. 跨时钟域
如果多个时钟都起源于同一时钟,并且它们的相位和频率关系是已知的,那么这些时钟可以看成是跨同步时钟域的时钟。按照相位和频率的关系,可以将这些时钟分成以下类型:
- 同频零相位差时钟
- 同频恒定相位差时钟
- 非同频可变相位差时钟
- 整数倍时钟
- 有理数倍时钟
下一节假定两个时钟之间的相位和时钟抖动相同,并假定它们之间的路径已经按同样的时钟延迟和偏移参数进行了平衡。除此之外,还假设这两个时钟起始处的相位差为零,而且触发器的“时钟到0端”的延时也为零。
4.1 同频零相位差时钟
实际上这种情况就是单时钟设计,在这里仅仅是出于全面考虑才提及它无论何时,从“clk1”到“clk2”的数据都有一个完整的有效时钟周期用于传输,以保证数据可以正常捕捉,如图12所示。
只要在源触发器和目的触发器之间的组合逻辑的延迟能满足电路建立和保持时间的要求,数据就能正确地传输。在这种情况下,对设计的唯一要求只是保证STA(静态时序分析)通过。如果这一条件满足,就不会出现亚稳态问题和数据丢失或不一致的问题。
4.2 同频恒定相位差时钟
这些时钟有相同的时钟周期,但是相位差恒定。典型例子是对某个时钟及其反相时钟的使用。另一个例子是某个时钟相对于其上级时钟发生了相位移动,例如,如果规定T为时钟周期,移动值为\(T/4\)。
如图12所示,时钟“clk1”和“clk2”的频率相同,但在图13中“clk1”相对于“clk2”相位前移了\(3T/4\)个单位时间。
每当数据从“clk1”传输到“clk2”时,由于更小的建立时间/保持时间裕量,对组合逻辑的延时约束都会变得更紧。如果组合逻辑的延时能保证满足采样沿处建立时间和保持时间的要求,数据就可以正确地传输,并且不会有亚稳态产生。在这种情况下是不需要同步器的。只需要使设计的STA 通过就可以了。
一般会在STA中创建这种情况以保证满足时序要求。如果组合逻辑有更多延时,通过在发射边沿和捕获边沿加入偏移(例如,使时序有相同频率和不同相位),会有助于满足时序的要求。
4.3 非同频、可变相位差时钟
可以将这类时钟分为两个子类,第一子类中各时钟周期之间是整数倍的关系,第二子类中各时钟周期是非整数倍(有理数倍)的关系。
4.3.1 整数倍频率的时钟
在这种情况下,一个时钟的频率是另一个时钟的整数倍,这两个时钟的有效边沿之间可能的最小相位差始终等于其中较快的那个时钟的时间周期。
在图14中,时钟“clk1”的频率是时钟“clk2”的三倍。假定T是时钟“clk1”的时钟周期,时钟“clk2”可以用来捕获数据的时间可能是\(T\)、\(2T\)或\(3T\),这取决于数据在时钟“clk1”的哪个边沿发出。因此,任意路径的最差延迟都应在时钟边沿相位差为\(T\)时满足建立时间的要求。
既然在这里数据由较快时钟发出,并由较慢时钟捕获,那么为了避免数据丢失,源数据应该保持至少一个目的时钟周期的稳定状态。可以使用一些控制电路来满足这一要求,例如,使用一个简单的有限状态机(FSM)。参考图14,如果使源数据每三个源时钟改变一次,就不会出现丢数据的问题了。
4.3.2 非整数倍频率的时钟
- 在源时钟有效沿和目的时钟有效沿之间有足够大的相位差,所以不会有亚稳态产生。
- 源时钟和目的时钟有效沿非常接近,导致产生亚稳态问题。然而,在这里时钟频率倍数关系需要满足以下条件,即一旦有时钟边沿接近出现,下一个时钟周期就会留出足够大的时间冗余,使得数据的捕获不会出现违背建立时间或保持时间的要求。
- 两个时钟的时钟沿在许多连续的周期中都非常接近。这与异步时钟的行为很相似,但因为这两个时钟的源头是相同的,所以它们之间的相位差是可以计算出来的。
4.3.2.1 例子1
在这种情况中两个时钟的有效沿永远不会过于接近,从而可以保留足够的冗余来满足电路对建立时间和保持时间的要求。
假设两个时钟“clk1”和“clk2”分别是对同一个时钟“clk”的3分频与2分频。那么“clk1”比“clk2”慢1.5 倍。如图15所示,“clk1”的周期是15ns,“clk2”的周期为10ns。这两个时钟间的最小相位差是2.5ns,这对于满足建立时间和保持时间已经足够了。
然而,由于该相位差很小,应该避免在跨越两个时钟的位置使用任何组合逻辑。对于增加的任何组合逻辑,必须满足建立和保持时间的要求以避免亚稳态,因此必须使用同步器。
进一步说,在数据从慢时钟域传递到快时钟域时,必须增加逻辑以保证数据在快时钟域中只取样一次,这时不会有数据丢失的现象。然而,在从快时钟域向慢时钟域传递数据时,就可能出现数据丢失。为了解决这个问题,必须将源数据保持至少一个目标时钟周期,以保证在两个连续变换的源数据之间至少有一个目标时钟到达。
4.3.2.2 例子2
在这种情况下,两个时钟的有效沿可能间隔性地非常接近。换句话说,两个时钟沿会出现挨着的情况,然后在再次出现挨着的情况之前,接来的几个周期两个时钟沿会保留足够的裕量(能正确捕捉到数据)。这里'挨着”的意思是接近到了足以产生亚稳态的程度。
图16中的信号B2
就表示这种情况。所期望的输出是B1
,实际的波形是B2
。注意,这里数据不会丢失,但是可能有数据不连贯的问题。
对于从快时钟域到慢时钟域的传输,可能出现数据丢失,为了阻止这种现象,源数据应该保持至少一个目标时钟周期不变。可以通过使用一个简单的FSM实现这一目的。
4.3.2.3 例子3
在这种情况下时钟间的相位差异很小,并能连续存在几个周期。除了变化的相位差异和周期性的重复现象,其余都与异步时钟很相似。
在图3.16中,时钟“clk1”和“clk2”的周期分别为10ns与9ns。可以看出两个时钟的有效时钟沿很接近,并持续4个连续周期。在前两个周期中可能会违背建立时间(源时钟在目的时钟之前),而在后两个时钟周期中可能会违背保持时间(目的时钟在源时钟之前)。
在这种情况下,将会出现亚稳态问题,因此需要进行同步。除了亚稳态的问题,数据从慢时钟域传递到快时钟域时也可能失丢。可以从图3.16中可以看到,B1是不含亚稳态的正确输出。但实际的输出可能是B2。这里数值1丢失了,因为第一个周期“1”由于违背建立时间未能捕捉到而在第二个周期中由于违背保持时间误捕捉到了“0”。
为了不丢失数据,数据需要保持稳定至少两个目的时钟周期。这既适用于从快到慢的传输,也适用于从慢到快的传输。可以通过使用简单的FSM对源数据产生进行控制完成这一任务。但数据不连贯的问题仍然存在。
这时,已标准化的技术(如握手和FIFO)对于传输数据更为有效:因为它们也解决了数据不连贯的问题。
5. 握手信号方法
图18是由两个时钟域分割成的两个单独的系统。
使用握手信号“xreq
”和“yack
”,“系统X”将数据发送给“系统 Y”下面是使用握手信号传输数据的例子。
- 发送器“系统X”将数据放到数据总线上并发出“
xreq
”(请求)信号,表示有效数据已经发到接收器“系统Y”的数据总线上。 - 把“
xreq
”信号同步到接收器的时钟域“yclk
”上。 - 接收器在识别“
xreq
”同步的信号后,锁存数据总线上的信号。 - 接收器发出确认信号“
yack
”,表示其已经接受了数据。 - 接收器发出的“
yack
”信号同步到发送时钟“xclk
”上。 - 发送器在识别同步的“
yack
”信号后,将下一个数据放到数据总线上。
握手信号序列的时序如图19所示,从图19中可以看出,安全地将一个数据从发送器传输到接收器需要5个时钟周期。
5.1 握手信号的要求
数据应该在发送时钟域内稳定至少两个时钟上升沿,请求信号“xreq
”的宽度应该超过两个上升沿时钟,否则从高速时钟域向低速时钟域传递可能无法捕捉到该信号。
5.2 握手信号的缺点
跨时钟域传输单个数据的延迟比使用FIFO传输相同的数据要大得多。
5.3 握手信号Verilog实现
5.3.1 tx模块
`timescale 1ns/1ns
module data_driver(
input clk_a,
input rst_n,
input data_ack,
output reg [3:0]data,
output reg data_req
);
reg [2:0] counter; // 用于发送 0~7 的计数器
reg [2:0] delay_counter; // 用于发送间隔的计数器
reg waiting; // 表示是否等待接收端确认
reg data_ack_1;
reg data_ack_2;
assign data_ack_rise = ~data_ack_2 & data_ack_1;
// 打两拍消除亚稳态
always @(posedge clk_a or negedge rst_n) begin
if (~rst_n) begin
data_ack_1 <= 1'b0;
data_ack_2 <= 1'b0;
end else begin
data_ack_1 <= data_ack;
data_ack_2 <= data_ack_1;
end
end
always @(posedge clk_a or negedge rst_n) begin
if (!rst_n) begin
counter <= 3'd0;
data <= 3'd0;
delay_counter <= 3'd0;
waiting <= 1'b0;
data_req <= 1'b0;
end else begin
if (waiting) begin
if (data_ack_rise) begin
waiting <= 1'b0;
data_req <= 1'b0;
end
end else begin
if (delay_counter == 3'd4) begin
counter <= counter + 1'b1;
data <= counter;
delay_counter <= 3'd0;
waiting <= 1'b1;
data_req <= 1'b1;
end else
delay_counter <= delay_counter + 1'b1;
end
end
end
endmodule
5.3.2 rx模块
module data_receiver(
input clk_b,
input rst_n,
input data_req,
input [3:0] data,
output reg data_ack
);
reg [3:0] data_in;
reg data_req_1;
reg data_req_2;
wire data_req_pos;
assign data_req_pos = data_req_1 && !data_req_2;
// 打两拍消除亚稳态
always @(posedge clk_b or negedge rst_n) begin
if (!rst_n) begin
data_req_1 <= 1'b0;
data_req_2 <= 1'b0;
end else begin
data_req_1 <= data_req;
data_req_2 <= data_req_1;
end
end
always @(posedge clk_b or negedge rst_n) begin
if (!rst_n) begin
data_ack <= 1'b0;
data_in <= 4'b0000;
end else begin
if (data_req)
data_ack <= 1'b1;
else
data_ack <= 1'b0;
end
if (data_req_pos)
data_in <= data;
else
data_in <= data_in;
end
end
endmodule
6. 使用同步FIFO传输数据
FIFO是用于对在通信总线上传输的数据进行排列的简单存储结构。因此,FIFO 常用来传输跨不同时钟域的数据。
本节介绍简单的同步FIFO架构,其读和写使用同样的时钟。
6.1 同步FIFO架构
图20展示了一个同步FIFO的通用架构。DPRAM(双端口RAM)用作FIFO的存储器以使读、写可以独立进行。
通过读、写指针产生各自的读、写地址,送到读、写端口。写指针指向下一个要写人的地址,读指针指向下一个要读取的地址。有效写使能使写指针递增,而有效的读使能使读指针递增。
图20中的“状态模块”“产生”fifo_empty
”和“fifo_full
”信号。如果“fifo_full
”有效,说明FIFO内的空间已满不能再写人数据。如果“fifo_empty
"有效说明FIFO内没有可供读取的下一个有效数据。通过对两个指针进行相同的逻辑,该模块也指示出任意时刻FIFO中空或满区域的个数。
6.2 同步FIFO的工作方式
复位后读写指针都归0
。此时“fifo_empty
”信号置为有效而“fifo_full
”保持低电平。因为FIFO为空,所以阻止对FIFO的读操作,只能进行写操作。后序的写操作会增加写指针的值,并将“ffo_empty
”信号置为无效。在没有空间可写数据时,写指针等于RAM_SIZE-1
。此时进行一个写操作会使写指针回滚到0
,并将“fifo_full
”信号置为高电平。
总之在读、写指针相等时,FIFO要么空要么满,所以需要对这两种情况进行区分。
6.2.1 FIFO空和满的产生
图21中的转换发生在随后的时钟中,在写操作使两个指针在下个时钟保持相等时,FIFO满。这使得在以下情况会发出“fifo_full
”信号。
fifo_full = (read_pointer == (write_pointer + 1)) && "write"
类似地,当读操作使两个指针在下一个时钟相等时,FIFO变空。以下情况会产生“fifo_empty
”信号。
fifo_empty = (write_pointer == (read _pointer + 1)) && "read"
6.2.2 另一种方法
另一种产生“fifo_full
”和“fifo_empty
”状态的方法是使用计数器来持续指示FIFO中空或满位置的个数。
计数器的宽度要与FIFO的深度相等以使其能记录FIFO中数据的最大个数。计数器在复位时初始化为0。随后的任何写操作会将其递增1,任何读操作会使其递减1。
现在,在计数器值为0时,很容易就判断FIFO处于空状态,而当计数器的值等于FIFO的大小时,就能判断出FIFO处于满状态。
本节提到的这种方法虽然在原理上简单,但是与6.2.1节中所提到的方法相比效率要低一些。因为这种方法要求增加额外的硬件(比较器)来产生FIFO空和满的条件。随着FIFO深度的增加,比较器的宽度也会增加,因此产生FIFO空、满信号需要更高级的序列比较器。这最终会降低FIFO 操作的最高频率。
6.3 同步FIFO verilog实现
6.3.1 计数器法
module sync_fifo_cnt
#(
parameter WIDTH = 8, // FIFO 位宽
parameter DEPTH = 16 // FIFO 深度
)(
input wire clk,
input wire rst_n,
input wire [WIDTH-1 : 0] data_wr,
input wire rd_en,
input wire wr_en,
output reg [WIDTH-1 : 0] data_rd,
output wire empty, // 空标志,高电平表示当前FIFO已被写满
output wire full, // 满标志,高电平表示当前FIFO已被读空
output reg [$clog2(DEPTH):0]fifo_cnt
);
reg [WIDTH-1:0] RAM[DEPTH-1:0];
reg [$clog2(DEPTH)-1:0] wr_addr; // 写地址
reg [$clog2(DEPTH)-1:0] rd_addr; // 读地址
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rd_addr <= 0;
data_rd <= 0;
end else if (!empty && rd_en) begin // 读使能有效且非空
rd_addr <= rd_addr + 1'b1;
data_rd <= RAM[rd_addr];
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
wr_addr <= 0;
else if (!full && wr_en) begin // 写使能有效且非满
wr_addr <= wr_addr + 1'b1;
RAM[wr_addr] <= data_wr;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
fifo_cnt <= 0;
else begin
case({wr_en, rd_en})
2'b00: fifo_cnt <= fifo_cnt;
2'b01: begin // 读不写
if (fifo_cnt != 0)
fifo_cnt <= fifo_cnt - 1'b1;
end
2'b10: begin // 写不读
if (fifo_cnt != DEPTH)
fifo_cnt <= fifo_cnt + 1'b1;
end
2'b11: fifo_cnt <= fifo_cnt; // 又读又写
default: fifo_cnt <= fifo_cnt;
endcase
end
end
assign full = (fifo_cnt == DEPTH) ? 1'b1 : 1'b0;
assign empty = (fifo_cnt == 0) ? 1'b1 : 1'b0;
endmodule
6.3.1.1 测试平台
module tb_sync_fifo_cnt();
parameter DEPTH = 8;
parameter WIDTH = 8;
reg clk;
reg rst_n;
reg [WIDTH-1:0] data_wr;
reg rd_en;
reg wr_en;
wire[WIDTH-1:0] data_rd;
wire empty;
wire full;
wire[$clog2(DEPTH):0] fifo_cnt;
sync_fifo_cnt
#(
.WIDTH (WIDTH), // FIFO 位宽
.DEPTH (DEPTH) // FIFO 深度
) u_sync_fifo_cnt (
.clk (clk),
.rst_n (rst_n),
.data_wr (data_wr),
.rd_en (rd_en),
.wr_en (wr_en),
.data_rd (data_rd),
.empty (empty),
.full (full),
.fifo_cnt (fifo_cnt)
);
initial begin
clk = 1'b0; //初始时钟为0
rst_n <= 1'b0; //初始复位
data_wr <= 'd0;
wr_en <= 1'b0;
rd_en <= 1'b0;
//重复8次写操作,让FIFO写满
repeat(8) begin
@(negedge clk)begin
rst_n <= 1'b1;
wr_en <= 1'b1;
data_wr <= $random; //生成8位随机数
end
end
//重复8次读操作,让FIFO读空
repeat(8) begin
@(negedge clk)begin
wr_en <= 1'b0;
rd_en <= 1'd1;
end
end
//重复4次写操作,写入4个随机数据
repeat(4) begin
@(negedge clk)begin
wr_en <= 1'b1;
data_wr <= $random; //生成8位随机数
rd_en <= 1'b0;
end
end
//持续同时对FIFO读写,写入数据为随机数据
forever begin
@(negedge clk)begin
wr_en <= 1'b1;
data_wr <= $random; //生成8位随机数
rd_en <= 1'b1;
end
end
end
always #10 clk = ~clk; //系统时钟周期20ns
endmodule
6.3.1.2 仿真结果
6.3.2 高位扩展指针法
module sync_fifo_ptr#(
parameter WIDTH = 8,
parameter DEPTH = 16
)(
input wire clk,
input wire rst_n,
input wire [WIDTH-1 : 0] data_wr,
input wire rd_en,
input wire wr_en,
output reg [WIDTH-1 : 0] data_rd,
output wire empty,
output wire full
);
reg [WIDTH-1 : 0] RAM[DEPTH-1 : 0];
reg [$clog2(DEPTH) : 0] wr_ptr;
reg [$clog2(DEPTH) : 0] rd_ptr;
wire [$clog2(DEPTH)-1 : 0] wr_LSB;
wire [$clog2(DEPTH)-1 : 0] rd_LSB;
wire wr_MSB;
wire rd_MSB;
assign {wr_MSB, wr_LSB} = wr_ptr;
assign {rd_MSB, rd_LSB} = rd_ptr;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
wr_ptr <= 0;
else if (wr_en && !full) begin
RAM[wr_LSB] <= data_wr;
wr_ptr <= wr_ptr + 1'b1;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_rd <= 0;
rd_ptr <= 0;
end else if (rd_en && !empty) begin
data_rd <= RAM[rd_LSB];
rd_ptr <= rd_ptr + 1'b1;
end
end
assign empty = (wr_ptr == rd_ptr) ? 1'b1 : 1'b0;
assign full = ((wr_MSB != rd_MSB) && (wr_LSB == rd_MSB)) ? 1'b1 : 1'b0;
endmodule
6.3.2.1 测试平台
module tb_sync_fifo_ptr();
parameter DEPTH = 8;
parameter WIDTH = 8;
reg clk;
reg rst_n;
reg [WIDTH-1:0] data_wr;
reg rd_en;
reg wr_en;
wire[WIDTH-1:0] data_rd;
wire empty;
wire full;
sync_fifo_ptr
#(
.WIDTH (WIDTH), // FIFO 位宽
.DEPTH (DEPTH) // FIFO 深度
) u_sync_fifo_ptr (
.clk (clk),
.rst_n (rst_n),
.data_wr (data_wr),
.rd_en (rd_en),
.wr_en (wr_en),
.data_rd (data_rd),
.empty (empty),
.full (full)
);
initial begin
clk = 1'b0; //初始时钟为0
rst_n <= 1'b0; //初始复位
data_wr <= 'd0;
wr_en <= 1'b0;
rd_en <= 1'b0;
//重复8次写操作,让FIFO写满
repeat(8) begin
@(negedge clk)begin
rst_n <= 1'b1;
wr_en <= 1'b1;
data_wr <= $random; //生成8位随机数
end
end
//重复8次读操作,让FIFO读空
repeat(8) begin
@(negedge clk)begin
wr_en <= 1'b0;
rd_en <= 1'd1;
end
end
//重复4次写操作,写入4个随机数据
repeat(4) begin
@(negedge clk)begin
wr_en <= 1'b1;
data_wr <= $random; //生成8位随机数
rd_en <= 1'b0;
end
end
//持续同时对FIFO读写,写入数据为随机数据
forever begin
@(negedge clk)begin
wr_en <= 1'b1;
data_wr <= $random; //生成8位随机数
rd_en <= 1'b1;
end
end
end
always #10 clk = ~clk; //系统时钟周期20ns
endmodule
6.3.2.2 仿真结果
7. 异步FIFO(或双时钟FIFO)
异步 FIFO用来在两个异步时钟域间传输数据。
图22中是两个系统,分别为“SystemX”和“SystemY”,从“System X”向“SystemY”传输数据,两个系统工作在不同的时钟域中。
“System X”使用“xclk
”将数据写人FIFO,并由“System Y”使用“yclk
”读出。
“fifo_full
”和“fifo_empty
”信号负责对上溢和下溢情况的监控。
“fifo_ful
”信号指示上溢情况。在“fifo_full
”置起时数据不应写人FIFO,否则会将FIFO内的数据覆盖掉。
由“fifo_empty
”信号指示下溢情况,如在“fifo_empty
”时不应读取FIFO,否则会读出垃圾数据。
与握手信号不同,异步FIFO用于对性能要求较高的设计中,尤其是时钟延迟比系统资源更为重要的环境中。
7.1 避免用二进制计数器实现指针
以写指针为例。在写请求有效时,写指针在写时钟作用下递增。同样,在读请求有效时读指针在读时钟作用下递增。在产生FIFO满信号时,要将写指针与读指针进行比较,由于两个指针与分别与其各自的时钟同步,但是彼此之间异步,在使用二进制计数器实现指针时,就会导致用于比较的指针值取样错误。
比如,二进制计数器的值会从FFF变为000。这时所有位会同时改变虽然能通过同步计数器避免亚稳态,但是仍然能得到极不相关的取样值所以同步计数器不是最终的解决方案。
从FFF到000可能的转换:
- FFF→000
- FFF→001
- FFF→010
- FFF→011
- FFF→100
- FFF→101
- FFF→110
- FFF→111
如果同步时钟边沿在FFF向000转换的中间位置到来,就可能将三位二进制数的任何值取样并同步到新的时钟域中。
因为FIFO满和FIFO空标记的产生要使用这些指针的值,所以错误的指针值将会误产生标记。可能是在FIFO满时没有产生FIFO满标记从而使数据丢失;或在FIFO空时没有产生FIFO空标记,从而读出垃圾数据。
7.2 使用格雷码取代二进制计数
实现 FIFO指针的一种方式是使用格雷码计数,如表1所示。
格雷码相对于二进制编码的优势在于在从一个数变为另一个数时,只有一位出现变化。
要得到不同的格雷码,只需要从任意一种位组合开始,每次只将其中一位随机从0改为1或从1改为0,就可以得到其他格雷码。因此格雷码也称为反射码。
因为格雷码是单位间距码,每下一个值与前一个值的区别只有一位距离,所以在转换中最多只会出现一位错误。例如,如果计数器从“1010”变为“1011”,取样逻辑要么读到“1010”(旧值)要么读到“1011”(新值),但是不会出现其他值。
同步指针的影响
在FIFO满时会阻止进一步访问FIFO。在FIFO满时,按各自时钟递增的读、写指针会进行比较。需要将读指针(格雷码)同步到写时钟上,下面用例子来说明这一点。
如图23所示,最初在\(t_0\)阶段读、写指针都为0。随着后续发生在FIFO上的写操作,写指针增加。当到达某个阶段时,读、写指针相等,这时FIFO满。在图3.23中在\(t_5\)阶段这种情况发生。
如果在\(t_6\)处发生了读操作,由于典型的同步电路至少包含两个触发器,将读指针同步到写时钟上将会导致同步后的读指针在两个写时钟后出现。这虽然增加了阻止数据写人的周期,但是对数据的准确性是无害的。只有在FIFO实际上已经满了但是没有阻止写动作时才会出现问题。
类似地,在FIFO空时也会阻止对FIFO进一步的读访问。
对于FIFO空计算,把写指针同步到读时钟并与读指针进行比较。因此,在读一边延迟了写操作(延迟了两个时钟信号)并且在实际上有数据时仍然会指示FIFO为空。这会导致阻止读操作,直到读一边能看到写操作为止。
如图25所示,在\(t_0\)处读、写指针初始值为0。随着对FIFO的写操作:写指针增加。在某个阶段写指针与读指针相等,这时FIFO变为满。这在图3.25 中的\(t_3\)阶段发生。
后续的读操作从\(t_4\)开始,并且FIFO在再次变为空。在\(t_7\)和\(t_8\)又写回FIFO,由于典型的同步电路由至少两个触发器组成,将写指针同步到读时钟上时将会导致产生最少两个读时钟的延迟。这会导致阻止对FIFO额外的读操作,但这是无害的。只有在FIFO实际上为空时没有阻止读操作才会产生问题。
7.3 异步FIFO verilog实现
module async_fifo
#(
parameter DATA_WIDTH = 16 ,
parameter FIFO_DEPTH = 8 ,
parameter PTR_WIDTH = 4 ,
parameter ADDR_DEPTH = $clog2(FIFO_DEPTH)
)
(
// reset signal
input wire wr_rst_n_i,
input wire rd_rst_n_i,
// write interface
input wire wr_clk_i ,
input wire wr_en_i ,
input wire [DATA_WIDTH-1:0] wr_data_i,
// read interface
input wire rd_clk_i ,
input wire rd_en_i ,
output reg [DATA_WIDTH-1:0] rd_data_o,
// flag
output reg full_o ,
output reg empty_o
);
//-- memory
reg [DATA_WIDTH-1:0] regs_array [FIFO_DEPTH-1:0] ;
//-- memery addr
wire [ADDR_DEPTH-1:0] wr_addr ;
wire [ADDR_DEPTH-1:0] rd_addr ;
//-- write poiter,write poiter of gray and sync
reg [PTR_WIDTH -1:0] wr_ptr ;
wire [PTR_WIDTH -1:0] gray_wr_ptr ;
reg [PTR_WIDTH -1:0] gray_wr_ptr_d1 ;
reg [PTR_WIDTH -1:0] gray_wr_ptr_d2 ;
//-- read poiter,read poiter of gray and sync
reg [PTR_WIDTH -1:0] rd_ptr ;
wire [PTR_WIDTH -1:0] gray_rd_ptr ;
reg [PTR_WIDTH -1:0] gray_rd_ptr_d1 ;
reg [PTR_WIDTH -1:0] gray_rd_ptr_d2 ;
/*-----------------------------------------------\
-- write poiter and bin->gray --
\-----------------------------------------------*/
always @ (posedge wr_clk_i or negedge wr_rst_n_i) begin
if (!wr_rst_n_i)
wr_ptr <= {(PTR_WIDTH){1'b0}};
else if (wr_en_i && !full_o)
wr_ptr <= wr_ptr + 1'b1;
end
assign gray_wr_ptr = wr_ptr ^ (wr_ptr >> 1'b1);
/*-----------------------------------------------\
-- gray_wr_prt sync --
\-----------------------------------------------*/
always @ (posedge wr_clk_i or negedge wr_rst_n_i) begin
if (!wr_rst_n_i) begin
gray_wr_ptr_d1 <= {(PTR_WIDTH){1'b0}};
gray_wr_ptr_d2 <= {(PTR_WIDTH){1'b0}};
end else begin
gray_wr_ptr_d1 <= gray_wr_ptr ;
gray_wr_ptr_d2 <= gray_wr_ptr_d1;
end
end
/*-----------------------------------------------\
-- read poiter and bin->gray --
\-----------------------------------------------*/
always @ (posedge rd_clk_i or negedge rd_rst_n_i) begin
if (!rd_rst_n_i)
rd_ptr <= {(PTR_WIDTH){1'b0}};
else if (rd_en_i && !empty_o)
rd_ptr <= rd_ptr + 1'b1;
end
assign gray_rd_ptr = rd_ptr ^ (rd_ptr >> 1'b1);
/*-----------------------------------------------\
-- gray_rd_ptr sync --
\-----------------------------------------------*/
always @ (posedge rd_clk_i or negedge rd_rst_n_i) begin
if (!rd_rst_n_i) begin
gray_rd_ptr_d1 <= {(PTR_WIDTH){1'b0}};
gray_rd_ptr_d2 <= {(PTR_WIDTH){1'b0}};
end else begin
gray_rd_ptr_d1 <= gray_rd_ptr ;
gray_rd_ptr_d2 <= gray_rd_ptr_d1;
end
end
/*-----------------------------------------------\
-- full flag and empty flag --
\-----------------------------------------------*/
assign full_o = (gray_wr_ptr == {~gray_rd_ptr_d2[PTR_WIDTH-1], gray_rd_ptr_d2[PTR_WIDTH-2:0]})? 1'b1 : 1'b0;
assign empty_o = (gray_rd_ptr == gray_wr_ptr_d2)? 1'b1 : 1'b0;
/*-----------------------------------------------\
-- write addr and read addr --
\-----------------------------------------------*/
assign wr_addr = wr_ptr[PTR_WIDTH-2:0];
assign rd_addr = rd_ptr[PTR_WIDTH-2:0];
/*-----------------------------------------------\
-- write operation --
\-----------------------------------------------*/
integer [PTR_WIDTH-1:0] i;
always @ (posedge wr_clk_i or negedge wr_rst_n_i) begin
if (!wr_rst_n_i) begin
for(i=0;i<FIFO_DEPTH;i=i+1)begin
regs_array[i] <= {(DATA_WIDTH){1'b0}};
end
end else if (wr_en_i && !full_o) begin
regs_array[wr_addr] <= wr_data_i;
end
end
/*-----------------------------------------------\
-- read operation --
\-----------------------------------------------*/
always @ (posedge rd_clk_i or negedge rd_rst_n_i) begin
if (!rd_rst_n_i) begin
rd_data_o <= {(DATA_WIDTH){1'b0}};
end else if (rd_en_i && !empty_o) begin
rd_data_o <= regs_array[rd_addr];
end
end
endmodule