做数字芯片的,不管是前端写RTL还是后端做物理实现,时序路径分析这道坎儿早晚都要过。尤其到了流片前的静态时序分析(STA)环节,一条setup违例或hold违例查上大半天,是很多工程师的日常。这篇文章先把时序路径这层窗户纸捅破:一条路径是怎么被定义的、建立时间和保持时间到底在算什么、SDC约束里的时钟和延迟信息又是怎么变成一串串时序报告数字的。内容定位在入门到进阶之间,适合刚转数字IC的工程师、在校学生,也适合做过一段时间后端但没系统梳理过STA的老手。
为什么非要揪着时序不放?说白了,数字电路里的所有功能都建立在时钟边沿对数据的正确采样上。信号从寄存器出发,经过组合逻辑,再进入下一个寄存器,这中间每一段都有延迟。这些延迟叠加起来如果超过一个时钟周期,数据来不及稳定,下一拍采到的就是错误电平。芯片频率上不去、功能跑飞、功耗异常,很多时候根源都在时序。所以时序路径分析不是EDA工具跑个报告那么轻松,它决定了一块芯片能不能真正工作。
这一篇是上篇,先老老实实把概念和计算拆清楚。等后面再聊更深入的时钟树综合、工程修复和ECO,那些都是建立在今天这些基础之上的。往后每个实操环节,我都会把“为什么”也讲一遍,毕竟干这一行,光会跑工具不叫懂,能解释清楚结果才算真正能上手。
1. 时序路径分析到底在解决什么问题
1.1 一个触发器一整天都在等数据
数字电路里最核心的存储单元是触发器,时钟上升沿来的时候,它把输入端的电平“拍”到输出上。很多人刚接触时序时只记住了这一点,却没意识到一个关键前提:数据必须在时钟沿到来之前就已经稳定,并且在时钟沿之后的一小段时间内不能变化。这两条时间窗口,一条叫建立时间(setup time),一条叫保持时间(hold time)。
打个比方,时钟沿就像上课铃,数据就是学生。学生必须在打铃前坐到座位上,这叫建立时间;铃声打完,老师开始讲课,学生也不能立刻跑掉,至少得坐稳一小会儿,这叫保持时间。如果一个学生总是踩着铃冲进教室,甚至铃响了还没进教室,那课堂秩序就乱了。对应到芯片里,数据到达太晚,触发器采到的可能是上一拍的旧值,逻辑直接错乱。数据变化太早,又会破坏时钟沿附近需要的稳定窗口,结果同样不可靠。
很多初学者有个误区,以为只要逻辑表达式正确,信号就一定是对的。实际完全不是这样。一个两输入与非门,如果输入端A和B几乎同时变化,输出中间可能先出现一个不该出现的窄脉冲,这就是毛刺。放到时序分析里,我们要关心的不是“稳态下输出是什么”,而是“在时钟沿那个瞬间,稳态是否已经达到”。这个思路贯穿整个静态时序分析,想明白了,后面所有的计算、报告、修复逻辑都顺理成章。
1.2 真正要算的是slack
每一条时序路径上,工具都会算出两个关键时间:数据实际到达时间,以及数据必须到达时间。两者相减,得到的就是时序裕量,业内叫slack。Slack为正,说明这拍数据有富余;Slack为负,说明路径不满足要求,也就是violation。STA工具会遍历整个设计里成千上万条路径,把所有违例路径和关键路径全找出来,工程师要做的就是把这些负slack一条条消掉。
这里要强调一个概念:静态时序分析和动态仿真完全不同。动态仿真需要给激励、看波形,跑一条路径验证一条路径,速度慢得吓人,而且只能验证你想到的场景。时序路径分析则是把所有可能路径都抽象成延迟网络,用数学方式计算每个端点的到达和需求,不依赖输入向量,所以叫“静态”。也正因为不依赖具体激励,它能覆盖到很多仿真很难触发的极端路径,比如异步信号最坏情况下的到达。
理解了slack,再回头看整个时序收敛流程就清晰了:综合阶段,工具约束住逻辑级数和单元负载;布局布线阶段,工具优化线长和负载;时钟树综合之后,clock skew成为主导项;最后签核阶段,用准确的寄生参数重新计算。每一步跑出来的报告,核心都只有一件事——看slack。后续文章的案例也都是围绕负slack怎么出现、怎么修复展开。
2. 时序路径的组成与分类
2.1 四类路径一个都不能漏
静态时序分析不是只分析寄存器到寄存器这一种情况。一颗芯片的边界包含输入引脚、输出引脚,内部有成千上万个触发器,它们组合起来,一共形成四类标准路径,工具在约束文件的基础上挨个分析。
| 路径类型 | 起点 | 终点 | 典型关注点 |
|---|---|---|---|
| 寄存器到寄存器 | 触发器的时钟引脚 | 下一级触发器的数据引脚 | 时钟偏斜、组合逻辑延迟、setup/hold |
| 输入到寄存器 | 芯片输入端口 | 触发器的数据引脚 | 外部输入延迟、板级接口时序 |
| 寄存器到输出 | 触发器的时钟引脚 | 芯片输出端口 | 输出延迟要求、外部负载 |
| 输入到输出 | 芯片输入端口 | 芯片输出端口 | 纯组合直通路径、组合逻辑是否合理 |
很多刚学STA的人,拿到时序报告看到cleanup path或edge path这些词会懵,其实就是把寄存器时钟端当作起点来追踪时钟树的延迟。做input-to-reg分析时,输入端口没有一个真正的时钟引脚,所以约束里必须通过set_input_delay告诉工具外部数据相对于时钟沿什么时候到;如果漏了这条约束,工具会默认数据有无限充裕时间,分析结果全都乐观到失真。同样,输出路径上没有真实采样寄存器,工具需要靠set_output_delay来模拟外部器件的建立时间要求。
这四类路径的约束差异,就是STA入门时最容易踩坑的地方。我见过不少人把板上外部存储器的接口时序分析得一团乱,根源往往就是输入输出延迟的方向和数值搞反了。后面第3节会专门展开这些命令的因果逻辑。
2.2 延迟不是简单相加
一条数据路径上的总延迟,可以粗略拆成单元延迟加线网延迟。单元延迟指信号经过逻辑门产生的延后,线网延迟指信号在金属连线上传输的延后。这俩都不是固定数字,跟输入信号的转换时间(slew)有关,也跟输出端挂了多大的负载有关。同一个反相器,接一个负载和接十个负载,延迟能差出好几倍。
所以现代EDA工具做时序分析时,用的不是“查个常数”的办法,而是查标准单元库里的非线性延迟模型(NLDM)。库里针对每个单元、每个输入转换时间、每个输出负载,都存了一张二维查找表,工具通过查表和插值得到更贴近工艺真实的延迟。这就是为什么同一颗工艺库,用不同版本的库文件或不同corner跑STA,结果会有肉眼可见的差异。流片前的签核分析,通常还要跑慢速低温、慢速高温、快速低压等多个corner,原因就在这里。
时钟路径的延迟则单独算。时钟信号从时钟源出发,经过PLL缓冲、时钟树上的各级buffer,再到达每个触发器的时钟端,这个延迟叫clock insertion delay,也叫source latency加network latency。问题在于,时钟到达两个不同触发器的时间往往不一样,这个时间差就是clock skew。Skew如果控制不好,会同时恶化setup和hold,因此后端的时钟树综合(CTS)本质就是一门控制skew的手艺。理解了延迟的来源和统计方式,后面看时序报告里的delay components那一栏,就不会被一堆数字牵着走了。
3. 时序约束:把“正确”这件事量化
3.1 SDC,静态时序分析的宪法
没有约束,STA工具什么都干不了——它不知道时钟周期、不知道哪些端口输入何时有效、不知道哪些路径不需要分析。EDA工具能跑,全靠一份叫SDC(Synopsys Design Constraints)的约束文件,相当于整个时序分析的宪法。后端工程师写的所有约束,最终都会汇成这份文件,跟网表、库文件一起送去时序签核。
最基础的约束是定义时钟。芯片里可能同时存在好几个时钟,每个时钟都要用create_clock命令明确周期和波形。比如一个100MHz的时钟:
create_clock -name clk_100m -period 10 [get_ports clk]这条命令告诉工具:每个时钟周期是10ns,起点是端口clk。更精细的情况要定义占空比可以用-waveform {0 5},也可以定义生成时钟用divide_by倍频分频。不管是哪种,目的都是让工具知道“时间基准是什么”。如果一颗芯片里的时钟没定义清楚,后面所有路径分析都会跟着崩。
除了时钟,SDC里还有成堆的约束命令:set_input_delay、set_output_delay、set_clock_uncertainty、set_false_path、set_multicycle_path、set_case_analysis、set_max_delay、set_min_delay等。很多新手喜欢把网上的模板约束一把抄过来,结果约束文件比RTL还难懂,跑出来的时序报告千奇百怪。这里我的建议很明确:每一条约束命令,都要能解释清楚“它约束的是哪条路径”“它的数值从哪里来”。如果解释不了,这条约束多半是有问题的。
3.2 输入输出延迟的设置逻辑
输入输出延迟约束是整个SDC里最容易出错的地方,因为涉及芯片外部接口的时序关系,建模必须准确。set_input_delay表示数据从外部源到达芯片输入引脚,相对于时钟沿的延迟时间。例如:
set_input_delay 2.0 -clock clk_100m [get_ports data_in]意思是data_in上的数据在时钟沿之后2ns到达芯片引脚。既然有到达时间,那么setup分析时,工具就会用10 - 2 = 8ns作为内部允许的最大数据路径延迟。如果不设置这条约束,工具默认输入数据在时钟沿之前无限早到达,内部逻辑怎么连都算满足,真正的接口时序风险会被完全掩盖。
输出延迟约束的建模方向则反过来。芯片输出引脚的数据要送到外部系统,外部系统有自己的建立时间要求。比如外部器件要求数据在时钟沿前3ns稳定,那么set_output_delay 3.0就表示芯片内部路径必须保证在那之前把数据送到引脚。很多工程师做接口时序老是不收敛,多半是没搞清楚外部建立时间对应的方向。
这类约束还有max和min两个版本。max版本对应setup要求,min版本对应hold要求。实际项目中,输入输出延迟要根据板级实际情况手动计算,不是随便填个数。严谨一点的做法,是从芯片数据手册的AC时序特性表里提取参数,再考虑PCB走线延迟、驱动芯片输出延迟等,最终形成约束。
3.3 时钟不确定性:把悲观提前算进去
时钟信号在芯片内部传输时,会受到电源噪声、温度变化、片上工艺波动的影响,同一个时钟沿到达各个寄存器的精确时刻,永远不可能完全相同。除了前面说的skew,还有一部分是时变的不确定性(jitter),以及PLL本身输出的相位误差。为了在设计中留出余量,SDC里会用set_clock_uncertainty命令把这些提前扣掉。
set_clock_uncertainty 0.1 -setup [get_clocks clk_100m] set_clock_uncertainty 0.05 -hold [get_clocks clk_100m]很多团队的约束库里,clock uncertainty的取值是根据芯片具体架构反复调出来的。设太小,工具分析过于乐观,流片后可能翻车;设太大,工具会觉得每条路径都难收敛,面积和功耗白白浪费。实际项目里,setup和hold的uncertainty常常是不同值,原因很简单:两者的安全裕量要求不一样。看时序报告时,如果发现一条路径的slack刚好是负0.05ns,先不要急着动逻辑,检查一下uncertainty设置很可能就有收获。
4. 建立时间与保持时间的路径分析实操
4.1 setup time计算实例
前面铺垫了这么多,现在来点硬核的。静态时序分析中的setup检查,本质上是比较数据路径到达终点的时间,和采样时钟沿到达终点的时间,二者之间必须留出一个setup time。标准计算公式可以写成:
data arrival time = 时钟源到达起点触发器的延迟 + TCQ + 组合逻辑延迟 + 线网延迟
data required time = 采样时钟沿到达终点触发器的延迟 + 时钟周期 - setup time - clock uncertainty
slack = data required time - data arrival time
举一个具体数字。假设时钟周期为10ns,起点寄存器TCQ(时钟到Q输出延迟)为0.5ns,中间组合逻辑延迟为2.5ns,线网延迟0.3ns,终点触发器的setup time为0.2ns,时钟从起点到终点的skew为0.2ns(终点时钟比起点晚到),uncertainty设为0.1ns。那么:
data arrival time = 0.5 + 2.5 + 0.3 = 3.3ns
data required time = 10 + 0.2 - 0.2 - 0.1 = 9.9ns
setup slack = 9.9 - 3.3 = 6.6ns
slack为正,说明这条路径很轻松。如果数据到达时间超过9.9ns,就会变成负slack。实际项目里,逻辑级数动辄十几级,时钟频率动辄几百兆赫兹,每个0.1ns的裕量都弥足珍贵。
这里特别提醒,公式里的clock skew符号取决于终点时钟相对起点是早到还是晚到。如果终点时钟比起点更晚到,相当于采样沿被推后,那么数据可用的时间窗口变长,对setup有利。反过来,如果终点时钟提前到,setup就会变差。很多新人看工具报告,发现skew对setup有正有负,很容易搞混。建议自己手动搭一个两触发器模型,按上述公式推一遍,比死记符号强得多。
4.2 hold time计算实例
保持时间检查处理的是另一个时间窗口:数据在采样时钟沿之后,不能太快发生变化。Hold分析对比的是数据最快到达时间和保持时间要求。标准逻辑是,数据在采样时刻到来之后必须再稳定一小段时间,而它最快到达终点的路径不能提前覆盖掉这个稳定窗口。
hold分析里的关键参数是数据路径的最小延迟,不是最大延迟。芯片制造出来之后,受到工艺偏差影响,路径延迟会有变化,最快情况(fast corner)下TCQ可能变小、组合逻辑延迟也可能变小。所以hold检查通常用min corner下的延迟来计算,目的是保证任何条件下数据都不会变得太快。
继续用上边的例子。假设最小TCQ为0.2ns,最小组合逻辑延迟为0.5ns,最小线网延迟为0.1ns,hold time为0.1ns,时钟skew同样是终点晚到0.3ns。那么:
最快数据到达时间 = 0.2 + 0.5 + 0.1 = 0.8ns
hold required time = hold time + clock skew = 0.1 + 0.3 = 0.4ns
hold slack = 0.8 - 0.4 = 0.4ns
slack为正,说明数据不会变化太快。但如果clock skew变成0.9ns,hold required time就成了1.0ns,hold slack变成-0.2ns,出现hold违例。这也是为什么时钟树偏斜太大时,hold修复会非常痛苦。在实际设计中,hold违例往往发生在数据路径特别短的寄存器对之间,比如两个并排放置的触发器,TCQ很快、中间几乎没有组合逻辑,再叠加时钟晚到,非常容易触发违规。
4.3 为什么setup和hold的修复思路完全不同
同样是时序违例,setup和hold的修复方向几乎相反。Setup违例通常是因为路径太长、延迟太大,解决办法是减少组合逻辑级数、插入流水线、增大驱动强度、减少扇出、或者优化时钟偏斜。Hold违例则是因为路径太短、数据太快,解决办法是插入延迟单元(delay cell)、增大缓冲器、增加线长,甚至人为把两个寄存器之间的距离拉远。
这种对立让不少新手很崩溃:给一条路径加了buffer修hold,结果setup又被拖坏了。这正是后端实现复杂的地方。实际工作中,工程师通常先修setup,再做时钟树综合,最后专门修hold。因为时钟树综合会改变每级寄存器的时钟到达时间,如果先修hold,CTS之后往往又要重来。先保证数据路径总体延迟满足setup,再通过CTS调整skew,最后把hold的死角逐个清掉,这个顺序可以省下大量重复劳动。
5. 时序报告怎么读、违例怎么排查
5.1 一张报告的阅读顺序
工具跑出来的时序报告密密麻麻,但核心信息其实很固定。以主流的report_timing为例,先看Startpoint和Endpoint,确认这条路径是从哪个寄存器或端口出发,通向哪里。再看Path Group和Path Type,确认它是setup还是hold检查。很多团队会按时钟域把路径分组,Path Group就是时钟名,方便定位是哪条时钟频率下的问题。
中间最重要的一段是延迟列表。工具会把整条路径上每个单元的延迟、每段网络的延迟、时钟到达时间全部列出来。工程师要做的,不是看第一个数,而是从起点开始,像剥洋葱一样逐级往后看,找到哪一级延迟占比最大、哪一段线网延迟异常。比如一个反相器输出带了一大堆负载,线网延迟0.8ns,这在深亚微米工艺下非常可疑,大概率是布局扇出没做好。数据到达时间和要求时间之间差的每一个0.1ns,都可能对应一个真实的物理问题。
最后看slack数值。如果setup slack是负的,报告一般会同时提示要求的时钟周期和路径延迟。我习惯先把报告里的时钟周期和SDC里的create_clock对一遍——如果工具算出来的周期跟约束定义不一致,说明约束定义里可能藏着重复定义或相位问题。
5.2 新手最容易翻车的约束坑
总结这么多年见过的时序问题,真正由电路延迟本身造成的violation,其实只占一部分。另一大批是约束写错或者没写全,导致报告出现“幽灵违例”或掩盖真实风险。最常见的有这么几类。
第一类,false path没设。跨时钟域的路径如果不做同步处理,工具会按同频时钟去分析,自然全是违例。正确的做法是识别出真正的异步时钟域,用set_false_path把这些路径从时序分析里关掉。但必须确认这些路径确实有同步器保护,不能为了出绿色报告就乱设。
第二类,multicycle path设错。DDR接口、读数据回传这类路径经常需要两三个周期才算有效,如果不设set_multicycle_path,工具会按一拍分析,大量false violation会淹没真实问题。这个命令用起来很简单,关键是要理解它到底调整的是setup捕获沿还是hold检查沿,改错方向可能比不改还危险。
第三类,reset信号没做case analysis。异步复位信号在没有实际复位时,通常处于无效电平,但它毕竟是一个逻辑信号。如果不通过set_case_analysis固定复位端电平,工具会认为复位随时会翻转,在复位路径上产生大量不存在的时序路径。很多新手看到复位相关的setup违例,第一反应是加大驱动,结果越修越乱,实际只是缺少一条约束。
6. 常见问题速查与实操习惯
6.1 常见问题速查表
把这几年最常被问到的问题整理成一个速查表,遇到类似情况可以直接对着排查。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 关键路径全集中在某个模块 | 模块本身逻辑级数高、扇出大 | 检查RTL关键路径,考虑重新综合或流水线 |
| 大量路径setup违例但偏离很小 | 时钟uncertainty或约束过紧 | 对照约束文件,确认uncertainty是否合理 |
| 时钟路径延迟特别大 | CTS没做好,或时钟源驱动弱 | 检查时钟树报告,确认buffer级数和位置 |
| hold违例集中在短路径寄存器对 | 时钟skew大、寄存器距离近 | 看CTS结果,调整skew或插入delay cell |
| 报告显示高扇出网络延迟异常 | 驱动单元过载、布线拥塞 | 优化扇出约束,对宽扇出信号做复制 |
| 时序报告和动态仿真结论不一致 | 约束遗漏或误设,异步路径未设false path | 逐条回读SDC,确认每个约束对应的路径 |
| 换一个corner后大量violation | 库文件或corner配置不对 | 重新检查库和PVT配置,确认用对了corner |
6.2 几条能让时序收敛提速的实操习惯
第一,每一轮跑STA只改一件事。这个习惯看上去简单,实际做起来很难。很多工程师看到整屏violation,恨不得同时修三五个问题,结果下一轮报告出来根本分不清到底是谁起到了作用。我的做法是:改约束单独跑、改源码单独跑、改布局策略单独跑,每轮报告都留档对比,这样定位问题才有依据。
第二,先看全局概览再看单条报告。用report_analysis_summary或者report_qor这类命令拉出全局表格,看总违例数、最差slack、WNS和TNS。如果全局TNS非常负,说明整个模块都需要重新审视,不是修一两条路径能解决的。如果只是个别路径违例,再逐条深挖。
第三,学会用时钟域分类思路。跑到签核阶段,路径数量动辄上百万,不可能一条条翻。按时钟域、按模块、按path group切分,把问题聚拢到几个热点区域,再集中处理,效率能高一个数量级。比如一颗SoC里CPU和GPU频率不同,时序难收敛的往往是高频率的那个域,先把频率域的顶层约束吃透,其他域的问题往往能自动缓解。
到了实际流片阶段,尤其做RK3588那种规模的大SoC,时序分析要处理的数据量和约束复杂度完全是另一个层级,但核心思想仍然是这些:路径分类、延迟计算、约束正确性、setup/hold两张网。基础打不牢,后面用再高级的灌入式ECO也救不回来。
做时序这几年,我最大的体会是:拿到violation报告先别急着改电路,把那几百行SDC逐条读一遍,往往能多救回来半个晚上。上篇先把概念和计算讲透,下一回再拿真实案例把修复流程走一遍。