第二章 时序路径基础:建立时间与保持时间
各位同学,今天我们来聊聊时序路径的核心。说白了,就是搞清楚信号在芯片里到底是怎么跑的,以及为什么跑快了会出错。
我刚入行那会儿,总觉得时序分析是件很玄乎的事。直到有一次,我设计的一个模块在仿真时跑得好好的,结果流片回来,温度一高就罢工。查了整整两周,最后发现是保持时间违例。嗯,从那以后,我再也不敢小看这两个时间参数了。
2.1 建立时间与保持时间——寄存器的“脾气”
每个寄存器都有它的“小脾气”。你想让它老老实实采样数据,就得遵守它的规矩。
建立时间(Setup Time):在时钟有效沿到来之前,数据必须保持稳定的最短时间。
保持时间(Hold Time):在时钟有效沿到来之后,数据必须保持稳定的最短时间。
我习惯这么理解:建立时间就像你赶火车,得提前到站台等着。保持时间呢,就像火车刚开走那几秒,你别急着跳车。数据在时钟沿前后都得“老实待着”。
为什么会这样?因为寄存器内部是用模拟电路实现的。数据需要时间让内部节点充放电,稳定下来。你想想看,如果数据在时钟沿附近乱跳,寄存器采到的值可能是0,也可能是1,甚至是不确定的中间态——这就是所谓的“亚稳态”。
⚠️ 注意:我曾经在一个项目中,因为忽略了保持时间,导致芯片在低温下频繁出错。温度越低,保持时间违例越严重。这个坑,希望大家别踩。
2.2 时序路径的四种类型
一条完整的时序路径,起点是某个寄存器的时钟端或输入端口,终点是另一个寄存器的数据端或输出端口。我把它分成四类,咱们一个一个说。
2.2.1 输入到寄存器(Input to Register)
这条路径从芯片的输入引脚开始,经过组合逻辑,到达第一个寄存器的数据端。
举个例子:
// 输入信号 din 经过两级组合逻辑后进入寄存器
module input_to_reg (
input clk,
input [7:0] din,
output reg [7:0] dout
);
wire [7:0] tmp1, tmp2;
assign tmp1 = din + 8'h01; // 第一级组合逻辑
assign tmp2 = tmp1 ^ 8'hFF; // 第二级组合逻辑
always @(posedge clk) begin
dout <= tmp2; // 寄存器采样
end
endmodule
这条路径的延迟,决定了输入数据相对于时钟的建立时间要求。我一般会在约束里加上 set_input_delay,告诉工具外部数据什么时候来。
2.2.2 寄存器到寄存器(Register to Register)
这是最核心的路径,也是我们最常分析的。它从一个寄存器的输出端出发,经过组合逻辑,到达下一个寄存器的输入端。
💡 小技巧:我习惯把这条路径的延迟拆成三部分:时钟偏斜(Clock Skew)+ 寄存器内部延迟(Clk-to-Q)+ 组合逻辑延迟。其中组合逻辑延迟是我们可以优化的重点。
看个例子:
// 两级寄存器之间的组合逻辑
module reg_to_reg (
input clk,
input [7:0] a, b,
output reg [7:0] result
);
reg [7:0] a_reg, b_reg;
wire [7:0] sum;
always @(posedge clk) begin
a_reg <= a;
b_reg <= b;
end
assign sum = a_reg + b_reg; // 组合逻辑
always @(posedge clk) begin
result <= sum;
end
endmodule
这条路径的时序公式很简单:
建立时间检查:T_clk_to_q + T_combo + T_setup ≤ T_clk + T_skew
保持时间检查:T_clk_to_q + T_combo ≥ T_hold + T_skew
嗯,公式看着有点枯燥。但说白了,就是数据跑得太慢会违例建立时间,跑得太快会违例保持时间。
2.2.3 寄存器到输出(Register to Output)
这条路径从最后一个寄存器的时钟端开始,经过组合逻辑,到达芯片的输出引脚。
我遇到过这样一个案例:一个高速接口的输出路径,因为输出缓冲级数太多,导致数据输出延迟过大,后级芯片采不到正确的值。后来我加了一级流水线,把组合逻辑拆成两拍,问题就解决了。
// 寄存器到输出路径
module reg_to_output (
input clk,
input [7:0] data_in,
output reg [7:0] data_out
);
reg [7:0] internal_reg;
always @(posedge clk) begin
internal_reg <= data_in;
end
// 输出组合逻辑
assign data_out = internal_reg ^ 8'h55;
endmodule
这条路径的约束,我用 set_output_delay 来告诉工具,外部器件需要数据提前多久准备好。
2.2.4 输入到输出(Input to Output)
这条路径最特殊——它完全不经过寄存器,直接从输入引脚跑到输出引脚。说白了就是纯组合逻辑路径。
⚠️ 注意:我曾经在一个项目中,为了省一个时钟周期,用了纯组合逻辑做数据通路。结果时序收敛不了,最后不得不加了一级流水线。纯组合路径的时序约束是最难做的,建议能加寄存器就加寄存器。
// 纯组合逻辑路径
module input_to_output (
input [7:0] a, b,
output [7:0] result
);
assign result = a & b; // 没有寄存器
endmodule
这条路径的约束,需要同时考虑输入延迟和输出延迟。我一般用 set_max_delay 和 set_min_delay 来约束。
2.3 四种路径的对比
| 路径类型 | 起点 | 终点 | 是否含寄存器 | 约束方式 |
|---|---|---|---|---|
| 输入到寄存器 | 输入引脚 | 寄存器数据端 | 是(终点) | set_input_delay |
| 寄存器到寄存器 | 寄存器时钟端 | 寄存器数据端 | 是(起点+终点) | create_clock |
| 寄存器到输出 | 寄存器时钟端 | 输出引脚 | 是(起点) | set_output_delay |
| 输入到输出 | 输入引脚 | 输出引脚 | 否 | set_max/min_delay |
2.4 知识体系总览
下面这张图,是我自己总结的时序路径知识体系。你看一眼,心里就有数了。
这张图把四种路径和它们的关键参数都串起来了。你仔细看看,会发现寄存器到寄存器路径是核心,其他三种路径都是它的变体。
2.5 我的几点建议
- 写RTL时就想好时序:我习惯在写代码之前,先画个时序图。哪些路径是关键的,哪些可以放松,心里有个数。
- 保持时间比建立时间更难修:建立时间违例,加流水线、优化组合逻辑都能解决。保持时间违例,往往要加缓冲器,或者调整时钟树。我吃过这个亏。
- 别忘了时钟偏斜:很多新手只盯着组合逻辑延迟,忽略了时钟偏斜。其实时钟偏斜对保持时间的影响很大,尤其是高频设计。
💡 实战技巧:在做时序约束时,我一般先约束时钟,再约束输入输出延迟。然后跑一次STA,看看哪些路径违例了。从最严重的开始修,不要眉毛胡子一把抓。
好了,时序路径的基础就讲到这里。记住这四个路径类型,后面的时序约束都是围绕它们展开的。