本页属于关于时序的系列文章之一。前几页解释了时序计算背后的理论,讨论了时钟周期约束,并展示了时序收敛的原则。现在开始进入时序约束的技术细节。
create_clock 作为 Tcl 命令的含义
前面几页都是围绕下面这条时序约束(timing constraints)展开的:
create_clock -period 4 -name clk [get_ports clk]
到目前为止,我刻意没有讨论这行命令的语法。现在是时候解释一下它的真正含义了。
这条时序约束是以 SDC(Synopsys Design Constraints)格式编写的,这是目前最常用的时序约束格式。Vivado 和 Quartus 以及其他多种 FPGA 工具都使用这种格式。
SDC 文件本质上是一个用 Tcl 编写的脚本(script)。因此,SDC 文件的内容是一小段计算机程序,而不仅仅是一组信息。不过,作为脚本,SDC 文件的能力仅限于一小部分用于编写约束的命令:并不是 Tcl 脚本中允许的所有内容都能在 SDC 文件中使用。
create_clock 命令用于定义一条时序约束。但实际上,这条命令是让 FPGA 工具创建一个新的时钟对象(clock object)。这里的“对象”一词,就是软件工程中通常所说的对象。因此,这个新时钟对象是存储在 Tcl 解释器内存中的一个实体,它拥有自己的属性。
例如,create_clock 命令中的 -name clk 部分就是把值 clk 赋给名为 name 的属性。回想一下前几页中某页的内容:这个名字出现在时序报告中,名为 clk 的时钟随着因这条约束(更准确地说,因这个时钟对象)而计算的时序路径一起出现。
在后面的页面里,我们看到另外一些时钟名字,例如 clk_out1_clk_wiz_1 和 clk_out2_clk_wiz_1。这些其实就是其他时钟对象的名字,是工具自动创建的。
有一个 Tcl 命令可以列出所有时钟:get_clocks。以上一页中两个时钟的例子来说,在 Vivado 的 Tcl 控制台上会看到下面这样的会话:
> get_clocks clk clkfbout_clk_wiz_1 clk_out1_clk_wiz_1 clk_out2_clk_wiz_1
关于 get_clocks 以及类似命令的更详细说明,请见下一页。
也可以查看这些对象的属性。你并不需要理解所有这些属性,我这里展示它们只是想说明:时钟就是一个对象。就我个人而言,我从未需要直接修改某个对象的属性。
> report_property [get_clocks clk] Property Type Read-only Value CLASS string true clock INPUT_JITTER double true 0.040 IS_GENERATED bool true 0 IS_PROPAGATED bool true 1 IS_USER_GENERATED bool true 0 IS_VIRTUAL bool true 0 NAME string true clk PERIOD double true 4.000 SOURCE_PINS string* true clk SYSTEM_JITTER double true 0.050 WAVEFORM double* true 0.000 2.000 > report_property [get_clocks clk_out1_clk_wiz_1] Property Type Read-only Value CLASS string true clock EDGES int* true 1 2 3 EDGE_SHIFT double* true 0.000 2.000 4.000 INPUT_JITTER double true 0.000 IS_GENERATED bool true 1 IS_INVERTED bool true 0 IS_PROPAGATED bool true 1 IS_RENAMED bool true 0 IS_USER_GENERATED bool true 0 IS_VIRTUAL bool true 0 MASTER_CLOCK clock true clk NAME string true clk_out1_clk_wiz_1 PERIOD double true 8.000 SOURCE pin true pll_i/inst/mmcme3_adv_inst/CLKIN1 SOURCE_PINS string* true pll_i/inst/mmcme3_adv_inst/CLKOUT0 SYSTEM_JITTER double true 0.050 WAVEFORM double* true 0.000 4.000
需要理解的关键之处是,create_clock 只是创建一个对象。这条命令的参数只是决定如何设置这个对象的各个属性。例如,命令中的 -period 4(在我反复展示的那条时序约束里)只是表示名为 PERIOD 的属性取值应为 4。
如果你想亲自试试这些 Tcl 命令,请注意不同 FPGA 工具之间是有差异的。
在 Vivado 中,只有打开实现后的设计(Implemented Design)之后才能使用这些命令。
在 Quartus 中,先打开 TimeQuest Timing Analyzer,然后依次点击 Create Timing Netlist、Read SDC File 和 Update Timing Netlist。然后在 Tcl 控制台上试试一些命令,例如:
> join [ query_collection -all [ get_clocks ] ] "\n" > get_clock_info -waveform [get_clocks clk]
单词“clock”的含义
当 FPGA 工具使用“clock”这个词时,通常指的是时钟对象,而不是 FPGA 内部的物理信号。在时序报告中尤其如此。
回想一下,在上一页我几次使用了“理论时钟(theoretical clocks)”这个说法。这些其实就是时钟对象。它们在时序报告里被称为 clocks,但从它们在时序分析中的用法可以看出,它们只是一些承载信息的容器而已。
那么,这些时钟对象和真实信号之间是什么关系?我们已经在时序分析中看到过时钟对象的名字。这一切是如何协同工作的呢?
当工具对设计进行静态时序分析(static timing analysis)时,会检查所有路径。如果一条路径从触发器开始,工具会检查连接到该触发器时钟输入端的信号(即网络 net):是否存在与这个信号相关的时钟对象?例如,当信号是 @clk 时,相关的时钟对象就是用 create_clock 命令命名为 clk 的那个。找到对应的时钟对象后,工具就可以从这个对象的属性中取得所需信息。
对于路径终点的触发器,也会进行同样的处理。于是,工具就得到与这条路径对应的两个时钟对象。利用这些对象中的信息,工具执行时序分析。
当然,同样的处理适用于任何时序元件(sequential element),而不仅仅是触发器。
为什么理解这一点很重要?原因之一是,时序报告中有时会看到一条错误消息,说有些寄存器没有时钟(registers with no clock)。通常,这并不意味着某个触发器的时钟输入端什么东西都没接,而是说工具没有找到与该时钟输入端相关的时钟对象。换句话说,工具没有找到关于这个触发器时钟输入端的任何信息。因此,问题通常不出在逻辑设计上,而是缺少了一条时序约束(或者时序约束写得不对)。
这一点值得再强调一遍:当时序报告里出现“clock”时,并不是说逻辑设计中存在一个同名信号,而是说创建了一个同名时钟对象。那么,怎么知道它对应哪个信号?这就是下一个话题。
这个时钟是谁的?
让时序报告难以阅读的一个原因,就是时钟的名字。逻辑设计中的大部分时钟信号都是由 PLL 产生的,而我们之前已经看到,时序报告中出现的名字可能并没有多大帮助。大多数 FPGA 工具都允许通过在 SDC 文件中添加命令来重命名时钟对象,但在大多数项目中人们并不会这样做。此外,黄金法则第 4 条(Golden Rule #4)也提醒我们,不要去搞那些只对你自己项目有意义的特殊做法。
当时钟来源是一个 IP 模块(IP block,例如千兆收发器、PCIe 模块或片上处理器核)时,命名问题就更加棘手了。这种情况下,时钟对象的名字往往很能难让你看出它来自何处、与什么相关。
那这个问题怎么解决?我们先从最简单的情况说起:时钟对象的名字来自我们自己 SDC 文件里的 create_clock 命令。这又是同一条时序约束:
create_clock -period 4 -name clk [get_ports clk]
这条命令的最后一部分是 [get_ports clk]。在 Tcl 语言中,方括号表示把括号里的内容当作一条 Tcl 命令来执行,然后用该命令返回的结果替换整个方括号。
get_ports 命令查找名为 clk 的 I/O 端口,其返回值是代表这个端口的对象。因此,在上面这条 create_clock 命令中,这个对象被作为参数传给了命令。就这样,create_clock 在时钟对象与真实信号之间建立起了联系。
注意,这里端口名和对象名都叫 clk。两者并不要求一致,但我建议保持一致,因为对象的名称会出现在时序报告中。因此,端口名通常是最好的时钟对象名。
也可以使用网络对象(net object)和引脚对象(pin object)来标识信号。在由 IP 模块自动生成的时序约束中,这种做法很常见。但如果你在自己写的约束中觉得需要这样做,那很可能说明你哪里做错了。
因此,只要某个 SDC 文件里有 create_clock 命令,就很容易判断这个时钟对象对应哪个信号。但由工具自动创建的时钟对象又该怎么办呢?
这种情况下,识别时钟的最好办法是查看时序报告。例如,在上一页的例子中,哪个时钟对象与 @pll_clk_8 相关?一个简单的办法是在时序报告里做文本搜索。搜索 pll_clk_8,会找到下面这一段:
Location Delay type Incr(ns) Path(ns) Netlist Resource(s)
-------------------------------------------------------------- -------------------
(clock clk_out1_clk_wiz_1 rise edge)
16.000 16.000
AG12 0.000 16.000 clk (IN)
net (fo=0) 0.000 16.000 pll_i/inst/clkin1_ibuf/I
AG12 INBUF (Prop_INBUF_HRIO_PAD_O)
0.738 16.738 pll_i/inst/clkin1_ibuf/INBUF_INST/O
net (fo=1, routed) 0.105 16.843 pll_i/inst/clkin1_ibuf/OUT
AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O)
0.049 16.892 pll_i/inst/clkin1_ibuf/IBUFCTRL_INST/O
net (fo=1, routed) 0.975 17.867 pll_i/inst/clk_in1_clk_wiz_1
MMCME3_ADV_X1Y0 MMCME3_ADV (Prop_MMCME3_ADV_CLKIN1_CLKOUT0)
-4.438 13.429 pll_i/inst/mmcme3_adv_inst/CLKOUT0
net (fo=1, routed) 0.501 13.930 pll_i/inst/clk_out1_clk_wiz_1
BUFGCE_X1Y1 BUFGCE (Prop_BUFCE_BUFGCE_I_O)
0.101 14.031 pll_i/inst/clkout1_buf/O
X2Y0 (CLOCK_ROOT) net (fo=1, routed) 1.369 15.400 pll_clk_8
SLICE_X49Y58 FDRE foo_reg_reg/C
这就是 clk_out1_clk_wiz_1 的源时钟路径(Source Clock Path),答案也就在这里。
另一种方法是用 Tcl 命令来获取信息。具体做法因 FPGA 工具而异。在 Vivado 中,打开实现后的设计之后,可以使用下面这样的命令:
> get_clocks -of_objects [ get_nets pll_clk_8 ] clk_out1_clk_wiz_1
这个方法要求你知道该网络的名称。有时像本例一样简单,有时则需要先把这个网络的名字找出来。FPGA 工具通常都会提供通过图形界面(GUI)来完成这件事的途径,也可以使用 Tcl 命令来达到目的。
事实上,我希望这几个 Tcl 示例能让你明白,学会正确使用 Tcl 是多么重要。这正是下一页要讲的内容。
使用 get_port 的意义
在上面的例子中,create_clock 命令依靠 get_port 在时钟对象与物理输入引脚之间建立联系。正如上面所说,这种联系是必要的,否则就无法知道哪些逻辑元件连接在这个时钟(或由它派生的其他时钟)上。
但使用 get_port 并不是唯一的选择。例如,也可以指向某个全局时钟缓冲器(global clock buffer)的输出引脚。像这样:
create_clock -name clk -period 4 [get_pins my_BUFG_inst/O]
区别在于,工具把全局时钟缓冲器的输出引脚视为时钟的源点。换句话说,时钟路径的计算从该位置开始。这个源点上的第一个时钟边沿发生在 0 ns,因此这个输出引脚成了时间基准。
这是一条合法的时序约束,但它有两个重要缺陷:
- 必须要求工具把这个时钟当作与其它所有时钟都无关的时钟(unrelated clock)来看待。即使是同一个 PLL 产生的时钟也不例外。原因是,工具把那个输出引脚当作时间基准,因此不会对不同时钟之间在时钟路径延迟上的差异做补偿(也就是说,时钟偏斜(clock skew)不会被考虑进去)。
- 要定义与 FPGA 外部可见时钟相关的I/O 时序约束是不可能的(或者说非常困难)。这是因为这类约束需要一个时钟对象作为时间基准,但如前所述,这里的时间基准却是全局时钟缓冲器的输出引脚。从外部时钟到全局时钟缓冲器之间的时钟偏斜是未知的(例如,它会随温度变化)。
因此,只要可能,就应该使用 get_port。否则,这个时钟与除它自身以外任何事物的时序关系都应当被视为未知。
本页介绍了不少 Tcl 命令,但都没有充分解释。下一页会补上这一课。