低功耗芯片的后端实现里,最让人头疼的往往不是时序收敛,而是这种查半天才发现的隐形漏电。我最近就栽在这么一条路径上:一根挂在 iso 输入端口前面的 buffer,被一条 set_dont_touch 钉死,硬生生把隔离单元本该挡住的漏电路径给放开了。先说清楚,这里的 iso 不是镜像文件,是 low-power 设计里的 isolation cell(隔离单元)。这条路径的问题非常有代表性,这篇就把来龙去脉讲透:iso 的 input 端为什么不能插 buffer,dont_touch 又是怎么变成漏电帮凶的,最后给出完整排查链路和可直接抄的约束纪律。
1. 现象复盘:常开域的静态漏电为什么凭空多了三成
1.1 项目背景:三个电源域和一个可关断外设
当时在做一个 MCU 级别的 SoC,规模不大,但电源域划分很典型:一个常开控制域 PD_AlwaysOn,一个数字核心域 PD_Core,还有一个传感器接口域 PD_Sensor。PD_Sensor 负责采集外部传感器数据,通过一组并行 status 信号和常开域交互,平时大部分时间都处于 power down 状态,只有在需要采样时才上电。常开域里只剩下 RTC、唤醒逻辑、一小部分 IO 控制逻辑,功耗预算卡得很死,静态漏电要控制在 2uA 以内。
UPF 里的电源意图也写得比较常规。对 PD_Sensor 到 PD_AlwaysOn 的输入端口,设置了 isolation cell,clamp 到 0,也就是说 PD_Sensor 断电之后,进入常开域的 sensor 状态信号会被隔离单元钳死,不能把 X 传进常开域内部逻辑。这套策略跑了很多轮都没出问题,直到某次功耗回归,数字对不上了。
1.2 功耗回归异常:数字对不上
那次回归用的是同一套库、同一份 UPF、同一个工艺角,唯一的变化是网表里多了几个综合阶段为了修 max_transition 插进去的 buffer。跑完 PrimeTime 功耗分析,报告显示常开域静态漏电 2.6uA,比预算多了 0.6uA。0.6uA 单看不算大,但在常开域这种所有 cell 都要精打细算的场景里,已经属于"必须查清楚"的级别了。
刚开始我以为又是多阈值电压替换没做干净。翻了 VT 分配报告,常开域里确实还有少量 SVT 单元,但这些单元都在带 dont_touch 的 retention 逻辑里,本来就有豁免,之前也是这么定的,不该突然多出来 0.6uA。接着怀疑 power switch 没有关干净,把 PD_Sensor 的 switch 控制逻辑查了一遍,没发现异常。后来逐个 cell 看 leakage 明细,发现 PD_AlwaysOn 域里有两个 buffer 的 State-Dependent Leakage 标注特别扎眼。
1.3 最初的排查方向全都不对
这两个 buffer 在库里标称漏电每个只有几十纳安,但功耗报表里给出来的却逼近 400nW。同一个库、同一个 VT、同一个尺寸,居然差出一个数量级,这基本只有一种解释:cell 输入端没有停在确定的 0/1 电平上。我顺着网表往上追,发现这两个 buffer 都挂在 PD_Sensor 过来的 input 端口上,处在端口的 isolation cell 前面。
到这里问题结构才清晰起来:这两个 buffer 是常开域的 buffer,输入却连着可关断域的端口信号。PD_Sensor 一断电,端口变 Z,buffer 输入浮空,于是 VDD 到 VSS 之间出现一条直流通路。更麻烦的是,网表里这两个 buffer 都被打了 set_dont_touch,后续所有能救它的优化手段全被锁死。这也就是标题里说的:一条 dont_touch 背后的漏电。
2. 隔离单元的真正职责:input 端为什么天然不该放 buffer
2.1 iso 到底在钳什么
isolation cell 的工作核心,用一句话说,就是在电源域边界上,把从可关断域漂过来的不定态钳到固定电平。可关断域断电之后,它的输出端口不会乖乖停在 0 或 1,而是变成高阻 Z,甚至因为漏电停在中间电平。这个中间电平进入常开域内部逻辑后,会导致后面一连串 cell 的输入都处于半开半关状态,每一个都可能产生贯通电流。
iso cell 内部实现通常是 AND 门或者 OR 门。clamp 到 0 用 AND 门,clamp 到 1 用 OR 门,控制端接 iso_en。iso_en 生效时,无论数据端是什么状态,输出都被钉死在预设值。可以把它理解成一道门禁:可关断域断电之后,门禁落下,不管里面的人是什么状态,外面都看不到。
2.2 input 端的常规构成:信号进来先过隔离
对一个接收端来说,正确结构非常单纯:input pin 直接接到 iso cell 的 data 端,iso cell 的输出再接内部逻辑。中间不应该有任何组合逻辑或 buffer。
为什么这么严格?因为 iso cell 的意义在于把"未知"挡在边界之外。如果 iso 前面挂了 buffer,那这个 buffer 就成了隔离边界之外的"不受控单元"。发送域断电时,buffer 的输入可能浮空,而 buffer 本身还接着常开域的电源,于是它就成了一个漏电点。iso cell 能钳住的是它自己的输出,钳不住它前面 buffer 的输入悬空问题。
我在实际项目中见过的正确做法,基本都遵循同一套标准:隔离边界上的输入网络,要么直接连线到 iso,要么只允许存在库特性明确支持浮空输入的专用单元。普通 buffer 不属于后者。
2.3 把 buffer 插在 iso 前面的后果,两条漏电路径
网表中那个错误的拓扑长这样:
input pin (from PD_Sensor) -> BUF_X2 (PD_AlwaysOn 域) -> ISO_AND2_X2 (clamp 0) -> 内部 DFFPD_Sensor 断电之后,input pin 变成 Z,buffer 输入端悬空。CMOS 工艺下,栅极浮空时电压会停在某个中间电平,PMOS 和 NMOS 同时部分导通,形成从 VDD 到 VSS 的贯通电流。这个电流就是 State-Dependent Leakage 里异常偏大的部分,和 cell 标称漏电完全不是一回事。
这里其实有两条漏电路径。第一条是 buffer 本身的贯通电流,这是最主要的。第二条出现在 iso cell 的输入端:在 iso_en 拉高之前,iso 的 data 端还短暂地维持着 buffer 输出的中间电平,iso 内部也会出现短暂的过渡电流。虽然 iso_en 生效后输出被钳到 0,但它输入端的中间电平并不会自动消失。两条路径叠加,就是报表里那 0.6uA 的来源。
2.4 工具为什么会主动这么插
其实工具不背这个锅。综合阶段(DC/Genus)在做时序优化时,input 端口如果存在 max_transition 违反,或者 input_delay 设置比较紧,工具很自然的处理方式就是在端口后面插一个 buffer 来增强驱动。这个操作本身没有问题,因为综合阶段 UPF 里的 isolation cell 通常还没插入,工具看不到"这个端口将来要被隔离"这层信息。
后端做 low-power 实现时,工具按 UPF 在指定端口位置插入 iso cell,但它不会去检查"隔离边界外是否已经存在 buffer"。如果这个 buffer 没有被设 dont_touch,后续的 optimization 还有机会把它挪到 iso 后面,甚至直接吸收掉。一旦被 dont_touch 钉住,所有可能性都被封死,问题就固化成了漏电。
| buffer 位置 | 断电后结果 | 是否产生漏电 | 工具能否自动修复 |
|---|---|---|---|
| input pin 到 iso 之间 | buffer 输入浮空,iso 钳制其输出 | 会,buffer 贯通电流 | 可删/可挪时能修,dont_touch 时不能 |
| iso 到内部逻辑之间 | buffer 输入被 iso 钳在 0/1 | 不会 | 可正常优化 |
| 无 buffer,直接接 iso | 整个路径都被钳住 | 不会 | 无需修复 |
3. 一条 set_dont_touch 如何把修复路径全部堵死
3.1 当时加 dont_touch 的理由
看到约束脚本的时候,我第一反应是"这谁写的"。查完 blame 才知道,这条 dont_touch 不是针对 input buffer 专门加的,而是项目里积累了很多轮的一个批量约束:所有以input_buf命名的单元,统一 set_dont_touch。
原因也算合理。那批input_buf里有相当一部分是跨时钟域异步信号的同步缓冲网络,制程越往后越脆弱,后端工具在 CTS 或者 optimization 阶段一旦动了这些手工保护的 cell,异步路径的时序就可能被悄悄破坏。所以项目组定了规矩:凡是手工例化、名字带input_buf的单元,一律不优化。
问题在于,这种批量约束根本没有区分"异步保护网络"和"普通输入驱动 buffer"。综合工具自动插出来的那两个 buffer,同样被命名成了input_buf*,于是自动享受了保护待遇。这是一次典型的"约束误伤"。
3.2 dont_touch 在低功耗实现里挡掉的四件事
dont_touch 这个约束在数字后端里几乎是最高优先级,它的意思很清楚:这个 cell/net 是祖宗,工具不许动它。听起来很安全,但在低功耗实现里,它一次挡住四件该做的事。
第一,不能删。工具发现这个 buffer 在 iso 前面完全冗余,想优化掉,做不到。第二,不能挪。想把 buffer 从 iso 前面挪到 iso 后面,等于是移动了这个 cell 的位置,做不到。第三,不能合。后端做逻辑优化时想把 buffer 和 iso cell 做合并或吸收,做不到。第四,不能换 VT。低功耗 flow 里常做的 VT swapping,把这个 buffer 从 SVT 换成 HVT 来降漏电,同样做不到。
这四件事单独拎出来每一项都能救半条命,但 dont_touch 把它们全关上了。尤其"不能换 VT"这条,哪怕不解决浮空问题,至少能把漏电数值压下去一部分,但约束在,工具连这个折中方案都不会尝试。
3.3 为什么综合与后端工具不会替你纠正
这里面有个工具分工的现实问题。综合工具做时序优化时,它根本不管 UPF,插 buffer 只为了满足 input_delay 和 max_transition。后端工具做 UPF 实现时,它按部就班地插 iso cell,但也默认 dont_touch 是不可触碰的红线。两个工具各干各的,没有一个阶段专门去检查"隔离边界外是否躺着受保护的 buffer"。
更隐蔽的是,CLP/MVRC 这类电源意图检查工具,虽然能报出隔离网络上存在未覆盖的组合单元,但这类告警在项目里经常是 warning 级别。如果 UPF 本身写的没错,工具不会因为 buffer 的存在而报 fatal error,它只会提示"isolated net 前面有不受控逻辑"。在项目进度压力下,这种 warning 很容易就被放过去了。
3.4 UPF 与 dont_touch 的隐性冲突
很多人以为 UPF 里写了set_isolation,工具就会自动搞定边界上一切可能漏电的东西,这是误解。UPF 描述的是"应该在哪隔离",而不是"隔离边界外必须没有 buffer"。dont_touch 和 UPF 之间更没有形式化的冲突检测机制,它们一个属于 SDC 领域,一个属于电源意图领域,日常 flow 里根本不会放在一起做交叉检查。
所以最阴险的地方就在于:所有检查都"通过"了,UPF 正确,SDC 合法,时序收敛,CLP 也只在 warning 级别提了一句。但芯片一旦跑起来,常开域就永远多了那 0.6uA 的静态漏电。这就是真实世界里 low-power 问题最难追查的原因:它不是一个工具能发现的,而是多个流程片段拼出来的。
4. 完整排查链路:从功耗报表一路查到约束脚本
4.1 第一步:功耗报表定位异常 cell
整个排查过程,我建议第一步不要直接扑到 UPF 和 SDC 上,而是先让功耗报表自己"说话"。把 PrimeTime 的 leakage 报告按 cell 逐个 dump 出来,按漏电值排序,异常 cell 立刻浮出水面。我当时看到的就是这个效果:
Cell: BUF_X2_A9TP_ALWAYSON Leakage Power: 412.3 nW Location: PD_ALWAYS_ON / u_iso_buf_001 Cell: BUF_X2_A9TP_ALWAYSON Leakage Power: 387.6 nW Location: PD_ALWAYS_ON / u_iso_buf_002同一行库里的同类 buffer,正常漏电只有几十纳瓦,这几根却到了 400nW 级别,说明它们的输入状态不是正常的 0/1,而是停在了中间电平。这一步基本就把嫌疑范围锁定在"输入浮空"这个方向上,接下来要做的只是确认浮空点在哪。
4.2 第二步:网表结构让问题显形
拿着这两个 cell 的 instance 名字回到网表,往前 trace,拓扑结构一目了然。BUF_X2的输入直接连到PD_Sensor域的 output port,输出接到ISO_AND2_X2的 data 端。再打开 UPF,看到对这两个端口明确设了set_isolation,clamp value 0。结构上已经坐实了:buffer 站在端口和 iso 之间,属于常开域,输入却来自可关断域。
读 UPF 的时候还注意了一点:set_isolation的作用对象是端口和网络,它没指定 cell。工具在插入 iso cell 的时候,只负责在指定端口后面挂上隔离单元,不会因为端口前面多了一个 buffer 而报错。换句话说,工具的合规视角里,隔离结构是"完整"的,但物理上漏电已经发生。这和工具行为完全吻合。
4.3 第三步:仿真波形确认浮空输入
结构分析是怀疑,波形才是实锤。我用 VCS + UPF flow 跑了一遍 PD_Sensor 上下电场景,在波形里盯着这条路径:
- PD_Sensor 关断瞬间,input pin 变成 Z;
- buffer 输入悬空,栅极电压开始漂移,输出进入中间电平;
- 一段时间后 iso_en 拉高,iso 把输出钳到 0;
- 但 buffer 输入这个 Z 状态会一直维持到下次上电。
波形上还能明显看到 buffer 输出端在钳位生效前有一段 X 窗口,那段窗口里 iso 的 data 端就是中间电平,iso 内部也在过渡。buffer 输入端的 Z 和中间电平,就是那 400nW 漏电的直接来源。这一步把理论上的"可能漏电"变成了"确实漏电"。
4.4 第四步:凶手是最不起眼的 set_dont_touch
到这一步,问题已经明确,但不查清楚"为什么工具没修"等于白查。回到约束里搜,才发现 SDC 里躺着这样一条批量约束:
set_dont_touch [get_cells -hier -filter "name =~ *input_buf*"]范围太宽了,把所有名字含input_buf的 cell 都保护起来了。报错?不会。warning?也没有。它只是安安静静地站在那里,让后面所有优化手段全部失效。排查结束后我在总结里写了一句:功耗异常往往不是某个 cell 的问题,而是结构和约束共同作用的结果,缺一环都不会漏。
5. 修复方案、取舍和同类雷区的规避
5.1 三个可用修法及各自代价
方案一,直接把 buffer 删掉,让 input pin 直接接 iso cell。这是最干净的修法,删掉之后浮空输入不存在了,漏电立刻消失。但需要重新跑一轮从 place 到 route 的流程,验证一下删掉 buffer 之后输入端的 max_transition 是否仍然满足。实际项目中这个 buffer 的 fanout 只有两个,iso cell 的输入电容也不算大,删掉后完全没问题。
方案二,把 buffer 从 iso 前面挪到 iso 后面。这样 buffer 的输入在断电后会被 iso 钳在 0,不再浮空,漏电也能消除。这个方案保留了驱动强度,适合那些内部负载较大的路径。代价同样是要重新做实现,而且时序行为会有一点变化,需要重新确认。
方案三,只把 dont_touch 去掉,把决策权交还给工具,让它在后续优化里决定是删是挪。这个方案改动最小,但风险也最高:工具可能因为别的原因重新插一个 buffer,也可能在优化之后仍然留下一根受保护的 buffer,结果问题没解决。我的建议是:如果项目还在实现阶段,可以直接走方案一;如果已经接近 signoff,结构改动越小越好,考虑方案二。
| 方案 | 改动范围 | 风险 | 适用阶段 |
|---|---|---|---|
| 删 buffer | 网表 + 实现 | 过渡时间需重新验 | 实现阶段 |
| buffer 移到 iso 后 | 网表 + 实现 | 时序需重新确认 | 接近 signoff |
| 去 dont_touch 交工具 | 约束 | 结果不可控 | 仅临时验证 |
5.2 input 边界上其他容易埋雷的插入物
这根 buffer 只是导火索,input 边界上类似的雷还有不少。delay cell 修 hold 是重灾区,性质和 buffer 完全一样:挂在 iso 前面的 delay cell 一旦在常开域里,断电后输入端浮空,同样漏电。异步信号的同步 buffer 网络也是高频雷区,这些网络常被 dont_touch 保护,如果其中某一段恰好跨过了电源域隔离边界,问题比普通 buffer 更难查。
另外,input 端口到 iso 之间的组合逻辑也需要注意,比如为了做时钟门控或毛刺过滤插入的 AND/OR 门。它们的输入一旦悬空,内部同样存在贯通电流。我这里说的只是"常开域里、输入端连着可关断域"这一类,真正的经验是:凡是隔离边界前出现的非常规单元,都应该上 review 名单。
5.3 流程上的几条约束纪律
踩过这次坑之后,我给团队立了几条规矩,现在项目里一直在用。
第一条,任何 set_dont_touch 都要和 UPF 隔离网络做交集检查。dont_touch 名单里如果出现了隔离边界前的 cell,不管什么理由都必须单独 review,不允许出现在批量约束的大网里。
第二条,隔离边界上的输入网络尽量保持"纯连线"。input pin 到 iso cell 之间不允许例化普通 buffer 和组合逻辑,如果确实需要驱动增益,必须放在 iso 之后。
第三条,综合脚本对 input 端口自动插入的 buffer,默认不设 dont_touch。只有明确手工保护的信号网络,才允许加保护,而且要注释清楚理由。
第四条,把 low-power 检查里"isolated net 上存在不受控组合单元"这类告警,从 warning 提到 error 级别。这类告警平时看着无关痛痒,但往往是漏电问题最早的信号,值得用一条 error 去拦住所有分支。
5.4 一条可复用的检查命令
配合流程纪律,我写了一段检查脚本放在综合和后端之间,每次跑完都自动执行一遍,把所有"隔离网络前挂了 dont_touch cell"的情况全筛出来:
foreach_in_collection port [get_ports -filter "direction == in"] { set cell [get_cells -quiet -of_objects $port -filter "dont_touch == true"] if {[sizeof_collection $cell] > 0} { set net [get_nets -of_objects $port] set iso [get_cells -quiet -of_objects $net -filter "is_isolation_cell == true"] if {[sizeof_collection $iso] > 0} { echo "WARN: dont_touch cell on isolated input net: $port" } } }这段脚本不复杂,但能把这一类问题在进入后端之前就拦住。真实的 low-power 项目里,工具之间互相不知道的事太多了,靠人记不如靠脚本查。
这次排查最后只改了几行约束,但前后花了两天。最让我感慨的是,这 0.6uA 不是某个 cell 的错,而是综合、后端、约束三块各做各事之后,漏掉了一个角落。现在每轮检查我都会多看一层"隔离边界外还有什么常开逻辑",也建议你把类似检查直接写进 flow 里,别等功耗报表来给你惊喜的时候再补窟窿。