4. 时序约束基础
Design Compiler 的综合优化目标不是“面积最小”或“速度最快”这类模糊描述,而是由一组精确的时序约束定义的。约束写错,工具会朝着错误的方向优化:要么过度约束导致面积和功耗暴涨,要么约束过松导致签核阶段出现大量违例。本节围绕时钟定义、I/O 延迟、时钟不确定性与时钟延迟四类基础约束,说明它们各自建模的物理含义、命令写法以及参数之间的配合关系。
一、create_clock:把真实时钟端口变成时序基准
create_clock 定义的是综合的“时间原点”。它告诉工具:某个端口或内部引脚上存在一个周期为 T 的时钟,所有时序路径的建立/保持检查都以此为参照。典型写法如下:
create_clock -name clk_core -period 5.0 -waveform {0 2.5} [get_ports clk]
create_clock -name clk_ddr -period 2.5 -waveform {0 1.25} [get_ports clk_ddr]
其中 -period 是周期(ns),-waveform 给出上升沿与下降沿时刻。若省略 -waveform,默认占空比 50%,上升沿在 0。需要特别注意:-waveform 的第一个值不一定是 0,例如 {1.25 3.75} 表示上升沿在 1.25ns,这会影响跨时钟域路径的相位关系。对于差分时钟,通常只在正端创建时钟,负端由工具自动推导;若对正负端都创建时钟,会引入重复定义。
另一个常见问题是时钟源经过时钟门控或分频器后,是否还需要在输出端再建时钟。答案是:如果该节点由已定义时钟派生,应使用 create_generated_clock,而不是再写一个独立的 create_clock,否则工具无法识别两者的同源关系,跨时钟路径会被当作异步处理。
二、create_generated_clock:描述派生时钟的相位与分频关系
生成时钟用于描述由主时钟经过分频、倍频、门控或相位偏移后产生的时钟。它的关键参数是 -source(源时钟对象)和 -divide_by / -multiply_by / -edges。例如一个 2 分频时钟:
create_generated_clock -name clk_div2 \
-source [get_ports clk] -divide_by 2 [get_pins u_div/Q]
若分频器输出经过反相,可用 -invert;若需要精确指定边沿位置,用 -edges {1 3 5} 表示生成时钟的上升沿对应源时钟的第 1、3、5 个边沿。生成时钟的周期由源时钟周期与分频/倍频比共同决定,工具会自动推导,不需要也不应该再写 -period。这一点是初学者最容易出错的地方:给生成时钟加 -period 会与源时钟关系冲突,导致约束不一致。
三、set_input_delay / set_output_delay:把外部世界折算到端口
综合工具看不到芯片外部的器件,因此必须用输入/输出延迟把外部路径的时序“折算”到端口上。set_input_delay 表示数据到达输入端口相对于时钟沿的延迟,set_output_delay 表示数据从输出端口出发后到达外部捕获器件所需的延迟。它们都相对于某个时钟定义:
set_input_delay -clock clk_core -max 1.2 [get_ports data_in*]
set_input_delay -clock clk_core -min 0.3 [get_ports data_in*]
set_output_delay -clock clk_core -max 1.5 [get_ports data_out*]
set_output_delay -clock clk_core -min 0.4 [get_ports data_out*]
这里 -max 用于建立时间检查,-min 用于保持时间检查。输入延迟的物理含义是:外部器件从时钟沿到数据稳定输出的时间(Tco)加上 PCB 走线延迟。输出延迟则是:外部器件的建立时间(Tsu)加上走线延迟。若只写一个值而不区分 max/min,工具会同时用于建立和保持检查,通常偏保守,但可能掩盖真实的保持违例风险。
对于输入端口,还有一个容易忽略的细节:set_input_delay 默认假设数据在时钟沿之后到达。如果外部器件是在时钟沿之前就输出数据(例如负沿触发或提前输出),需要用 -clock_fall 或负的延迟值来表达。参照系一旦确定,全文和图示必须保持一致,否则建立/保持的松紧判断会完全反过来。
四、时钟不确定性与时钟延迟:区分“抖动”和“插入延迟”
时钟不确定性(uncertainty)建模的是时钟边沿的随机抖动和时钟树上的偏差(skew),它直接压缩可用时序窗口。写法为:
set_clock_uncertainty -setup 0.15 [get_clocks clk_core]
set_clock_uncertainty -hold 0.08 [get_clocks clk_core]
建立检查时,不确定度从数据要求时间中扣除;保持检查时,不确定度加到数据要求时间上。因此 -setup 和 -hold 通常取不同值,保持不确定度一般更小。若只写一个数值,工具会同时用于两者,这在早期评估阶段可以接受,但签核前应分开设置。
时钟延迟(latency)建模的是时钟从源端口到寄存器时钟引脚的实际插入延迟。综合阶段通常还没有时钟树,因此用 set_clock_latency 给出估计值:
set_clock_latency -source 0.8 [get_clocks clk_core]
set_clock_latency 0.5 [get_clocks clk_core]
其中 -source 表示源端延迟(时钟源到芯片端口),不带 -source 表示网络端延迟(端口到寄存器时钟引脚)。综合阶段常用网络端延迟来模拟时钟树插入延迟;布局布线后,实际时钟树延迟由工具提取,此时应移除或更新这些估计值。不确定性与延迟的区别可以这样理解:延迟是确定性的、可预测的偏移,不确定性是随机或难以精确建模的偏差。两者都会影响时序余量,但来源和处理方式不同。
五、约束之间的配合与常见错误
这四类约束不是孤立的。时钟定义决定了所有路径的参照系;I/O 延迟把外部器件折算到端口;不确定性和延迟则调整时序窗口的松紧。一个典型的错误是:只定义了 create_clock,却没有设置输入/输出延迟,工具会默认端口无延迟,导致输入路径过于宽松、输出路径过于乐观。另一个错误是给生成时钟重复写 -period,造成约束冲突。还有一类问题是把 set_clock_latency 和 set_clock_uncertainty 混用:前者是确定性的插入延迟,后者是抖动和偏差,两者不能互相替代。
在实际项目中,建议按以下顺序建立约束:先定义所有主时钟,再定义生成时钟,然后设置输入/输出延迟,最后设置不确定性和延迟。每完成一步,用 report_clock、report_timing 检查约束是否被正确识别。例如 report_clock -skew -attributes 可以查看时钟的周期、边沿和不确定度;report_timing -delay max 可以观察建立检查的余量是否合理。若发现某条路径的余量异常大或异常小,应优先检查该路径的时钟定义和 I/O 延迟是否写错,而不是急于调整综合策略。
-clock_fall 或负值,必须在约束注释和时序报告中明确说明,否则后续维护者极易误判建立/保持的松紧方向。