简介
本页讲解如何用 01 信号采样(01-signal sampling)来对接源同步输入(source-synchronous input)。其他用于对接此类数据源的策略,在关于源同步输入的页面中做了概述。那页也解释了什么是源同步输入。
01 信号采样的基本思路是:把外部时钟当作一个数据信号来处理。也就是说,用一个寄存器来采样这个时钟。该寄存器使用一个稳定且不受外部时钟影响的内部时钟。
数据信号则由另外的寄存器采样,并且使用同一个内部时钟。所有实现 01 信号采样的逻辑也同样使用这个内部时钟。
逻辑会检测采样外部时钟的那个寄存器的变化。当这个寄存器的值从 '0' 变为 '1' 时,说明外部时钟上出现了一个上升沿。逻辑响应这一事件的方式,是把其他寄存器的值写入一个 FIFO。这些寄存器中保存的,是外部时钟上升沿到来那一刻数据信号的值。
这种逻辑达到的效果,与“把 FIFO 的时钟接外部时钟、把 FIFO 的数据输入直接接数据信号”是一样的。区别在于逻辑用的是哪个时钟:外部时钟还是内部时钟。01 信号采样的优势在于,所有逻辑只依赖内部时钟。这个时钟稳定可靠。即使外部时钟行为异常,逻辑仍然会按合理的方式运行。
下图展示了 01 信号采样:
在这幅图中,@stable_clk 是 FPGA 的内部时钟。@data_clk 和 @data 是到达 FPGA 的信号。@data_clk_samp 和 @data_samp 是 FPGA 内部的寄存器。外部信号 @data_clk 对应着 @data_clk_samp,@data 则对应着 @data_samp。
图中还展示了,当 @data_clk_samp 上出现 “0 1” 这样的形态时,@data_samp 的值就会被写入一个 FIFO。这正是这种方法被称为 “01 信号采样” 的原因。
这里用 FIFO 只是作为处理到达数据的一个示例。这往往是合适的做法,因为内部时钟的频率通常明显高于数据速率,因此后续经常用频率更低的时钟来处理数据会更为方便。而 FIFO 是将数据移交给另一个时钟域(clock domain)中的逻辑的一种便捷方式。
不过,也完全可以使用进行 01 信号采样的那同一个内部时钟来实现其余逻辑。使用 FIFO 仅仅是众多可能性中的一种。
Verilog 示例
下面这段 Verilog 代码说明了这一思路。这里的外部时钟是 @data_clk。
module top (
input stable_clk,
input data_clk,
input [7:0] data
);
reg [7:0] data_guard, data_samp;
reg data_clk_guard, data_clk_samp, data_clk_samp_d;
wire fifo_wr_en;
always @(posedge stable_clk)
begin
data_guard <= data;
data_clk_guard <= data_clk;
data_samp <= data_guard;
data_clk_samp <= data_clk_guard;
data_clk_samp_d <= data_clk_samp;
end
assign fifo_wr_en = data_clk_samp && !data_clk_samp_d;
data_fifo fifo_i
(
.wr_clk(stable_clk),
.din(data_samp),
.wr_en(fifo_wr_en),
[ ... other ports connected here ... ]
);
endmodule
关键部分是 FIFO 的写使能信号:@fifo_wr_en。这个信号等于 “data_clk_samp && !data_clk_samp_d”。因此,当在 @data_clk_samp 上检测到 “0 1” 形态时,该信号为高,从而把 @data_samp 的值写入 FIFO。
注意,逻辑中只把 @stable_clk 用作时钟。@data_clk 被当作一个普通的 I/O 输入来对待。
@data_guard 和 @data_clk_guard 的时序要求并不能得到保证:输入端口相对于 @stable_clk 是异步的。因此,@data_guard 和 @data_clk_guard 起的是亚稳态防护(metastability guard)的作用。逻辑不会直接依赖这两个寄存器的值。另一方面,@data_samp 和 @data_clk_samp 会被逻辑直接使用,因为这两个寄存器处于亚稳态防护的第二级。
但这种方法真的可行吗?答案就在时序分析中。
时序分析
01 信号采样的时序分析与通常的做法不同:一般情况下,时钟边沿与数据信号被采样的时刻之间有一个固定的时间差。这是因为用于采样的时钟与数据信号是同步的。但在 01 信号采样中,对 @data 的采样使用的是 @stable_clk,因此采样时刻与数据信号自身的时序毫无关系。相反,逻辑只是选择那些接近时钟上升沿的 @data_clk_samp 值。
也就是说,数据时钟上升沿出现的时刻与实际发生采样的时刻之间有一个随机的时间差。那么,这种方法凭什么可靠呢?
在关于逻辑设计中的时序基础的专页中,解释了 tsu 和 thold 的含义。长话短说就是:触发器的输入端口在时钟上升沿(假设触发器由上升沿触发)前后必须保持稳定。tsu 规定输入端口在上升沿之前必须保持稳定的时间,thold 则规定输入端口在上升沿之后必须保持稳定的时间。如果这两个条件中有一个不满足,触发器对上升沿的响应就是不可预测的。
我们也可以换一个角度来看:输入端口必须在上升沿附近的某一段特定时间内保持稳定。把这段时间记为 Δt = tsu + thold。接下来,我们将基于 Δt 以及其他参数,找出能够保证可靠运行的时序要求。
整个时序分析都基于“@data_clk_guard 为高、@data_clk_samp 为低”这一情形。这种情况发生后,一个时钟周期之后就会检测到 “0 1” 形态。换句话说,在下一个时钟周期时,@data_clk_samp_d 将为 '0',而 @data_clk_samp 将为 '1'。于是 @fifo_wr_en 变为高,因此 @data_samp 必须包含数据源有意发送的值。
因此,下面的分析将聚焦于这样一个问题:当 @data_clk_guard 为高、@data_clk_samp 为低时,为了保证信息正确到达,@data 需要满足什么要求?
整个分析分成两部分。在这两部分中,我都假设 @data_clk_guard 为高、@data_clk_samp 为低。第一部分的问题是:在不破坏这一假设的前提下,@data_clk 从低变高的最晚时刻是什么?随后我会找出能够确保 @data_samp 包含正确值的、关于 @data 的时序要求。
在分析的第二部分,我会问相反的问题:在不破坏“@data_clk_guard 为高、@data_clk_samp 为低”这一假设的前提下,@data_clk 从低变高的最早时刻是什么?随后我会就时序要求做类似的分析。
不过,先定义几个符号:
- tclk:@stable_clk 的时钟周期。
- tskew:这是 @data_clk 与 @data 各端口之间的偏移(skew),也就是从信号源到 FPGA 内各个触发器之间,所有连接在信号延迟上的最大差值。
- tj:抖动(jitter)对时钟周期造成的最大影响。换句话说,两个相邻时钟边沿之间的时间差总是介于 tclk - tj 与 tclk + tj 之间。或者更准确地说,时钟周期超出这个范围的可能性可以忽略不计。
分析的第一部分
这张时序图展示了 @stable_clk 的两个时钟周期。在下文的讨论中,@data_clk_guard 是在右侧那个上升沿处从 @data_clk 采得新值的。同样,@data_guard 也在同一个时钟边沿处从 @data 采得新值。
按照同样的原理,@data_clk_samp 和 @data_samp 与左侧的 @stable_clk 上升沿相关。
在这个场景中,@data_clk 恰好在黄色区域结束时变为高电平。此时触发器的时序要求被破坏,因此结果是不可预测的。但有可能出现 @data_clk_guard 为高、@data_clk_samp 为低的情况。
但是,如果 @data_clk 再晚一点才改变取值,那么 @data_clk_guard 必定为低,因为此时触发器的行为是可预测的:在这个场景里,输入端口在整个黄色区域内为低。因此我们可以这样说:如果 @data_clk_guard 为高、@data_clk_samp 为低,那么 @data_clk 从低变高的时刻一定比上面时序图所示更早。因此,这张时序图所展示的,正是 @data_clk 在最晚可能时刻发生变化的场景。
为了保证 @data_guard 含有一个可靠的值,@data 必须在黄色区域之前保持稳定。这便给出了第一个时序要求:@data 必须在 @data_clk 上升沿之前,稳定一段至少为 Δt 的时间。
请注意,如果 @data_clk 从低变高的时刻更早,这个时序要求仍然能保证 @data_guard 满足 tsu 的要求。因此,在所有 “@data_clk_guard 为高、@data_clk_samp 为低” 的场景中,触发器的 tsu 都能得到保证。
上面的时序图没有展示任何与偏移(skew)相关的信息。为了把偏移考虑进去,时序要求应写为:@data 必须在 @data_clk 上升沿之前,稳定一段至少为 Δt + tskew 的时间。
在这个场景中不需要考虑抖动(jitter),因为讨论只涉及一个时钟边沿。
分析的第二部分
下面是这个场景的时序图:
在这个场景中,@data_clk 恰好在前一个 @stable_clk 时钟边沿的黄色区域开始时变为高电平。
这种情况下,@data_clk_guard 无疑会为高。然而,@data_clk_samp 的值是不可预测的,因为前一个时钟周期里出现了时序违例。和前面一样,@data_clk_samp有可能为低。但如果 @data_clk 比这个更早改变取值,那么 @data_clk_samp 就一定会为高。
因此,如果 @data_clk_guard 为高、@data_clk_samp 为低,那么 @data_clk 从低变高的时刻一定比上面时序图所示更晚。因此,这张时序图所展示的,正是 @data_clk 在最早可能时刻发生变化的场景。
为了保证 @data_guard 含有正确的值,@data 必须在右侧黄色区域之后保持稳定。
根据上面的时序图,@data_clk 上升沿与第二个黄色区域结束之间的时间差是 Δt + tclk。但我们还必须把偏移(skew)和抖动(jitter)考虑进去。因此,@data 必须在 @data_clk 上升沿之后,稳定一段至少为 Δt + tclk + tskew + tj 的时间。
请注意,如果 @data_clk 从低变高的时刻更早,这个时序要求仍然能保证 @data_guard 满足保持时间的要求。因此,在所有 “@data_clk_guard 为高、@data_clk_samp 为低” 的场景中,触发器的 thold 都能得到保证。
时序要求
综合上面的时序分析,可以得出保证 01 信号采样可靠运行的两个时序要求:
- @data 必须在 @data_clk 上升沿之前稳定一段至少为 Δt + tskew 的时间。
- @data 必须在 @data_clk 上升沿之后稳定一段至少为 Δt + tclk + tskew + tj 的时间。
这些要求看起来可能有些复杂,但通常很容易保证满足。例如,如果 @data 跟随 @data_clk 的下降沿而改变,那么通常很容易保证这两个要求。如果 @stable_clk 的频率是 @data_clk 的三倍,往往也就足够了。
在大多数情况下,并不需要知道 Δt、tskew、tj 的精确值。通常只需要根据上面两个要求,算出 Δt 和 Δt + tskew + tj 最多可以是多少。如果 @stable_clk 的频率足够高,这两个表达式的取值通常都会大于 FPGA 实际可能达到的值。
应该使用 IOB 寄存器来尽量减小 FPGA 各输入端口之间的时序差异(即 tskew)。此外,还应该针对这些输入端口编写时序约束(timing constraints),以确保使用了 IOB 寄存器。
01 信号采样的变体
到目前为止的讨论中,我一直假设 @data 是跟随 data_clk 的下降沿而改变的。如果 @data 是跟随 @data_clk 的上升沿而改变,那么逻辑应相应调整为在 @data_clk_guard 为低、@data_clk_samp 为高时启动。换句话说,逻辑应该通过寻找 “1 0” 形态来检测 @data_clk 的下降沿。
也可以选择其他时机来获取 @data 的值。例如,也许把 @fifo_wr_en 延迟几个时钟周期会更好。有时,让 @fifo_wr_en 比上面建议的更早触发反而更好。这取决于 @data_clk 与 @data 之间的时序关系。请查阅信号源的数据手册,并按照上面的方法进行时序分析。
如果 @data_clk 的频率相对较高,也可以使用 DDR 寄存器来采样 @data_clk 和 @data。实现这一方案的逻辑会稍复杂一些,但原理完全相同。
@data_guard 真的是亚稳态防护吗?
这个问题的简短答案是:“是的”。@data 与 @stable_clk 是异步的,因此 @data_guard 的时序要求并不能得到保证。
但我们不妨把讨论范围缩小到 @data_guard 的内容真正被使用的那些时钟周期上:注意,上面的两个时序要求保证了实现 @data_guard 的触发器可靠工作。这些触发器的 tsu 和 thold 都能得到保证。
因此,@data_guard 实际上并不是用作亚稳态防护。就这个寄存器的使用方式而言,它只是起到一个延迟寄存器的作用。不过,多一个额外的寄存器来防范物理信号(@data)行为异常时可能出现的时序违例,总归没有坏处。这一点在信号通过连接器连到 FPGA 时尤其有意义。
一个实际例子
在另一个页面上,有一个与 OV7670 摄像头传感器对接的逻辑示例。该逻辑依靠 01 信号采样从这个摄像头传感器获取像素数据。来自摄像头传感器的输入如下:
input pclk_in;
input [7:0] D_in;
input hsync_in, vsync_in;
执行 01 信号采样的逻辑如下:
(* IOB = "TRUE" *) reg [7:0] D_guard;
(* IOB = "TRUE" *) reg pclk_guard, hsync_guard, vsync_guard;
reg [7:0] D;
reg pclk, hsync, vsync;
wire sample_valid;
reg previous_pclk;
always @(posedge stable_clk)
begin
// Metastability guards on asynchronous inputs
D_guard <= D_in;
pclk_guard <= pclk_in;
hsync_guard <= hsync_in;
vsync_guard <= vsync_in;
D <= D_guard;
pclk <= pclk_guard;
hsync <= hsync_guard;
vsync <= vsync_guard;
previous_pclk <= pclk;
end
assign sample_valid = pclk && !previous_pclk;
为了叙述清晰,这里展示的 Verilog 代码与关于 OV7670 的页面上的 Verilog 代码略有不同。这些差异对逻辑的工作方式没有影响。
把这段 Verilog 代码与本页开头给出的代码比较一下。信号名称变了,但含义是一样的:原来代码中的 @data,现在变成了 @D_in、@hsync_in 和 @vsync_in;@data_clk 变成了 @pclk_in;@fifo_wr_en 变成了 @sample_valid。名称的变化可能会让人困惑,但除了名称之外,逻辑与上面的 Verilog 代码并无不同。
注意各寄存器声明前面的 “(* IOB = \"TRUE\" *)”。在使用 Vivado 时,这是请求把寄存器放入 IOB 的一种可行方法。
在这个示例中并没有给出 FIFO。因为我们并不想把所有数据都写入 FIFO:当 @sample_valid 为高时,说明 @D、@hsync 和 @vsync 包含了来自摄像头传感器的正确值。但这并不意味着我们想把 @D 写入 FIFO,这还要取决于 @hsync 和 @vsync。因此,在关于 OV7670 的示例中还有额外的逻辑,用来确保只有像素数据被写入 FIFO。
不过,还是先来看看最有趣的部分:时序分析。
在这个示例中,@stable_clk 的频率是 100 MHz,@pclk_in 的频率是 25 MHz。根据 OV7670 的数据手册,信号 @D_in、@hsync_in 和 @vsync_in 在 @pclk_in 从高变低(下降沿)之后的 5 ns 内被保证保持稳定。
@pclk_in 的时钟周期是 40 ns,因此从下降沿到上升沿的距离是 20 ns。现在我们把它与上面的时序要求做个对比。
第一个时序要求是:@D_in、@hsync_in 和 @vsync_in 必须在 @pclk_in 上升沿之前稳定一段至少为 Δt 的时间。实际上,这些信号从下降沿之后 5 ns 起开始保持稳定,因此在下一个上升沿之前至少稳定 15 ns。回忆一下 Δt = tsu + thold,所以这个时序要求的实质是 tsu + thold 必须小于 15 ns。对任何 FPGA 来说这都是可以满足的。
第二个时序要求是:这些信号必须在 @pclk_in 上升沿之后稳定一段至少为 Δt + tclk + tskew + tj 的时间。但这些信号只会因为下降沿而改变,因此这个要求的实质是 Δt + tclk + tskew + tj 必须小于 20 ns。由于 @stable_clk 的频率是 100 MHz,tclk 等于 10 ns,所以实际要求就成了 Δt + tskew + tj 小于 10 ns。同样,这对任何 FPGA 来说都是显而易见的。
这个例子说明了,即使不了解 FPGA 精确的时序参数,也很容易保证满足时序要求。
结论
当数据速率相对于 FPGA 所能支持的时钟频率较低时,01 信号采样是一种极佳的解决方案:数据时钟不必稳定,而且事先也不需要知道数据时钟的精确频率,只要保证上面两个时序要求就够了。
这种方法还有额外的优势:如果数据时钟短暂地停止工作,唯一的后果就是在这段时间内没有采集到数据。这个时钟的任何故障,其危害都被限制在数据流中断这一范围内。这会导致系统出现明显的功能异常,但这种异常看起来就像是时钟出了问题(而不是像FPGA 被鬼缠身那样)。
因此,尽管对数据信号的采样带有一定的固有随机性,01 信号采样对于源同步输入来说仍然是一种可靠、稳健的解决方案。它唯一的真正缺点是对数据速率有所限制。


