3、SDC约束编写实战:主时钟、生成时钟、输入输出延迟、伪路径、多周期路径、最大最小延迟

SDC约束,说白了就是告诉工具你的芯片到底想怎么跑。我见过不少新手,一上来就照着模板抄,结果跑出来的时序报告根本没法看。嗯,今天咱们就聊聊实战中那些最常用的约束怎么写,以及我踩过的那些坑。

3.1 主时钟(create_clock)—— 芯片的心跳

主时钟是所有时序分析的起点。没有它,STA根本跑不起来。我个人习惯,拿到设计的第一件事就是先把所有主时钟定义清楚。

create_clock -name clk_sys -period 10.0 [get_ports clk]

这里有个细节:-period 10.0 表示周期10ns,也就是100MHz。但实际项目中,你往往需要留点余量。比如目标100MHz,我通常会设成9.5ns甚至9ns。为什么?因为后端实现过程中会有各种偏差,留点余量心里踏实。

注意: 主时钟必须定义在时钟树的根部,通常是芯片的输入端口。千万别定义在内部节点上,否则工具会搞混时钟源。

我曾经在一个项目中,把主时钟定义在了PLL的输出端,结果CTS阶段工具死活不认,折腾了两天才发现是约束位置错了。嗯,从那以后我再也不敢乱放主时钟了。

3.2 生成时钟(create_generated_clock)—— 时钟的分身

生成时钟是从主时钟派生出来的,比如分频、倍频、门控时钟。写生成时钟的关键是告诉工具:这个时钟和主时钟是什么关系。

create_generated_clock -name clk_div2 -source [get_ports clk] \
  -divide_by 2 [get_pins u_div/clk_out]

这里 -source 要指向生成时钟的源时钟,也就是主时钟。而 -divide_by 2 表示二分频。你想想看,如果源时钟是100MHz,那生成时钟就是50MHz。

小技巧: 对于分频时钟,我建议用 -edges 来精确指定上升沿和下降沿的位置,这样更可控。比如:
create_generated_clock -name clk_div2 -source [get_ports clk] \
    -edges {1 3 5} [get_pins u_div/clk_out]
这表示生成时钟的上升沿对应源时钟的第1、3、5个边沿。

我记得有一次,一个同事把生成时钟的 -source 写成了内部节点,结果时序分析完全乱套。生成时钟的源必须是主时钟,这是铁律。

3.3 输入输出延迟(set_input_delay / set_output_delay)—— 芯片与外界的约定

芯片不是孤岛,它要和外部器件通信。输入输出延迟就是描述这种通信关系的。说白了,就是告诉工具:数据从外部进来,或者出去到外部,需要多长时间。

set_input_delay -clock clk_sys -max 2.5 [get_ports data_in]
set_input_delay -clock clk_sys -min 1.0 [get_ports data_in]

-max-min 分别对应最大和最小延迟。最大延迟用于建立时间检查,最小延迟用于保持时间检查。这两个值怎么定?通常来自芯片的datasheet或者系统级的时序预算。

核心要点: 输入延迟是外部器件发送数据到芯片输入端口的时间。输出延迟是芯片输出数据到外部器件被采样的时间。这两个值设错了,整个时序分析就是空中楼阁。

我建议,对于输入延迟,可以留10%~20%的余量。比如外部器件要求2ns,我设成2.2ns。这样即使实际PCB走线有点偏差,也不至于翻车。

3.4 伪路径(set_false_path)—— 那些不需要关心的路径

不是所有路径都需要时序检查。比如跨时钟域的同步器路径、测试模式下的路径、复位路径等。这些路径如果硬要分析,只会产生一堆假违例,浪费精力。

set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]
set_false_path -from [get_pins u_rst/rst_n_reg/CK] -to [get_pins u_rst/rst_n_reg/D]

伪路径用得好,可以大大减少STA的工作量。但用不好,可能会漏掉真正的时序问题。我见过有人把整个模块都设成伪路径,结果流片回来功能不正常。

警告: 伪路径一定要有充分的理由。比如跨时钟域路径,必须确认有同步器处理。复位路径,必须确认复位信号是异步的。不要为了省事而滥用。

我曾经在一个项目中,把一条跨时钟域路径设成了伪路径,但后来发现那个路径根本没有同步器。结果芯片在特定条件下会采样到错误数据。嗯,从那以后我每次设伪路径都会再三确认。

3.5 多周期路径(set_multicycle_path)—— 给路径多一点时间

有些路径不需要在一个时钟周期内完成。比如从慢速模块到快速模块的路径,或者某些计算密集型路径。这时候就可以用多周期路径来放松约束。

set_multicycle_path -setup 2 -from [get_pins u_calc/data_reg/CK] \
  -to [get_pins u_calc/result_reg/D]
set_multicycle_path -hold 1 -from [get_pins u_calc/data_reg/CK] \
  -to [get_pins u_calc/result_reg/D]

这里 -setup 2 表示建立时间检查放宽到2个周期。注意,-hold 通常设为 -setup - 1,也就是1。为什么?因为保持时间检查是基于建立时间检查的边沿往前推的,如果不调整,保持时间约束会变得过于严格。

经验之谈: 多周期路径的 -hold 设置很容易被忽略。我建议每次设完 -setup 后,立即补上 -hold。否则工具默认 -hold 为0,会导致保持时间检查过于悲观。

我记得有一次,一个设计团队设了多周期路径但忘了设 -hold,结果保持时间违例一大堆。后来加上 -hold 1,违例全消失了。你想想看,一个参数就能省下多少修时序的时间。

3.6 最大最小延迟(set_max_delay / set_min_delay)—— 自定义约束

有时候,标准时序检查不够用。比如异步信号之间的相对延迟要求,或者某些特殊路径的延迟限制。这时候就可以用最大最小延迟来约束。

set_max_delay 5.0 -from [get_pins u_async/a_reg/CK] \
  -to [get_pins u_async/b_reg/D]
set_min_delay 1.0 -from [get_pins u_async/a_reg/CK] \
  -to [get_pins u_async/b_reg/D]

最大延迟用于保证信号不会太慢,最小延迟用于保证信号不会太快。这两个约束通常用于异步路径或者跨时钟域路径的额外保护。

注意: 最大最小延迟会覆盖默认的建立保持时间检查。如果你设了 set_max_delay 5.0,工具就不会再检查这条路径的建立时间了。所以使用时要格外小心。

我个人习惯,只在以下场景使用最大最小延迟:

  • 异步信号之间的握手路径
  • 跨时钟域的同步器路径(作为伪路径的补充)
  • 某些特殊模拟接口的时序要求

嗯,以上就是SDC约束编写的核心内容。说白了,约束就是你和工具之间的沟通语言。写得好,工具就能帮你做出一个靠谱的芯片;写得不好,那就是在给自己挖坑。我建议,每写完一条约束,都问自己一句:这条约束合理吗?有没有更好的写法?这样反复推敲,你的约束质量一定会越来越高。