☰
Innovus postRoute冗余hold buffer自动清理与PHC识别实战
2026/10/3 15:15:14 网站建设 项目流程

做数字后端的人,八成都在postRoute阶段被hold buffer坑过。Innovus里跑完optDesign -postRoute之后,打开layout经常会看到一大片BUF_X占着地方,signoff前通常都要花时间清理。我最早遇到过最夸张的一次,一个28nm项目在postRoute阶段因为约束反复迭代,插进去的hold buffer最后冗余了四百多个,手动删到怀疑人生。后来我整理了一套自动清理流程,核心思想就是先识别PHC(Potential Hold Candidate,潜在保持时间修复候选单元),再用Tcl脚本批量删除、ecoRoute收尾,整个流程跑完不到五分钟。这篇文章就把这套流程、脚本以及几个容易踩的坑都放出来,适合正在做Innovus后端ECO、或者刚上手Innovus还不清楚怎么处理冗余buffer的工程师参考。

1. 为什么postRoute阶段会残留大量冗余hold buffer

1.1 从hold修复机制说起

要理解冗余buffer从哪来,得先明白hold修复为什么非要插buffer。数据路径比时钟路径短,或者时钟到达寄存器的时间差大到一定程度,hold就会违例。修hold不是改时钟树,最直接的办法就是在数据路径上插入延迟单元,让数据晚一点到达。这个延迟单元通常是buffer或者inverter。正常来说,工具在postRoute阶段会结合真实绕线寄生参数去修hold,但有几种情况会让buffer“修过头”。一种很常见的是约束切换——前端在某次版本迭代里把hold margin从0.05改成0.2,工具就会重新插一批buffer,之前插的又不会全部自动退掉。另一种是修setup的时候工具挪动了附近cell,让本来就刚好的hold路径变得过于乐观,于是又多插了缓冲。总之,修hold是一个增量过程,而不是从零开始重排,冗余buffer自然就累积下来了。

1.2 冗余buffer的三大来源

我自己总结下来,postRoute阶段冗余hold buffer主要有三个来源:

  • 约束迭代造成的“过期buffer”。这类最常见。每次SDC版本更新,hold margin、时钟uncertainty一变,工具基于新约束再插buffer,旧的buffer不会被清理,就留在netlist里。一个项目跑五六版约束,留下几十个到上百个过期buffer都很正常。
  • ECO移动单元造成的“孤立buffer”。后端做完ECO以后,工具为了满足congestion或者其他物理要求,会把一些标准单元搬走。原来在数据路径上串得好好的buffer,被搬到旁边之后就变成了纯摆设,信号根本不再经过它。这种buffer最隐蔽,不看layout你很难发现它已经不在关键路径上了。
  • 工具自动优化留下的“双buffer”。有些时钟单元或逻辑单元因为max_transition/max_cap违例,会被工具加倍buffer,但后续路径延时被其他修复手段优化掉,这第二个buffer就不再需要了。

这三个来源里,约束迭代是最要命的,因为每次改动都可能留下十几个到几十个冗余buffer,累积起来就是上百个。

1.3 冗余buffer不清理的代价

冗余buffer不清理,短期看时序还能过,但长期看风险很大。首先面积白白增加,尤其是一些high-Vt的buffer,漏电功耗还不小,对低功耗项目来说就是硬伤。其次,buffer占据了place区域,会让density分布变差,后期IR drop分析和EM检查更容易冒红。还有一个容易被忽略的问题:冗余buffer对应net上会多出输入pin的电容,这对时序分析来讲是乐观的,但实际芯片上这个pin还挂在net上,一旦这个buffer因工艺偏差翻转异常,反而可能引入毛刺。所以做signoff之前,把这些东西清掉,不只是好看,是真的必要。尤其在先进工艺节点,cell area和power都是签核指标的一部分,冗余buffer留下的面积和功耗浪费,最后都要你来解释。

2. PHC识别技巧:先找到“该清”的buffer

2.1 什么是PHC

PHC这个叫法在我们项目组内部很常用,全称是Potential Hold Candidate,翻译过来就是“潜在保持时间修复候选单元”。说白了,就是那些看起来在修hold,但实际上已经不再起作用的buffer。PHC最直观的特征是输入pin和输出pin连在同一个逻辑网络上。你想,buffer的作用是把一个信号延迟后再送出去,如果它输入pin和输出pin接的是同一个net,那就等于在一条导线上硬生生串了个缓冲器,信号从buffer里穿过去之后还是回到了原来的网络,这个buffer对逻辑功能没有任何改变,纯粹就是“视觉上在修hold”,实际没有任何缓冲意义。这种单元的物理表现就是input net和output net同名,或者在layout上可以看到一条信号线穿过buffer。

2.2 从时序角度筛PHC

识别PHC最快的方式,是先看时序报告,把hold slack特别大的路径挑出来。我用Innovus的get_timing_paths命令,例如:

set hold_paths [get_timing_paths -hold -nworst 5000 -slack_lesser_than 10.0]

当然这个命令是抓所有hold路径,接下来遍历每条路径,提取上面的实例,再过滤出buffer类型。为什么要先筛时序?因为如果一条路径的hold slack只有20ps,说明这个buffer可能还在“勉强干活”,贸然删掉风险极大。但如果hold slack大于300ps甚至500ps,那这条路径上的多数buffer都是可以被牺牲的。我一般会把阈值设成300ps,低于这个值的路径一律不动。这一步可以得到一个初步的候选实例列表。

2.3 从物理角度二次确认

时序筛选之后,还不能马上删,必须再用layout层面的信息做一次确认。原理很朴素:用dbGet拿到每个候选buffer的输入pin对应net和输出pin对应net,对比两边的net name。如果两个net是同一个名字,那这个buffer就是纯冗余,可以安全删掉。脚本示意:

set inNet [dbGet [dbGet -p top.insts.name $inst].instTerms -if {.pin.dir == "input"}].net.name set outNet [dbGet [dbGet -p top.insts.name $inst].instTerms -if {.pin.dir == "output"}].net.name if {$inNet == $outNet} { puts "REDUNDANT: $inst $inNet" }

这里有个小细节要注意:有些buffer是双pin输入(比如带enable的时钟buffer),直接用pin.dir==input可能会取到多个net。遇到这种情况,我一般会限定取第一个input的net,或者先检查单元类型。还有,对于inverter来说,输入输出天然不在一个net上,所以这一类要单独处理,不能直接套用“same net”逻辑。好在我清理的对象以buffer为主,inverter我一般只做时序判断,不强行做物理判断。

2.4 顺带小技巧:用dbGet选中标准单元的PG term

写脚本的时候,还有一件事绕不开,就是确认待删buffer的PG引脚连接是否正常。有次我准备删一个buffer,结果删完发现它还有一个悬浮的VDD pin,直接导致下一轮LVS报错。后来学聪明了,删除之前先看一眼PG连接。有人会问,Innovus里怎么选中一个标准单元比如名字为biasnw的pg term?其实很简单:

dbGet [dbGet -p top.insts.name biasnw].pgInstTerms

这条命令会返回该instance下所有PG terminal的名字和连接net。如果只想看VDD:

dbGet [dbGet -p top.insts.name biasnw].pgInstTerms.name

这一招在排查PG悬空、检查power switch cell是否接对电源域时特别实用。放在这个场景里,就是在批量删除buffer之前,过滤掉那些PG没有正常连接的异常单元,避免删完留下一堆dangling pin。

3. 5分钟自动清理流程

3.1 准备工作:确认约束和时序收敛

动手清扫之前,有几个前提条件必须确认好,否则后患无穷。

第一,确认当前postRoute的时序已经收敛,尤其是hold,不能还有一大批红色violation就急着清理,那是拆东墙补西墙。正常流程是先把hold用optDesign修干净,再进入冗余buffer清理。第二,记得在清理前存一个干净的database快照。我习惯用saveDesign clean_before_buffer_cleanup.enc存一份,万一删完跑完时序发现问题,还能快速回滚。第三,设置好清理阈值参数。我把这些参数定义在脚本最前面,比如hold margin阈值、最大删除数量、buffer cell name关键字等,方便根据不同项目调整。

3.2 核心Tcl脚本解析

整套脚本剥离掉项目定制部分以后,大概长这样:

set holdThreshold 0.3 ;# hold slack >= 0.3ns 才删 set maxDelete 500 ;# 单次最多删除个数 set bufPattern {BUF*CKBD*CLKBUF*} ;# 需要匹配的buffer单元名 set deleteList {} set phcCount 0 # Step 1: 遍历所有实例,按cell名粗筛buffer foreach inst [dbGet top.insts.name] { set cellName [dbGet [dbGet -p top.insts.name $inst].cell.name] if {[regexp -- $bufPattern $cellName]} { # Step 2: 获取input net和output net set inputNets [dbGet [dbGet -p top.insts.name $inst].instTerms -if {.pin.dir=="input"}].net.name set outputNet [dbGet [dbGet -p top.insts.name $inst].instTerms -if {.pin.dir=="output"}].net.name set inNet [lindex $inputNets 0] # Step 3: 物理层面验证——输入输出在同一net if {$inNet == $outputNet} { # Step 4: 时序层面验证——路径hold slack足够大 # 这里调用查询hold slack映射表,判断是否大于阈值 if {$holdSlackMap($inst) > $holdThreshold} { lappend deleteList $inst incr phcCount } } } if {$phcCount >= $maxDelete} { break } } # Step 5: 批量删除 foreach inst $deleteList { deleteInst $inst } puts "Totally deleted $phcCount redundant hold buffers"

我来说明几个关键点。第一步的regexp匹配可以根据你自己lib里buffer的命名来调整,比如有些工艺叫BUF_X1/BUF_X2,有些叫CKBD0BWP,我建议把CLKBUF也加进去,因为时钟buffer同样会变成冗余。第二步的小心点在于instTerms返回的可能是一个列表,要取第一个input net作为参考。第三步是整个脚本的核心,确认输入输出在同一net。第四步的时序验证其实可以用更简单的方式,比如在进入循环前,先跑一遍所有hold路径,生成一个hold slack与buffer实例的映射表,再在这个循环里查表,而不是每删一个就现场跑一次时序,那样会慢很多。第五步删除的时候,Innovus会同时移除该实例的PG pin连接,只要PG连接正常,就不会有残留问题。执行完这五步,再用ecoRoute把相关net重新绕一遍,整个清理就算完成了。

3.3 清理后处理:ecoRoute与重新验证

删除buffer以后,工具会自动把原net断开重建,但绕线资源不会立刻优化,所以我会顺手跑一次:

ecoRoute -modifyNet <删除涉及的所有net>

如果你删的buffer涉及几十个net,也可以直接ecoRoute不加参数做全局eco,不过时间会稍微长一点。做完ecoRoute后,再跑一轮optDesign -postRoute -hold做增量收敛。这一步非常重要,因为删除buffer后,数据路径的延时变小了,哪怕我们只删那些hold slack大于300ps的buffer,理论上不会产生新的hold违例,但实际项目中时钟树和模块边界相互影响,偶尔也会出现个别路径反弹。所以我在流程里一定会安排一次postRoute hold的增量优化。

清完之后,再做一次面积和功耗对比。这里有个直观的方法,清理前后分别跑report_qor,对比total cell area和total power。我印象最深的一次,清掉两百多个冗余buffer,面积直接降了1.2%,漏电功耗降了大概3%,这是一个很可观的数据,尤其对面积敏感的芯片来说。

4. 常见问题与排查技巧实录

4.1 删了buffer反而报hold违例

这个坑我踩过不止一次。最典型的情况是,你删掉的buffer虽然输入输出net同名,但它本身是某个don't care路径上的单元,或者它的物理位置正好支撑了其他net的绕线布局,删掉之后旁边的net绕线资源重构,导致真正的hold路径变差了。解决办法是:第一,别贪心,把hold阈值往上调,只删那些hold slack非常大的buffer;第二,删除后必须重新跑postRoute hold优化,不能删完就直接导网表;第三,如果违例总是集中出现在某一小片区域,大概率是congestion影响,建议把这片区域的buffer全部保留,不要硬删。

4.2 输入输出同名net但物理相隔很远

脚本判断冗余的标准是input net等于output net,但如果碰到那种同名net分布在两个很远物理位置的场景,直接合并会有问题。比如一个buffer的输入pin在坐标(100,100),输出pin在坐标(5000,5000),虽然net name一样,但中间经过复杂的绕线,直接删除可能导致RC变化很大。我的经验是在脚本里加一道物理距离检查,取buffer输入pin和输出pin的坐标,计算欧氏距离,如果超过某个值(比如200um),就跳过不删。代码大概是:

set inPt [dbGet [dbGet -p top.insts.name $inst].instTerms.pin.pt] set outPt [dbGet [dbGet -p top.insts.name $inst].instTerms.pin.pt]

然后算距离,这个在项目里实测有效,能过滤掉大概10%到20%的“假冗余”。

4.3 buffer的PG term悬空问题

前面提到过,删buffer前一定要确认PG连接。正常P&R流程里,VDD/VSS会通过globalNetConnect连接好,删除实例不会造成问题。但如果你用的是某些特殊的power switch cell,或者buffer在floorplan边缘,有可能出现PG pin连接到dangling net上的情况。查看标准单元PG term的方法我上面讲过,用dbGet选中实例的pgInstTerms。如果发现某个待删buffer的PG连接异常,那就先手动处理掉,或者直接跳过这个cell,避免留下隐患。

4.4 清理后DRC变差

有些项目清理完buffer后,ecoRoute跑完却冒出新的DRC violation,尤其是最小面积、最小间距之类的。这是因为删除buffer后留下的空间被工具随机填充了一些小碎片metal。解决方法有两种:一是单独针对清理涉及的net跑ecoRoute -modifyNet,二是删完后在那一小片区域跑一次addFiller把空隙填上,再重新布线。大多数情况下,局部ecoRoute就能解决,只有大面积删除时才需要全局处理。

我个人实际操作的体会是,这套流程最值钱的地方不是“删buffer”本身,而是“识别哪些buffer可以删”。把PHC识别逻辑做扎实,后面删得又快又安全。我通常在项目后期每跑完一版ECO就清理一次,每次用时控制在五分钟以内,效果很稳定。最后再分享一个小技巧:清理前可以把hold阈值先设置成500ps跑一次,看看删除清单里有没有你预期之外的多余buffer,如果有,大概率是约束或者时钟树哪里还有问题,值得停下来查一查,而不是急着执行删除。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询