ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于SKILL的Cadence Virtuoso原理图Pin Label自动标注脚本

基于SKILL的Cadence Virtuoso原理图Pin Label自动标注脚本 不知道大家有没有这种经历画完一张几十个引脚的原理图检查网表的时候突然看到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 旁边创建一个 labellabel 文本就是 net 名字。如果同一个 net 上已经存在同名 label就不再创建避免重复。这个思路不挑工艺库不挑设计类型只要你的原理图里 pin 确实连到了 net 上就能跑。后来在团队里推广开大家发现它还能顺手救一个很常见的场景顶层模块画完后发现内部 net 取名和外部端口名不一致用这个脚本选中所有端口 pin一键把所有 net label 全部刷成端口名省掉了大量手工改名的工作。2. 动手前必须摸清的对象关系2.1 入口CIW与SKILL加载SKILL 是 Cadence Virtuoso 自带的类 Lisp 脚本语言入口就在 CIWCommand 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 里它们分别是不同类型的对象cellviewgeGetEditCellView()返回当前正在编辑的 cellview。pin原理图中真正意义上的端口图元objType pin。instPininstance 上的引脚比如某个 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 label3.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]变成data0第二个坑也很经典。设计里有 128 bit 的总线端口选中一个 bus pin 运行脚本结果在原理图上生成了 128 个 label而且每个 label 名字都变成了data0、data1这种格式不是我期望的data[0]这种带方括号的格式甚至一部分 label 直接是重复的。原因出在 Virtuoso schematic 界面的 bus expansion 机制。默认情况下编辑器会把 bus 引脚展开成一个个单 bit 的引脚显示和选择导致geGetSelSet()拿到的是展开后的底层引脚对象。这些对象的name是data0这种内部表示形式和设计里真正的data[0]存在差异。排查方法是在脚本里临时打印pin~name看到data0的那一刻就明白了。解决办法有两种第一种临时关闭 bus expansion在脚本开头保存旧值处理完以后再恢复oldExpand envGetVal(schematic schBusExpand) envSetVal(schematic schBusExpand boolean nil) ; ... do labeling ... envSetVal(schematic schBusExpand boolean oldExpand)第二种不做环境变量切换而是对拿到的名字做字符串归一化把0这种内部格式换成[0]。我个人更推荐第一种因为第二种正则处理如果遇到data10这种多位数总线容易写错边界。需要说明的是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 KeyF9 AutoPinLabel())这样在原理图窗口按下 F9就直接对当前选中的 pin 执行自动标注。bindkey 文件位置每家公司的环境变量不一样一般是.cdsinit里 load 的某个bindkeys文件。如果你的版本中hiSetBindKey的 handler 名不是Schematics可以在 CIW 里输入hiGetBindKeyList()看看当前编辑器实际注册的 handler。5.2 一键处理当前cellview全部pin“选中 pin 再打 label”已经很快了但还能更快。很多场景下我们想要的是打开一张原理图直接一键为所有端口 pin 打上 net label连选都不用选。实现的思路很简单把上文脚本里的输入源从geGetSelSet()换成遍历 cellview 的全部 pinpins 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 标注搞得头大按这篇文章的思路自己写一个大概率只会觉得写晚了。
返回列表