01signal.com

使用 Tcl 命令选择逻辑元件

本页属于关于时序的系列文章之一。前面的页面解释了时序计算背后的理论,讨论了时钟周期约束,展示了时序收敛的原则,并开始初步了解 Tcl 环境。本页介绍那些能让时序约束变得有具体指向的命令。

概述

回顾一下,时序约束(timing constraints)的目的是保证设计中所有路径(path)的时序要求都得到满足。不同的路径可能有不同的要求,因此每条时序约束都必须指向相关的路径组,而不能把其他路径也包括进来。

定义路径的唯一方法,是引用与这些路径相关的逻辑元件。因此,能够写出精确的表达式来选择正确的逻辑元件组,就显得非常重要。换句话说,必须正确使用 get_clocks、get_ports 和 get_cells 这类命令,使它们的返回值正好是你所需要的。

本页将介绍描述逻辑元件组的基本技术。这里几乎一切都与 SDC 语法有关。另外,我假定你已经有一个可用的 Tcl 命令行界面。Vivado 和 Quartus,以及近年来大多数 FPGA 工具,都具备这个条件。

有些 FPGA 厂商公开表示,其工具中集成了 Synopsys 的软件,所以这些工具自然使用相同的 Tcl 命令。Vivado 则不同,它并不被认为是派生于 Synopsys。然而,两者之间还是有不少惊人的相似之处,尤其是在 Tcl 接口方面。Quartus 似乎是独立开发的,因此它的 Tcl 界面有些不一样。

乍一看,本页似乎过于深入 Tcl 脚本(script)的细节。事实正好相反:精确性极其重要,而要做到精确,只能靠准确理解命令是如何被解释的。实际上,本页只解释基本原理。阅读相关文档仍然是不可替代的。

从 FPGA 工具获得帮助

编写时序约束往往难以入门,因为需要照顾的细节实在太多。FPGA 工具可以在这方面提供帮助。

在 Tcl 控制台中可以使用 help 命令查看某个 Tcl 命令的文档。这些信息通常与官方文档(例如 PDF 文档)里的完全一样。

大多数 FPGA 工具都有用于自动创建时序约束的图形界面。通常的做法是,先用它选择需要哪种类型的时序约束,然后再从列表中选择相关逻辑元件。结果是生成一条或多条加入 SDC 文件的时序约束。

使用这类向导有助于获得 Tcl 语法方面的提示。偶尔,自动生成的约束也可以直接使用。但在大多数情况下,最好抵抗住直接使用向导输出内容的诱惑,而是仔细想清楚,用什么方式最能实现这条时序约束的目的。同时还要考虑项目将来会如何演进,确保时序约束长期正确。在图形界面上快速点几下,很难做到这一点。

一些 FPGA 工具还提供用于在设计中查找逻辑元件的图形界面。使用这个界面时,屏幕上往往会显示与你的搜索相对应的 Tcl 命令。这是一种获得查找特定逻辑元件的表达式的便捷方法。同样,这些表达式应被当作进一步工作的起点。

大多数 FPGA 工具的第三个特性,是一个 Tcl 命令行控制台。它允许你输入命令(或使用复制粘贴),并在屏幕上看到结果。这样可以测试命令及其搜索模式,查看能找到哪些对象,有助于验证搜索模式是否正确。

总而言之,FPGA 工具可以帮助创建 Tcl 命令。但如前所述,工具生成的 Tcl 命令只应作为编写精确搜索模式的基础,并且要确保这些搜索模式在长期内始终正确。

现在,既然知道了偷懒创建时序约束的方法,是时候学习如何正确地做了。

网表:简要回顾

在更具体地讨论 Tcl 环境之前,我想先简短回顾一下网表(netlist)。

最常见的网表文件格式是 EDIF,但许多 FPGA 工具也有自己的专用格式。网表通常由综合器(synthesizer)生成,尽管工具在后续阶段也可能对它进行修改。这个文件用基本组件以及这些组件之间的连接来描述逻辑设计。它就像一张接线图,只不过是用文本而不是图形来表示。

网表中的组件称为 cell,可以译为单元。绝大多数 cell 是 LUT、其它小型组合逻辑元件或触发器。此外,Verilog 代码中黑盒的实例化(instantiation)(例如 IP 核(IP core))在网表中也用一个 cell 来表示。其它 cell 还包括 PLL、块 RAM(block RAM)以及大型逻辑元件(“硬核 IP”):PCIe 模块、MGT 收发器、处理器等。

每个 cell 都有若干个引脚(pin),它们就像实体电子元件的外部连接点。但不要把引脚与 FPGA 的外部 I/O 混淆:这些 cell 和引脚都位于 FPGA 内部。

网表中的互连由网络(net)组成,它们类似于实际的导线。例如,在 Verilog 中用 wire 定义的信号,在网表中就对应一个网络。一个网络连接两个或多个引脚,从而保证这些引脚始终具有相同的逻辑电平。

逻辑元件如何表示为对象

当 FPGA 工具执行一个项目的实现时,真正发生的是 Tcl 脚本在被执行。Vivado、Quartus 以及其它几种 FPGA 工具都是如此。即使某些软件的工作方式不同,做这样的假设也基本正确:约束 API 以及其它脚本文件,会给人一种“一切都是一大段 Tcl 脚本”的感觉。

在这个 Tcl 脚本环境里(无论是不是假想),所有逻辑元件都被表示为从不同类创建的对象。时序约束(SDC 文件,或 Xilinx 的 XDC 文件)可以访问这些对象;同样,Tcl 命令行控制台和 Tcl 脚本中的命令也可以访问它们。

下面这五个 Tcl 命令,受到所有支持 SDC 语法的 FPGA 工具的支持。这些命令用来查找不同类型的对象(也就是不同类的对象)。在前面的时序约束示例中,我已经用过这些命令。事实上,没有这些命令,几乎不可能写出有意义的时序约束。

不带任何参数时,这些命令会查找相应类型的所有对象。稍后我们看看如何缩小搜索范围。

除了这五个命令,每家 FPGA 工具还有自己的补充命令和补充对象。例如,Vivado 还有 all_ffs、all_registers、all_inputs、all_outputs、all_rams 等命令。Quartus 支持其中一部分,也提供 get_registers、get_keepers、get_nodes、get_fanins、get_fanouts 等命令。

这些对象之所以重要,是因为时序约束需要用它们来引用 FPGA 设计中的逻辑元件。它们也可以用在 Tcl 脚本中获取信息,例如时钟频率。每种 FPGA 工具都有自己的 API,让 Tcl 脚本访问这些对象。例如,在 Vivado 中可以用下面的 Tcl 命令获得时钟周期:

> get_property PERIOD [get_clocks clk]
4.000

在 Quartus 中则是这样:

> get_clock_info -period [get_clocks clk]
4.000

尽管存在这些差异,大多数 FPGA 工具在 SDC 格式时序约束的语法和含义上是统一的。每种工具支持的 API 信息,通常可以在标题包含 Tcl scripting 或 Timing Closure 的用户指南中找到。

除非特别说明,本页示例都基于 Vivado。

关于 Tcl 的几点说明

Tcl 是一门很古老的语言,但它在逻辑设计领域根深蒂固,短期内不太可能消失。幸运的是,即使对 Tcl 并不十分熟悉,也能用它做不少有用的事。

首先要说的是我前面简单提过的方括号([ 和 ])。在 Tcl 中,方括号表示先执行其中的命令,然后把结果放在方括号的位置。熟悉 shell 脚本或 Perl 的人会发现,这与反引号相同。例如,下面这条命令中的 get_ports 会被替换为名为 clk 的端口对象:

create_clock -period 4.000 -name clk [get_ports clk]

同样的命令也可以写成下面这样:

set the_clk_port [get_ports clk]
create_clock -period 4.000 -name clk $the_clk_port

如第二个示例所示,变量用 set 命令定义并赋值;读取变量值时用美元符号($)。这又和 shell 脚本、Perl 相似。

花括号({ 和 })则是另一回事:和不少语言一样,它的含义在很大程度上取决于上下文。在 Tcl 中,花括号一个不太常见的含义是:括号内的字符串保持原样。也就是说,不做任何替换,空白字符也当作普通字符对待。例如,同一条时序约束也可以写成这样:

create_clock -period {4.000} -name {clk} [get_ports {clk}]

在这个例子里,花括号完全没有必要,这条命令与之前的含义完全相同。不幸的是,无意义的花括号很常见,而且正如本例所示,它们常常确实没有任何作用。

使用 Tcl 控制台的一个小技巧

经常遇到的情况是,找到的对象数量太多,搜索结果难以阅读。用简单的 Tcl 命令可以解决这个问题,具体做法取决于你使用的是哪个工具。下面这条命令在 Vivado 中会打印出设计中的所有 cell,每个 cell 占一行,因此即使 cell 很多,输出也仍然可读:

> join [get_cells -hierarchical] \n
GND
VCC
bar__0_i_1
bar_reg_OBUF_inst
bar_reg__0
[ ... ]

join 命令会在 get_cells 返回的各个元素之间插入换行符。

在 Quartus 中,可以用下面这条命令得到同样的结果:

join [query_collection -all [get_cells -hierarchical] ] \n

或者更合理地使用 query_collection:

query_collection -all -report_format [get_cells -hierarchical]

查找特定元素

经过这么长的开头,终于到了讨论真正有意思的内容的时候。为了下面这些示例,请参考以下 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

正如本页开头所说,时序约束能否准确,取决于能否选中正确的逻辑元件组。因此,缩小上述五个 get_* 命令的搜索结果不仅是可能的,而且是必要的。缩小结果有好几种方法,但最常见的是基于对象的名字。最简单的模式,是找一个名字完全符合我们要求的对象。例如,在 Vivado 的 Tcl 控制台中:

> get_ports clk
clk

同样,也可以找出所有名字符合某个特定模式的对象:

> get_pins pll_i/*
pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2

注意,上面两个示例的输出都是对象。Vivado 的 Tcl 控制台为了方便,会把这些对象的名字打印出来。

更重要的是,每种 FPGA 工具对搜索模式的处理都略有差异。这里的示例都使用 Vivado,但同样的原则也适用于其他工具。

星号(*)是通配符,可以匹配任意数量的字符;问号(?)匹配一个字符。这与文件名的通配符规则相同。

还要注意,与文件名一样,通配符不会匹配层次结构分隔符(例如 Vivado 中的 / 和 Quartus 中的 |)。

层次路径与文件目录之间还有一点相似:搜索是相对于顶层层次结构进行的,就像文件系统里的根目录。例如:

> get_pins */clk_*
pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2
> get_pins pll_i/clk_out?
pll_i/clk_out1 pll_i/clk_out2

必须明确指出每个逻辑元件在层次结构中的位置,这常常是个明显的缺点:我们想找的逻辑元件往往位于不同位置。用 -hierarchical 可以解决这个问题:加入这个选项后,会在层次结构中的所有位置搜索与模式匹配的对象。

一般来说,不建议用通配符查找逻辑元件。唯一的例外是搜索模式非常简单,或者别无选择。单独的一页解释了如何使用通配符和 -hierarchical,也展示了这种方法的局限。

使用 -filter

基于通配符的搜索模式有几个缺点。最明显的一点是:要么精确地定义层次结构,要么就完全不能定义。问题之一在于,人们常常希望把搜索限制在某个子层次结构中的对象上;但通配符做不到这一点,-hierarchical 选项也解决不了这个问题。

鉴于这些原因以及其它原因,定义搜索模式的首选方式是 -filter 选项。这个选项带一个布尔表达式。使用它时,只有使该表达式为真的搜索结果才会被保留。

例如,

> get_pins */clk_*
pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2
> get_pins */clk_* -filter {name =~ *2*}
pll_i/clk_out2

在这个示例中,对通配符找到的每个对象,检查名为 name 的属性。只有当该属性匹配 *2* 时,这个对象才会保留在搜索结果里。换句话说,就是对象名字中带有 2。

从实际角度看,这个例子没什么意思。当使用 -hierarchical 时才更有意思:不加任何搜索模式时,会找到所有对象。也就是说,下面这条命令会找出所有层次里的所有引脚:

get_pins -hierarchical

在此基础上,就可以用 -filter 缩小搜索结果了:

> get_pins -hierarchical -filter {name =~ pll_i/*/*out1 }
pll_i/inst/clk_out1
> get_pins -hierarchical -filter {name =~ pll_i/*out1 }
pll_i/clk_out1 pll_i/inst/clk_out1
> get_pins -hierarchical -filter {name =~ *out1 }
pll_i/clk_out1 pll_i/inst/clk_out1

注意,花括号({ 和 })在这里的含义,只是告诉 Tcl 解释器不要修改括号里的内容。

还要注意,这里的搜索模式属于 -filter 选项,它的行为不一样:它把 name 当作对象的一个属性来对待。因此所有字符都一视同仁,/ 不再有特殊含义。无论 / 是不是层次结构分隔符,它都可以被通配符(*)匹配,也都可以出现在搜索模式里。不使用 -filter 时当然不是这样。

任何字符都能被 * 匹配,这让 -filter 更强大,但也更容易出错:很容易忘记一个不起眼的 * 可能同时意外匹配 pll_i/clk_ 和 pll_i/inst/clk_,就像上面的例子那样。

这个特性的一个正确用途,是在层次结构里某个已知位置寻找名字已知的逻辑元件:

> get_pins -hierarchical -filter {name =~ */clkout2_buf/O }
pll_i/inst/clkout2_buf/O

如果我们确定设计里只有一个名为 clkout2_buf 的 cell,那这条命令就是正确的。使用它总能找到这个 cell 的输出引脚,即使包含这个逻辑元件的模块在项目层次结构中被移动了。在实际场景中,最好选一个比 clkout2_buf 更独特的名字。

相同的方法也适用于 get_cells 和 get_nets。例如:

> get_cells -hierarchical -filter {name =~ */clk*_buf}
pll_i/inst/clkf_buf pll_i/inst/clkout1_buf pll_i/inst/clkout2_buf

但 -filter 可以作用于所有属性,而不仅仅是名字。所以下面这条命令会找出名字中包含 bar 的所有寄存器:

get_cells -hier -filter {primitive_type =~ register.*.* && name =~ *bar*}

注意这里的 && 运算符,它表示逻辑与(和 Verilog、C 里一样)。

这条命令里的 primitive_type 和 name 只是属性的名字。=~ 运算符进行比较,并允许使用通配符。

回想一下,在 Vivado 中可以用 report_property 命令列出对象的属性。

由此可见,-filter 用起来相当灵活。遗憾的是,并不是所有 FPGA 工具都支持它。

注意,Vivado 有一个名为 filter 的命令,功能与 -filter 相同。所以下面两条命令是等价的:

set result [get_cells -hierarchical -filter {name =~ *_reg}]
set result [filter [get_cells -hierarchical] {name =~ *_reg}]

第二种格式在对象列表已经存入变量时很有用。所以上面两条命令也等价于:

set all_cells [get_cells -hierarchical]
set result [filter $all_cells {name =~ *_reg}]

使用 -regex

熟悉正则表达式的人可能会想用这个选项。通常这不是个好主意,主要原因是它会让别人更难理解你的时序约束。支持正则表达式的 FPGA 工具通常也支持 -filter,因此几乎总是应该优先使用 -filter。

回想一下,普通搜索模式的主要问题在于,层次结构分隔符无法被通配符匹配。

因此 -regexp 可以解决这个问题,如下面两个例子所示:

> get_pins -hierarchical -regexp {.+/clk_out[123]}
pll_i/clk_out1 pll_i/clk_out2 pll_i/inst/clk_out1 pll_i/inst/clk_out2
> get_pins {pll_i/[^/]+/clk[^/]+} -hierarchical -regexp
pll_i/inst/clk_in1 pll_i/inst/clk_out1 pll_i/inst/clk_out2

第一个命令说明 . 可以匹配任意字符,包括 /(层次结构分隔符)。

第二个命令说明如何用 [^/]+ 匹配除层次结构分隔符以外的任意内容。这样就能控制搜索结果在层次结构中的具体深度。这条命令还说明,搜索模式并不是 -regexp 的参数;-regexp 改变的是搜索模式的语法。

注意,正则表达式必须匹配对象的完整名字。换句话说,工具会在正则表达式开头隐式添加 ^,并在末尾添加 $。

但是,如果有其它办法能达到同样目的,就不要用 -regex。大多数其它 FPGA 设计师将无法理解这种搜索模式。

使用 -of_objects

与根据对象名字来查找对象不同,-of_objects 允许根据对象与其它对象的关系来查找。在多数情况下,-of_objects 大致可以理解为“与……相连”。

例如,要找出连接到某个网络的所有引脚:

> get_pins -of_objects [get_nets bar]
bar_reg_reg/D baz_metaguard_reg/D bar_reg__0/Q

或者某个 cell 的所有引脚:

> get_pins -of_objects [get_cells bar_reg_reg]
bar_reg_reg/Q bar_reg_reg/C bar_reg_reg/CE bar_reg_reg/D bar_reg_reg/R

或者某个时钟的源引脚:

> get_pins -of_objects [get_clocks clk_out1_clk_wiz_1]
pll_i/inst/mmcme3_adv_inst/CLKOUT0

同样的方法也适用于查找网络。例如,哪些网络连接到了某个特定引脚?

> get_nets -of_objects [get_pins bar_reg_reg/C]
pll_clk_6

或者,与某个 cell 相连的网络有哪些?

> get_nets -of_objects [get_cells bar_reg_reg]
bar_reg_OBUF pll_clk_6 <const1> bar <const0>

同样,也可以用这种方式查找 cell。例如,哪些 cell 连接到了 @pll_clk_6?

> get_cells -of_objects [get_nets pll_clk_6]
bar_reg__0 bar_reg_reg pll_i

注意,pll_i 是 Clocking Wizard IP 的实例化(instantiation)名。这很可能不是你想找的结果。那把搜索结果缩小到只剩触发器如何?

> get_cells -of_objects [get_nets pll_clk_6] -filter {primitive_type =~ register.*.*}
bar_reg__0 bar_reg_reg

到目前为止,上面 -of_objects 的示例说明了引脚、网络和 cell 如何相互引用。但也可以用这个选项来查找时钟对象:

> get_clocks -of_objects [get_nets pll_clk_6]
clk_out2_clk_wiz_1
> get_clocks -of_objects [get_cells bar_reg_reg]
clk_out2_clk_wiz_1
> get_clocks -of_objects [get_pins bar_reg_reg/C]
clk_out2_clk_wiz_1

这三条命令展示了如何根据与时钟相连的逻辑元件来找到时钟。注意,当这个逻辑元件是一个 cell 时,结果可能不止一个。例如:

> get_clocks -of_objects [get_cells pll_i]
clk clk_out1_clk_wiz_1 clk_out2_clk_wiz_1

回想一下,pll_i 是一个 IP,所以它的引脚就是这个模块的端口:

> get_pins -of_objects [get_cells pll_i]
pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2

因此,要查找某个特定的时钟,使用 get_pins 会更稳妥:

> get_clocks -of_objects [get_pins pll_i/clk_out2]
clk_out2_clk_wiz_1

当然,位于外部 I/O 端口上的时钟对象,最好用下面的方式查找:

> get_clocks -of_objects [get_ports clk]
clk

或者用等价的更短命令:

> get_clocks [get_ports clk]
clk

说到 get_ports,它也可以与 -of_objects 一起使用。具体有哪些用法,请参考相关文档;其中不少用法是 get_ports 特有的。与其它命令类似的用法反而没什么意义,例如:

> get_ports -of_objects [get_nets clk]
clk

总之,-of_objects 是选择特定逻辑元件的极佳方法。这一点尤其重要,因为按对象名字搜索有种种困难,这正是下面要讨论的内容。

遗憾的是,许多 FPGA 工具不支持 -of_objects,这时就只能依靠不那么可靠的方法了。

按名字搜索的问题

如前所述,时序约束的准确性取决于能否精确地找到逻辑元件。至少有一部分搜索结果依赖于对象的名字。下面来看看这会带来怎样的麻烦。

例如,get_ports clk 用来在时钟对象与某个特定 I/O 引脚上的信号之间建立联系。这个 I/O 引脚由一个名字为 clk 的端口对象表示:

create_clock -period 4.000 -name clk [get_ports clk]

但为什么这个端口对象会叫 clk?答案与综合器如何创建网表有关:Verilog 代码中顶层端口叫什么,网表的顶层端口通常就叫什么。这是顺理成章的选择,任何正常的综合器都会这样做。

那 cell 的名字呢?以上面 Verilog 代码中名为 foo_reg 的寄存器为例。代表这个寄存器触发器的 cell 对象叫什么?Vivado 的综合器给它取名为 foo_reg_reg。由此可见,这个综合器习惯在 Verilog 代码原来的名字后面加 _reg 后缀。听起来这是条可以依赖的规律。但换一个综合器,很可能就会有不同做法。

那名为 bar 的寄存器呢?相应 cell 对象的名字本应是 bar_reg,但综合器做出了不同的选择:bar_reg__0。这是因为 Verilog 代码里已经有一个叫 bar_reg 的寄存器。为了避免名字冲突,综合器挑了一个略有不同的名字:它加的是 _reg__0,而不只是 _reg。这个简单的例子充分说明了依赖对象名字的问题。

更麻烦的是,假设这条时序约束是在名为 bar_reg 的寄存器被加入 Verilog 之前写的。那时与 @bar 对应的 cell 对象按惯例会叫 bar_reg,所以时序约束就用了这个名字。后来设计中加入了名为 bar_reg 的寄存器,于是原来那个目标 cell 对象的名字从 bar_reg 变成了 bar_reg__0。那条依赖 bar_reg 这个名字的时序约束突然就不对了。运气好的话,工具会对此发出警告。

依赖对象名字还可能出于其它原因而出错。例如,当某个寄存器的扇出(fan-out)超过上限时,工具可能会自动复制这个寄存器。一旦发生复制,那个新增寄存器可能不会被纳入时序约束,因为新寄存器的名字与搜索模式不匹配。

更严重的问题是,有些逻辑元件可能被意外纳入时序约束。例如,属于 IP 核(IP core)的逻辑就可能发生这种情况。因为我们无法控制这些逻辑元件的名字,它们有可能恰好与某条时序约束里的模式匹配。

意外纳入逻辑元件也可能是偷懒造成的:时序约束往往是随着给逻辑设计添加新功能而一起写的。如果搜索模式是靠试错写出来的,很可能没有考虑到将来会出现哪些元件名字。于是,当新逻辑加入后,其中某些元件的名字可能无意间匹配上已有的时序约束。

如何避免时序约束出错

第一条也是最重要的一条规则:仅仅测试哪些逻辑元件匹配某条时序约束的搜索模式,这还不够。即使你把它们完整列出来并仔细审查,也无法保证将来加入的逻辑不会出问题。同样,这种审查也无法保证,当综合器在实现过程中改变了对象名字后,时序约束还能按预期工作。

因此,要把搜索模式当作数学表达式来对待:如果它“现在工作正常”,这还不够。如果它现在工作不正常,只做点小修补让它能工作,也仍然不够。相反,你必须能在逻辑上解释清楚,为什么这个搜索模式是对的,以及为什么它在长期内大概率仍然是对的。

同样重要的是,搜索模式应依赖那些不太容易改变的东西。例如,工具不会修改 Verilog 代码中实例化(instantiation)的名字。无论是对模块、IP 还是原语(primitive)的实例化,都是如此。因此,当层次路径只由 Verilog 代码中写下的实例化名字构成时,依赖它是安全的。但依赖 IP 核内部创建的名字就不那么安全了。为了说明这一点,请看下面这条命令:

get_clocks -of_objects [get_pins pll_i/inst/clkout1_buf/O]

这是通过引用分配该时钟的全局时钟缓冲器输出引脚来获取时钟对象的方法。问题是,我们能否保证这种方法在长期内一直有效?

问题在于,inst 和 clkout1_buf 是 Clocking Wizard IP 内部创建的实例化名字。虽然这些名字不太可能改变,但谁也无法保证它们永远不会变。

一个可行的办法,是找到实现这个 IP 的 Verilog 代码,并把这个 Verilog 直接放进项目。这样可以保证将来不会发生变化。

另一种办法是看这个 IP 在 Verilog 中的实例化代码。回想一下,它是这样写的:

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

因为这里实例化的是一个黑盒,所以 IP 的每个端口在网表中都对应一个引脚。这些名字不会变,因为端口名就是 IP 的端口标识。因此可以保证,名为 pll_i/clk_out1 的引脚确实连接着 @pll_clk_8。所以下面这种获取时钟对象的方式是安全的:

get_clocks -of_object [get_pins pll_i/clk_out1]

注意,如果 clk_wiz_1 只是项目中的另一个 Verilog 模块,这种方法很可能行不通:此时不会为这个模块的实例化创建引脚(因为综合器通常通过合并网络来实现端口连接)。一种可能的解决方案是使用层次结构中更低层出现的名字。

所以,有几种名字是可以放心依赖的:

最好让失败明显一些

最糟糕的情况,是一条时序约束几乎正确:只有几条路径没有被包含进去,或者只有几条路径被错误地包含进来。这类错误最难发现。

这正是短小、简洁、有数学风格的时序约束更胜一筹的主要原因。如果一小群逻辑元件就对应一条约束,那么这一长串规则里就很容易混入错误。

为了说明这个想法,回到上面查找时钟对象的例子:

get_clocks -of_objects [get_pins pll_i/inst/clkout1_buf/O]

如前所述,这个表达式的问题在于:如果 pll_i 的实例化在层次结构中被移动到别的位置,就找不到任何时钟对象了。后果有多严重呢?

假设把这个时钟对象存在一个 Tcl 变量里,像这样:

set my_clock [get_clocks -of_objects [get_pins pll_i/inst/clkout1_buf/O]]

然后,$my_clock 被用在很多时序约束里,涉及到很多路径。这种情况下,如果 $my_clock 因为某个问题突然不包含任何时钟对象,其实算不上大问题:这个错误很可能会很快被发现,因为会有很多东西一起出错。

但如果 $my_clock 只被用在一条时序约束里,而这条约束的目的只是解决一个很少产生影响的小问题,那就麻烦了。这种错误很可能会被忽略。

总之:一条写得好的时序约束,要么完美地工作,要么完全不工作。


本页展示了如何精确地选择逻辑元件。在下一页中,我们会运用这些知识来定义有针对性的时序约束。

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