2、时序基础理论:建立时间与保持时间、时钟抖动与时钟偏移、时序裕量计算、同步电路时序约束

各位同学,咱们今天聊点硬核的。时序分析,说白了就是数字芯片的命根子。我见过太多设计,功能仿真跑得飞起,一上板子就歇菜,十有八九是时序出了问题。这一章,咱们把时序的底裤扒干净。

2.1 建立时间与保持时间:寄存器的脾气

每个寄存器都有它的脾气。你给它喂数据,它得有个准备过程。这个准备过程,就是建立时间和保持时间。

  • 建立时间(\( t_{su} \)):时钟有效沿到来之前,数据必须稳定住的最短时间。
  • 保持时间(\( t_h \)):时钟有效沿到来之后,数据必须继续稳定的最短时间。

我打个比方。你坐公交车,车门关上的前一秒你得站好位置,这就是建立时间。车门关上后,你不能马上跳下去,得站稳了,这就是保持时间。寄存器也一样,数据得在时钟沿前后都老老实实的。

⚠️ 我曾经踩过的坑: 有一次做DDR3接口,仿真怎么都过不了。查了三天,发现是保持时间违例。数据在时钟沿之后跳变了,寄存器采到了错误的值。从那以后,我每次看时序报告,第一眼先扫保持时间。

2.2 时钟抖动与时钟偏移:理想很丰满,现实很骨感

理想时钟是完美的方波,但现实中的时钟,总有点小毛病。两个最常见的毛病:抖动和偏移。

2.2.1 时钟抖动(Jitter)

抖动,就是时钟沿的位置在时间轴上随机晃动。说白了,时钟沿不是固定的,它有点「哆嗦」。抖动主要来自电源噪声、热噪声、PLL的相位噪声。

抖动分为两种:

  • 周期抖动:相邻两个时钟周期的差值。
  • 累积抖动:多个周期后,时钟沿偏离理想位置的总量。

我个人习惯,在高速设计中(比如DDR5、PCIe Gen5),会把抖动预算留足。你想想看,如果时钟抖了,建立时间和保持时间的窗口就被压缩了,时序裕量直接缩水。

2.2.2 时钟偏移(Skew)

偏移,是同一个时钟源到达不同寄存器的路径长度不同,导致的相位差。比如时钟从PLL出来,走到A寄存器用了1ns,走到B寄存器用了1.2ns,那A和B之间就有0.2ns的偏移。

偏移不一定是坏事。有时候我们故意做「有用偏移」,让时钟早到一点或晚到一点,来优化时序。但大多数时候,偏移是敌人。

💡 关键点: 抖动是随机的,你没法完全消除,只能留裕量。偏移是系统性的,你可以通过时钟树综合(CTS)来平衡。

2.3 时序裕量计算:算清楚,别含糊

时序裕量,就是你的设计到底还有多少余量。公式很简单,但用起来有讲究。

先看建立时间裕量:

Setup Slack = T_clk - t_co - t_logic - t_net - t_su - t_jitter - t_skew

再看保持时间裕量:

Hold Slack = t_co + t_logic + t_net - t_h - t_jitter - t_skew

其中:

  • T_clk:时钟周期
  • t_co:寄存器时钟到输出的延迟
  • t_logic:组合逻辑延迟
  • t_net:连线延迟
  • t_su:建立时间
  • t_h:保持时间
  • t_jitter:时钟抖动(通常取峰峰值)
  • t_skew:时钟偏移

嗯,这里要注意。建立时间裕量如果为负,说明你的组合逻辑太长了,得插流水线。保持时间裕量如果为负,说明数据跑得太快了,得加缓冲器。

🔧 实战技巧: 我建议你在做STA(静态时序分析)时,把建立时间裕量留10%的余量,保持时间裕量留5%的余量。为什么?因为PVT(工艺、电压、温度)变化会吃掉你的裕量。我曾经有个设计,常温下时序全绿,高温下一片红,就是因为没留够余量。

2.4 同步电路时序约束:告诉工具你的要求

时序约束,就是告诉EDA工具:「嘿,我的时钟跑多快,哪些路径是关键路径,你给我优化好。」

最常用的约束是SDC(Synopsys Design Constraints)格式。我列几个必用的:

# 创建时钟
create_clock -name clk -period 10.0 [get_ports clk]

# 设置输入延迟
set_input_delay -clock clk -max 2.0 [get_ports data_in]
set_input_delay -clock clk -min 0.5 [get_ports data_in]

# 设置输出延迟
set_output_delay -clock clk -max 3.0 [get_ports data_out]
set_output_delay -clock clk -min 1.0 [get_ports data_out]

# 设置伪路径(不需要时序检查的路径)
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]

# 设置多周期路径
set_multicycle_path -setup 2 -from [get_pins reg_a/Q] -to [get_pins reg_b/D]

你想想看,如果不设约束,工具会默认所有路径都要在一个时钟周期内完成。但有些路径,比如跨时钟域的同步器,根本不需要时序检查。你设了伪路径,工具就不会浪费资源去优化它。

我个人习惯,在项目初期就把约束写好。不要等到综合完了再补约束,那时候改起来很痛苦。我记得有一次,一个同事把时钟周期写错了,写成了20ns,结果综合出来的电路跑不了200MHz,只能跑100MHz。后来重做,浪费了两周。

📊 时序约束检查清单:
  • ✅ 所有时钟都定义了create_clock
  • ✅ 输入输出延迟都设了
  • ✅ 跨时钟域路径设了false_path或sync
  • ✅ 多周期路径设了set_multicycle_path
  • ✅ 时钟抖动和偏移在约束中考虑了

2.5 实战案例:一个简单的同步电路

咱们来个实际的。假设有一个数据通路,时钟频率100MHz(周期10ns),寄存器t_co=0.5ns,组合逻辑t_logic=4ns,连线t_net=0.5ns,建立时间t_su=0.2ns,保持时间t_h=0.1ns,抖动t_jitter=0.3ns,偏移t_skew=0.2ns。

算一下建立时间裕量:

Setup Slack = 10 - 0.5 - 4 - 0.5 - 0.2 - 0.3 - 0.2 = 4.3ns

算一下保持时间裕量:

Hold Slack = 0.5 + 4 + 0.5 - 0.1 - 0.3 - 0.2 = 4.4ns

两个都是正数,说明时序没问题。但如果我把组合逻辑改成8ns,建立时间裕量就变成负的了。这时候就得插一级流水线,把组合逻辑拆成两段。

⚠️ 注意: 保持时间违例比建立时间违例更难修。建立时间违例,你可以降频、插流水线、优化逻辑。保持时间违例,你得加缓冲器,但加缓冲器又会增加延迟,可能反过来影响建立时间。所以,保持时间违例要尽早发现,尽早修。

好了,这一章的内容就到这儿。时序基础是芯片设计的根基,你把它搞透了,后面的跨时钟域、STA、DDR接口什么的,学起来就轻松多了。下一章咱们聊跨时钟域同步,那个坑更多,我到时候给你们讲讲我当年是怎么被异步FIFO折磨的。