第二章 时序路径基础:建立时间与保持时间

各位同学,今天我们来聊聊时序路径的核心。说白了,就是搞清楚信号在芯片里到底是怎么跑的,以及为什么跑快了会出错。

我刚入行那会儿,总觉得时序分析是件很玄乎的事。直到有一次,我设计的一个模块在仿真时跑得好好的,结果流片回来,温度一高就罢工。查了整整两周,最后发现是保持时间违例。嗯,从那以后,我再也不敢小看这两个时间参数了。

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 知识体系总览

下面这张图,是我自己总结的时序路径知识体系。你看一眼,心里就有数了。

时序路径知识体系 时序路径 输入到寄存器 Input to Register 寄存器到寄存器 Register to Register 寄存器到输出 Register to Output 输入到输出 Input to Output 关键参数 建立时间 (Setup) 保持时间 (Hold) 时钟偏斜 (Skew) 组合逻辑延迟 时钟到输出延迟 输入延迟

这张图把四种路径和它们的关键参数都串起来了。你仔细看看,会发现寄存器到寄存器路径是核心,其他三种路径都是它的变体。

2.5 我的几点建议

  • 写RTL时就想好时序:我习惯在写代码之前,先画个时序图。哪些路径是关键的,哪些可以放松,心里有个数。
  • 保持时间比建立时间更难修:建立时间违例,加流水线、优化组合逻辑都能解决。保持时间违例,往往要加缓冲器,或者调整时钟树。我吃过这个亏。
  • 别忘了时钟偏斜:很多新手只盯着组合逻辑延迟,忽略了时钟偏斜。其实时钟偏斜对保持时间的影响很大,尤其是高频设计。

💡 实战技巧:在做时序约束时,我一般先约束时钟,再约束输入输出延迟。然后跑一次STA,看看哪些路径违例了。从最严重的开始修,不要眉毛胡子一把抓。

好了,时序路径的基础就讲到这里。记住这四个路径类型,后面的时序约束都是围绕它们展开的。

可可精品资料库