3. 时钟约束工程

在时序收敛流程中,时钟约束是所有分析计算的起点。如果时钟定义不完整或相互冲突,后续的建立/保持时间检查、跨时钟域分析、时钟偏斜估算都会失去意义。本节围绕 create_clock 与 create_generated_clock 两个核心 SDC 命令,讨论主时钟、衍生时钟、时钟使能、分频与切换场景的约束方法,并给出避免遗漏与冲突的工程实践。

主时钟:时序分析的绝对基准

主时钟是进入 FPGA 的原始时钟信号,通常来自外部晶振、时钟芯片或板上其他器件的输出。在 SDC 中,主时钟通过 create_clock 定义,它同时确定了时钟周期、占空比、上升沿与下降沿位置,以及该时钟所驱动的源节点。

# 50 MHz 输入时钟,接在 clk_50m 端口
create_clock -name sys_clk -period 20.000 \
    -waveform {0.000 10.000} [get_ports clk_50m]

# 100 MHz 差分参考时钟,接在专用时钟引脚
create_clock -name ref_clk -period 10.000 \
    -waveform {0.000 5.000} [get_ports ref_clk_p]

这里 -period 的单位由 set_units 决定,工程中通常统一为 ns。-waveform 给出上升沿和下降沿时刻,默认是 {0 period/2}。对于 DDR 源同步接口,占空比可能不是 50%,必须显式写出波形,否则时序模型会偏离实际。

主时钟定义中最容易出问题的是源节点选择。如果时钟从输入端口进入后先经过 IBUF、PLL 或时钟网络,create_clock 应作用在端口上,而不是内部节点。Quartus 的 Timing Analyzer 会自动把时钟传播到其驱动的寄存器时钟引脚,但前提是源节点确实位于时钟路径的起点。若把主时钟错误地定义在 PLL 输出上,PLL 输入路径就会缺少约束,形成隐性未约束路径。

判断主时钟源节点是否正确的简单方法:在 Timing Analyzer 中执行 report_clocks,检查每个时钟的 Source 字段是否指向预期的端口或引脚。若 Source 显示为内部节点,说明约束位置需要调整。

衍生时钟:PLL、分频器与时钟使能

FPGA 内部大量时钟并非直接来自外部,而是由 PLL、MMCM、分频计数器或时钟使能逻辑产生。这类时钟必须用 create_generated_clock 描述其与主时钟的相位、倍频/分频关系,否则工具无法建立正确的时钟到达时间模型。

以 PLL 为例,输入 50 MHz,输出 100 MHz 和 25 MHz 两路时钟:

create_clock -name sys_clk -period 20.000 [get_ports clk_50m]

# PLL 输出 100 MHz,与输入同相
create_generated_clock -name pll_clk_100m \
    -source [get_ports clk_50m] \
    -divide_by 1 -multiply_by 2 \
    [get_pins pll_inst|altpll_component|auto_generated|pll1|clk[0]]

# PLL 输出 25 MHz,与输入同相
create_generated_clock -name pll_clk_25m \
    -source [get_ports clk_50m] \
    -divide_by 2 \
    [get_pins pll_inst|altpll_component|auto_generated|pll1|clk[1]]

关键参数是 -source,它指向衍生时钟的参考时钟源。对于 PLL,-source 应指向 PLL 的输入时钟端口或输入引脚,而不是 PLL 输出。若 -source 写错,工具会认为衍生时钟与主时钟无关,跨时钟路径将被当作异步处理,可能掩盖真实的时序问题。

对于用计数器实现的分频时钟,例如 50 MHz 经 5 分频得到 10 MHz:

create_generated_clock -name div_clk_10m \
    -source [get_ports clk_50m] \
    -divide_by 5 \
    [get_pins div_cnt_reg[0]/Q]

这里源节点是分频计数器的输出寄存器 Q 端。注意 -divide_by 5 表示输出周期是源时钟周期的 5 倍,即 100 ns,对应 10 MHz。若分频比不是整数,或占空比有特殊要求,应改用 -edges 显式指定边沿序列。

时钟使能:不是时钟,但影响时序

时钟使能(Clock Enable)常被误认为需要单独创建时钟。实际上,CE 信号并不改变时钟频率,它只是控制寄存器是否在某个时钟沿更新。因此不需要为 CE 创建 create_generated_clock,但必须确保 CE 路径本身满足时序要求。

典型场景是低速逻辑用高频时钟加使能实现。例如系统时钟 100 MHz,某模块每 10 个周期工作一次,等效 10 MHz 行为:

# 不创建 10 MHz 衍生时钟,只约束 CE 路径
# CE 由计数器产生,需满足 100 MHz 下的建立/保持
set_max_delay -from [get_pins ce_cnt_reg[*]/Q] \
    -to [get_pins data_reg[*]/CE] 8.000

如果错误地为 CE 创建了一个 10 MHz 衍生时钟,工具会认为数据寄存器工作在 10 MHz,从而放松时序要求,导致实际在 100 MHz 下无法收敛。这是时钟约束中常见的“过度约束放松”错误。

时钟切换:多路选择与冲突避免

时钟切换电路(Clock Mux)允许系统在多个时钟源之间动态选择。SDC 中必须为每个输入时钟分别定义,并对切换后的输出节点做正确处理。

create_clock -name clk_a -period 10.000 [get_ports clk_a]
create_clock -name clk_b -period 20.000 [get_ports clk_b]

# 切换输出节点:不能同时属于两个时钟
# 方法一:为输出创建独立衍生时钟,指定默认源
create_generated_clock -name clk_mux_out \
    -source [get_ports clk_a] \
    -divide_by 1 \
    [get_pins clk_mux_inst|out]

# 方法二:使用 set_clock_groups 声明互斥
set_clock_groups -logically_exclusive \
    -group {clk_a} -group {clk_b}

时钟切换的核心矛盾是:同一个物理节点在不同时刻可能由不同时钟驱动。若不加处理,工具会认为该节点同时受两个时钟约束,产生冲突。工程上通常采用两种策略:一是为切换输出创建独立的衍生时钟,并指定一个默认源;二是用 set_clock_groups -logically_exclusive 声明两个时钟不会同时有效,从而避免跨时钟路径被错误分析。

需要注意的是,-logically_exclusive 表示两个时钟在逻辑上互斥,工具会切断它们之间的时序路径。如果实际电路中两个时钟可能同时存在(例如毛刺或切换瞬间),应改用 -physically_exclusive 或增加切换同步逻辑,否则会遗漏真实的亚稳态风险。

约束遗漏与冲突的检查方法

完成时钟约束后,必须进行系统性检查。以下是在 Quartus Timing Analyzer 中常用的验证命令:

  • report_clocks:列出所有已定义时钟及其源节点、周期、波形,检查是否有遗漏或重复定义。
  • report_clock_transfers:显示时钟之间的传递关系,确认跨时钟路径是否被正确识别为同步或异步。
  • check_timing:报告未约束的时钟、未约束的路径、冲突的时钟定义等问题。
  • report_sdc:回读 SDC 约束,确认命令顺序和优先级符合预期。

常见的冲突包括:同一节点被两个 create_clock 定义;衍生时钟的 -source 指向了另一个衍生时钟而非主时钟;时钟使能被误建为衍生时钟;时钟切换输出未做互斥声明。这些问题在 check_timing 中通常会有明确提示,应在布局布线前解决。

时钟约束关系图:主时钟 → 衍生时钟 → 时序路径 主时钟(create_clock) clk_50m 端口 · 20 ns ref_clk_p 端口 · 10 ns 衍生时钟(create_generated_clock) PLL clk[0] · 100 MHz · ×2 PLL clk[1] · 25 MHz · ÷2 div_cnt_reg[0]/Q · 10 MHz · ÷5 时钟使能 CE(不建衍生时钟) 时钟切换(Clock Mux) clk_a / clk_b 互斥 clk_mux_out 独立衍生 -source -source clk_a / clk_b 时序路径与检查点 同步路径 同一时钟域 建立/保持检查 setup / hold 跨时钟路径 不同时钟域 时钟组声明 set_clock_groups 未约束路径 遗漏或冲突 check_timing 报告 必须清零 约束检查清单 report_clocks 确认源节点 report_clock_transfers 确认传递 check_timing 无未约束路径 时钟切换已声明互斥 CE 未被误建为衍生时钟 衍生时钟 -source 指向主时钟

工程实践中的参数与判断依据

以下是一份可直接参考的时钟约束检查表,用于在 Quartus 工程中逐项确认。表中参数为示例值,实际工程应根据器件手册和板级时钟方案填写。

检查项 示例参数 判断依据 常见问题
主时钟周期 20.000 ns 与板级晶振频率一致 周期写错导致全链路时序偏差
主时钟源节点 get_ports clk_50m report_clocks 中 Source 为端口 定义在内部节点,输入路径未约束
PLL 衍生时钟 ×2 / ÷2 与 PLL 配置的倍频/分频比一致 -source 指向 PLL 输出而非输入
分频时钟 ÷5 → 10 MHz 输出周期 = 源周期 × 5 分频比与实际计数器不符
时钟使能 不建衍生时钟 CE 路径用 set_max_delay 约束 误建衍生时钟导致时序放松
时钟切换 logically_exclusive 两时钟不会同时有效 未声明互斥,路径冲突
未约束路径 check_timing 报告为空 所有时钟路径均有约束覆盖 遗漏衍生时钟或跨时钟组

时钟约束的质量直接决定时序分析的可信度。一个完整的约束集应当做到:每个主时钟有明确的源节点和周期;每个衍生时钟有正确的 -source 和分频/倍频关系;时钟使能不误建为时钟;时钟切换有互斥声明;check_timing 报告无未约束路径。在此基础上,后续的时序例外、多周期路径和跨时钟域处理才有可靠的基准。