☰
Cadence SKILL脚本实战:从CIW到自动化版图与PCB设计效率提升
2026/10/2 7:24:22 网站建设 项目流程

干过版图或 PCB 设计的人,大概都经历过那种“人肉重复劳动”的崩溃时刻:几千个符号要改属性、上百个封装要核对位号、导出 BOM 的时候格式不对又得从头再来。Cadence 家的 Virtuoso、Allegro、IC251 这些工具,图形界面能帮你干八成的事,但剩下那两成最耗时的批量操作,恰恰是 SKILL 语言的绝对主场。SKILL 是 Cadence 内置的脚本语言,和 Lisp 有血缘关系,你在菜单里点到的几乎所有功能,底层都对应着一组 SKILL 函数;把函数按逻辑串起来,就是能代替你熬夜的自动化脚本。这篇指南不打算从语言史讲起,而是直接按“环境准备 → 函数清单 → 实战脚本 → 工程化 → 避坑”的顺序,把我自己在实际项目里验证过的东西一次说透。适合正在用 Cadence 做模拟版图、数字后端或 PCB 设计,又不想继续在重复操作里耗时间的工程师。读完后你会拿到可以直接抄的脚本骨架,也会知道哪些坑是文档里不会写的。

1. 为什么你的 Cadence 工作流里缺一个 SKILL 脚本

1.1 我最早被 SKILL 震撼到的场景

入门 SKILL 之前,我在 Virtuoso 里做过一次非常痛苦的版图整理:两千多个 instance 需要在指定层上加标识文本,并且要根据 cellName 自动生成内容。用界面一个个加,做得我想把鼠标摔了。后来旁边一位老工程师看不过去,在我 CIW 窗口敲了几行命令,一段循环跑完,不到一分钟,全部搞定。

就是从那一刻起我意识到:同样的时间,会 SKILL 的人已经在喝咖啡了,不会的人还在点鼠标。那次经历之后我开始系统查函数列表、看官方文档、拿真实项目练手,越深入越觉得这东西才是 Cadence 工具链里最被低估的生产力工具。

1.2 SKILL 到底是一门怎样的语言

很多人一听“脚本语言”就以为是 Python 那种风格,但实际上 SKILL 的语法非常特别。它有两种写法:一种是 Lisp 风格的括号形式,比如(plus 1 2);另一种是代数形式,比如1 + 2。两种写法在交互环境下都能被解释器接受。

这门语言从 1980 年代末就出现在 Cadence 工具里了,几十年积累下来,形成了一座非常庞大的函数库。粗略分一下,SKILL 函数大致覆盖这几类:

  • 通用编程:列表操作、字符串处理、流程控制、数学计算
  • 文件 I/O:读文件、写文件、目录扫描
  • 数据库操作:访问当前设计、遍历元件/Instance/Shape、创建和修改对象、设置属性
  • 图形界面:创建 Form、弹出提示框、注册菜单命令
  • 回调与控制:用户触发器、快捷键绑定、命令注册

也就是说,SKILL 不只是一个“宏录制工具”,它是一门能完成复杂业务逻辑的完整语言。

提示:SKILL 脚本文件常见的扩展名是.il,系统和用户自定义的函数都可以存放在这类文本文件里,加载后即可调用。命名时尽量别用check、skill这类容易被工具内部命令冲突的单词。

1.3 这篇实战指南的目标读者

如果你满足下面任一条件,这篇文章就是写给你的:

  • 你在用 Virtuoso / Allegro / IC251 做设计,每天有大量重复性操作
  • 你是 CAD 或 EDA 支持工程师,经常需要帮团队批量修数据
  • 你想把团队里的“老师傅经验”沉淀成可复用的工具
  • 你暂时还没写过 SKILL,但希望有一条不那么陡峭的学习路径

这篇文章不会停留在语法层面,我会尽量把“函数列表怎么查、脚本怎么搭、坑怎么躲”讲清楚。

2. 跑通环境与第一个脚本:从 CIW 到 Skill Debugger

2.1 不用安装任何额外环境,先找这些入口

SKILL 的运行时就在 Cadence 工具里面,不需要你单独安装。根据你用的工具不同,入口略有区别:

工具入口交互窗口
Virtuoso / IC251CIW 窗口下方的命令行直接输入 SKILL 表达式
Allegro / OrCAD PCB Editor命令窗口(输入skill进入)支持逐行输入和脚本加载
批量环境在 shell 里启动工具时附加脚本参数以-replay或脚本方式运行

以 Virtuoso 为例,启动后你会在 Command Interpreter Window(CIW)底部看到一行输入框,那就是 SKILL 解释器。你输入1+1回车,它会立刻返回2。

Allegro 稍微绕一点,默认命令窗口接收的是 Allegro 自身的命令,要先输入skill进入 SKILL 模式,才能写 SKILL 表达式。不想手动切换的话,也可以把调试内容直接写在.il文件里用load加载。

2.2 编辑器选型:VSCode、Vim 还是自带 IDE

SKILL 脚本本质是纯文本,用系统自带的文本编辑器就能写。但实际开发中,没有语法高亮和括号匹配,脚本稍微长一点就非常痛苦。

我自己的组合是:VSCode + SKILL 扩展 + 系统终端。VSCode 里搜索 SKILL 插件,可以让.il文件获得基础高亮和括号匹配,还能在文件侧边栏快速看到函数定义列表。这个“函数列表”能力对脚本开展特别重要,尤其当你的脚本积累到几百行时,能一眼跳到某个 procedure 所在位置,效率完全不一样。

如果你习惯在终端环境下工作,Vim 配合syntax on也能胜任。Cadence 工具链自带的 SKILL IDE(在部分版本中可用)虽然集成度更高,但启动稍重,我自己用得反而不多。

2.3 加载脚本的基本命令

SKILL 里加载一个脚本文件,最常用的命令就是load:

load("~/skill_scripts/demo.il")

路径可以写绝对路径,也可以写相对于当前工作目录的路径。加载成功后,解释器通常不会有特别显眼的提示,但脚本里定义的函数和变量就已经可用了。

如果你只是临时想试一段代码,不想写文件,可以直接在交互窗口粘贴表达式,回车即执行。在 Allegro 的 SKILL 模式下也是同理。

2.4 用一个最小脚本验证环境

新建一个文件,命名为hello.il,内容如下:

procedure( helloWorld( ) printf("Hello SKILL!\n") ) helloWorld()

然后在工具的交互窗口里执行:

load("hello.il")

如果看到Hello SKILL!的输出,说明环境已经通了。别小看这个流程,很多人的第一个坑是路径问题:load不支持~展开到某些工作目录,或者脚本文件的权限不对。我建议路径里优先用绝对路径,少用相对路径,能省掉很多低级报错。

3. 函数清单不是背出来的:按用途分类吃透核心 API

3.1 列表与字符串处理:SKILL 的 Lisp 基因

SKILL 里的核心数据结构是列表(list),它同时扮演了数组、队列、栈等多种角色。最开始写脚本时,你不需要精通所有列表函数,但这几个一定会频繁用到:

函数作用示例
list/'()构造列表list(1 2 3)或'(1 2 3)
car/cdr取首元素 / 去掉首元素car('(1 2 3))=>1
nth按下标取元素,下标从 0 开始nth(1 '(10 20 30))=>20
append合并列表append('(1 2) '(3 4))
cons在头部插入元素cons(0 '(1 2))=>(0 1 2)
length取长度length('(a b c))=>3
member判断元素是否存在member('b '(a b c))
foreach遍历列表foreach(x '(1 2 3) printf("%d\n" x))
mapcar对每个元素执行函数mapcar('lambda((x) x*x) '(1 2 3))
setof按条件筛选列表setof(x '(1 2 3 4) x>2)=>(3 4)

这里面最常用也最好用的是foreach和setof。setof相当于 Python 里的列表推导式,比如你想找出当前设计里所有 cellName 为INV的 instance,可以直接写:

setof(inst cellView~>instances inst~>cellName == "INV")

字符串方面,重点掌握三个:

  • strcat:字符串拼接;strcat("a" "b")返回"ab"
  • sprintf:格式化生成字符串,用于拼报告非常方便
  • parseString/buildString:字符串拆成列表 / 列表拼成字符串
strList = parseString("a,b,c" ",") ; 返回 ("a" "b" "c") newStr = buildString(strList "-") ; 返回 "a-b-c"

3.2 对象与数据库操作:和设计数据对话

如果说列表函数负责“处理数据”,那么数据库操作函数才是 SKILL 真正发光发热的地方。这类函数通常以db、ge、axl等前缀开头,不同工具覆盖不同领域。

在 Virtuoso 的版图环境里,最常用的几个:

函数作用
geGetEditCellView()获取当前正在编辑的 cellView
dbOpen/dbClose打开/关闭数据库
dbGetq查询对象属性
dbCreateRect创建矩形
dbCreatePath创建路径
dbCreateInstance创建 instance
dbGet按条件搜索并返回对象列表
dbCommit提交对数据库的修改

实际使用中你经常会看到类似这样的代码:

cellView = geGetEditCellView() foreach(inst cellView~>instances printf("Instance %s, cellName %s\n" inst~>name inst~>cellName) )

在 Allegro/PCB Editor 环境下,函数前缀通常是axl:

design = axlDBGetDesign() foreach(comp design~>components printf("位号 %s\n" comp~>refdes) )

这里的~>是 SKILL 里的属性访问符,类似 C 语言的.。先通过~>取出对象属性,再通过函数修改对象,几乎就是数据库操作的全部套路。

注意:不同版本中,元件的“封装名”字段可能是package,也可能是symbol。第一次在陌生版本上写脚本时,建议先用类似comp~>?的方式查看对象可用属性,再确定字段名。这样能避免绝大多数“函数名或属性名写错”的冤案。

3.3 用户交互与回显:让脚本更可用

写脚本时,如果所有结果都只在终端里输出,别人用起来会很不友好。SKILL 提供了一系列交互函数,能让脚本具备简单的“界面感”。

最基础的是回显函数:

  • printf:格式化输出到当前窗口
  • println:输出一行,自动加换行
  • hiDisplayMessage:在图形界面弹出一段提示

更进一步,你可以用axlUIPopup(Allegro)或hiDisplayForm(Virtuoso)创建简单的弹窗。对于大多数内部自动化脚本,我不建议一上来就设计庞大的图形界面,先用一个弹窗提示结果就够了,后面有需要再迭代。

菜单注册也是交互增强的关键。Allegro 里可以通过axlCmdRegister把 SKILL 函数注册成命令,这样用户在命令栏输入一个自定义单词就能执行脚本:

procedure( myCheck( ) printf("运行检查...\n") ) axlCmdRegister("myCheck" 'myCheck)

注册之后,在 Allegro 命令窗口直接输入myCheck,就等价于调用这个函数。Virtuoso 则可以通过hiRegMenu往菜单栏挂按钮,方式虽不同,思路一致。

3.4 如何高效查阅官方函数参考

Cadence 自带的 SKILL 函数参考文档非常厚重,第一次看的人容易懵。我的经验是:遇到不确定的函数时,在 SKILL 交互窗口使用在线帮助函数,比翻 PDF 快得多。常见的帮助函数包括:

  • describe('axlDBGetDesign):查看函数说明
  • ? axlDBGetDesign:查看属性帮助(部分环境支持)
  • getSkillPath():查看搜索路径

搜索资料时要留一个心眼:网络上的 SKILL 代码风格差异很大,有些老脚本用了不少已经被替代的旧函数,跑起来会报 deprecated 警告。优先以你实际安装版本的官方文档为准,网上的代码只做参考思路,不要无脑照抄。

4. 实战拆解:用 80 行脚本完成元件位号与封装体检

4.1 先别写代码,先拆需求

很多初学者拿到需求就开写,结果写到一半发现逻辑漏洞百出。正确的做法是先快速拆解任务边界。我以一个真实场景为例:PCB 设计交付前,需要检查所有元件是否有位号、封装是否缺失、坐标是否超出板框范围。这个任务如果靠人工翻版图看属性,几千个元件看到怀疑人生;用 SKILL 做,逻辑其实很清晰:

  1. 获取当前设计对象
  2. 遍历所有元件,逐个读取位号、封装、坐标属性
  3. 按规则判断是否异常
  4. 把异常信息收集到列表里
  5. 生成报告文件并用弹窗提示摘要

这五步不涉及任何复杂的算法,核心就是对“元件对象列表”的一次过滤和统计。我刻意选择这个例子,是想说明一个最重要的观点:任何自动化脚本的第一产出物不是代码,而是逻辑流程图。你先把步骤画出来(在纸上或文档里),再动手会顺利很多。

4.2 获取设计数据和元件清单

在 Allegro 环境下,当前打开的设计可以通过axlDBGetDesign()获取。拿到 design 对象后,再通过design~>components就能得到元件列表。

design = axlDBGetDesign() components = design~>components printf("当前设计共 %d 个元件\n" length(components))

如果你的环境里components字段名不一致,用design~>?查看一下当前设计对象有哪些可访问属性,再替换即可。

4.3 遍历元件并执行三项检查

位号检查最直接,判断comp~>refdes是否为空即可。封装检查要稍微小心,因为有的元件(比如测试点、安装孔)可能本身就不需要封装,这时需要额外的排除规则。坐标检查则要和板框范围比较。

下面是一段核心遍历代码:

foreach(comp components when( comp~>refdes == nil problems = cons("发现无位号元件" problems) ) when( comp~>package == nil problems = cons(sprintf(nil "元件 %s 缺少封装" comp~>refdes) problems) ) when( comp~>x < 0 || comp~>x > boardWidth problems = cons(sprintf(nil "元件 %s 超出板框 X 范围" comp~>refdes) problems) ) )

这里我用cons把问题字符串不断加到列表头部。之所以用cons而不是append,是因为cons是 O(1) 操作,append在循环里反复调用会不断创建新列表,性能差一个数量级。遍历结束后再用reverse把列表翻回来,保证报告里的顺序和元件顺序一致。

提示:sprintf(nil "格式串" arg...)这种写法会把格式化结果作为返回值直接交给变量或列表,在需要拼接报告行时非常常用。注意sprintf的第一个参数传nil在大多数 SKILL 版本里返回生成的字符串,但个别版本行为略有差异,遇到问题就改成先声明一个变量再接收。

4.4 生成报告文件并弹窗提示

问题收集完,接下来是落盘和提示。文件操作在 SKILL 里非常直观:用outfile打开一个输出流,用fprintf写入内容,最后close关闭。

outPort = outfile(reportFile) fprintf(outPort "元件体检报告\n") fprintf(outPort "检查时间:%s\n" getCurrentTime()) fprintf(outPort "共检查 %d 个元件,发现问题 %d 项\n\n" length(components) length(problems)) foreach(prob problems fprintf(outPort "%s\n" prob) ) close(outPort)

这一步有几个容易忽略的细节:

  • 输出路径所在目录要存在,否则outfile会失败
  • 写完一定要close,否则文件内容可能没有真正刷新到磁盘
  • 报告里最好带时间戳和统计总数,这在多人协作时能快速确认数据是否最新

最后用axlUIPopup弹一个摘要提示,用户体验立刻提升:

if( problems then axlUIPopup(sprintf(nil "发现 %d 个问题,详见 %s" length(problems) reportFile)) else axlUIPopup("检查通过,未发现问题") )

4.5 完整脚本示例与注册命令

把所有片段拼起来,加上头注释和命令注册,就是一个完整可用的工具脚本:

; 元件体检脚本 ; 适用:Allegro / PCB Editor ; 用法:load 本文件后,在命令窗口输入 ckdesign procedure( ckDesign( @optional (reportFile "/tmp/design_check.txt") ) let( (design components problems outPort total) problems = nil design = axlDBGetDesign() when( design == nil error("未获取到当前设计对象,请先打开PCB设计\n") ) components = design~>components total = length(components) ; 逐项检查 foreach(comp components when( comp~>refdes == nil problems = cons("发现无位号元件" problems) ) when( comp~>package == nil problems = cons(sprintf(nil "元件 %s 缺少封装" comp~>refdes) problems) ) when( comp~>x < 0 || comp~>x > 1000 || comp~>y < 0 || comp~>y > 1000 problems = cons(sprintf(nil "元件 %s 坐标超出预设范围(%.2f, %.2f)" comp~>refdes comp~>x comp~>y) problems) ) ) problems = reverse(problems) ; 写报告 outPort = outfile(reportFile) fprintf(outPort "元件体检报告\n") fprintf(outPort "检查对象:%s\n" design~>name) fprintf(outPort "共检查 %d 个元件,发现问题 %d 项\n" total length(problems)) foreach(prob problems fprintf(outPort "%s\n" prob) ) close(outPort) ; 弹窗提示 if( length(problems) > 0 then axlUIPopup(sprintf(nil "发现 %d 个问题,详见 %s" length(problems) reportFile)) else axlUIPopup(sprintf(nil "%d 个元件检查通过" total)) ) ) ) axlCmdRegister("ckdesign" 'ckDesign)

从“函数列表”到“自动化脚本”,本质上就是这一步:先确定你要操作的对象类型,然后去函数清单里找对应的访问函数和属性,再按业务逻辑用列表函数把它们组织起来。这个脚本虽然不算长,但已经具备了一个生产级工具的基本要素:参数默认值、错误检查、日志输出、用户反馈、命令注册。

5. 从“能干”到“高效”:脚本工程化改造

5.1 把脚本从一次性“临时文件”变成复用工具

刚学会 SKILL 时,很多人写的脚本是“一次性”的:需求来了,临时写一个几十行的tmp1.il,跑完就扔。这种方式的痛点是:三个月后同样的需求再来,你发现自己完全不记得当初的逻辑,只能重新写一遍。

我建议从第一次写正式脚本时就做一个动作:把常用功能封装成procedure,并把文件名、函数名、用途都写清楚。前面的ckDesign就是一个例子。你甚至可以往这个函数里加一个@optional参数,让它可以支持不同的板框范围或报告路径,这样它就能在不同项目中复用。

5.2 参数化设计与默认值处理

SKILL 的procedure支持三种参数:必选参数、可选参数@optional、关键字参数@key。设计一个可复用函数时,优先考虑哪些值应该暴露成参数。例如:

procedure( ckDesign( @key (boardWidth 1000) (boardHeight 1000) (reportFile "/tmp/design_check.txt") ) let( (...) ... ) )

调用时的写法会变成:

ckDesign(?boardWidth 2000 ?reportFile "./report.txt")

@key参数的优点在于:调用时不需要记住参数顺序,代码可读性也更好。别把默认值写死在业务逻辑里,这是脚本工程化的第一课。

5.3 注重性能:减少重复查询和路径遍历

SKILL 脚本处理几百个对象时,性能根本不是问题;但一旦碰到上万级的 instance 或 shape,写法和写法之间的差距就会非常明显。我踩过的最典型的性能坑,是在循环里反复调用dbOpen或反复访问同一个对象属性,导致每次循环都触发一次数据查询,最终把一个本该几秒完成的脚本拖成几分钟。

优化策略其实就三条:

  • 循环外缓存对象引用:能在foreach外面取到的对象,绝不在循环里重复取
  • 避免频繁拼接大字符串:反复用strcat拼接一个几千行的报告,性能极差;改用列表收集,最后一次性buildString或逐行fprintf
  • 能用setof遍历就用setof:筛选逻辑优先用声明式写法,而不是手写foreach+when再加cons

下面这段对比很直观:

; 低效写法:每次判断都查一次属性链 foreach(inst cellView~>instances when( inst~>cellName == "INV" invList = cons(inst invList) ) ) ; 高效写法:声明式筛选,语义也更清晰 invList = setof(inst cellView~>instances inst~>cellName == "INV")

5.4 错误恢复与事务提交

还有个容易被忽视的问题:脚本运行到一半报错,设计数据库可能处于“半改动”状态。在 Virtuoso 里,很多dbCreate*操作必须显式调用dbCommit或依赖特定的事务机制;在 Allegro 里,修改数据库后要确保设计被正确保存。我写脚本的习惯是:

  • 所有可能失败的输入,先用when或if做保护判断
  • 对“只读类”的检查脚本,不修改数据库,从源头规避风险
  • 对“写操作类”脚本,跑完立刻验证关键对象是否存在,再决定是否保存
  • 关键步骤之间加printf日志,这样即使中途挂了,也能定位到在哪一步挂的

6. 翻车记录与经验补丁:SKILL 开发的真实代价

6.1 改了数据库却没生效:事务和保存的坑

我在 Virtuoso 里写第一个自动创建图形的脚本时,跑完没有任何报错,对象却始终看不到。查了半天才发现,创建的对象停在内存里,没有提交到数据库。类似的问题在 Allegro 里也会出现:修改了元件属性,replay 结束一看,界面还是老样子。

建议:写写操作脚本时,把“提交/保存”步骤当成一个独立的验证节点。完成操作后主动执行保存或同步操作,并确认这一步真的在脚本流程里被执行到了。另外,有些版本的 Allegro 在命令行模式下操作完需要手动刷新视图,脚本里也可以主动执行redraw或类似命令让界面同步。

6.2 整数与浮点数混用的隐性错误

SKILL 对数字类型的处理相对宽松,但并不是没有坑。尤其是坐标计算:如果两个整数相除被当作浮点,某个版本的语义可能和你的预期不一致;坐标精确到小数点后几位时,用整数计算和用浮点计算的结果可能完全不同。

我的建议是:涉及坐标、长度、角度等物理量的运算,一开始就把所有数值显式转换成浮点:x = float(comp~>x)。不要依赖 SKILL 的自动类型提升,显式转换能让行为可预期。

6.3 全局变量污染与作用域泄漏

SKILL 的变量作用域设计很自由,自由到容易出事。如果你在脚本顶层写了一个problems = nil,然后在某个procedure里又直接用了problems,这个变量就会在全局空间里被共享。多个脚本加载时,很容易出现“这个脚本改了那个脚本的变量”的诡异问题。

最安全的做法是:所有临时变量都放进let或prog的局部作用域里。procedure的参数本身就是局部的,函数内部再套一个let包住中间变量,基本就能杜绝变量污染。我在前面示例代码里已经用了let包裹,这不是炫技,是真的被坑怕了。

6.4 异常处理不够健壮导致半途崩溃

SKILL 有errset和error机制,可以捕获运行时异常:

errset( ; 可能出错的代码 load("/path/to/some.il") )

errset在代码出错时不会中断整个脚本,而是把错误信息返回。这在批量处理多个文件时很有用:一个文件坏了,跳过它,继续处理下一个。我自己在写批量脚本时几乎必用errset,它能把脚本的“单点失败”变成“单点告警”。

6.5 命令行注册冲突

用axlCmdRegister注册自定义命令时,一定要确认命令名没有和工具内置命令冲突。我遇到过注册一个叫update的命令,结果某个操作触发了内置同名命令,行为完全不可控。自定义命令最好加一个独特的前缀,比如公司缩写或你的名字缩写,像qaUpdate、ckdesign这样。

6.6 交付给别人的脚本要留“逃生舱”

最后一条建议,和语法无关,但很重要:如果你写的脚本要交给团队里其他人用,一定要让脚本在最开始、最容易出错的地方给出明确提示。比如判断当前有没有打开设计、文件路径是否存在、对象列表是否为空。别让同事拿到脚本后面对一个莫名其妙的nil报错,还要反过来找你排查。

我通常会在脚本头部加一段环境自检:

when( axlDBGetDesign() == nil error("请先打开需要检查的 PCB 设计文件\n") )

这一句看似多余,实际能节省所有使用者的理解成本。

7. 最后分享一点个人体会

从函数列表到自动化脚本,真正质变的节点不是“记住了多少函数”,而是“看到需求后能稳定地拆出数据流”。我刚入门时,总喜欢到处收集别人的脚本代码,后来发现最有用的还是自己翻官方文档、在交互窗口里一个个试函数、拿真实设计练手。SKILL 的学习曲线初期稍陡,但只要过了“列表操作 + 属性访问 + 流程控制”这三关,你就能应付绝大多数日常自动化需求。

我自己现在的工作习惯是:任何操作如果在界面上需要重复超过三次,就停下来想想能不能用 SKILL 解决。哪怕第一次写脚本花的时间比手动操作还久,但它的价值会在第二次、第三次使用时迅速体现出来。工具链里最有价值的不是某个菜单项,而是你能不能把几个函数列表里的碎片拼成你自己的“时间机器”。希望你读完这篇文章后,也能动手写一个属于自己的.il脚本,然后把省下来的时间花在真正需要设计师判断力的事情上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询