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。我建议你养成好习惯:周期写精确到三位小数,避免后续计算误差。
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给外部器件的时间。
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 关键路径分析
时序报告里,违例最严重的路径叫“关键路径”。工具会列出这条路径的详细信息:
- 起点: 源寄存器或输入端口
- 终点: 目标寄存器或输出端口
- 路径延迟: 包括逻辑延迟和布线延迟
- 时钟偏斜: 时钟到达起点和终点的差异
我个人习惯,先看最差的几条路径。如果最差的都能修,其他的自然就过了。
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 实战建议——我的“三板斧”
最后,分享一个我自己的时序调试流程:
- 先看最差路径: 打开时序报告,找到最差的3条路径。分析是建立时间还是保持时间违例。
- 判断瓶颈: 看路径延迟组成。逻辑延迟高就加流水线,布线延迟高就复制寄存器或调整布局。
- 迭代优化: 改一处,跑一次时序。不要一次改太多,否则不知道哪个改动有效。
嗯,时序约束这东西,刚开始觉得难,做多了就习惯了。关键是动手,多跑几次,多看看报告。你想想看,连我都踩过那么多坑,你踩几个也正常。慢慢来,总能学会的。