01signal.com

时序约束(timing constraints)与时钟域交叉

本页属于关于时序的系列文章之一。前面的页面解释了时序计算背后的理论,展示了如何编写若干时序约束,并讨论了时序收敛(timing closure)的原则。本页介绍如何定义与时钟域(clock domain)相关的时序约束。

引言

现在,我们已经掌握了如何查找特定逻辑元件、如何定义路径(path),下面开始学习如何利用这些知识来书写时序约束。

我们首先要看的是时序约束与时钟域之间的关系。这两个话题紧密相连,讨论其中一个不可能不涉及另一个。因此,如果你对时钟域还不熟悉,我建议你先读一读这个关于时钟域的系列页面。这对理解我下面将要使用的术语是必要的。

逻辑本身决定了时钟之间的关系

我们来看下面这段 Verilog 代码:

module top(
    input clk,
    input foo,
    output reg bar_reg,
    output reg baz
);
    reg foo_reg;
    reg bar;
    reg baz_metaguard;
    wire pll_clk_8, pll_clk_6;

   clk_wiz_1 pll_i
   (.clk_in1(clk),
    .clk_out1(pll_clk_8),
    .clk_out2(pll_clk_6));

always @(posedge pll_clk_8)
  foo_reg <= foo;

always @(posedge pll_clk_6)
  begin
    bar <= !foo_reg;
    bar_reg <= bar;
  end

always @(posedge clk)
  begin
    baz_metaguard <= bar;
    baz <= baz_metaguard;
  end

这与我们前面见过的带 PLL 示例几乎一样。区别在于,这里出现了一个新的时钟域交叉(clock domain crossing):@bar 的值通过一个亚稳态保护(metastability guard)寄存器被复制到 @baz。

因此,这个示例中有两种类型的时钟域交叉:

我判断一条路径属于哪种时钟域交叉时,只看一个标准:是否存在亚稳态保护。如果逻辑预期两个时钟是无关的,它就会使用必要的安全机制。因此,其它因素都不重要:即使我们能够保证这些时钟之间路径上的时序要求,也没有任何理由去这么做。

反之亦然:如果逻辑预期两个时钟是相关的,它就没有任何针对时序违例的保护机制。因此,必须满足这些时钟之间所有路径上的时序要求。

所以,结论是:逻辑总是会对哪些时钟是相关时钟、哪些不是,作出自己的假设。这些假设体现在有没有保护机制上。但真正让这些假设成为现实的,是 FPGA 工具。尤其是,工具会确保相关时钟之间路径上的时序要求得到满足。

因此,我们必须确保工具了解逻辑对时钟之间关系的假设。

注意,在上面的例子中,工具没有任何办法知道逻辑对 @pll_clk_6 与 @clk 之间关系的预期:逻辑可能假定它们是相关时钟,也可能假定它们不是。事实上,我之前在“想法 #9”中用过类似示例,来演示两个相关时钟之间的路径。而在上面的例子中,这两个时钟是按无关时钟来处理的。

也许工具可以依据 baz_metaguard 这个名字作出某种聪明的猜测,也许典型的亚稳态保护结构也能提供一些线索。但要凭这些东西来决定如此重要的问题,还远远不够。

把时钟之间的关系告诉工具

到目前为止的示例中,始终都只有一条时序约束:

create_clock -period 4.000 -name clk [get_ports clk]

首先想到的一个问题是:默认情况下,工具会怎样对待从 @pll_clk_6 到 @clk 的时钟域交叉?如果这是唯一的一条时序约束,工具会在 @bar 到 @baz_metaguard 的路径上强制任何要求吗?

答案是:这取决于你用的是哪一款 FPGA 工具。尽管几乎所有 FPGA 工具都会把 @pll_clk_6 与 @pll_clk_8 视为相关时钟,但其它时钟对会被怎样处理,就不那么明确了。工具通常默认把所有时钟都当作相关时钟,但依赖这个默认行为并不安全。

许多 FPGA 工具都支持 set_clock_groups 命令。这是定义时钟之间关系的最佳方式。正因为这条命令非常重要,在设计中正式使用它之前,建议先查阅工具的官方文档。

对于上面的例子,在 Vivado 中可以这样使用这条命令(其它工具也类似):

set_clock_groups -asynchronous \
  -group [list \
     [get_clocks -of_objects [get_pins pll_i/clk_out1] ] \
     [get_clocks -of_objects [get_pins pll_i/clk_out2] ] ] \
  -group [get_clocks -of_objects [ get_pins pll_i/clk_in1] ]

这里 get_clocks 和 get_pins 的用法前面已经解释过:每条 get_clocks 命令都会根据 clk_wiz_1 的相应端口名字找到对应的时钟对象(注意,clk_wiz_1 的实例化(instantiation)名字是 pll_i)。例如,最后一条 get_clocks 命令得到的是与 @clk 对应的时钟对象。

这个示例展示了 set_clock_groups 是如何定义时钟组的。本例中,一组包含 @pll_clk_6 和 @pll_clk_8,另一组只含 @clk。这条约束告诉工具:同一组内的所有时钟都是相关时钟;同样,如果两个时钟属于不同的组,工具就把它们当作无关时钟。

换句话说,当且仅当一条路径两侧的时钟属于同一个组时,工具才会在该路径上强制执行时序约束。

一个设计的时序约束中可以写多条 set_clock_groups 命令,但最好只用一条命令把所有时钟划分成组。这样可以避免矛盾,也有助于防止混乱。这条命令的主要优点,就是它可以简洁地描述时钟之间的关系。短小、简洁、像数学表达式一样。这正是我们想要的方式。

注意,set_clock_groups 应该在所有 create_clock 命令之后使用,因为其中引用的时钟对象应当已经存在。

时钟之间的关系出现矛盾

如果设计中的某些地方把几个时钟当作相关时钟,而在另一些地方又把它们当作无关时钟,那么就无法使用 set_clock_groups。

例如,在上面的 Verilog 代码中,@clk 与 @pll_clk_6 被当作无关时钟。但理论上,还可能存在一些额外逻辑,需要把这两个时钟当作相关时钟来对待。即使这不是个好主意,但这些额外逻辑确实需要强制 @pll_clk_6 与 @clk 之间的时序约束。

如果因这种原因无法使用 set_clock_groups,那么首先问自己:是否愿意修改一下逻辑,使 set_clock_groups 可以用?这不仅仅是因为 set_clock_groups 是一条很好的命令。若没有一条简单规则来明确哪些时钟是相关时钟、哪些不是,时钟域交叉就很容易出错。如果你仍然不想修改逻辑,那么解决方案就是定义伪路径(false path)。

简单说说 set_false_path 命令

用于声明伪路径的两个主要命令是:set_clock_groups 和 set_false_path

set_clock_groups 命令已经在上面介绍了:凡是位于两组不同时钟之间的路径,都会被当作伪路径。但 set_clock_groups 并不总是能覆盖所有需要声明为伪路径的路径。有些情况下,对路径的选择必须比划分时钟组更具体。set_false_path 通过允许精确地选择路径来解决这个问题。

例如,把上面的 set_clock_groups 命令替换成 set_false_path 命令。唯一真正需要修正的,是从 @bar 到 @baz_metaguard 的路径。其它所有路径的时序要求本来就正确地得到了执行。下面就是针对这条路径写出的一条 set_false_path。不过,不要从下面这个例子中学习:

set_false_path -from [get_cells bar_reg__0] -to [get_cells baz_metaguard_reg]

这条命令用 get_cells 来选择路径的起点和终点,因此你需要知道两侧的 cell 对象叫什么名字。这里出现了 bar_reg__0 这个名字,恰好说明了依赖名字会有问题:它本来应该叫 bar_reg,但正如前面已经解释过的,因为一个巧合,它最终变成了 bar_reg__0。

所以,这个例子说明了 set_false_path 的一个问题:要精确地选择设计中的逻辑元件,往往不得不依赖对象的名字。这个问题以及可能的解决办法,前面已经讨论过

这条命令还可以简化:既然 @baz_metaguard 是一个亚稳态保护寄存器,那么到达这个寄存器的路径从哪里来就不重要了。那么,为什么不把到达这个寄存器的所有路径都忽略掉呢?

set_false_path -to [get_cells baz_metaguard_reg]

这条命令的效果与上面完全一样。

用 set_false_path 代替 set_clock_groups

另一种可能性,是按时钟来定义路径。实际上,上面的 set_clock_groups 命令完全可以用下面这些约束来代替:

set_false_path -from [get_clocks -of_objects [get_pins pll_i/clk_out1]] \
               -to [get_clocks -of_objects [ get_pins pll_i/clk_in1] ]

set_false_path -from [get_clocks -of_objects [get_pins pll_i/clk_out2] ] \
               -to [get_clocks -of_objects [ get_pins pll_i/clk_in1] ]

set_false_path -from [get_clocks -of_objects [ get_pins pll_i/clk_in1] ] \
               -to [get_clocks -of_objects [get_pins pll_i/clk_out1] ]

set_false_path -from [get_clocks -of_objects [ get_pins pll_i/clk_in1] ] \
               -to [get_clocks -of_objects [get_pins pll_i/clk_out2] ]

这是一种既冗长又繁琐的写法,却生成了同样的伪路径。它没有把时钟划分成组,而是为每一对无关时钟写两条 set_false_path 命令:每个方向一条。显然,面对这么多条约束,出错是多么容易。而这只是一个只有三个时钟的简单例子而已。

尽管如此,人们还是常常用 set_false_path 这样写,而不是用 set_clock_groups。这很可能是因为有人直接从某处复制了时序约束。

错误是如何发生的:一个示例

我们来看一个可能出现错误的简单例子:根据上面的讨论可以清楚地看出,真正需要声明为伪路径的只有一条,即从 @bar 到 @baz_metaguard。因此,上面例子中的四条 set_false_path 命令里,只有下面这一条有意义:

set_false_path -from [get_clocks -of_objects [get_pins pll_i/clk_out2] ] \
               -to [get_clocks -of_objects [ get_pins pll_i/clk_in1] ]

另外三条 set_false_path 命令实际上没有覆盖任何路径。不过且慢,能不能把约束再简化一点?也许所有到达 @clk 的路径都应该是伪路径?那么看看这条命令如何:

set_false_path -to [get_clocks -of_objects [ get_pins pll_i/clk_in1] ]

实际上,由于定义 clk 的 create_clock 命令就在 set_false_path 命令上方,所以这条命令还可以简写成:

set_false_path -to [get_clocks clk]

这条约束短小、优雅。遗憾的是,它错得离谱:我前面说“所有到达 @clk 的路径都应该是伪路径”,可别忘了从 @baz_metaguard 到 @baz 的那条路径,那是一条从 @clk 到 @clk 的路径。这条路径当然不应该是伪路径。然而,上面最后两条 set_false_path 命令把所有到达 @clk 的路径都包含了,哪怕路径是从 @clk 出发的也一样。

这类错误很容易发生,尤其当伪路径约束的写法缺少数学表达式般的精确性时。此时,创建一些专门的时序报告会很有帮助,它们能显示哪些路径被当作了伪路径,哪些没有。


到这里,关于 FPGA 内部路径时序约束的实践讨论就结束了。下一页会介绍多周期路径(multicycle path),它同样属于这个话题。不过,多周期路径约束通常并不值得推荐。因此,直接跳到I/O 时序约束入门也是完全可以的。

本页由机器从英文翻译而来。如有疑问,请参阅原文
Copyright © 2021-2026. All rights reserved. (dcc38493)