ARTICLE DETAIL

资讯详情

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

用SKILL脚本实现Virtuoso版图自动化:从入门到实战

用SKILL脚本实现Virtuoso版图自动化:从入门到实战 1. 为什么一个天天手动画版图的人最后被SKILL“逼”成了脚本选手1.1 那些反复进行到想摔鼠标的操作我先交代一下自己的背景模拟版图工程师画了快六年的Virtuoso从IC 618用到IC 231项目从几万晶体管的小模块到上千万晶体管的混合信号芯片都碰过。按理说一个老版图工程师应该对软件了如指掌。但说句实话我前四年一直停留在“鼠标流”阶段移模块、搭线、打PIN、改label一个一个来。真正让我产生“必须自动化”的念头是在一个SRAM项目的收尾阶段。那段时间连续加班三周内容是什么把四十多个子模块按spec排到顶层然后给每个子模块统一加电源环补齐PIN最后在IO区域打上统一的金属标识。这些操作本身没有任何技术难度但数量大到令人绝望。手动挪一个模块点选、拖拽、对齐、确认坐标两步还行五十个模块下来肩周炎都要犯了。更离谱的是我中途发现某层金属规则改了得把所有电源环整体外扩两微米——那天晚上我重复删掉重建了三十多个电源环做到凌晨一点第二天还被后端同事问“这版图怎么还没更新”。这个场景很多版图工程师都懂。版图这个活儿真正吃时间的往往不是设计而是“重复性的几何操作”。而Cadence公司显然比我们更早意识到这个问题所以在Virtuoso里内置了一套完整的脚本语言名字就叫SKILL。1.2 SKILL解决的是“流程自动化”而非简单画图我第一次听说SKILL脚本是看到一个同事用几行命令批量生成了一排PCELL管子的pin当时惊为天人。后来才知道SKILL根本不是那种只能做做批处理的工具它是Virtuoso自身的“母语”——你在图形界面里点的每一个菜单按钮底层几乎都能找到对应的SKILL函数调用。换句话说你鼠标能完成的版图操作脚本基本都能实现而脚本能做的一些循环、参数化、批量处理鼠标则永远学不会。所以SKILL的本质是“流程自动化”。拿前面那个例子来说四十个模块摆到顶层如果手动就是一场体力劳动如果写脚本可以先定义每个模块的初始坐标和摆放规律再写一个循环统一完成摆放最后按既定间距挨个生成电源环。操作步骤完全一样脚本的优势是稳定、可重复、不抱怨而且中途如果发现规则变了只需要改一个参数然后重跑一遍。这个过程不需要什么高深的计算机理论。后来我逐渐明白SKILL更像是一套“版图领域专用语言”它的函数命名、参数设计都围绕版图数据库展开不需要你去封装什么底层逻辑。我们只需要学会两件事一是看懂它的语法结构二是记住常用的版图API函数然后像堆积木一样把它们组合起来。1.3 正确的学习心态先会用再懂原理很多工程师不敢碰SKILL主要是有心理障碍觉得自己“不是写代码的料”。我当初也一样看到满屏括号就头大。但踩过坑之后想告诉你的第一句话是学SKILL不需要编程基础但需要一个“把你手上的重复操作翻译成步骤清单”的习惯。什么叫步骤清单比如“我想要给当前cellview加一圈metal1矩形做dummy”翻译成步骤就是打开当前编辑的版图数据库确定矩形的起始坐标和结束坐标在指定层上创建矩形保存刷新。这个清单里只有“打开数据库”“创建矩形”两个动作需要查API其余都是普通的数值计算。你真正需要练的不是编程技巧而是“拆解动作”的能力。有了这个心态SKILL入门就成功了一半。2. 进入Virtuoso脚本世界SKILL语法速览与运行环境2.1 在Virtuoso里运行SKILL的4个入口学习SKILL的第一步是找到一个能运行代码的地方。Virtuoso里运行SKILL的入口至少有四个我日常最常用的是前两个入口位置适用场景CIW窗口打开Virtuoso后最下面的命令行区域单条命令验证、临时变量调试SKILL IDECIW菜单 Tools - SKILL IDE或直接敲skill ide写完整脚本、断点调试、查看函数帮助il文件加载CIW里执行load xxx.il把脚本文件加载进环境正式复用bindkey/菜单自定义快捷键或菜单按钮绑定脚本把常用自动化流程做成“一键操作”建议新手之类千万不要直接在CIW里写多行代码。CIW适合验证单条函数比如想知道某个函数参数怎么用敲一行试一下。真正写脚本要么用SKILL IDE要么在外部编辑器里写好.il文件再load进来。这里我得专门提一下SKILL IDE。很多人不知道它自带函数浏览器和变量监视窗口可以直接查函数签名还能设置断点。用编辑器写脚本时最痛苦的事情是“不知道函数参数对不对”在SKILL IDE里把光标放在函数名上就能看到参数提示能省一半查文档时间。所以我后来的习惯是所有脚本一律在SKILL IDE里写调试通过后再存成独立.il文件维护。2.2 理解括号和列表被Lisp支配的恐惧与快乐SKILL的语法像Lisp也可以说它就是Cadence基于Lisp扩展出来的语言。第一次看到(dbCreateRect ...)这种括号套括号的写法很多人都会晕。但换个角度想这种“前缀表达式”其实有一个巨大的优势函数名永远在最前面参数顺序非常统一读代码时一眼就能看出你要调用谁、传什么参数。举一个最简单的例子连续定义几个变量再打印出来; 这是注释分号后面都不执行 a 3 b 5.2 c hello printf(a%d, b%.2f, c%s\n a b c)看起来和普通编程语言没太大区别。但如果你想实现“判断a是否大于b如果是就打印否则打印另一句话”语法就变成这样if( a b then printf(a is bigger\n) else printf(b is bigger\n )注意所有关键字和表达式都用括号包起来if后面跟条件条件成立走then分支不成立走else分支。括号必须成对出现。刚开始我会犯的错误就是“写了很多嵌套函数最后少了一个右括号”这类错误在SKILL里非常典型报错信息有时候会把你带到完全无关的地方。至于列表可以简单理解成“一组有序的数据”point list( 1 2 ) ; 两个点 rectBox list( list( 0 0 ) list( 2 2 ) ) ; 一个由两个点组成的矩形边界 pointList ( (0 0) (1 1) (2 2) ) ; 用单引号括号也可以快速构造列表版图里几乎所有几何操作都要用到坐标而坐标在SKILL里就表示成list(x y)。矩形边界就是两个坐标包在一个列表里路径就是一组坐标包在一个列表里。想通了这一点版本图API的时候就轻松了大半。2.3 变量、函数和循环能用到的语法其实不超过20个我统计过自己写版图脚本时常用的语法结构真的不超过20个。大部分时间都是用变量存坐标和对象句柄用循环生成重复图形用条件判断控制流程然后用函数把整块逻辑包起来。变量赋值用变量名 值字符串用双引号数字没有整数和浮点数之分。定义函数用procedureprocedure( myRect( x y w h ) let( (box) box list( list( x y ) list( xw yh ) ) dbCreateRect( geGetEditCellView() metal1 box ) ) )这里引入了一个let关键字它的作用是在函数内部声明局部变量避免变量泄漏到全局环境。第一次写脚本可以不深究但一定要养成习惯函数内部需要临时变量时记得用let包一层。循环最常用的是for和foreach。for适合按次数循环foreach适合遍历列表for( i 0 9 printf(i%d\n i) ) foreach( pt list( list(0 0) list(1 1) list(2 2) ) printf(point%L\n pt) )条件判断就是前面提过的if以及它的亲戚when只有满足条件才执行、unless不满足条件才执行。这两个亲戚在实际脚本里出场率极高因为很多场景只需要“满足条件就做某事”不需要写复杂的else分支。语法就这么多。是不是比想象中简单接下来真正的重头戏是版图API。3. 版图自动化核心APIdb家族函数是版图自动化的灵魂3.1 dbOpen与数据库对象在写代码前先搞清楚“当前单元”SKILL操作版图本质上是在操作一个叫“cellview”的数据库对象。Virtuoso里你打开一个版图背后就是加载了一个cellview的图形数据库里面保存了图形、实例、PIN、label、层次关系等所有信息。获取这个数据库对象的方式最简单的是用geGetEditCellView()。它返回当前正在编辑的cellview对象也就是你眼前这张版图cvId geGetEditCellView()有了cvId你才能在这个cellview上创建图形。所有dbCreate*函数第一个参数几乎都是这个cvId它指定了“你要在哪个版图上操作”。这个逻辑就像你要在CAD里画图总得先确定当前激活的是哪个文件窗口。如果你不打算操作当前界面里的cellview而是想直接打开库里某个cell可以用dbOpenCellViewByTypecvId dbOpenCellViewByType( myLib myCell layout r )这里的r表示只读打开不会把文件锁定。如果要对它做修改就把模式改成a或者w。需要注意的是只读模式下创建的图形不会真正写入库中这一点我后面在排错章节里会详细说。3.2 创建图形dbCreateRect和dbCreatePath版图里最常见的基础图形是矩形rectangle和路径path这两个对应了两个核心APIdbCreateRect和dbCreatePath。先看矩形。dbCreateRect(cvId layerName bBox)其中bBox是一个由两个对角坐标组成的列表。比如我要在当前cellview的metal1层创建一个左下角(0,0)、右上角(2,1)的矩形cvId geGetEditCellView() dbCreateRect( cvId metal1 list( list( 0 0 ) list( 2 1 ) ) )就这么简单。你可能会问层名能不能用变量能而且我强烈建议用变量。因为一个稍微复杂点的脚本会有几十上百次图形创建如果每个函数里直接写metal1后期换工艺库时就要改一堆字符串。用变量可以把层名集中管理layerM1 metal1 layerM2 metal2 dbCreateRect( cvId layerM1 list( list( 0 0 ) list( 2 1 ) ) ) dbCreateRect( cvId layerM2 list( list( 0 0 ) list( 2 1 ) ) )再看路径。路径代表一条有宽度的连线dbCreatePath(cvId layerName pointList width)dbCreatePath( cvId metal1 list( list(0 0) list(0 5) list(5 5) ) 0.5 )这行代码创建一条从(0,0)向上到(0,5)再向右到(5,5)的金属连线线宽0.5微米。路径适合画电源线、信号线以及各种带角度的连线。相比矩形画线时经常需要计算拐点坐标容易出错我的做法是先把要经过的点列在纸上确认无误后再写进脚本。3.3 摆放实例与打PINdbCreateInst和dbCreatePin比创建图形更常用的是摆放实例。比如要把库里的一个标准反相器放到当前版图上dbCreateInst( cvId myLib INV layout I0 list( 0 0 ) R0 )参数从左到右分别是目标cellview、库名、单元名、视图名、实例名、放置坐标、朝向。这里有个小细节很多新手会漏掉“实例名”。版图上允许同名实例吗不允许。如果同一段脚本里创建了两个名字一样的实例第二次执行会报错。所以批量摆放时通常都把循环变量拼进实例名里for( i 0 9 instName strcat( I sprintf(nil %d i) ) dbCreateInst( cvId myLib INV layout instName list( i*5 0 ) R0 ) )strcat是字符串拼接函数sprintf的用法是把数字转成字符串。这一行在SKILL里出现频率很高建议直接背下来。打PIN也是版图自动化的高频操作。SKILL里创建PIN的函数是dbCreatePin它需要指定pin名、网络名、层、边界dbCreatePin( cvId VDD VDD metal1 list( list( 0 0 ) list( 1 1 ) ) )这里的第一个VDD是pin的名字第二个VDD是它连接的net名两者通常一样但不强制。创建一个PIN后往往还要在它上面加一个label才方便识别dbCreateLabel( cvId metal1 VDD list( 0.5 0.5 ) lowerLeft R0 pin )3.4 坐标与层次设计版图脚本最容易翻车的环节基本理解了这几个API之后版图脚本的骨架就算搭起来了。但真正写起来最容易出问题的不是API用法而是坐标计算和层次关系。坐标计算考验的是“坐标系统一”意识。比如我在放置一排管子时每个管子的尺寸是确定的摆放间距要考虑两个因素管子的宽度和dummy的要求。如果间距公式写错轻则DRC报出一堆间距错误重则版图完全无法通过LVS。我的习惯是先在脚本开头定义所有几何参数用有意义的变量名devW、devH、pitchX、pitchY这样后期改尺寸只需要改一个值。层次关系同样关键。dbCreateRect里的layerName必须严格存在于当前工艺库的tf文件里否则会报“layer not defined”错误。不同工艺层的命名五花八门有的叫M1_TEXT有的叫METAL1有的把drawing层和pin层当成不同层。写脚本前先到LSW窗口确认层名不要凭感觉猜。层名错了脚本不会修改你的版图结构但会让你排查半天白白消耗耐心。4. 实战用50行SKILL脚本自动生成一个可用的测试版图4.1 这个脚本要完成什么理论说再多不如跑一个完整例子。这个实战案例我设计得很接近实际工作需求在当前的版图视图里自动生成一个由metal1矩形组成的dummy阵列再在四周画一圈metal2的guard ring走线最后在左上角加一行标注文字。为什么选dummy阵列因为这是版图自动化最典型的应用场景之一。芯片制造时金属密度不达标会导致CMP工艺不平衡所以经常需要大范围填充dummy图形。手动填充时一排排矩形点到手软用脚本只要几十行。为了让脚本更灵活我会把它写成带参数的形式。这样同一个脚本既可以生成5行5列也可以生成100行100列只需在执行时传参数不需要改代码。4.2 完整脚本与逐段拆解直接上完整脚本保存为genDummyArray.il; genDummyArray.il ; 在当前版图中生成 metal1 dummy 矩形阵列 metal2 边框 文字标注 procedure( genDummyArray( numRow numCol ) let( (cvId layerM1 layerM2 rectW rectH pitchX pitchY x y i j box ptList) ; 1. 获取当前编辑的cellview cvId geGetEditCellView() if( cvId nil then printf(Error: Please open a layout cellview first.\n) return(nil) ) ; 2. 定义层名和几何参数 layerM1 metal1 layerM2 metal2 rectW 1.0 ; 矩形宽度 rectH 1.0 ; 矩形高度 pitchX 3.0 ; X方向间距 pitchY 3.0 ; Y方向间距 ; 3. 循环生成Dummy矩形阵列 for( i 0 numRow-1 for( j 0 numCol-1 x j * pitchX y i * pitchY box list( list( x y ) list( xrectW yrectH ) ) dbCreateRect( cvId layerM1 box ) ) ) ; 4. 生成四周的金属边框路径guard ring ptList list( list( -5 -5 ) list( -5 (numRow-1)*pitchY rectH 5 ) list( (numCol-1)*pitchX rectW 5 (numRow-1)*pitchY rectH 5 ) list( (numCol-1)*pitchX rectW 5 -5 ) ) dbCreatePath( cvId layerM2 ptList 1.0 ) ; 5. 在左上角添加一行文字标注 dbCreateLabel( cvId layerM2 DUMMY_ARRAY_GEN list( 0 -10 ) lowerLeft R0 drawing ) ; 6. 输出统计信息 printf( Generated %d dummy rects (%d rows x %d cols).\n numRow*numCol numRow numCol ) t ) )下面逐段拆解。第一步获取cellview这个不用多说。如果当前没有打开任何版图geGetEditCellView()会返回nil所以做了个判断防止后面创建图形时报一堆莫名错误。第二步定义层名和几何参数。这里我把矩形尺寸、间距都抽成了变量后续要改成50x50间距1微米只需修改pitchX和pitchY或者定义一个起始坐标参数。这里有个设计细节pitchX是相邻矩形相同角点之间的间距不是矩形之间的净距所以矩形间隙等于pitchX - rectW 2微米。别小看这个区别很多人写循环时把它搞混结果生成出来的图形不是重叠就是间距超了。第三步是双层循环。外层循环控制行数i内层循环控制列数j每个矩形的左下角坐标为(j*pitchX, i*pitchY)右上角坐标在此基础上加宽度和高度。这个坐标公式本质上是把二维阵列映射到一维坐标计算里理解了这个批量化摆放器件也是同一个套路。第四步生成边框路径。注意ptList里的四个点我是按“左下-左上-右上-右下”顺序围一圈路径会沿着这四个点画一条闭合的折线。路径宽度设成1微米实际项目中会根据电流大小和DRC规则调整。第五步创建label层用metal2文字内容不明显坐标放在阵列下方避免和矩形重叠。第六步输出统计信息方便在CIW里确认脚本执行结果。4.3 在Virtuoso中加载运行并获得结果有了脚本文件运行方式非常简单。在Virtuoso里任意打开一个layout cellview然后在CIW窗口执行load genDummyArray.il这一步是把函数定义加载进内存。如果文件路径不对或者文件里有语法错误加载时会报错。加载成功后再执行genDummyArray( 5 8 )我设计成两个参数5行、8列。正常情况下CIW会打印Generated 40 dummy rects (5 rows x 8 cols).然后你再看版图窗口40个metal1矩形整整齐齐排列出来外围多了一圈metal2的边框左上角多了一行文字。整个过程只要几秒钟。这就是自动化相对手动操作的直观优势。如果你希望脚本加载后自动执行可以在文件末尾直接调用genDummyArray( 5 8 )这样load完就同时运行省掉手动敲命令的步骤。但我不建议在一开始就这么做因为调试时你会反复load每次都执行一遍如果版图上已经有图形就会出现重复创建。后面进阶时可以给脚本加一个“是否覆盖已有图形”的参数或者先删除指定层上的图形再重建。5. SKILL排错实战地图脚本跑不动的真正原因5.1 高频报错信息排查看板我见过太多人学SKILL半途而废不是因为难而是因为遇到报错不知道从哪里下手。这里把我在实际项目中踩过的报错归纳成一张表每一个都带真实场景报错特征常见原因解决方向syntax error/unbalanced parenthesis括号不匹配某个函数少写了右括号用SKILL IDE的括号匹配功能定位nil或*Error*出现在函数调用处函数名拼错或者该函数在当前版本不存在用buildString或getd查函数是否存在*Error* hiGetCurrentWindow: too few arguments函数参数个数不够查函数签名补齐参数图形没有出现在版图上但脚本没报错当前数据库没保存或脚本操作的是另一个cellview检查cvId获取方式是否用错layer not defined层名在当前工艺库中不存在LSW窗口确认层名后重新加载tfinstance already exists创建的实例名和已有实例重名换实例名或先删除同层/同名对象给新手一个建议看到报错第一反应不是去搜“什么原因”而是先打开SKILL IDE的调试器把错误栈展开看看报错前最后成功执行的是哪一行函数调用。报错信息在CIW里有时会淹没在一堆提示里但它在SKILL IDE里会高亮标出配合变量监视窗口问题基本能定位到具体某一句话。5.2 用打印输出定位逻辑问题print与printf有一种最隐蔽的错误脚本能跑完没有报错但生成的版图和你想象的不一样。这种时候打印输出是最好的朋友。我在写脚本时基本上每完成一个关键步骤都会加一句printf。比如前面那个dummy阵列脚本我会在创建每个矩形前打印一次当前坐标printf(Creating rect at (%f, %f) ...\n x y)这种方法看起来很土但确实是最快定位逻辑错误的方式。尤其当坐标是动态计算出来的你肉眼很难判断j*pitchX rectW是不是算对了打印出来一对比就清楚了。另一种场景是检查某个API调用是否成功。很多dbCreate函数失败时不会抛异常而是返回nil。如果你不检查返回值脚本会继续往下跑最后发现图少了半截。我的习惯是在一次批量创建图形前先单独打印返回对象rectId dbCreateRect( cvId layerM1 box ) if( rectId nil then printf(WARNING: Failed to create rect at (%f, %f)\n x y) else printf(Created rect id%x\n rectId) )这样一旦某个图形创建失败你能立刻知道是哪一层、哪个位置出了问题而不是在整个版图上瞎找。5.3 数据保存、窗口刷新与回滚习惯还有一个容易被忽略的环节保存与回滚。SKILL操作的是内存中的数据库对象如果没有执行dbSave关闭Virtuoso后一切回到原点。日常我建议在脚本最后显式保存dbSave( cvId )不过要注意如果你用的是geGetEditCellView()获取的在编辑cellview保存后的内容不会立即刷新到版图窗口。有时候要手动敲一下hiRedraw()或者dbSave之后等一会儿才能看到变化。我在脚本里一般不会主动调hiRedraw而是在CIW执行完脚本后按一下CtrlShiftR刷新视图。更重要的习惯是“可回滚”。脚本批量创建图形时一旦中间参数写错整个版图可能被塞满错误图形。手动删除几万个矩形噩梦。所以自动化脚本一定要设计回滚方案。最常见的方式是先做记录脚本开头用geGetWindowCellView()或者dbGetAllInsts记录当前已有的图形对象列表脚本执行后用dbDeleteObject删除。另一种方式是在脚本执行前复制一个cellview在Library Manager里copy一份但这种方式成本高、适合一次性验证不适合日常流程。我个人更推荐“删除重建”的思路在脚本开头增加一个参数overwrite如果为真就把指定层上所有相关图形全部删除然后再生成新的。这样脚本可以反复执行而不会产生垃圾图形。6. 从一次性脚本走向参数化单元SKILL的进阶路线建议6.1 怎么把脚本变成Virtuoso菜单里的按钮当你写了几个好用的脚本之后下一个需求一定是“能不能别每次都在CIW里敲load和函数名了”。答案是能而且方法不止一种。最简单的方法是在.il文件末尾直接调用函数这样只需要load一次。但更好的方法是把脚本绑定到菜单在Virtuoso的.cdsinit文件里加载脚本然后通过hiRegMenu或者菜单配置文件把命令挂到版图编辑器菜单栏。举个例子CIW菜单Tools - Customize - Menus可以打开菜单配置界面往里添加自己的命令。配置起来稍微有点繁琐但好处是一劳永逸以后每次启动Virtuoso菜单都在那里点击一下就能执行批量操作。另外一个是bindkey就是快捷键绑定。绑定的语法简答理解是“按某个键执行某段SKILL代码”。比如把F9绑定到dummy生成函数hiSetBindKey( Layout KeyF9 genDummyArray( 5 8 ) )这行代码放到.cdsinit里每次启动后按F9脚本自动执行。熟练之后你完全可以给自己配一套常用的版图操作快捷键把Virtuoso用成“自己的定制版”。6.2 从脚本到PCELL的参数化之路如果把SKILL脚本比作“一次性工具”PCELL就是“参数化工具”。PCELLParameterized Cell可以在放置实例时动态调整尺寸、层、电压等参数在模拟版图里被广泛应用——比如画一个尺寸可调的电阻或者MOS管。从脚本到PCELL本质上是从“执行一段固定流程”到“提供一个参数化描述对象”的升级。写PCELL需要用到dbCreateParamInst、pcDefinePCell等更底层的API还要理解“PCell评估”机制。这已经超越了入门范围但路线是清晰的先熟练掌握脚本里的坐标计算和图形创建再学会把设计的尺寸参数抽象成变量然后去了解PCell的代码结构。我个人对PCELL的看法是如果没有被重复折磨到一定程度不必一上来就硬啃。先实用地写普通脚本解决眼前问题等积累了几个脚本之后自然会发现PCELL适合那些“参数变来变去但结构不变”的器件。6.3 我对自动化脚本的“投资回报率”判断最后说一点个人体会。很多同事问我学SKILL到底值不值得我的判断标准很简单一个操作重复三次以上就值得写成脚本一个流程耗时超过半小时就值得认真设计自动化方案。正常情况下一个能解决实际问题的脚本花半天时间写但之后每次使用只花几分钟用个三五次就能回本。但也有一个反面教训不要过度自动化。有些脚本使用频率极低写一次要折腾一整天需要处理一堆边界情况还不如手动画。我有个判断口诀频率高、规则明确、步骤重复这三条满足两条以上才值得写脚本如果一次性或者没有固定规则就老老实实手动。写脚本这几年来我最大的收获不是省了多少时间而是养成了一种“结构化思考”的习惯。以前对着版图上的几百个图形只觉得是一团密密麻麻的多边形现在看它们会自动想到坐标、层次、连接关系甚至在动手之前就脑内跑了一遍操作步骤。这种视角的转变可能才是SKILL带给我最值钱的东西。
返回列表