不知道大家有没有这种经历:画完一张几十个引脚的原理图,检查网表的时候突然看到WARNING: net "VDD" has multiple names,或者更头疼的floating net提示。排查半天,结果往往是某个 pin 旁边忘了放 label,或者放 label 时手一抖,把 net 名拼错了一位。
我自己就被这个问题折磨过好几回。最惨的一次是给芯片顶层 60 多个 I/O 补 label,补到第 40 个的时候眼睛已经花了,漏了 3 个,第二天仿真报错又从头查。后来我干脆花了一个下午,用 Cadence Virtuoso 自带的 SKILL 语言写了个小脚本:选中原理图里的 pin,自动把它所属 net 的名字作为一个 pin label 放到旁边,顺手处理重复、总线展开、多页 page 这些麻烦事。这个脚本我在团队里用了三年,从 IC6.1.6 到 IC6.1.8 一直稳定运行。
这篇文章把这个脚本彻底拆开聊透:为什么要写它、底层对象模型怎么理解、完整代码怎么组织、实际踩过哪些坑、最后怎么打包成快捷键和批量工具。文章内容适合用过 Virtuoso 但没怎么写过 SKILL 的人,也适合想进一步理解 schematic 数据库结构的 CAD 工程师。
1. 为什么需要这个脚本:原理图pin标注的痛点
1.1 pin label到底在标注什么
先说清楚对象。Virtuoso schematic 里的 net 是一个电气连接概念,它本身并不自带一个“可见名字”。你在原理图里看到的那些标着VDD、GND、data[7:0]的文本,本质上是叫做 label 的图形对象,被贴在 net 上,告诉读图的人、告诉 LVS 工具、告诉 netlister:这根线叫什么名字。
而 pin 是 net 的“对外接口”。在一个 cellview 里,pin 可以是层次化引脚(hierarchical pin,就是这张图纸对外暴露的端口),也可以是从某个 symbol instance 上带出来的 instance pin。无论哪种,只要它们连接到某个 net 上,这个 net 就应该有一个对应的 label,否则整个设计在 netlist 导出时很容易出现命名混乱。
手动标注的标准流程是:点Place -> Label,弹出表单,输入 net 名,选层、选字高、选方向,再点一下坐标放下去。一个 pin 重复一次。引脚少还好,一到几十个引脚的时候,这就是纯粹的机械劳动,而且越到后面注意力越差,漏标、错标、标完自己都记不住有没有标过。
1.2 手动操作的三个典型麻烦
第一个麻烦是重复劳动。一个 32 bit 的 bus 端口,每个 bit 都要有对应的 pin 和 label。虽然很多人习惯只标一个data[7:0]然后用总线标注,但很多场景下工具并不会自动给每个 bit 展开 label,最终还是要手工处理。
第二个麻烦是错别字和大小写。VDD和vdd在 Virtuoso 里面是两个不同的 net name,手一抖大小写写错,netlist 里就多一根悬空的“幽灵 net”,仿真发现信号悬空了,回原理图找,眼睛都快找花。
第三个麻烦是 bus 位序。data[7:0]和data[0:7]看起来只是顺序不同,放在 label 上却代表完全不同的物理连接。手动逐个 bit 打 label 的时候很容易写乱,尤其是倒序总线。总而言之,pin label 的自动化不是“懒不懒”的问题,而是准确性本身就有需求。
1.3 自动化的核心思路
写这个脚本之前,我给自己定的目标很简单:选中一个或多个 pin,脚本自动获取每个 pin 所在 net 的名字,然后在 pin 旁边创建一个 label,label 文本就是 net 名字。如果同一个 net 上已经存在同名 label,就不再创建,避免重复。
这个思路不挑工艺库,不挑设计类型,只要你的原理图里 pin 确实连到了 net 上就能跑。后来在团队里推广开,大家发现它还能顺手救一个很常见的场景:顶层模块画完后,发现内部 net 取名和外部端口名不一致,用这个脚本选中所有端口 pin,一键把所有 net label 全部刷成端口名,省掉了大量手工改名的工作。
2. 动手前必须摸清的对象关系
2.1 入口:CIW与SKILL加载
SKILL 是 Cadence Virtuoso 自带的类 Lisp 脚本语言,入口就在 CIW(Command Interpreter Window)底部的输入框。最简单的加载方法,是把脚本保存成.il文件,然后在 CIW 里执行:
load("/home/user/scripts/autoPinLabel.il")日常调试时,我也会直接在 CIW 里敲一两行测试语句,确认当前选中的对象是什么、属性是什么。写 SKILL 脚本和写普通代码不一样,最大的特点是你完全可以“半交互式”地开发:先选中对象,再在 CIW 里执行geGetSelSet()看看返回什么,然后逐步验证各种属性引用是否正确。
2.2 cellview、pin、instPin、net、label之间的关系
这段我用一个生活化的类比来展开。把 cellview 理解成一张工程图纸;pin 是这张图纸边缘伸出来的一根接线柱;instPin 是图纸里某个子模块符号上的接线柱;net 是“这些接线柱和线段都属于同一根电气导线”的账本记录;label 是贴在这根导线旁边的姓名牌;而 wire 是根据账本画出来的物理线段。
在 SKILL 里,它们分别是不同类型的对象:
cellview:geGetEditCellView()返回当前正在编辑的 cellview。pin:原理图中真正意义上的端口图元,objType == "pin"。instPin:instance 上的引脚,比如某个 symbol instance 带出来的引脚,objType == "instPin"。net:电气连接关系对象,pin~>net返回这个 pin 所连的 net。label:图形文本对象,objType == "label",文本内容存在theText属性里。
有一点特别重要:在 Virtuoso schematic 中,同名 wire 会自动合并成同一个 net 对象。也就是说,net 就像是数据库里的一张索引表,维系着所有属于它的图形。label 可以挂在 net 上,也可以挂在某个 fig 上。我们要创建的 pin label,本质上是往 net 上挂一个名字。
2.3 选中集合与当前编辑环境
脚本里有两个入口函数的地位无可替代:
cv = geGetEditCellView() ; 当前编辑的 cellview sels = geGetSelSet() ; 当前在原理图窗口里选中的对象列表geGetSelSet()返回的永远是 list,哪怕你只选中了一个对象,也是一个单元素 list。这意味着你后续的遍历逻辑完全可以统一用foreach处理,不需要单独判空,只需要在遍历前判断一下 list 是否为 nil 即可。
2.4 label的显示参数
创建一个 label 不是只给一串文本就行,它有几个关键参数:layer(层)、height(字高)、orient(方向)、justify(对齐方式)。这些参数决定 label 长得什么样、由哪个工艺层显示、能不能被 netlist 和 LVS 识别。
对于 pin label,层名通常应该保持与设计里的其他 pin 标注一致。有的工艺库叫pin,有的叫wireName,有的叫text。写脚本时我强烈建议把层名设成可以外部传入的参数,不要硬编码。字高则一般取 0.0625 或者 0.1,取决于你公司画图规范,没有统一标准。方向通常用"R0",对齐方式常用"centerCenter"。
Virtuoso 里还有更上层的schCreateLabel接口,它封装了一些交互逻辑,适合在图形界面里用;但我的脚本选择的是更底层的dbCreateLabel,因为它不依赖 UI 状态,批量处理和后台运行时更稳定。
3. 核心脚本:从选中pin到自动生成net label
3.1 主流程一瞥:过滤选中对象
主流程的第一步,是把选中集合里“真正需要处理的对象”筛出来。用户可能不只选中 pin,也可能选中 wire、label、instance、甚至一块矩形图形。如果不对对象类型做判断,脚本在访问~>net这样的属性时很容易报 nil 错误。
用objType做分类过滤即可:
pinList = nil foreach( obj sels case( obj~>objType ("pin" pinList = cons(obj pinList)) ("instPin" pinList = cons(obj pinList)) (t printf("*WARN* skipped object type: %s\n" obj~>objType)) ) )这里我把pin和instPin都收进来。逻辑上它们都可能连接到 net,而且在很多场景里,用户选中的往往是 instance 的引脚而不是 cellview 自身层次化 pin。
3.2 获取net与label文本的细节
拿到 pin 后,下一步是得到 net 对象和要写的文本。
net = pin~>net text = if( pin~>objType == "instPin" then if( pin~>term then pin~>term~>name else pin~>name ) else pin~>name )这里为什么要区分 instPin?因为 instance 引脚的属性里面,pin~>name可能是自动生成的内部名,而pin~>term~>name才是用户在 symbol 上定义的真正端口名。对于原理图层次化 pin,直接取pin~>name即可。
有一个隐蔽的细节:pin~>net在 pin 完全悬空时会返回nil。这种情况不需要报错,跳过即可,后面我在排坑章节里会单独讲。
3.3 label位置计算:为什么从bBox中心偏移
label 要放在哪里,不是一个可以随便拍脑袋的问题。放太远,看起来不像 pin 的名字;放太近,又可能和 pin 符号本身重叠。我选择的策略,是取 pin 图形包围盒bBox的中心点,再往右下方偏移一小段距离。这样 label 会整齐地出现在 pin 旁边,而不是压在符号正中间。
SKILL 里取 bBox 中心点,可以用两个内置函数配合:
ll = lowerLeftBox(pin~>bBox) ur = upperRightBox(pin~>bBox) pos = list( (xCoord(ll)+xCoord(ur))/2.0 (yCoord(ll)+yCoord(ur))/2.0 )偏移量就通过pos + 0.125:0.0叠加。为什么是 0.125?这是因为在我的公司设计规范里,schematic 的 grid 是 0.0625,两格刚好是 0.125,这样 label 做到任何一个 grid 点上,后续移动时也不会歪。
3.4 完整脚本与使用方法
把上面的逻辑串起来,加上去重处理,就是下面这个完整的脚本。我个人实测在 IC6.1.6 和 IC6.1.8 上都能正常运行,核心 API 用的都是比较稳定的接口。
;=========================================================== ; autoPinLabel.il ; Auto create pin label on the net of selected pin/instPin. ; Usage: load this file, then call AutoPinLabel() ;=========================================================== procedure( AutoPinLabel @optional (cv geGetEditCellView()) (lHeight 0.0625) (lLayer "pin") let( (sels pinList processed net text ll ur pos labelExists) sels = geGetSelSet() if( null(sels) then printf("*ERROR* Nothing selected. Select at least one pin first.\n") return(nil) ) ; Step 1: filter pins/instPins from selection pinList = nil foreach( obj sels case( obj~>objType ("pin" pinList = cons(obj pinList)) ("instPin" pinList = cons(obj pinList)) (t printf("*WARN* skipped object type: %s\n" obj~>objType)) ) ) printf("*INFO* %d pin(s) selected for labeling.\n" length(pinList)) ; Step 2: create label for each pin processed = 0 foreach( pin pinList net = pin~>net when( net text = if( pin~>objType == "instPin" then if( pin~>term then pin~>term~>name else pin~>name ) else pin~>name ) ; Step 3: skip if same label already on the net labelExists = nil foreach( fig net~>figs when( fig~>objType == "label" && fig~>theText == text labelExists = t ) ) when( labelExists printf("*WARN* label %s already exists on net %s, skip.\n" text net~>name) ) unless( labelExists ; Step 4: compute label position from pin bBox center ll = lowerLeftBox(pin~>bBox) ur = upperRightBox(pin~>bBox) pos = list( (xCoord(ll)+xCoord(ur))/2.0 + 0.125 (yCoord(ll)+yCoord(ur))/2.0 ) dbCreateLabel( cv net text lLayer lHeight "R0" "centerCenter" pos 0.0 ) printf("*INFO* label %s -> net %s\n" text net~>name) processed = processed + 1 ) ) ) printf("*INFO* finished. %d label(s) created.\n" processed) processed ) )加载方式,在 CIW 里执行:
load("/path/to/autoPinLabel.il")然后去原理图窗口选中几个 pin,再回到 CIW 执行:
AutoPinLabel()你会看到 CIW 里打印出类似这样的信息:
*INFO* 12 pin(s) selected for labeling. *INFO* label VDD -> net VDD *INFO* label GND -> net GND ... *INFO* finished. 12 label(s) created.跑完之后,回原理图窗口用redraw刷一下,就会看到 label 已经贴在每个 pin 旁边了。
dbCreateLabel是直接改数据库的,所以脚本运行前最好File -> Save一下当前 cellview。虽然实际运行中很少出问题,但养成这个习惯,万一工艺库的层表有问题导致异常,也有后悔药。
4. 踩坑记录:实测中必须处理的边界情况
4.1 选中的对象不总是pin
第一次把脚本给同事用,他跑完就来找我:“你这个脚本报错”。我去一看,他在原理图里用鼠标框选了一大片区域,里面有 pin、wire、label、instance,其中一根 wire 被脚本访问pin~>net时直接报 nil,因为 wire 对象根本没有net属性。
排查思路当时是这样:先geGetSelSet()把选中对象打出来,接着逐个看objType,发现报错的是line类型。问题本质上不是语法,而是我没有在访问属性前统一做类型过滤。
后面我把代码改成先case过滤,非pin和instPin的对象一律打印 WARNING 并跳过,问题就消失了。这也是我在前面完整代码里第一步就过滤的由来。写任何原理图操作脚本,第一件事永远是判断objType,这个习惯能帮你挡掉一半以上的运行时错误。
4.2 bus展开导致命名从data[7:0]变成data<0>
第二个坑也很经典。设计里有 128 bit 的总线端口,选中一个 bus pin 运行脚本,结果在原理图上生成了 128 个 label,而且每个 label 名字都变成了data<0>、data<1>这种格式,不是我期望的data[0]这种带方括号的格式,甚至一部分 label 直接是重复的。
原因出在 Virtuoso schematic 界面的 bus expansion 机制。默认情况下,编辑器会把 bus 引脚展开成一个个单 bit 的引脚显示和选择,导致geGetSelSet()拿到的是展开后的底层引脚对象。这些对象的name是data<0>这种内部表示形式,和设计里真正的data[0]存在差异。
排查方法是在脚本里临时打印pin~>name,看到data<0>的那一刻就明白了。解决办法有两种:
第一种,临时关闭 bus expansion,在脚本开头保存旧值,处理完以后再恢复:
oldExpand = envGetVal("schematic" "schBusExpand") envSetVal("schematic" "schBusExpand" 'boolean nil) ; ... do labeling ... envSetVal("schematic" "schBusExpand" 'boolean oldExpand)第二种,不做环境变量切换,而是对拿到的名字做字符串归一化,把<0>这种内部格式换成[0]。我个人更推荐第一种,因为第二种正则处理如果遇到data<10>这种多位数总线,容易写错边界。
需要说明的是,schBusExpand这个 env 变量名在不同版本里可能有出入,拿到别的版本环境以后先用envGetVal("schematic" "schBusExpand")确认一下再写进脚本。
4.3 悬空net会让pin~>net返回nil
第三个坑藏得很深,但一旦遇到就很要命。有一次处理整张拓扑图时,有个 pin 下面没连着任何东西,脚本访问pin~>net返回了nil,后面所有引用net~>name的行全部报错,整个脚本中断。
这个问题的原因很好理解:pin 没有连接到任何 wire,也就没有任何 net 对象。处理方式很简单,所有涉及net的逻辑都用when(net ...)包裹,跳过悬空 pin。
但这其实是一个价值很高的“副作用”:跑一遍脚本,看哪些 pin 被 WARNING 提示为悬空,基本上就等于做了一次连通性检查。后来我在脚本里把悬空 pin 的名字也打印出来:
when( null(net) printf("*WARN* pin %s is not connected, skip.\n" text) )这个改动让脚本从“标注工具”多了一个“电气检查工具”的功能,算是意外收获。
4.4 label层名错误导致显示异常
还有一次,脚本跑完以后 label 确实创建了,但颜色是诡异的紫色,还带个小方块标记,点击以后属性列表里也能看到theText是正确内容,但原理图上就是不像正常 label 那样显示。
问题出在dbCreateLabel的layer参数上。我给脚本默认传入的是"pin",但这套工艺库的 schematic techfile 里,pin label 实际所在的层名不是pin,而是另一个映射层。把文本放到了没有正确显示映射的层上,自然呈现异常。
解决方法是打开这个工艺库的显示层设置,或者用一个已知正常的 label 对象,打印它的layer属性:
; select a known good label first, then: geGetSelSet()~>layer~>name看到真实层名后,把脚本的lLayer参数改成这个值即可。这个踩坑经历告诉我们,层名永远不要写成死代码,作为参数传进来是最稳的。
4.5 跨页原理图里label该放哪一页
最后一个坑是在大模块里遇到的。一个 cellview 可能由多个 page 组成,pin 分布在不同页面上。如果只是对当前页面的 pin 操作,dbCreateLabel默认写入的位置可能不是 pin 所在的那个 page,导致 label 和 pin 不在同一页,看起来就像 label 凭空消失。
Virtuoso 的 cellview 对象里,page 是一个隐形的容器属性。Schematic 的 pin 对象通常可以通过pin~>page或者pin~>bBox所在的坐标系来定位。稳妥的做法是在脚本里判断页数,只有单页时才直接写入;多页时,用页对应的 fig 集合来创建 label,或者干脆提示用户切换到目标 page 再运行。
我这里采用的是最保守的策略:脚本只在当前活动页面上操作选中的对象,如果跨页,先让用户切换页面再分别执行。这样虽然在自动化程度上打了折扣,但正确性优先,不会出现 label 跑到别的页面的诡异问题。
5. 让它真正好用:快捷键、全图标注与批量处理
5.1 绑定快捷键
脚本能跑只是第一步。真正让你每天愿意用它的,是把它绑定到一个顺手的位置。我习惯在用户的 bindkey 文件里加一行:
hiSetBindKey("Schematics" "<Key>F9" "AutoPinLabel()")这样在原理图窗口按下 F9,就直接对当前选中的 pin 执行自动标注。bindkey 文件位置每家公司的环境变量不一样,一般是.cdsinit里 load 的某个bindkeys文件。如果你的版本中hiSetBindKey的 handler 名不是"Schematics",可以在 CIW 里输入hiGetBindKeyList()看看当前编辑器实际注册的 handler。
5.2 一键处理当前cellview全部pin
“选中 pin 再打 label”已经很快了,但还能更快。很多场景下,我们想要的是打开一张原理图,直接一键为所有端口 pin 打上 net label,连选都不用选。
实现的思路很简单,把上文脚本里的输入源从geGetSelSet()换成遍历 cellview 的全部 pin:
pins = cv~>pins when( null(pins) pins = setof( fig cv~>figs fig~>objType == "pin" ) )如果cv~>pins在你的版本里不可用,就退回到遍历cv~>figs再过滤objType。每个 pin 的处理逻辑和核心脚本完全一样,只是输入源变化了。这个“全图一键标注”功能在我团队里使用频率最高,特别是用来自动生成顶层包装(wrapper)的 pin label。
5.3 批量处理整个library的cellview
再进一步,如果你手头有一整套 library 需要统一补全 pin label,逐张打开再执行全图标注也嫌麻烦。可以用dbOpenCellViewByType只读或写模式打开指定 cellview,处理完以后dbSave保存并dbClose关闭。
procedure( AutoPinLabelBatch( lib listOfCells @optional (height 0.0625) (layer "pin") let( (cv) foreach( cell listOfCells cv = dbOpenCellViewByType( lib cell "schematic" nil "a" ) when( cv AutoPinLabelAll( cv height layer ) dbSave( cv ) dbClose( cv ) ) ) ) )这个批量脚本虽然简单,但杀伤力很大。我建议使用前做一个完整备份,并且限定到明确的 cell list,不要一上来就遍历整个 library 的所有 cell,否则一旦脚本有 bug,污染范围会很大。批量场景下,"a"是写模式,Cadence 会锁住 cellview,防止其他进程同时修改同一份数据。
5.4 后续还能怎么扩展
这个脚本的思路还可以继续延伸到几个方向。一是自动生成 hierarchical pin:不仅给内部 net 打 label,还在 cellview 边缘生成对应名字的层次化 pin,这对自底向上的模块集成非常有用。二是与 netlist 导出衔接:在跑 LVS 或仿真前,先用这个脚本做一次“所有端口 pin 是否都有 label”的检查,把漏标问题挡在 netlist 之前。三是把 label 的位置策略改成按端口方向自动落到 pin 的不同侧,让图纸更整齐。
我在团队里推这个脚本的时候,有个同事一开始表示“放 label 这种事还要写脚本?”,结果后来他自己的模块里刚好有 128 bit 的总线端口,靠这个脚本一次性全部标完,他再也没手动放过 pin label。这个脚本本质上不复杂,核心就是理解对象模型,然后让 SKILL 替你承担重复劳动。
最后提醒一句:跑脚本前务必先 save 一下 cellview。dbCreateLabel是直接操作数据库的,虽然正常运行很少出问题,但如果你正在调试新的参数组合,保险一点总没错。如果你也经常被原理图 pin 标注搞得头大,按这篇文章的思路自己写一个,大概率只会觉得写晚了。