9、时序约束入门:时钟约束、输入输出延迟约束、时序报告分析、常见时序违例修复方法

时序约束,说白了就是给FPGA设计定规矩。

你告诉工具:我的时钟跑多快,数据什么时候来,什么时候走。工具才能帮你把布局布线做好。我刚开始学的时候,总觉得这步可有可无,直到有一次板子调了三天,发现是约束没写对……嗯,从那以后我再也不敢偷懒了。

9.1 时钟约束——设计的“心跳”

时钟是FPGA的脉搏。所有时序分析,都围绕时钟展开。

紫光同创的PDS工具,支持的时钟约束语法和SDC基本一致。我个人习惯,先约束主时钟,再管生成时钟。

9.1.1 主时钟约束

主时钟,就是从外部晶振进来的时钟,或者PLL的输入时钟。用 create_clock 命令定义。

# 约束一个50MHz的主时钟,连接到FPGA的CLK0引脚
create_clock -name sys_clk -period 20.000 [get_ports {CLK0}]

这里 -period 20.000 表示周期20ns,对应50MHz。我建议你养成好习惯:周期写精确到三位小数,避免后续计算误差。

小技巧: 如果你不确定时钟频率,可以用示波器先测一下。我曾经遇到过客户给的原理图标注50MHz,实际板子上是25MHz……这种坑,踩一次就够了。

9.1.2 生成时钟约束

PLL出来的时钟,或者分频器产生的时钟,属于生成时钟。用 create_generated_clock 约束。

# PLL输出200MHz时钟,源时钟是sys_clk
create_generated_clock -name pll_clk_200m \
  -source [get_ports {CLK0}] \
  -multiply_by 4 \
  -divide_by 1 \
  [get_pins {PLL_INST/CLKOUT0}]

这里 -multiply_by 4 表示4倍频。你想想看,如果源时钟是50MHz,4倍频就是200MHz,周期5ns。工具会自动计算。

注意: 生成时钟的 -source 一定要指向源时钟的物理节点(引脚或端口),不要写成时钟名字。我见过有人写成 -source [get_clocks sys_clk],结果时序分析全乱套了。

9.2 输入输出延迟约束——管好数据“上下车”的时间

时钟约束好了,数据怎么进出芯片?这就靠输入输出延迟约束了。

说白了,就是告诉工具:数据相对于时钟,是提前到还是晚到?提前多少?

9.2.1 输入延迟约束

外部器件发送数据到FPGA,数据相对于时钟的延迟,用 set_input_delay 定义。

# 外部器件在时钟上升沿后2ns输出数据
set_input_delay -clock sys_clk -max 2.000 [get_ports {DATA_IN}]
set_input_delay -clock sys_clk -min 0.500 [get_ports {DATA_IN}]

-max 是最大延迟,-min 是最小延迟。为什么要有两个值?因为外部器件的输出延迟有波动。我在项目中遇到过,有些芯片的datasheet只给典型值,没给最大最小值。这时候我一般按典型值的±30%估算,留点余量。

9.2.2 输出延迟约束

FPGA输出数据到外部器件,数据相对于时钟的延迟,用 set_output_delay 定义。

# 外部器件要求在时钟上升沿前1ns数据稳定
set_output_delay -clock sys_clk -max 1.500 [get_ports {DATA_OUT}]
set_output_delay -clock sys_clk -min 0.500 [get_ports {DATA_OUT}]

这里 -max 和 -min 的含义和输入延迟相反。嗯,这个容易搞混。我教你一个记忆方法:输入延迟看外部,输出延迟看内部。输入延迟是外部器件给FPGA的时间,输出延迟是FPGA给外部器件的时间。

核心要点: 输入输出延迟的值,必须从外部器件的datasheet中提取。不要自己拍脑袋写。我曾经见过有人随便写个1ns,结果板子跑起来数据老是错……查了三天,发现是约束和实际不匹配。

9.3 时序报告分析——学会看“体检报告”

约束写完了,跑一下时序分析,工具会给你一份报告。这份报告,就是设计的“体检报告”。

紫光同创PDS的时序报告,主要看这几个地方:

9.3.1 建立时间余量(Setup Slack)

建立时间余量,就是数据到达时间比要求时间早多少。正数表示满足,负数表示违例。

Path: reg1/Q -> reg2/D
Slack: 0.123 ns
Data Path Delay: 1.234 ns
Clock Skew: 0.056 ns

这个例子中,余量0.123ns,是正的,说明没问题。如果余量是负的,比如 -0.2ns,那就说明数据跑慢了,需要优化。

9.3.2 保持时间余量(Hold Slack)

保持时间余量,就是数据变化后,还能稳定多久。同样,正数表示满足。

Path: reg1/Q -> reg2/D
Slack: 0.089 ns
Data Path Delay: 0.567 ns
Clock Skew: -0.023 ns

保持时间违例比较少见,但一旦出现,往往很难修。我遇到过保持时间违例,原因是时钟偏斜太大。最后是通过调整时钟树解决的。

9.3.3 关键路径分析

时序报告里,违例最严重的路径叫“关键路径”。工具会列出这条路径的详细信息:

  • 起点: 源寄存器或输入端口
  • 终点: 目标寄存器或输出端口
  • 路径延迟: 包括逻辑延迟和布线延迟
  • 时钟偏斜: 时钟到达起点和终点的差异

我个人习惯,先看最差的几条路径。如果最差的都能修,其他的自然就过了。

读报告技巧: 不要只看Slack值。要看路径延迟的组成。如果逻辑延迟占比高,说明组合逻辑太深;如果布线延迟占比高,说明布局太分散。对症下药,才能药到病除。

9.4 常见时序违例修复方法——实战中的“救火”指南

时序违例了怎么办?别慌。我总结了几个常用的修复方法,按优先级排序:

9.4.1 方法一:流水线(Pipeline)

这是最常用的方法。把长组合逻辑拆成多级,中间插入寄存器。

// 原代码:组合逻辑太长
assign result = a + b + c + d + e + f + g + h;

// 改进后:分两级流水
reg [7:0] sum1, sum2;
always @(posedge clk) begin
  sum1 <= a + b + c + d;
  sum2 <= e + f + g + h;
end
assign result = sum1 + sum2;

这样,原来一级完成的加法,变成了两级。每级的逻辑深度减半,时序自然就好了。

9.4.2 方法二:寄存器复制(Register Duplication)

当一个寄存器驱动太多负载时,布线延迟会变大。复制一份寄存器,分担负载。

// 原代码:一个寄存器驱动太多
reg data_reg;
always @(posedge clk) data_reg <= data_in;
assign out1 = data_reg & ctrl1;
assign out2 = data_reg & ctrl2;
// ... 还有out3, out4, out5

// 改进后:复制寄存器
reg data_reg1, data_reg2;
always @(posedge clk) begin
  data_reg1 <= data_in;
  data_reg2 <= data_in;
end
assign out1 = data_reg1 & ctrl1;
assign out2 = data_reg2 & ctrl2;

这个方法,说白了就是用面积换速度。紫光同创的PDS工具,在综合选项里也有“寄存器复制”的开关,可以自动做。

9.4.3 方法三:调整逻辑结构

有些逻辑,换个写法,时序就好很多。比如:

// 原代码:多级比较
if (a > 100 && b > 200 && c > 300) ...

// 改进后:先比较,再与
wire cmp_a = (a > 100);
wire cmp_b = (b > 200);
wire cmp_c = (c > 300);
assign result = cmp_a & cmp_b & cmp_c;

这样,比较器可以并行执行,减少了逻辑深度。

9.4.4 方法四:约束调整

有时候,不是设计有问题,而是约束写得太紧了。比如:

  • 时钟频率设高了?实际跑不了那么快,就降频。
  • 输入输出延迟估大了?重新看datasheet,确认准确值。
  • 伪路径没设?有些路径不需要时序约束,用 set_false_path 排除。
# 跨时钟域的路径,设为伪路径
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]
警告: 设置伪路径要谨慎。只有你确定这条路径不会影响功能时,才能设。我曾经因为误设伪路径,导致一个异步信号没被约束,板子跑起来偶尔出错……查了整整两天。

9.4.5 方法五:综合选项调整

紫光同创PDS的综合选项里,有几个和时序相关的:

选项 作用 适用场景
retiming 自动重定时,移动寄存器位置 组合逻辑深度不均
register_duplication 自动复制高扇出寄存器 高扇出导致布线延迟大
logic_optimization 逻辑优化,减少面积 面积紧张但时序有余

我建议,先不开这些选项,手动优化。实在不行了,再开工具自动优化。因为自动优化有时候会引入意想不到的问题。

9.5 实战建议——我的“三板斧”

最后,分享一个我自己的时序调试流程:

  1. 先看最差路径: 打开时序报告,找到最差的3条路径。分析是建立时间还是保持时间违例。
  2. 判断瓶颈: 看路径延迟组成。逻辑延迟高就加流水线,布线延迟高就复制寄存器或调整布局。
  3. 迭代优化: 改一处,跑一次时序。不要一次改太多,否则不知道哪个改动有效。

嗯,时序约束这东西,刚开始觉得难,做多了就习惯了。关键是动手,多跑几次,多看看报告。你想想看,连我都踩过那么多坑,你踩几个也正常。慢慢来,总能学会的。

一句话总结: 时序约束不是束缚,而是你和工具沟通的桥梁。约束写好了,工具才能帮你做出稳定可靠的设计。