Virtuoso 操作的高效替代方案

从 GUI 到 SKILL:操作背后的命令

众所周知,Virtuoso 界面里的大部分操作,都可以通过 SKILL 完成。如图:

在 Virtuoso 中操作 GUI 输出的 SKILL 代码

可以看到,打开 Logger 后,我们在 GUI 上的每一步操作,本质上都会对应执行某条 Virtuoso 的 SKILL 命令。

窗口的显示、操作与关闭都需要时间和资源;通过“操作 GUI 录制 SKILL 脚本”的方式去完成自动化设计,显然并不是一个优雅的方案。好在 Virtuoso 本身就提供了大量 API 供开发者直接调用,后续文章我也会逐步介绍。

SKILL 是 Cadence 基于 Lisp 的一门“方言”。这是官方的描述:

Cadence SKILL is a high-level, interactive programming language based on the popular
artificial intelligence language, Lisp. It lets you customize and extend your design
environment. Using SKILL, you can validate the steps of your algorithm incrementally before
incorporating them into a larger program.

SKILL 有两种常见写法:一种是 Lisp 风格(括号满天飞那种,我个人对这种风格有点不适),另一种是类 C 风格(后续文章将主要使用这种风格),对有现代编程语言经验的同学更友好。

前言

一般来说,快速学习一门新的编程语言,需要先了解这门语言的基本概念。

SKILL 与 Python、JavaScript 类似,是一门动态类型语言:这让调试与迭代非常方便,但也意味着一旦编码不规范,排查问题会异常艰难。
由于 SKILL 这门语言实在太过冷门,再加上 EDA 领域信息相对闭塞、生态偏闭源,导致即便到了 2026 年,依旧很难找到好用的语法分析/补全插件。
很多模拟电路工程师也并不以“写代码”为日常习惯,而 Cadence 提供的调试工具也确实不太友好……

所以在编写 SKILL 时,良好的编码习惯非常重要:注意空格、注意括号、注意缩进(多说无益,漫长的找错过程就是最好的课本~)

学好 SKILL,你就会明白:哪怕夜里点盏煤油灯,翻开实体书查 API 文档,用记事本编程也并非难事。
你也会明白:真正能拯救你的 SKILL 代码的,往往只有 Cadence 安装目录里的静态文档,以及 Cadence Community 讨论区里 Andrew Beckett 的回答……

OK,吐槽归吐槽,我知道各位为了工作还是得捏着鼻子用~下面就静下心来,先把基础语法掌握住。


基础语法

Hello World

古法编程老套路,先来一个我奶奶看了都会的 Hello World~

1
2
printf("Hello, World!")
; 将在 CIW 中输出 "Hello, World!"

标准输出:printf

printf 是 SKILL 中最常用的标准输出函数,用于将格式化的字符串输出到 Virtuoso 中的 CIW (Command Interpreter Window) 窗口中。

1
printf( t_formatString [ g_arg1 g_arg2 ... ] )

其中 t_formatString 的格式化控制字符串与 C 语言类似,具体可查看文章底部提供的 Format Symbol Table。

注释

单行注释(;

在 SKILL 中,使用分号(;)来添加单行注释,从分号开始到行末的内容将被解释器忽略。

1
; 这是一条 SKILL 注释:执行这行代码,什么都不会发生

多行注释(/* ... */

在 SKILL 中,使用 /* 和 */ 来包裹多行注释,其间的任何内容都会被解释器忽略。

1
2
3
4
5
/* 
这是 SKILL 中的多行注释
可以跨越多行
解释器会忽略这段内容
*/

数据类型与常用结构

字符串(string)

和其他编程语言类似,字符串用双引号包裹。

1
2
3
s = "This is  a string in SKILL"
printf("%s" s)
; >> This is a string in SKILL

布尔(bool)

在 SKILL 中,布尔类型通过 tnil 表达。

1
2
3
4
5
6
7
8
9
10
11
12
x = t
y = nil
if(x
printf("x is true\n") ; If x is true, then it will output
printf("x is false\n") ; If x is false, then it will output
)
; >> x is true
if(y
printf("y is true\n") ; If y is true, then it will output
printf("y is false\n") ; If y is false, then it will output
)
; >> y is false

符号(symbol)

这是 SKILL 中比较独特的类型,初学者容易将其与字符串混淆。Symbol 是一个唯一的、不可变的标识符。类似 JavaScript 中的 Symbol。

  • 语法:在标识符前加单引号 '
1
2
3
4
5
6
7
8
9
sym1 = 'myParam
sym2 = 'myParam
str1 = "myParam"

printf("Are symbols eq? %L\n" sym1 == sym2)
; >> Are symbols eq? t (相同 symbol 可以被视为是同一个)

printf("Is symbol eq string? %L\n" sym1 == str1)
; >> Is symbol eq string? nil (类型不同)

链表(list)

list 是一个链表结构,可以存储任意类型的数据作为其中的元素。在 SKILL 中,我们可以这样声明一个 list。

1
2
3
x = list(1 2 3) ; 放置了三个元素的 list 结构
printf("%A\n" x)
; >> (1 2 3)

list 这个关键字很容易让人误以为它是类似 Python List 的“数组结构”,但在 SKILL 中它是真正的单链表;如果用 nth 做“索引访问”,时间复杂度是 O(n)

链表访问:nth 与性能

严格意义上,list 并不存在真正的“索引访问”,因为它在 SKILL 内部以单链表形式存在,所以如上文所说是 O(n) 的复杂度。数据量很大时,尽量避免用 list + nth 访问指定元素,改用 makeVectormakeTable 往往更合适。

1
2
3
4
5
6
7
8
data = list("a" "b" "c")
val = nth(1 data)
printf("Index 1 is: %s\n" val)
; >> Index 1 is: b

; 如果索引越界,不会报错,而是返回 nil
outOfBound = nth(99 data)
; >> nil

哈希表(table)

在 SKILL 中,table 是一种键值对数据结构,用于存储与快速查找数据,类似 Python 的字典,通过 makeTable(<s_name> <?default_value>) 创建。

  • 创建/赋值/访问
1
2
3
4
5
6
7
myTable = makeTable("myTable")  
myTable["a"] = "value1"
myTable["b"] = 42
printf( "myTable.a: %s\n" myTable["a"])
printf( "myTable.b: %d\n" myTable["b"])
; >> myTable.a: value1
; >> myTable.b: 42
  • 查看所有键(keys)
1
2
3
4
5
myTable = makeTable("myTable")  
myTable["a"] = "value1"
myTable["b"] = 42
printf("keys: %A\n" myTable->?)
; >> keys: ("a" "b")
  • 访问不存在的键
    如果不指定 default_value 默认参数,那么访问 table 中不存在的键时,会得到 'unbound(一个 Symbol 值),表示该键未绑定。
1
2
3
myTable = makeTable("myTable")
printf("myTable[\"missing\"]: %s\n" myTable["missing"])
; >> myTable["missing"]: unbound

尤其需要注意:因为 'unbound 是 Symbol 类型,在 SKILL 中会被隐式当作真值(t)参与条件判断。这会导致“键不存在却被误判为存在”,从而引发逻辑错误。

1
2
3
4
5
6
myTable = makeTable("myTable")
if(myTable["missing"]
printf("This will print even though the key doesn't exist!\n")
printf("This won't print.\n")
)
; >> This will print even though the key doesn't exist!

nil:false / null / 空表

值得注意的是,nil 在 SKILL 中的作用不止是布尔值的 false。对于空的 list,其结果同样是 nil。更进一步,nil 往往同时承担现代编程语言里 falsenull、以及 空单链表 的含义:

  • 当你用 list() 构建空单链表时,返回值也是 nil
  • 因此 list() == nil 的结果为 t

这样的设计或许不“现代”,但它天然具备一定的惰性/节省内存的效果;对于 CellView 这类庞大数据结构,灵活的 nil 确实有现实意义。

迭代与遍历

既然你已经了解了 list 的底层结构(链表)以及 nth 的性能陷阱,那么现在介绍遍历机制就水到渠成了。
在 SKILL 中,控制流主要分为两类:基于集合的遍历 (foreach)基于数值的循环 (for)
说人话就是:foreach 的迭代以元素为单位,而 for 的迭代以数值为单位。一个迭代元素,一个迭代数值。

集合遍历:foreach

一般来说,针对 list 的遍历,我更建议使用 foreach,因为它足够简单、也更快。

  • 基本语法
1
2
3
4
foreach( iterator_variable list_expression
; 循环体语句
; ...
)
  • 示例
1
2
3
4
5
6
7
8
shapes = list("rect" "path" "polygon")

foreach(shape shapes
printf("Processing shape: %s\n" shape)
)
; >> Processing shape: rect
; >> Processing shape: path
; >> Processing shape: polygon

数值循环:for

for 循环在 SKILL 中主要用于 固定次数的迭代,类似 Python 的 for in range(start, end);但不同的是,Python 迭代范围是 [start, end),而在 SKILL 中是 [start, end]包含 end)。

  • 基本语法
1
2
3
for(i start end
; 循环体
)

它会自动将 istart 递增到 end(包含 end),步长固定为 1

  • 示例
1
2
3
4
5
6
for(i 1 5
printf("Count: %d\n" i)
)
; >> Count: 1
; ...
; >> Count: 5 (注意:包含了 5)

性能陷阱:for + nth

初学者常犯的错误是用 for 配合 nth 去遍历列表,我当年也是这么干的,直到一个 for 循环把 Virtuoso 干崩。

1
2
3
4
5
6
7
8
9
10
11
; ❌ 错误的写法 (极慢)
len = length(myBigList)
for(i 0 (len - 1)
val = nth(i myBigList) ; 随着 i 增大,nth 越来越慢
printf("%A\n" val)
)

; ✅ 正确的写法
foreach(val myBigList
printf("%A\n" val)
)

函数(procedure)

定义与语法

在 SKILL 中,通过关键字 procedure 定义函数。(虽然你可能会在一些古老代码中见到 defun,但本文只推荐类 C 的书写风格。)
使用 procedure 定义函数时,语法格式为:procedure(函数名(参数1 参数2) 语句),例如:

1
2
3
4
5
6
; 标准的函数定义
procedure(add(a b)
a + b
)
add(1 1)
; >> 2

这里需要理解一个核心概念:SKILL 是面向表达式的(expression-oriented)。这意味着函数体内的每一行代码都是一个表达式,都有返回值。

隐式返回(Implicit Return)

SKILL 函数的默认行为非常类似 Rust:函数不需要显式写出 return 关键字。函数体中最后一个表达式的计算结果,就是该函数的最终返回值。

1
2
3
4
5
6
procedure(getArea(width height)
printf("Calculating area...\n")
width * height ; 这一行的结果将自动作为函数返回值
)

area = getArea(10 20) ; area = 200

注意:在 C 语言习惯中,我们常随手写 return。但在 SKILL 中,return 并非一个简单的语法关键字,而是一个需要特定上下文(如 prog)才能生效的操作符。关于如何实现“提前返回(Early Return)”以及更复杂的控制流,我将在后续文章中详细展开。

若是没有 return,我无法想象编写 SKILL 会有多痛苦~

参数:必选 / @optional / @key

SKILL 的函数定义非常强大,支持普通位置参数、可选参数以及关键字参数,这在调用复杂的 Cadence API 时非常常见。

  • 必选参数:必须按顺序传入。
  • @optional:可选参数。如果不传,默认为 nil,或者可以指定默认值。
  • @key:关键字参数(Key-Word Arguments)。类似 Python 的 kwargs,调用时需要指定参数名,极大提高了代码的可读性。
1
2
3
4
5
6
7
8
9
10
11
12
procedure(createRect(layer box @optional (purpose "drawing") @key cv)
; layer: 必选
; box: 必选
; purpose: 可选,默认值为 "drawing"
; cv: 关键字参数,调用时需指定 ?cv
printf("Creating rect on %s:%s in cellview %A\n" layer purpose cv)
)

; 调用示例
createRect("Metal1" list(0:0 1:1)) ; 使用默认值为 drawing 的 purpose,cv 为 nil
createRect("Metal1" list(0:0 1:1) "pin") ; 覆盖 purpose
createRect("Metal1" list(0:0 1:1) ?cv myCvId) ; 指定 cv 参数

动态特性:运行时重定义

作为解释型语言,SKILL 的函数可以在运行时被 重新定义(redefined)。这对调试 SKILL 脚本非常有用——你可以在不重启 Virtuoso 的情况下,重新加载定义了函数的 .il 文件,新逻辑会立即生效。CIW 通常会提示 Function xxx redefined,这就表示热更新成功。

利用这一特性,再配合一些 IPC 操作,理论上也能实现类似 Web 前端开发中常见的“热更新”式 SKILL 开发调试机制。

讲到最后

本文虽是本公众号第一次编写文章,内容基础,却酝酿许久。
就基础而言,依然还有很多内容没有编写完全,但篇幅所限,一篇文章确实很难将这一大片内容都做解释,后续有时间再进行补充,也希望能对想学习 SKILL 的开发者有所帮助~


附录

printf 格式符号表(Format Symbol Table)

格式符 参数类型 输出描述
%d fixnum 十进制整数 (Decimal)
%o fixnum 八进制整数 (Octal)
%x fixnum 十六进制整数 (Hexadecimal)
%f flonum 浮点数,格式为 [-]ddd.ddd
%e flonum 科学计数法浮点数,格式为 [-]d.ddde[-]ddd
%g flonum 自动选择 %f%e 中较短的一种格式以节省空间。
:若指定宽度,可能会导致精度丢失。
%s string, symbol 打印字符串内容(无引号)或 Symbol 的名称
%c string, symbol 仅打印第一个字符
%n fixnum, flonum 通用数值打印
%P list 打印 Point 坐标点 (如 200:100)
%B list 打印 Box 边界框 (如 ((0 0) (10 10)))
%N any 旧式打印:不调用 对象的 printself 函数,直接打印内部结构。
%L list 列表的标准打印格式。
行为取决于 printpretty 变量:
nil: 行为同 %N
non-nil (默认): 对标准对象调用 printself 进行格式化
%A any 打印任意对象(会调用 printself 展示对象的代表形式)
公众号二维码

公众号:「芯上视图」