01signal.com

Clock domain crossing 带数据

本页是关于 clock domains的三页系列中的最后一页。

当一台 bit 不够用时

很多时候,需要通过 clock domains 的信号是一个数据字,而不是单个 bit。在这种情况下,直接的解决方案是由 FPGA的供应商提供的 dual-clock FIFO ,如前所述。但有时这不是一个选择。此外,过去必须有人实现 FIFO 。

所以目标是让一个 vector signal 在另一个 clock domain上正确显示。让我们首先从 clock domain crossing的一个幼稚且不正确的示例开始,只是为了解释为什么它不那么容易:

reg [7:0] foo, bar, bar_metaguard;

always @(posedge clk1)
  foo <= foo + 1;

always @(posedge clk2)
  begin
    bar_metaguard <= foo; // This will fail sometimes!
    bar <= bar_metaguard;
  end

这与上一页的简单 metastability guard 示例完全相同,但 registers 是 8-bit vectors,而 @foo 是 counter ,而不是在 '0' 和 '1'之间转换。

那么为什么这是错误的呢?问题是 paths 从 @foo 到 @bar_metaguard在 routing delays 中的差异: 八个 bits 中的每一个都有不同的 delay。当 @foo 发生变化时,这些 bits 的一些变化可能会随着合法的 timing 到 @bar_metaguard的 flip-flops,而其他的则不会及时到达。

所以即使组成 @bar_metaguard的8个 flip-flops 中没有一个 metastability ,也会出现 @foo 发生变化的情况, @bar_metaguard 的 bits 中只有一部分获得了新的值,而其他的则保持旧值。因此,如果 @foo 从 0xff 变为 0x00,则 @bar_metaguard 的下一个值可能是任何值。那是因为有些 bits 在更改前获得了 @foo的值,而有些则在更改后获得了 @foo的值。这个不正确的值将在 @clk2的 clock cycle 之后在 @bar 上可见。

要解决这个问题,首先需要定义需求: @bar 是否需要始终包含一个有效值,或者它是否打算偶尔将信息从一个 clock domain 传递到另一个 clock domain ?我将分别讨论这两个选项。

选项1: 连续采样

如果目标字(示例中为@bar )需要连续采样另一个 clock domain (@foo)上的字,并且始终包含合法且有意义的值,则只有一种方法可以确保这一点: 使用上面显示的 clock domain crossing 的朴素方法,但要确保在 @clk1的每个 clock cycle 上,只有一个 @foo的 bits 发生变化(或没有变化)。换句话说, metastability guard 和 vector signal一起使用,但要确保避免在同一个 clock cycle上更换多个 bits 时出现问题。

因为每个 clock cycle上只有一个 bit 可以更改,所以每个更改都被 @bar_metaguard采样或遗漏,无论哪种方式,它都反映了 @foo 所具有的值之一。

因此,如果上面示例中的 @foo 是使用 Gray code 而不是普通 binary code的 counter ,它会工作得非常好: Gray coding 的本质是每次字数增加只有一个 bit 变化,所以 @bar 可以保证总是携带一个有意义的值。

但是等等,如果 @clk1 的频率高于 @clk2会发生什么? @bar 跳过 @foo的一些值是否可以,这并不重要。例如,如果 @foo 是 Gray code 格式的计数器,则在查看 @bar时可能会跳过一些计数的数字。然而,在 @bar 上看到的所有值都是正确的,因为它们确实在某个时间点出现在 @foo 中。

这种方法最常见的使用是在 dual-clock FIFOs内部,其中 Gray code 用于跨两个 clock domains传输 FIFO的 RAM 中的地址: FIFO的写入端将其写入 RAM 的最后一个字的地址编码为 Gray code。通过对字使用 metastability guards ,将编码后的字传输到另一台 clock domain上的读取端。在另一个方向使用相同的方法。

因为双方都知道对方的更新地址( metastability guards延迟后),所以双方也可以计算出 FIFO中有多少元素,从而产生 empty 和 full这样的信号。

以更高的速率传送事件

回想一下上一页, metastability guard 搭配单颗 bit 对音源 clock domain的 clock 频率有限制。如果此 bit 的每次更改都是向对方告知事件的一种方式,那么如果这些事件经常发生,接收方就有可能错过事件。

解决方案是在 clock domains上传送一个以 Gray code 编码的 counter 。这样,接收方就知道发生了多少事件,因此不会丢失任何信息。选择这个 counter 的 bits 的数量,以便即使事件发生在 @clk1的每个 clock cycle 上,接收方仍然能够推断出发生了多少事件。

所以从这个意义上说,使用 vector 比使用单个 bit更容易: 如果单个 bit 发生变化,并且由于目的地的 clock 速度较慢而错过了这一事实,那么结果就是错过了任何发生的事情。但是,通过 clock domains 正确传输的单词(例如,使用 Gray code),不会丢失任何信息。

如果一个 bit 就足以达到此目的,那么 Gray code counter 只是一个在每个事件上改变其值的位。换句话说,使用单个 bit,使用 Gray code counter 的解决方案与简单的 metastability guard完全相同。

对 paths的时机的一点评论

正如本节的标题所暗示的,跳到下一节可能是个好主意。

关于 vector signals的 metastability guard 有一个基本假设: 这些 paths 的 delays 之间的差异不超过源的 clock的 clock cycle 。

几乎可以肯定地满足了这个假设,而无需对其进行任何特殊处理,仍然让我们考虑这个理论示例: 假设 @clk1 的频率为 500 MHz,而 @foo的 paths 到 @bar_metaguard 中的一个具有 1 ns的 routing delay 。另外,假设另一个 path 有一个 routing delay 的 4 ns,这当然是极不可能发生的,但让我们看看会发生什么:

其中一台 bits 改变了它的价值,而改变开始了 4 ns的旅程。在后面的 clock cycle,以后的 2 ns 上,另一个 bit 变化,经过 1 ns时间到达 @bar_metaguard 。但这比第一台 bit的到来要早 1 ns 。因此, @bar_metaguard 可以使用 @foo 从未有过的值对整个单词进行采样。

由于 routing delays 通常比本示例中的短得多,因此预计不会在现实中发生这种情况。尽管如此,理论上任何 routing delay 都是可行的。为了完全消除这种可能性,可以使用如下的 timing constraint (以 Vivado 格式编写):

set_max_delay -datapath_only -from [ get_pins -hier -filter {name=~*/C} ] -to [ get_pins -hier -filter {name=~*_metaguard*/D} ] 1.5

这个 constraint 类似于上一页中给出的带有 set_max_delay 的那个。但是请注意,在上一页中, metastability guard 位于“-from”部分,而此处位于“-to”部分。所以 constraints 不适用于同一个 paths。上一页中 constraint 的目的是给 metastability guard 一些时间从 metastability恢复。因此, constraint 适用于相同 clock之间的 paths 。另一方面,上面的 constraint 与 clock domain crossing 本身有关。

因此,这个 constraint 的写法不同: 由于相关的 paths 连接在 unrelated clocks的 clock domains 之间,因此将这些 clocks 的 skews 和这些 clocks 的 jitters 考虑在内是没有意义的。这就是 -datapath_only 部分所说的: 不要介意 clocks 到达 flip-flops所需的时间。只需测量 path。

使这个 constraint 令人困惑的是 path 从源 flip-flop的 clock pin 开始,并在目的地的 data input pin (D) 结束。因此,秒表在源的 flip-flop 获得其 clock 时开始,并在更新的信号到达 destination时结束,这是满足其 setup time所必需的。因此,此 path 包括双方的时序规格和要求。

通过将所有这些 paths 限制为 1.5 ns,就像在这个 timing constraint中一样,没有一个 path 可以超过这个时间限制,因此 path delays 之间的 skew 也被限制在这个数量之内。所以即使 @clk1 有 2 ns的 clock period , paths 也不可能以错误的顺序到达。再一次,无论如何,这是极不可能的,但这是确保这一点的方法。

请注意,如果 path 到 metastability guard 受到 false path constraint (例如 set_false_paths 或 set_clock_groups)的影响,则 set_max_delay 可能不会产生任何影响: false path constraint 可能优先。因此,请始终检查 timing report中的 paths ,以验证工具是否按需要解释 constraints 。关于 timing的不同页面对此进行了讨论。

选项#2: register的不定期更新

每个 clock cycle 上只能更改一个 bit 的限制通常过于严格。当数据偶尔更新时,可以使用另一种技术。对于以下示例,假设 @do_update 在多个 clock cycles中仅激活一次(即具有值 '1')。另外,我们假设这个 register 用于指示 @foo 中的值应该用 @new_value更新:

reg [7:0] foo, bar;
reg       toggle, toggle_metaguard, toggle_a, toggle_b;
reg       new_value_bar;

always @(posedge clk1)
  if (do_update)
    begin
      foo <= new_value;
      toggle <= !toggle;
    end

always @(posedge clk2)
  begin
    toggle_metaguard <= toggle;
    toggle_a <= toggle_metaguard;
    toggle_b <= toggle_a;

    if (toggle_a != toggle_b)
      bar <= foo; // No metastability guard, because foo is stable
    new_bar <= (toggle_a != toggle_b); // Not necessary, just side info
  end

现在,忽略 @new_bar。稍后我会谈到这一点。

这就是它的工作原理: @foo 仅在 @do_update 处于活动状态时更新。发生这种情况时, @toggle 在同一个 clock cycle上更改为相反的值。

在 @clk2的 clock domain上, @toggle_metaguard 获取 @toggle 的值作为 metastability guard。在下一个 clock cycle上,这个值被复制到 @toggle_a中。之后的 clock cycle ,直接将 @foo 中的值复制到 @bar中。这是因为 @toggle_a 和 @toggle_b 在恰好一个 clock cycle期间具有不同的值。

@bar 和 @foo 在不同的 clock domains 中这一事实没有任何意义,因为 @foo 已经稳定了远远超过足够的时间来满足时序要求。

为什么我对此如此肯定?这次我有一个很好的理由,它是这样的: 整个过程从 @toggle_metaguard 改变值开始,因为 @toggle 改变了。如果 @bar 在同一个 @clk2 周期对 @foo 进行采样,那将是不安全的,但如果运气好的话,可能会没事。但是在 @toggle_metaguard的新值达到 @toggle_a之前,还有另一个 clock cycle 的 @clk2 。而且 @bar 甚至没有更新,只是在 @clk2的下一个 clock cycle 上。

所以从 @foo 发生变化到 @foo 被 @bar 采样到有一段时间,对应 @clk2中至少两个 clock cycles 。与任何 flip-flop的 setup time相比,那是永恒的。也就是说,应用 set_max_delay 是有意义的,如 @toggle_metaguard上一页所示。 paths 到 @bar也可以这样做,尽管由于刚才提到的永恒,这不太可能是必要的。

这种方法的致命弱点是 @do_update 必须很少被激活,以确保 @foo 在被 @bar采样时保持稳定。此类更新之间的合理最短时间是对应于 @clk2的四个 clock cycles 的时间。所以计算的是 @clk1 中有多少个 clock cycles 对应 @clk2的四个 clock cycles ,并向上取整到最接近的整数。如果 @clk1 比 @clk2 慢四倍(或更慢),那根本不是限制。否则, logic 中必须有某种机制来确保 @do_update 不会比允许的更频繁地激活。

事实是,在现实生活中的 designs中,当更新速度非常慢时,有时会不小心越过 clock domains ,而没有任何 @toggle 提供的保护。这样一来, @foo 就会被连续复制到 @bar 中。当 @foo 长时间改变一次时, @bar 可能在一个 clock cycle中包含不正确的值,但谁在乎呢?很多时候,这个错误是忽略整个 clock domains问题的结果,因为嘿,它有效。直到它没有,偶尔

说到草率,请注意在上面的示例中, @toggle 及其任何相关的 registers 都没有被重置或分配初始值。这通常很好,因为很可能 synthesizer 为它们分配了一个初始值 0。即使这些 registers 最初没有相同的值,它也会导致对 @foo进行一次不必要的采样,仅此而已。不过,重置这些 registers 可能是个好主意。

更高级的变体

到目前为止,我已经提出了三个简单的例子:

这些简单的例子是其他几种机制的基础。

首先,我答应在上面的例子中说一下 @new_bar 。所以它只是一个 register 在一个 clock cycle期间是高的,当 @bar 有一个新的值时。这没什么特别的,但请注意, @bar 和 @new_bar 在另一个 clock domain中反映了 @foo 和 @do_update 。所以这是一种通过 clock domain 传递命令和状态消息的方法(我是否提到过应该尽可能使用 FIFO ?)。

最后一个例子的另一个有趣的扩展是: 用 dual port RAM 代替 registers、 @foo 和 @bar对。这是一种跨 clock domains传输 buffers 数据的方法: 假设 @clk1 的 clock domain 中的 logic 向 RAM写入数据,一段时间后填充了这个 RAM 的一半。随着 logic 继续填充 RAM的后半部分,它改变了 @toggle的值。这个 register 复制到 @clk2的 clock domain 上,一模一样。但不是像示例中那样更新 @bar ,而是 logic 消耗 RAM前半部分的数据。

这就是这个简单的 register 可以同步 double-buffer 机制的方式,其中一侧从 RAM 读取数据,另一侧从 RAM读取数据。事实上, @toggle 的作用不仅仅是改变值,它还告知对方当前正在写入 RAM 的哪一半。

然而,最好尽可能使用 FIFO 。尽管这种 double-buffer 机制听起来很诱人,但只有在没有更好的选择时才应该使用它。例如,当从 RAM 读取数据的顺序与写入数据的顺序不同时。

概括

最后,归结为: 在 clock domains 和 unrelated clocks之间的转换中,总是涉及到 resynchronization logic 。通过这个 resynchronization 的数据字是有限的,所以只有一个 bit 可以改变源 clock 的每个 clock cycle 上的值(示例中为@clk1 )。否则,非法数据可能会到达目的地。

在某些应用程序中,这已经足够了,但是当这种限制过于严格时,数据可以在 clock domains 与 vector register 之间或通过 RAM之间移动,而无需在数据本身上使用任何 resynchronization logic 。这要归功于 logic 在数据的写入操作和读取操作之间保持最小的时间间隔。这个时间间隔确保数据字在目的地被采样时是稳定的。尽管如此,这款 logic 还是基于与 clock domain crossing相同的技术。因此,此解决方案涉及 resynchronization logic ,它仅限于一次更改一个 bit ,可能通过使用 Gray code。

所以当涉及到 unrelated clocks 时, resynchronization logic 和一个 bit 的规则总是存在的。这只是他们如何应用的问题。

此页面由英文自动翻译。 如果有不清楚的地方,请参考原始页面
Copyright © 2021-2024. All rights reserved. (6f913017)