3、时序约束入门:创建时钟约束、生成时钟约束、输入延迟约束、输出延迟约束

时序约束这东西,说白了就是告诉工具你的芯片想跑多快、怎么跑。我刚开始做数字设计那会儿,总觉得约束是后端的事,随便写几条就完事了。结果呢?综合出来的网表时序一塌糊涂,改都改不动。后来我学乖了——约束写得好,后端少烦恼。

今天咱们就聊聊最基础的四种约束:创建时钟、生成时钟、输入延迟、输出延迟。嗯,这四样搞明白了,时序分析就算入门了。

3.1 创建时钟约束(create_clock)

创建时钟,是所有约束的起点。没有时钟,STA根本没法跑。你想想看,时序分析总得有个参考点吧?时钟就是那个心跳。

核心语法:

create_clock -name <时钟名> -period <周期> [ -waveform <上升沿 下降沿> ] [get_ports <端口名>]

举个例子,一个100MHz的时钟,占空比50%,从端口clk_in进来:

create_clock -name clk -period 10 [get_ports clk_in]

这里period是10ns,对应100MHz。默认波形是0ns上升、5ns下降。如果你想自定义波形,比如占空比60%:

create_clock -name clk -period 10 -waveform {0 6} [get_ports clk_in]

我在项目中遇到过一个坑:有人把period写成了10MHz对应的100ns,结果综合出来的面积大了两倍。为什么?因为工具以为时钟很慢,用了大量慢速单元。所以,周期单位是ns,不是MHz,千万别搞反了。

个人习惯:我一般会在创建时钟时显式指定-waveform,哪怕就是默认的50%占空比。这样看代码的人一眼就知道你的意图,不会猜。

3.2 生成时钟约束(create_generated_clock)

生成时钟,就是从已有的主时钟分频、倍频或相移得到的时钟。比如你用PLL或者分频器产生的时钟,不能直接create_clock,得用create_generated_clock。

核心语法:

create_generated_clock -name <时钟名> -source <源时钟> -divide_by <分频系数> [ -multiply_by <倍频系数> ] [get_pins <目标引脚>]

来个实际例子:主时钟100MHz,经过一个2分频器,输出50MHz时钟:

create_clock -name clk -period 10 [get_ports clk_in]
create_generated_clock -name clk_div2 -source [get_ports clk_in] -divide_by 2 [get_pins div_reg/Q]

注意看,-source指向的是源时钟的端口或引脚,而不是源时钟的名字。这个细节我吃过亏。我曾经把-source写成了get_clocks clk,结果工具报错说找不到源。后来查手册才发现,-source要接物理节点。

生成时钟还有几个常用选项:

  • -edges:直接指定使用源时钟的第几个边沿,比-divide_by更灵活
  • -edge_shift:对边沿进行偏移,用于相移时钟
  • -invert:反相时钟,相当于-divide_by 1再加180度相移

举个例子,一个三分频时钟,用-edges来写:

create_generated_clock -name clk_div3 -source [get_ports clk_in] -edges {1 3 5} [get_pins div3_reg/Q]

这里{1 3 5}表示取源时钟的第1、3、5个边沿作为新时钟的上升沿、下降沿、下一个上升沿。说白了就是每3个源时钟周期产生一个生成时钟周期。

注意:生成时钟的-source必须是一个时钟网络的驱动点,通常是时钟端口或寄存器时钟引脚。不要随便接一个组合逻辑输出,否则工具会搞不清时序关系。

3.3 输入延迟约束(set_input_delay)

输入延迟,描述的是外部器件发送数据到芯片输入端口的时间关系。说白了,就是告诉工具:数据相对于时钟,到底晚到多久?

核心语法:

set_input_delay -clock <时钟名> <延迟值> [get_ports <输入端口>]

假设外部器件在时钟上升沿后2ns输出数据,经过1ns的PCB走线到达芯片引脚,那么输入延迟就是3ns(max)。min路径呢?可能是1.5ns。

set_input_delay -clock clk -max 3.0 [get_ports data_in]
set_input_delay -clock clk -min 1.5 [get_ports data_in]

这里-max用于建立时间分析,-min用于保持时间分析。你想想看,数据到得越晚,建立时间越紧张;数据到得越早,保持时间越紧张。所以max和min要分别给。

我记得有一次,一个实习生只写了-max没写-min,结果保持时间违例一大堆。工具默认-min为0,但实际外部器件可能早于时钟输出数据,这就导致内部寄存器保持时间不够。所以,min和max都要写,别偷懒。

输入延迟还有两个变体:

  • -rise/-fall:分别指定上升沿和下降沿的延迟,用于数据在不同边沿变化的情况
  • -add_delay:当同一个端口有多个输入延迟约束时,用这个选项叠加

避坑指南:我曾经在一个DDR接口项目里,忘了加-add_delay,结果两个输入延迟约束互相覆盖,时序分析全乱套。后来每条约束都加上-add_delay,问题解决。

3.4 输出延迟约束(set_output_delay)

输出延迟,和输入延迟正好相反。它描述的是芯片输出数据到外部器件的时间关系。说白了,就是告诉工具:外部器件需要数据在时钟沿之前多久准备好?之后保持多久?

核心语法:

set_output_delay -clock <时钟名> <延迟值> [get_ports <输出端口>]

假设外部器件要求数据在时钟沿前2ns到达,PCB走线延迟1ns,那么输出延迟就是3ns(max)。min路径呢?外部器件要求数据保持1ns,PCB走线延迟0.5ns,那么输出延迟就是1.5ns(min)。

set_output_delay -clock clk -max 3.0 [get_ports data_out]
set_output_delay -clock clk -min 1.5 [get_ports data_out]

注意,输出延迟的-max对应的是外部器件的建立时间要求,-min对应的是外部器件的保持时间要求。这个逻辑别搞反了。

输出延迟也有-rise/-fall和-add_delay选项,用法和输入延迟一样。

重要提醒:输出延迟的时钟,必须是芯片内部用来驱动输出寄存器的那个时钟。如果输出路径是组合逻辑直接输出,那就要用虚拟时钟(virtual clock)来约束。虚拟时钟怎么创建?很简单:

create_clock -name vclk -period 10

不加get_ports,就是一个虚拟时钟,只用于约束,不连到任何物理端口。

3.5 四种约束的关系

这四种约束,其实构成了一个完整的时序路径闭环:

  • 输入路径:外部器件 → 输入延迟 → 芯片内部寄存器
  • 内部路径:寄存器 → 组合逻辑 → 寄存器
  • 输出路径:芯片内部寄存器 → 输出延迟 → 外部器件

创建时钟和生成时钟,定义了所有路径的参考时间点。输入延迟和输出延迟,则定义了芯片边界上的时间窗口。缺了任何一个,STA都不完整。

我画了一张图,帮你理清这四者的关系:

时序约束四要素关系图 外部器件 (发送端) 芯片内部 输入寄存器 组合逻辑 输出寄存器 时钟生成 外部器件 (接收端) 输入延迟 输出延迟 创建时钟 / 生成时钟 输入/输出延迟 创建/生成时钟 内部寄存器 组合逻辑 时钟生成逻辑

从图上可以看得很清楚:输入延迟约束的是芯片左边边界,输出延迟约束的是右边边界,而创建时钟和生成时钟则定义了内部所有寄存器的参考时间。四者配合,才能完整描述一个时序系统的行为。

我的经验:写约束的顺序,我建议先写创建时钟,再写生成时钟,然后写输入延迟,最后写输出延迟。这样逻辑上是从内到外,不容易遗漏。另外,每写完一条约束,记得用report_clocks、report_timing检查一下,别等到最后才跑STA,那时候发现问题改起来就费劲了。

好了,时序约束入门就聊到这儿。这四种约束是STA的基石,你花时间把它们搞透,后面的时序分析就会顺风顺水。嗯,记住一句话:约束写到位,时序不流泪。


可可精品资料库