01signal.com

时序约束(Timing constraints)为 multi-cycle 路径

此页面属于关于时序(timing)的一系列页面。前几页解释了时序计算背后的理论,展示了如何编写几个时序约束(timing constraints)并讨论了时序收敛(timing closure)的原理。本页针对multi-cycle paths讨论时序约束

介绍

关于 multi-cycle 路径的第一件事是它通常是一个坏主意。尽管此页面解释了如何使用 multi-cycle path 约束,但结论应该是完全避免这种技术。在 ASIC 世界中使用这种技术是有原因的,但在 FPGA 设计中通常最好添加一个时钟。

也就是说,让我们看看这种时序 exception (timing exception)什么时候才有意义。

考虑这 Verilog 代码示例:

reg foo, bar;
   reg en, pre_en;

   always @(posedge clk)
     begin
	pre_en <= !pre_en;
	en <= pre_en;

	if (en)
	  begin
	     foo <= !foo;
	     bar <= foo;
	  end
     end

请注意,此示例不完整: 您可能需要将综合属性(synthesis attributes)添加到 @pre_en 和 @en。否则,由于综合工具(synthesizer)的优化可能会发生意想不到的事情。更多关于下面的内容。

@pre_en 在每个时钟周期(clock cycle)上都在 '0' 和 '1' 之间来回变换。 @en 也是如此,但略有时延(delay)。

“if (en)”部分意味着对于“begin”和“end”之间的所有内容, @en 充当时钟使能(clock enable)的角色: 当 @en 为低电平时,这部分 Verilog 代码没有任何反应。换句话说,当 @en 为低电平时, @foo 和 @bar 的行为就好像时钟边沿(clock edge)不存在一样。

在此示例中,每两个时钟周期中就有一次 @en 为高。因此, @foo 和 @bar 的行为就好像时钟的频率是其实际值的一半。因此,可以相应地放宽时序要求: tsetup 的计算可以使用两倍大的时钟周期(clock period)来完成。

至于 thold,没有变化: 此时序要求的计算假设相同的时钟边沿达到两个触发器(flip-flops)。因此,时钟周期没有任何意义,正如在 thold 分析示例中所讨论的那样。因此,较慢的时钟的错觉对 thold没有影响。

何时使用时钟使能

使用时钟使能只有两个理由:

目前尚不清楚时钟使能是否会改善 FPGA的功耗。可以说,额外的时钟会浪费电量。但是时钟使能也是扇出(fan-out)高配的信号。平均而言,时钟使能更改其值的频率与它所替代的时钟一样频繁。所以在逻辑状态的变化上(耗电大户),没有区别。

除非有特殊原因必须使用 multi-cycle 路径,否则请随意跳过此页面。就算你在设计中有一个时钟使能(clock enable)在技术上是合适的,但相关的时序 exception可能还是不要用比较好。特别是,如果没有这个时序 exception(timing exception)也能轻松实现时序约束,那犯错误的风险是不值得的。

timing exception

关于上面的 Verilog 代码,这些是 Vivado的 multi-cycle path 约束:

set en_regs [all_fanout -endpoints_only -only_cells -flat [get_nets en]]
set_multicycle_path -setup -from $en_regs -to $en_regs 2
set_multicycle_path -hold -from $en_regs -to $en_regs 1

Quartus也一样:

set en_regs [get_fanouts en]
set_multicycle_path -setup -from $en_regs -to $en_regs 2
set_multicycle_path -hold -from $en_regs -to $en_regs 1

Vivado 和 Quartus 的区别就在第一行: 命令 "all_fanout" (command "all_fanout")与 Vivado配合使用,而“get_fanouts”与 Quartus配合使用。约束(constraints)与 SDC配合使用的其他工具类似。

第一行查找所有同步逻辑单元(synchronous elements)(cells),这些同步逻辑单元是任何以 @en开始的路径(path)的结尾。 cell 对象的列表存储在 $en_regs中。接下来的几行将列表中的时序要求(timing requirements)更改为路径,这些路径的起始和结束位置均为 cells 。

这需要很多解释。为什么我要这样写 $en_regs 的定义?为什么有两个 set_multicycle_path 命令?为什么第二个命令上写着“-hold”,尽管我说过 thold 的时序要求不受 multi-cycle 路径的影响?

我将从 set_multicycle_path 命令开始,因为这是比较容易解释的部分。

set_multicycle_path 命令

如上所示,命令

set_multicycle_path -setup -from $en_regs -to $en_regs 2
set_multicycle_path -hold -from $en_regs -to $en_regs 1

为了稍微概括上面的示例,让我们也考虑一下: 如果时钟使能在每八个时钟周期中激活一次,则 Verilog 代码将是:

reg en;
   reg [2:0] pre_en;

   always @(posedge clk)
     begin
	pre_en <= pre_en + 1;
	en <= (pre_en == 0);
     end

请注意, @en 的行为类似于 strobe,并且每次仅在一个时钟周期期间处于活动状态。它不是时钟分频器(clock divider)的 MSb 。

这种可能性的 multi-cycle 约束是:

set_multicycle_path -setup -from $en_regs -to $en_regs 8
set_multicycle_path -hold -from $en_regs -to $en_regs 7

通过这两个例子,很明显第一个 set_multicycle_path 命令中的数字只是时钟使能的分频比。

至于第二个命令,其分频比相同,但减一。因此它始终是 N 和 N-1。

关于第一台命令没有太多需要解释的: 如果时钟使能在 N中的一个时钟周期中处于活动状态,则允许的时延乘以 N。这与 tsetup的时序要求相关。

但为什么会有第二个命令?为什么需要说 thold?答案是第一个命令也改变了最小时延的要求。换句话说, thold 的计算也受到带有“-setup”选项的命令的影响。为什么?可能没有很好的解释。

第二台命令纠正了这个问题: 它将最小时延的要求改回原始值。因此在第二个命令之后, thold 的计算与之前一样。

文档中对第二个命令中为何使用 N-1 进行了冗长的解释。但说实话,这些额外的信息并没有什么有趣的。 set_multicycle_path 相对于 thold 的行为很奇怪,理解为什么数字应该是 N-1 并不能让它变得不那么奇怪。

但 set_multicycle_path 是比较容易的部分。真正的困难来了: 生成正确的 cell 对象列表以用作 $en_regs。

选择寄存器(registers)

为了选择 $en_regs中应列出的寄存器,有必要了解是什么使路径符合 multi-cycle 路径的条件。所以这是规则: 只有当时钟使能控制两侧时,才允许时序要求缓解路径的压力。这意味着当时钟使能处于非活动状态时,可以保证在时钟边沿之后,两个时序逻辑元件(sequential elements)都不会改变其值。

时钟域(clock domains)的角度考虑: 所有受时钟使能控制的时序逻辑元件(sequential elements)都属于一个假想的时钟域,这个假想的时钟域里面的时钟又有一个较低的频率,所以这个时钟域(clock domain)里面的时序要求是可以调整的。

但是,如果路径的其中一侧不属于这个虚构的时钟域,则这是两个related 时钟之间的虚构跨时钟域(clock domain crossing)。这样的路径不需要特别做什么,因为现有的时序约束已经照顾好了。但是把 multi-cycle exception 套用在这种路径上是不对的。

上面显示的时序约束反映了这个想法: 所有受 @en 控制的时序逻辑元件均列在 $en_regs 中作为 cell 对象。然后,两个 set_multicycle_path 命令应用于路径,它们都以属于此列表的时序逻辑元件开始和结束。

关于 multi-cycle 路径最困难的部分是确保时序逻辑元件的这个列表是正确的: 此列表应包含由时钟使能控制的所有时序逻辑元件。但没有其他时序逻辑元件应该在此列表中。

如果此列表中缺少时序逻辑元件,则相关路径的时序执行将比必要的更严格。这不是灾难,但它降低了 timing exception 的效率。

但是如果误将一个不应该在列表中的时序逻辑元件加入其中,可能会产生严重的后果: 这将导致路径的时序要求要求不够严格。换句话说,这些工具不能保证时序逻辑元件的正常运行要求。而当时序的要求不满足时,就会发生奇怪的事情

我选择使用 "all_fanout" 命令(或“get_fanouts”)来创建此时序逻辑元件列表。这不一定能保证正常工作,这就是我接下来要讨论的内容。之后,我将讨论创建此列表的其他选项。这些其他选项尤其适用于 FPGA 工具不支持“all_fanout”或类似的命令的情况。

all_fanout 和 get_fanouts可能出现的问题

multi-cycle 约束最可能的错误是时钟使能本身(即 @en)包含在列表中(即 $en_regs)。如果发生这种情况, multi-cycle exception 将应用于从 @en 本身到它控制的时序逻辑元件的所有路径。这实际上意味着无法保证这些路径的时序要求。这可以产生可见的效果,因为时钟使能通常是信号具有高扇出。

在上面的示例中, @pre_en避免了这种情况。您可能会问自己,为什么 @en 不是这样定义的:

always @(posedge clk)
  en <= !en; // Wrong!

如果 @en 是这样定义的,那么从 @en 到它自己就会有一条路径(path)。因此, @en 将包含在 $en_regs中。

所以 @pre_en 解决了这个问题。但重要的是要确保综合工具不会为了优化而淘汰这个寄存器(register)。例如, Quartus的综合工具检测到 @pre_en 的唯一用途是给 @en 赋值(在本页顶部的 Verilog 代码中)。因此综合工具删除了 @pre_en,并继续说“en <= !en”。因此, @en 包含在 $en_regs中。一种可能的解决方案是按如下方式声明 @pre_en :

reg pre_en /* synthesis preserve */;

这个简单的示例演示了综合工具的意外优化如何导致灾难性的结果。尽管解决方案很简单,但很容易忽略阻止这种优化的必要性。

时钟使能的另一个可能的问题与此信号通常具有较高的扇出这一事实有关。因此,这些工具可能会自动复制寄存器,以便每个副本都有一个低于特定限制的扇出。但这将如何影响 $en_regs?列入此列表的标准是基于特定的网络(net)。因此,不包括由 @en 副本控制的时序逻辑元件。

然而,这种情况的结果并不是灾难性的: 如前所述,这仅意味着对某些路径的时序强制执行将比必要的更严格。设计的可靠性不受影响。

高扇出的背景下,已经讨论了寄存器的复制。正如那里提到的,手动复制 @en 比等待综合工具完成复制要好。为了避免意外,请始终添加一个不允许复制此寄存器的综合属性。如果高扇出稍后导致时序收敛出现问题,请使用手动复制解决这些问题。这样更容易了解问题的根源。如果由于项目扩大,综合工具突然复制了 @en ,那么就不那么容易理解为什么没有实现时序约束。

无论哪种方式,定义 $en_regs 的命令都必须更新,以便包含 @en 的副本。

说到这里,请注意 $en_regs 的定义依赖于网络的名称。如前所述,这意味着如果综合工具只是将网络的名称更改为与“en”不同的名称,则 $en_regs 将变为空列表。因此, multi-cycle path 约束变得完全无用。这种可能性也不是灾难: 设计仍然可靠,但要实现时序约束则更加困难。

另一个可能的问题是,由于 $en_regs 的定义方式, @en 不能用作时钟使能以外的任何其他用途。例如,考虑这 Verilog 代码:

reg [7:0] counter;

always @(posedge clk)
  if (en)
    counter <= counter + 1;

在此示例中, @en 显然用作时钟使能。因此可以认为与 @counter 相关的所有路径都是 multi-cycle 路径。但是这个呢?

reg [7:0] counter;

always @(posedge clk)
  if (en)
    counter <= counter + 1;
  else
    counter <= counter - 1;

在这里, @en 的使用与任何寄存器一样。 @counter 的值在每个时钟周期上都会发生变化。所以 @counter 绝对不应该成为 multi-cycle 路径的候选者。然而, @counter 的所有触发器(flip-flops)都包含在 $en_regs中: 从 @en 到所有这些触发器都有路径。

这相对容易解决,方法是创建 @en的副本:

reg [7:0] counter;
reg non_ce_en;

always @(posedge clk)
  non_ce_en <= pre_en;

always @(posedge clk)
  if (non_ce_en)
    counter <= counter + 1;
  else
    counter <= counter + 2;

请注意,需要综合属性以防止综合工具将 @en 和 @non_ce_en 合并为一个寄存器。

综上所述,以 @en 开始的所有路径为基础的 $en_regs 定义简洁明了。但这个定义也是一个雷区。因此,让我们看看一些替代方案。

创建 $en_regs的替代方法

为 multi-cycle path timing exception 创建时序逻辑元件列表的最安全方法是依赖设计 hierarchy(design hierarchy): 由时钟使能控制的所有逻辑都应该在单独的模块中(也可能在 sub-modules中)。这允许通过基于 cell 对象的全名查找 cells 来创建 $en_regs 。例如,对于 Vivado:

set all_sync [all_fanout -endpoints_only -only_cells -flat \
  [get_nets -of_objects [get_clocks clk]]]
set en_regs [filter $all_sync {name =~ module_ins/multicycle_ins/* }]

第一个命令查找除时钟缓冲器(clock buffer)本身之外的所有与 @clk 相连的逻辑单元(logic elements)(此时钟由名称为“clk”的时钟对象(clock object)表示)。结果存储在 $all_sync中。这是一种可能的方法,可以创建一个包含所有可能相关的同步逻辑单元的列表。第二个命令创建一个列表,其中包含 $all_sync 中位于所述单独模块内的所有逻辑单元。

请注意,此方法依赖于时钟对象的名称和例化(instantiations)的名称。这些预计不会改变。使用这种方法,时钟使能是否被复制或它的名称是否被综合工具更改都无关紧要。

单独的模块的另一个优点是可以更轻松地与 Verilog 代码配合使用: 由时钟使能控制的时序逻辑元件和不由时钟使能控制的时序逻辑元件之间混淆的可能性较小。

但是,将依赖于时钟使能的逻辑分离出来并放入单独的模块并不总是很自然的。此外,如果 Verilog 代码已经编写并且已知可以正常工作,则对其进行更改可能不是一个好主意。

我还要提到另一种选择,它可能适用于某些情况: 所有寄存器的命名约定。例如,可以将所有受时钟使能控制的寄存器命名为以“MC_”开头的名称。这种选择使得创建 $en_regs 的命令变得简单: 只需根据名称搜索 cells 对象即可。其他逻辑单元(例如 block RAMs)也可以通过选择其例化的名称来包含。有些人会说这种方法让 Verilog 代码变得丑陋,而有些人会说它使它更容易使用。没有一种方法是完美的。

为什么不使用 -of_objects

根据一个简单的标准来定义 $en_regs 似乎很有吸引力: 找到名称为“en”的网络,并添加所有连接到此网络的寄存器。例如,对于 Vivado ,这可以写成:

set en_regs [get_cells -of_objects [get_nets en]]

这是错误的有几个原因。第一个原因是这包括 @en 本身。因此, multi-cycle 约束适用于从时钟使能本身到它控制的时序逻辑元件的所有路径。如上所述,这是一个严重的错误。

第二个原因是某些时序逻辑元件可能会被忽略。根据上面的命令,纳入的标准是 cell 应该连接到特定的网络(@en)。当此网络直接连接到触发器的 CE 输入时,此方法有效。但通常综合工具会选择使用基于 @en 的组合函数(combinatorial function)。

例如,综合工具可以选择实现 @foo ,就好像 Verilog 代码是这样的:

foo <= foo ^ en;

这在功能上等同于原始表达式:

if (en)
  foo <= !foo;

这种优化是合法的,应该是预期的: 无论如何,综合工具都必须使用 LUT 才能实现非门(NOT gate)。那么为什么不直接使用这 LUT 来获得触发器的 next value ?为什么要在触发器的 CE 输入上再加一根线(wire)?

这种优化的副作用是触发器本身并没有直接连接到 @en。因此它不会包含在 $en_regs中。已经讨论过这种情况的影响。

通常可以告诉综合工具仅使用 @en 作为同步逻辑单元的时钟使能输入(clock enable input)。例如,某些综合工具支持称为“direct_enable”或类似名称的综合属性。请注意,使用此功能时,综合工具优化逻辑的自由度会降低。因此,为了解决工具的技术问题,设计的性能可能会受到负面影响。

最重要的是,如果复制或重命名 @en ,则会出现与上述相同的问题。

因此,出于所有这些原因,以直接连接到网络作为标准是一个糟糕的选择。

与复位(reset)交互

假设我们将同步复位(synchronous reset)添加到上面的 Verilog 示例中:

always @(posedge clk)
     begin
	pre_en <= !pre_en;
	en <= pre_en;
     end

   always @(posedge clk)
     if (reset)
       begin
	  foo <= 0;
	  bar <= 0;
       end
     else if (en)
       begin
	  foo <= !foo;
	  bar <= foo;
       end

回想一下, multi-cycle timing exception 背后的想法是,所有同步逻辑单元的行为都好像是虚时钟域的一部分。此时钟域内的虚时钟具有 @clk的一半频率。因此,当 @en 为低时,所有寄存器都必须忽略 @clk 。在 Verilog 代码的最后一个示例中,情况并非如此: @reset 与 @en无关。

例如,考虑一下如果像这样定义 @reset 会发生什么:

assign reset = foo;

这是合法的同步复位,尽管它可能没有实际用途。但是这个定义显示了 multipath exception的问题: 当 @en 为高电平时,之后时钟周期上的 @foo 也变为高电平。但这也使 @reset 变为高电平。因此在下一个时钟周期上, @foo 再次变为低电平。 @foo 在每个时钟周期上改变值。因此从 @foo 到自身的 multicycle 路径导致时序要求在该路径上不够紧密。

这很容易通过让 @en 控制同步复位来解决:

always @(posedge clk)
     if (en && reset)
       begin
	  foo <= 0;
	  bar <= 0;
       end
     else if (en)
       begin
	  foo <= !foo;
	  bar <= foo;
       end

这是正确的,但是 @reset 必须与 @en一起激活。一个简单的解决方案是将 @reset 保持在几个时钟周期的高电平。

相同的原则适用于异步复位(asynchronous reset)。页面上写的关于异步复位的所有内容在这里也相关,但 multi-cycle path 约束更复杂。最简单的解决方案可能是使用 synchronizer,如另一页所建议的那样。

@en 和异步复位

@pre_en 的好处之一是它确保 @en 的所有副本始终具有相同的逻辑级(logic level)。这听起来很明显,但如果异步复位使用不当则不能保证。例如,假设原始 Verilog 代码为:

reg en;

always @(posedge clk or posedge reset)
  if (reset)
    en <= 0;
  else
    en <= !en; // This is not recommended!

如果 @en 被复制,结果可以等同于:

reg en, en_1, en_2;

always @(posedge clk or posedge reset)
  if (reset)
    begin
      en <= 0;
      en_1 <= 0;
      en_2 <= 0;
    end
  else
    begin
      en <= !en;
      en_1 <= !en_1;
      en_2 <= !en_2;
    end

请注意, @en_1 的 next value 取决于其自身的值,而不是 @en的值。这是复制寄存器的实际结果。

如果以不安全的方式停用异步复位会怎样?有可能 @en 的一些复制品对复位之后的第一个时钟边沿(clock edge)有反应,而其他复制品则忽略了这个时钟边沿。结果将是副本的逻辑状态永远不会变得相同(直到下一个复位(reset))。

@pre_en 解决了这个问题,因为所有副本都从同一源复制它们的 next value 。这确保了长期的正常运行,即使有一个艰难的开始。

概括

使用时钟使能很容易。使用 set_multicycle_path 作为时序 exception的命令也很容易。但要让这一切可靠地工作却一点也不容易。有很多事情都可能出错,有时原因是综合工具会随着逻辑设计的增长而改变其行为。

这些意外问题的结果可能是 multi-cycle 路径无法实现其目的: 如果简化的时序要求未应用于某些路径,则此方法的益处值得怀疑。更糟糕的是,如果将 multi-cycle path exception 应用于不应受到影响的路径,则错误可能会导致不可靠的逻辑设计。

因此,为相关的路径读取时序报告(timing reports)是至关重要的,以确保没有发生意外。不幸的是,这并不能防止将来出现意外: 随着设计的发展,综合工具的行为很难预测。

因此,如果可能的话,最好从同一个锁相环(PLL)生成一个额外的时钟,而不是使用时钟使能。跨时钟域加上这个新的时钟是靠谱的,因为两个时钟都是related 时钟 。此新时钟的时序约束由工具自动生成。这样不会有任何意外的风险。

因此,如果您正在阅读本文是因为您想为设计添加一个时钟使能和一 multi-cycle exception ,我希望此页面能让您有所思考。


FPGA中关于时序约束for路径的部分到此结束。但是 I/O呢?这就是下一页开始讨论的内容。

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