1. 从一次脚本翻车说起为什么我要死磕 Cadence Skill刚入行那会儿我在一家做模拟版图的小公司干活。有次接到一个任务要把三百多个标准单元的版图按不同驱动能力重新排布还要批量修改每个单元的某些层次属性。我当时年轻气盛觉得手动改改就完事了结果改到第一百多个的时候眼睛花了把一个关键尺寸改错了后面流片直接报废。那次教训之后我开始认真研究 Cadence 的 Skill 语言——说白了它就是寄生在 Virtuoso 这类工具里的一门脚本语言专门用来把那些重复、繁琐、容易出错的版图与原理图操作自动化掉。Skill 这门语言圈外人基本没听过圈内人也是又爱又恨。爱的是它真的能把你从重复劳动里捞出来恨的是它语法古怪、文档零散、报错信息还经常让人摸不着头脑。它基于一种类似 Lisp 的方言满屏的括号第一次看的人往往一脸懵。但只要你熬过前两周的适应期就会发现它其实逻辑很清晰而且能直接操作 Virtuoso 的数据库对象这是任何外部脚本语言都做不到的。这篇文章适合谁看如果你是从版图、原理图、验证或者 CAD 支持岗位入行的工程师每天被重复操作折磨得想砸键盘那这篇内容就是给你写的。我会从语言基础、核心对象模型、实操脚本、调试技巧几个维度把 Skill 编程这件事讲透。哪怕你之前一行代码没写过跟着走也能上手如果你已经会写一点那里面关于数据库访问和性能优化的部分应该能帮你少踩几个坑。2. Skill 语言到底是个什么东西先搞懂它的脾气2.1 为什么是 Lisp 方言而不是 Python很多人第一反应是为什么不用 Python现在什么自动化不是 Python 一把梭这个问题我当年也问过带我入门的师傅。他的回答很直接因为 Virtuoso 的底层数据库是用 Lisp 系语言构建的Skill 能直接拿到内存里的对象指针而 Python 要通过一层接口去调用性能和实时性差一大截。举个具体场景。你要遍历一个版图里所有的矩形图形检查它们的宽度是否小于某个工艺最小值。用 Skill你可以直接拿到dbShape对象读取它的bBox属性整个过程在同一个进程内完成。而如果用外部语言你得先把数据导出成某种中间格式再读进来处理处理完还要写回去。对于几十万个图形的大版图这个来回开销是灾难性的。Skill 的另一个特点是它和工具环境深度绑定。你在 CIWCommand Interpreter Window里敲的每一行命令本质上都是 Skill 表达式。你平时点的那些菜单背后也是 Skill 函数在跑。这意味着你写的脚本可以无缝调用工具自带的任何功能不存在“这个 API 不支持”的尴尬。2.2 基本语法括号、函数、返回值Skill 的语法核心就三条一切皆表达式、函数调用用前缀表示、返回值就是最后一个表达式的值。看个最简单的例子plus(1 2 3)这行代码返回 6。注意不是1 2 3而是plus(1 2 3)。函数名在前参数在后用空格分隔整体用括号包起来。刚开始写会觉得别扭写多了反而觉得这种一致性很舒服——不管什么操作都是函数名(参数...)的形式。变量赋值用setq或者letx 10 y 20 sum x y等等这里要小心。Skill 里x 10这种写法在 CIW 里能用但在正式脚本里更推荐用setqsetq(x 10) setq(y 20) setq(sum x y)实际上x y这种中缀写法 Skill 也认但仅限于少数运算符。为了代码可读性和一致性我建议新手统一用函数式写法plus(x y)。这样你去看别人写的复杂脚本时不会因为混用两种风格而晕头转向。2.3 数据类型列表才是亲儿子Skill 里最常用的数据结构是列表list。你可以把它理解成 Python 的 list 或者 Java 的 ArrayList但更灵活——里面可以装任何东西包括其他列表。myList list(1 2 3 abc 4.5)取第一个元素用car取剩下的用cdrcar(myList) ; 返回 1 cdr(myList) ; 返回 (2 3 abc 4.5)这两个名字来自 Lisp 传统car 是 Contents of Address Register 的缩写cdr 是 Contents of Decrement Register。记不住没关系你就记成“取头”和“取尾”。实际写脚本时遍历列表最常用的组合是foreachforeach(item myList println(item) )这段代码会依次打印列表里的每个元素。foreach是 Skill 里最常用的循环结构没有之一。你后面写版图遍历、图形筛选、属性批量修改全靠它。3. 核心对象模型不懂这个等于白学3.1 数据库对象层级从 Cellview 到 ShapeSkill 编程最核心的部分是操作 Virtuoso 的数据库对象。这些对象有严格的层级关系搞不清层级你的脚本就会到处报“对象不存在”的错。顶层是库Library一个库里可以有多个Cell一个 Cell 可以有多个View最常见的 View 类型是layout和schematic。一个 View 打开后就是一个Cellview对象。Cellview 里面装着各种Shape图形比如矩形、多边形、路径、标签等。用 Skill 打开一个 Cellview 的标准写法cv dbOpenCellViewByType(MYLIB MYCELL layout maskLayout r)这行代码的意思是以只读模式打开 MYLIB 库里 MYCELL 这个 Cell 的 layout 视图。最后一个参数r表示只读如果要修改就改成aappend或者wwrite。打开之后你可以用cv~来访问它的属性。比如cv~shapes返回这个 Cellview 里所有的图形列表。注意这个~符号它是 Skill 里访问对象属性的专用语法读作“箭头”。你可以把它理解成其他语言里的点号。3.2 图形对象的常用属性拿到一个 Shape 对象后你能读哪些属性下面这张表是我平时查得最频繁的属性名含义示例值objType对象类型rect、polygon、pathlayerName图层名M1、POLYpurpose图层用途drawing、pinbBox包围盒((0 0) (10 5))width路径宽度0.5points多边形顶点((0 0) (5 0) (5 5))读取属性的写法layer shape~layerName box shape~bBox这里有个坑要注意bBox返回的是一个嵌套列表格式是((左下角x 左下角y) (右上角x 右上角y))。如果你想算宽度得这样写ll car(shape~bBox) ur cadr(shape~bBox) width xCoord(ur) - xCoord(ll) height yCoord(ur) - yCoord(ll)cadr是取列表第二个元素的简写等价于car(cdr(list))。xCoord和yCoord是 Skill 内置函数用来从坐标点里提取 x 和 y 值。3.3 选择集与当前编辑对象除了直接遍历数据库另一个常用场景是操作用户当前选中的对象。Skill 提供了geGetSelSet()函数返回当前选择集里的所有对象selSet geGetSelSet() foreach(obj selSet println(obj~objType) )这个函数在写交互式工具时特别有用。比如你想做一个“一键对齐选中矩形”的功能就可以先拿到选择集再遍历修改每个矩形的坐标。注意geGetSelSet()返回的对象可能是不同层级的有的是 Shape有的是 Instance甚至有的是 Pin。写脚本时一定要先判断objType否则后面访问属性时会报错。4. 从零写一个实用脚本批量修改图形属性4.1 需求拆解到底要改什么假设你接到一个任务某个版图里所有 M1 层上宽度小于 0.1 微米的矩形都要把宽度扩展到 0.1 微米并且保持中心点不变。手动改几百个图形改到天亮。用 Skill二十行代码搞定。先拆解需求打开目标 Cellview遍历所有图形筛选出 layerName 为 M1 且 objType 为 rect 的图形计算每个矩形的宽度如果宽度小于 0.1以中心为基准扩展到 0.1保存并关闭4.2 完整代码与逐行注释; 打开 Cellview可写模式 cv dbOpenCellViewByType(MYLIB MYCELL layout maskLayout a) ; 计数器记录修改了多少个图形 count 0 ; 遍历所有图形 foreach(shape cv~shapes ; 判断是否为矩形且图层为 M1 when(shape~objType rect shape~layerName M1 ; 获取包围盒 ll car(shape~bBox) ur cadr(shape~bBox) ; 计算当前宽度和高度 curWidth xCoord(ur) - xCoord(ll) curHeight yCoord(ur) - yCoord(ll) ; 如果宽度小于 0.1进行扩展 when(curWidth 0.1 ; 计算中心点 centerX (xCoord(ll) xCoord(ur)) / 2.0 centerY (yCoord(ll) yCoord(ur)) / 2.0 ; 计算新的左下角和右上角 newLL list(centerX - 0.05 centerY - curHeight / 2.0) newUR list(centerX 0.05 centerY curHeight / 2.0) ; 修改矩形的包围盒 shape~bBox list(newLL newUR) ; 计数器加一 count count 1 ) ) ) ; 保存修改 dbSave(cv) ; 关闭 Cellview dbClose(cv) ; 打印结果 printf(共修改了 %d 个图形\n count)这段代码里几个关键点值得展开说。第一when是 Skill 里的条件判断相当于其他语言的if。它和if的区别在于when只在条件为真时执行没有 else 分支if可以带 else。我平时写筛选逻辑基本都用when因为大多数时候不需要处理“不满足条件”的情况。第二shape~bBox ...这种直接赋值的方式是 Skill 修改数据库对象的常用手段。但要注意不是所有属性都能这样改。有些属性是只读的你赋值会报错。具体哪些能改最靠谱的办法是查官方文档或者自己试。第三printf是 Skill 里的格式化输出函数%d是整数占位符\n是换行。调试脚本时printf比println好用因为可以带格式。4.3 性能优化别让脚本跑一晚上上面这段代码在图形数量少的时候没问题但如果你的版图有几十万个图形直接遍历cv~shapes会非常慢。我实测过一个五十万图形的版图用上面的写法跑了将近四十分钟。后来优化了一下降到三分钟以内。优化手段主要有两个。第一个是用dbShapeQuery代替全量遍历。这个函数允许你按图层、按区域筛选图形数据库层面就帮你过滤掉了大部分不需要的对象shapes dbShapeQuery(cv M1 drawing 0.0 0.0 100.0 100.0)这行代码只返回 M1 层 drawing 用途、且在指定矩形区域内的图形。范围缩小了遍历自然快。第二个优化是减少属性访问次数。shape~bBox这种操作每次都要和数据库交互能缓存就缓存。比如你在循环里多次用到同一个图形的 bBox就先存到变量里别每次都去读。实操心得写 Skill 脚本时养成“先筛选、后遍历”的习惯。能用数据库查询搞定的绝不自己写循环过滤。这是性能差距最大的地方。5. 调试与排错那些文档不会告诉你的坑5.1 常见报错与排查思路Skill 的报错信息有时候很模糊比如*Error* eval: unbound variable意思是用了未定义的变量。但具体是哪个变量它不一定告诉你。这时候我的排查步骤是在可疑代码段前后加printf打印关键变量的值用println打印整个对象看看里面到底有什么把复杂表达式拆成多个简单表达式逐步执行下面这张表是我这些年遇到的高频报错和对应解法报错信息常见原因解决方法unbound variable变量未定义或拼写错误检查变量名确认已用 setq 赋值undefined function函数名拼错或未加载检查函数名确认已 load 相关文件illegal type参数类型不对用 println 打印参数确认类型object not found对象已被删除或路径错误检查 Cellview 是否已打开对象是否有效cannot modify read-only试图修改只读属性确认属性是否可写或改用其他方式5.2 交互式调试CIW 是你的好朋友很多人写 Skill 脚本喜欢在外部编辑器里写完再整体加载出了错就从头再来。这种方式效率很低。我的习惯是先在 CIW 里一行一行敲确认每行都能正确执行再把它们拼成完整脚本。CIW 的好处是即时反馈。你敲一个表达式回车立刻看到结果。如果报错马上就能定位。比如你不确定cv~shapes返回的是什么直接在 CIW 里敲cv~shapes它会打印出所有图形的列表。你可以接着敲car(cv~shapes)看第一个图形长什么样。这种探索式的调试方式比看文档快十倍。5.3 脚本加载与函数定义当你写好一个完整的脚本后通常需要把它保存成.il文件然后用load函数加载load(/path/to/myScript.il)如果脚本里定义了函数加载后就可以在 CIW 里直接调用。定义函数的语法是procedure(MyFunction(arg1 arg2) let((localVar1 localVar2) ; 函数体 result arg1 arg2 return(result) ) )procedure定义函数let声明局部变量。局部变量只在函数内部有效不会污染全局环境。这一点很重要——我见过太多人所有变量都用全局的结果脚本一复杂就互相干扰查半天查不出来。注意Skill 的函数参数是按值传递的但如果你传的是列表或对象函数内部修改会影响外部。这是因为列表和对象本质上是引用。如果你不想让函数修改外部数据就在函数内部用copy复制一份。6. 进阶方向把 Skill 用到极致6.1 写交互式工具让用户点选而不是输参数基础的 Skill 脚本是“运行前设好参数运行后看结果”。但很多时候你希望用户能在界面上点选对象、输入参数。Skill 提供了一套简单的 UI 函数可以弹出对话框、获取用户输入。比如hiGetPoint()让用户在版图上点一个点hiGetString()弹出一个输入框让用户输字符串。把这些和你的核心逻辑结合起来就能做出体验不错的交互式工具。point hiGetPoint() when(point printf(你点的位置是%L\n point) )hiGetPoint返回一个坐标点列表如果用户按了取消返回nil。所以用when判断一下避免对nil做后续操作。6.2 批量处理多个 Cell循环打开关闭单个 Cell 的脚本写熟之后下一步自然是批量处理。比如你要对某个库里的所有 Cell 执行同样的检查。思路很简单先用ddGetObj拿到库对象遍历它的 Cells再对每个 Cell 打开、处理、关闭。lib ddGetObj(MYLIB) foreach(cell lib~cells cv dbOpenCellViewByType(MYLIB cell~name layout maskLayout a) when(cv ; 处理逻辑 dbSave(cv) dbClose(cv) ) )这里有个细节dbOpenCellViewByType如果打开失败会返回nil所以要用when判断一下。另外批量处理时一定要加日志记录每个 Cell 的处理结果否则出了问题都不知道是哪个 Cell 挂的。6.3 性能监控你的脚本到底慢在哪当脚本运行时间超过预期时你需要知道时间花在哪里。Skill 提供了getCurrentTime()函数返回当前时间单位是秒。你可以在关键代码段前后各调一次算差值。startTime getCurrentTime() ; 你的代码 endTime getCurrentTime() printf(耗时%f 秒\n endTime - startTime)我一般会在遍历循环内部也加计时看看是不是某个特定操作特别慢。实测下来最耗时的操作通常是数据库写入修改属性和跨 Cell 操作。读取操作相对快很多。7. 我踩过的那些坑真实经验分享第一个坑在只读模式下修改对象。有次我打开 Cellview 时用了r模式然后试图修改图形属性脚本直接报错退出。后来才知道修改操作必须用a或w模式打开。这个错误很低级但新手很容易犯因为打开时不会报错只有修改时才暴露。第二个坑忘记保存。Skill 修改数据库对象后修改是在内存里的必须调用dbSave才会写回磁盘。我有次跑完脚本看 CIW 里打印的结果都对就直接关了结果重新打开发现什么都没改。从那以后我的脚本最后一行永远是dbSave。第三个坑列表操作搞混 car 和 cadr。car取第一个cadr取第二个caddr取第三个。这个命名规则是 Lisp 传统刚开始特别容易搞混。我的记忆方法是car是取头cdr是去头后的列表cadr就是去头后再取头所以是第二个。虽然绕但用多了就形成肌肉记忆了。第四个坑在 foreach 里修改正在遍历的列表。这个坑最隐蔽。如果你在foreach循环里删除或添加列表元素循环行为会变得不可预测。正确的做法是先把要修改的元素收集到另一个列表循环结束后再统一处理。最后分享一个小技巧把你常用的 Skill 函数整理成一个自己的工具库文件每次启动 Virtuoso 时自动加载。这样你积累的每一个实用函数都能复用不用每次都重新写。我的工具库现在已经有两百多个函数了涵盖版图检查、属性修改、数据导出等场景工作效率比刚入行时高了不止一个量级。
阅读完成 · 觉得有帮助?