SKILL 教程
基础语法 流程与函数 版图数据库 实战进阶 参考篇
SKILL · Virtuoso · for layout engineers

把版图操作,
写成代码

一本给版图工程师的 SKILL 教程 —— 从一对括号讲起,到会写批量脚本、快捷键工具和 pcell。会画版图就够了,不用先会编程。

零代码基础 5 部分 · 26 章 可直接复制运行
Part 01 · 衬底 基础语法
第 1 章 · 认识 SKILL

SKILL 和 Virtuoso 是什么关系

SKILL 是 Cadence 公司 Virtuoso 平台内置的脚本语言。它不是外挂,而是 Virtuoso 的一部分 —— 你在 CIW 里敲的每一行命令、你点的很多菜单按钮,底层都是 SKILL 代码在跑。

它有两个特点:

  • 专门操控版图 / 原理图数据库:你看到的每个矩形、每根 path、每个 instance,背后都是数据库里的对象(dbId),SKILL 能直接读写它们。
  • 语法长得像 Lisp:Lisp 是 1958 年诞生的语言,极其古老也极其强大。SKILL 继承了它的核心思想。

为什么版图工程师要学它

  • 省时间:同样的操作做 100 遍 → 写个循环,1 秒搞定。
  • 不出错:批量改宽度、批量打 via、自动画 guard ring → 手动画错一个就废一片,脚本不会手抖。
  • 能看懂环境:很多 PDK 环境靠一堆 .il 文件运转,会 SKILL 才能读懂、修改、排障。
  • 能写 pcell:参数化单元是 SKILL 的高级应用,是版图工程师的进阶硬技能。

一句话看懂 SKILL 的样子

println("Hello, Virtuoso!")

读法:调用函数 println,参数是字符串 "Hello, Virtuoso!"

核心语法只有一条:一切都要用括号 () 包起来,函数名写在最前面,参数跟在后面用空格隔开。

本教程里 => 表示「这段代码的结果是」,不是代码的一部分,不要敲进去。

第 2 章 · 运行环境

CIW、.cdsinit、.il 文件、load

CIW:SKILL 的命令行

打开 Virtuoso 后,标题叫 CIW(Command Interpreter Window) 的窗口就是 SKILL 的交互环境,分三块:顶部菜单栏、中部日志区、底部输入框(提示符 >)。

println("Hello, Virtuoso!")

回车,如果日志区打印出这句话,你就已经会跑脚本了。

新手第一大坑:引号必须是英文双引号 "。中文引号 “” 会直接报错。

.il 文件:把代码存起来

正式做法是把代码写进后缀为 .il 的文本文件,再加载。

; 分号开头是注释,SKILL 会忽略它,写给自己看的
println("我是从文件里加载的")
load("/home/you/skill/my_first.il")

load绝对路径。Windows 反斜杠写成 \\ 或改成 /

两个救命函数

loadi("/path/to/file.il")   ; 报错时精确定位到第几行(调试神器)
help("函数名")              ; 查帮助,如 help("dbCreateRect")

调试一律用 loadi 代替 load

.cdsinit:开机自动执行

Virtuoso 启动时会自动执行 .cdsinit。想每次开机就加载你的脚本、设置快捷键,就把 load(...) 写进去。新手建议先手动 load 测通过再写进去。

第 3 章 · 语法基础

括号、前缀表达式、注释

规则一:一切皆括号

println("你好")      ; 1 个参数
strcat("a", "b")    ; 2 个参数,结果是 "ab"
list(1 2 3)         ; 3 个参数

规则二:函数名在最前面(前缀表达式)

(+ 3 4)        ; => 7
(* 2 5)        ; => 10
(- 10 3)       ; => 7
(/ 10 4)       ; => 2     整数相除得整数!
(/ 10.0 4)     ; => 2.5   有一个是小数才得小数

类比:就像 Excel 里 =SUM(A1:A10),函数名在前、参数在后,只是 SKILL 用空格分隔参数。

多层嵌套从最里面往外读:

(* (+ 1 2) 4)     ; 先算 (+ 1 2) = 3,再算 (* 3 4) = 12

规则三:分号是注释

; 这一整行都不执行
width = 2.5   ; 这里也可以写注释

空白、换行、大小写

SKILL 不依赖换行和缩进(和 Python 完全不同),只要括号配对正确,随便换行加空格。缩进是给人看的,约定每个嵌套层缩进 2~4 格。

变量名和函数名区分大小写Widthwidth 是两个变量。约定全小写,多词用下划线或驼峰。

你熟悉的SKILL 里
3 + 4(+ 3 4)
MAX(1,2)(max 1 2)
A * (B + C)(* A (+ B C))
备注文字; 备注文字
第 4 章 · 数据类型

八种数据类型

类型例子说明常用度
整数 fixnum5没有小数点★★★
浮点 flonum2.5有小数点★★★
字符串 string"M1"必须英文双引号★★★
符号 symbol'M1前面加单引号★★
列表 list(1 2 3)第 7 章专门讲★★★
布尔t / nil真 / 假★★★
dbIddb:12345678版图对象的身份证号★★★
数组 arraymakeArray先了解

整数与浮点

a = 10        ; 整数
b = 10.0      ; 浮点
c = 1.5e3     ; 科学计数法 => 1500.0
(/ 5 2)       ; => 2   (整数除法砍掉小数)
(/ 5.0 2)     ; => 2.5
版图坐标、尺寸几乎都用浮点,养成习惯:尺寸写 10.0 而不是 10,避免整数除法出意外。

字符串与符号

name = "M1"    ; 字符串,版图层名用它
'M1            ; 符号,前面是单引号

版图层名用字符串 "M1",别用符号 'M1,否则很多函数会报错。

布尔 t 与 nil

flag  = t      ; t = 真
flag2 = nil    ; nil = 假,也是"空/没有"

nil 特别重要:操作没拿到对象时(没选中的图形、找不到的 cell),结果就是 nil。判断「有没有东西」就靠它。

dbId:版图对象的身份证

cv = geGetEditCellView()   ; cv 就是一个 dbId(当前 cellview)
shapes = cv~>shapes         ; shapes 是一堆 dbId 的列表

SKILL 干的事,本质是三步:拿到 dbId → 读它的属性 → 改它。把 dbId 想象成「这块矩形的身份证号」。

dbIsId(x)      ; x 是不是 dbId
dbIsShape(x)   ; x 是不是图形
dbIsInst(x)    ; x 是不是实例

查看类型

type(5)        ; => fixnum
type(2.5)      ; => flonum
type("M1")     ; => string
第 5 章 · 变量与作用域

赋值、全局、局部

赋值

width = 2.5
count = 10
name  = "M1"
setq(width 2.5)   ; 和 width = 2.5 完全一样

作用域:全局 vs 局部

全局变量:函数外面直接 = 赋值,到处能用。

局部变量:函数里用 let 声明,函数结束就消失,外面看不到。

procedure( calcArea(w h)
  let((area)          ; area 是局部变量
    area = w * h
    area              ; 返回 area
  )
)
println(area)   ; 报错!area 在函数外不存在
铁律:函数内部的临时变量,一律放进 let否则脚本一长,全局变量互相污染,改一个变量别处莫名跟着变。

let 的写法

let((x)
  x = 5
  x * 2
)
; 声明多个,变量和初值成对出现
let((x 0  y 10)
  x + y          ; => 10
)

prog 与 return

proglet 类似,但支持 return 提前退出:

procedure( findFirstRect(cv)
  prog((result)
    result = nil
    foreach(s cv~>shapes
      when(s~>objType == "rect"
        result = s
        return(result)   ; 找到就提前退出
      )
    )
    nil
  )
)
第 6 章 · 运算符

算术、比较、逻辑

算术(都是函数)

(+ 3 4)     ; => 7
(- 10 3)    ; => 7
(* 2 5)     ; => 10
(/ 10 4)    ; => 2
(max 1 5 3) ; => 5
(min 1 5 3) ; => 1
(abs -5)    ; => 5
(mod 10 3)  ; => 1   (取余)

判断奇偶常用 mod i 2 == 0(偶数)。

比较

(== 5 5)    ; => t   等于
(!= 5 4)    ; => t   不等于
(> 5 4)     ; => t
(< 4 5)     ; => t
(>= 5 5)    ; => t
(<= 4 5)    ; => t
== 是比较,= 是赋值,别混。= 永远不用于判断。字符串比较用 equal / strcmp

逻辑

(and 条件1 条件2)     ; 与
(or  条件1 条件2)     ; 或
(! 条件)              ; 非
; 中缀写法更常用:
s~>objType == "rect" && s~>lpp == list("M1" "drawing")
a > 5 || b > 5

运算优先级

前缀表达式没有优先级问题 —— 括号已经定死了顺序:

; 平时写 (3 + 4) * 5
(* (+ 3 4) 5)     ; => 35
第 7 章 · 列表与坐标

SKILL 的核心数据结构

创建列表

myList = list(1 2 3 4)
myList = '(1 2 3 4)     ; 单引号 = "照原样别计算",等价

坐标点:特殊的列表

pt  = 0:0        ; 坐标点 (x:y),等价 list(0 0)
pt2 = list(10 5)

常用操作(全都要会)

L = list(10 20 30 40)
car(L)             ; => 10         第一个元素
cdr(L)             ; => (20 30 40) 去掉第一个后剩下的
nth(0 L)           ; => 10         第 0 个(从 0 开始数!)
nth(2 L)           ; => 30
length(L)          ; => 4
cons(5 L)          ; => (5 10 20 30 40)
append(L list(50)) ; => (10 20 30 40 50)
member(20 L)       ; => (20 30 40) 找不到返回 nil
memberp(20 L)      ; => t          "在不在列表里",好用
reverse(L)         ; => (40 30 20 10)
last(L)            ; => 40
nth 从 0 开始数nth(0 L) 是第一个元素)。新手 90% 的 off-by-one(差一位)错误都在这。

高阶函数

mapcar('length '((1 2) (1 2 3)))     ; => (2 3)
mapcar(lambda((x) x * 2) '(1 2 3))   ; => (2 4 6)
sort(list(3 1 2) 'lessp)             ; => (1 2 3)

版图里列表长什么样

path~>points   ; => ((0 0) (10 0) (10 5))
cv~>shapes      ; => (db:123 db:456 ...)
shape~>bBox     ; => (0:0 10:5)

学会列表操作,就是学会操作版图数据的「容器」。

第 8 章 · 字符串

拼接、转换、匹配

基础操作

strcat("M", "1")           ; => "M1"
strlen("hello")            ; => 5
substring("hello" 0 2)     ; => "he"
upperCase("m1")            ; => "M1"
lowerCase("M1")            ; => "m1"

数字与字符串互转

atoi("123")      ; => 123
atof("2.5")      ; => 2.5
sprintf(nil "%d" 123)    ; => "123"
sprintf(nil "%.2f" 2.5)  ; => "2.50"
拼接「文字 + 数字」必须先转字符串:strcat("第" sprintf(nil "%d" 3) "个"),直接 strcat("第" 3 "个") 会报错。

拆分与匹配

parseString("M1 M2 M3")     ; => ("M1" "M2" "M3")
rexMatchp("M[0-9]+" "M1")   ; => t   M1 是否匹配"M后跟数字"
rexReplace("M" "V" "M1")    ; => "V1"

正则表达式是另一个专题,入门会用 rexMatchp 判断「字符串是否符合模式」即可。

Part 02 · 多晶 流程与函数
第 9 章 · 条件与分支

if / when / cond / case

if

if( w > 10 then
  println("很宽")
else
  println("不宽")
)

单行可写 if(w > 10 "big" "small")then / else 是关键字。

when 与 unless

when( w > 10 println("很宽") )     ; 只有真才执行
unless( w > 10 println("不宽") )   ; 只有假才执行

when 能执行多句(自动包住后面所有表达式)。版图脚本里 when 用得极多。

cond:多分支

cond(
  (w > 10  "big")
  (w > 5   "medium")
  (t       "small")   ; t 兜底
)

case:精确匹配

case(layerName
  ("M1"  "第一层金属")
  ("M2"  "第二层金属")
  (t     "其他层")
)
重要习惯:读对象属性前,先判断对象非 nil(and (obj != nil) (obj~>width > 2))
第 10 章 · 循环

for / foreach / while

for:数数循环

for(i 0 4
  println(i)
)
; 打印 0 1 2 3 4

for(i 0 10 2       ; 带步长 i = 0,2,4,6,8,10
  println(i)
)

foreach:遍历列表(最常用!)

foreach(shape cv~>shapes
  println(shape~>objType)      ; 打印每个图形类型
)

; 并行遍历多个列表
foreach((x listA) (y listB)
  println(list(x y))
)

while:条件循环

i = 0
while(i < 3
  println(i)
  i = i + 1     ; 千万别忘,否则死循环
)

版图脚本的循环套路

90% 的循环是这两种:

; 1. 遍历图形
foreach(shape cv~>shapes ...)

; 2. 生成阵列坐标
for(r 0 rows-1
  for(c 0 cols-1
    x = x0 + c * pitchX
    y = y0 + r * pitchY
  )
)
第 11 章 · 函数进阶

可选参数、可变参数、lambda

基本定义回顾

procedure( rectArea(w l)
  let((area)
    area = w * l
    area
  )
)
rectArea(2 5)   ; => 10

可选参数 @optional

procedure( greet(name @optional (title "工程师"))
  strcat(title, " ", name)
)
greet("张三")            ; => "工程师 张三"
greet("张三" "版图")     ; => "版图 张三"

可变参数 @rest

procedure( mySum(@rest nums)
  let((total 0)
    foreach(n nums
      total = total + n
    )
    total
  )
)
mySum(1 2 3 4)   ; => 10

关键字参数 @key

procedure( makeRect(@key (width 10) (height 5))
  list(width height)
)
makeRect(?width 20 ?height 8)   ; 用 ?名字 传参,顺序可乱
makeRect()                      ; => (10 5)

lambda:匿名函数

mapcar(lambda((x) x * 2) '(1 2 3))   ; => (2 4 6)
apply(lambda((a b) a + b) list(3 4)) ; => 7

递归(了解)

procedure( countdown(n)
  when(n > 0
    println(n)
    countdown(n - 1)   ; 调用自己
  )
)
countdown(3)   ; 打印 3 2 1
递归必须有终止条件。遍历大量数据优先用 foreach,递归留给层次化结构(第 18 章)。

返回值

SKILL 函数返回最后一个表达式的值,提前返回用 return(在 prog / procedure 里)。

Part 03 · 金属 1 版图数据库
第 12 章 · 数据库结构

库 → 单元 → 视图 → 图形/实例

你屏幕上的版图,在数据库里这样一层层套起来:

Library(库,如 mylib)
  └─ Cell(单元,如 INV)
       └─ CellView(视图:layout 版图 / schematic 原理图)
            ├─ Shape(rect 矩形 / path 线 / polygon 多边形 / label 文字)
            └─ Inst(实例:引用别的 cell 的复制品)
术语含义你熟悉的说法
Library库管理器里的「库」
Cell单元INV、NAND 等设计单元
CellView单元视图layout 版图 / schematic 原理图
Shape图形矩形、线、多边形、文字
Inst实例按 i 放进去的、引用别的 cell 的对象
Master母体被实例引用的那个源 cellview

SKILL 所有版图操作,本质都是在这棵树里「找到某个节点,然后读写它」。理解这四层,就能读懂任何版图脚本的骨架。

第 13 章 · 获取 cellview

geGetEditCellView / dbOpenCellViewByType

拿到当前正在编辑的 cellview

cv = geGetEditCellView()

有了 cv 就等于拿住了整个画布,这是版图脚本最常用的第一行。返回 nil 表示当前没有正在编辑的版图窗口。

读基本信息

cv~>libName      ; 库名
cv~>cellName     ; 单元名
cv~>shapes       ; 所有图形(dbId 列表)
cv~>insts        ; 所有实例(dbId 列表)
cv~>bBox         ; 整个 cellview 的包围盒

打开指定 cellview

cv = dbOpenCellViewByType("mylib" "INV" "layout" "" "r")
; 库名 单元名 视图名 视图类型 模式
; "r" 只读,"w" 可写,"a" 追加
dbClose(cv)     ; 处理完关闭
lib = ddGetObj("mylib")            ; 拿库对象
cell = ddGetObj("mylib" "INV")     ; 拿单元对象
第 14 章 · 读属性

`~>` 运算符与对象类型

~> 是「读属性」运算符:

shape~>objType      ; 类型
shape~>lpp          ; 图层对
shape~>bBox         ; 包围盒

等价写法 dbGetq(shape 'objType),但 ~> 更简洁。

常用属性速查

字段含义例子
shape~>objType类型"rect" "path" "polygon" "label" "inst"
shape~>lpp图层对("M1" "drawing")
shape~>bBox包围盒(0:0 10:5)
shape~>points顶点((0 0) (10 0))
shape~>width线宽2.0
shape~>xy原点0:0
shape~>textlabel 文字"VDD"
inst~>cellName引用单元名"INV"
inst~>transform变换((10 20) "R0")

lpp 是什么

lpp = layer + purpose(图层 + 用途)。("M1" "drawing") 表示 M1 层的绘图层;文字标签用途通常是 "text"

你在 LSW 里点的每一层,对应数据库里就是这么一对字符串。过滤某层图形,就写 list("M1" "drawing") 这样的完整 lpp。

列出所有属性

shape~>?    ; 列出这个图形的所有属性
第 15 章 · 遍历与过滤

setof / dbGetOverlaps

遍历所有图形

foreach(shape cv~>shapes
  println(shape~>objType)
)

过滤:setof(最高频)

; 只要 M1 drawing 层
m1Shapes = setof(s cv~>shapes  s~>lpp == list("M1" "drawing"))
; 只要矩形
rects = setof(s cv~>shapes  s~>objType == "rect")
; 组合:M1 层且是矩形
m1Rects = setof(s cv~>shapes
                s~>lpp == list("M1" "drawing") && s~>objType == "rect")

判断存在 / 全满足

exists(s cv~>shapes  s~>objType == "rect")   ; 有矩形吗
forall(s cv~>shapes  s~>width > 1.0)         ; 都满足吗

按范围抓取

m1Shapes = dbGetOverlaps(cv cv~>bBox list("M1" "drawing"))

dbGetOverlaps 比遍历再 setof 更快,适合局部操作。

遍历实例

foreach(inst cv~>insts
  printf("instance: %s @ %L\n" inst~>cellName inst~>xy)
)

实战:统计 M1 矩形数量和总面积

procedure( reportM1Stats()
  let((cv m1Shapes count total)
    cv = geGetEditCellView()
    m1Shapes = setof(s cv~>shapes  s~>lpp == list("M1" "drawing"))
    count = 0
    total = 0.0
    foreach(s m1Shapes
      when(s~>objType == "rect"
        count = count + 1
        total = total + bboxArea(s)
      )
    )
    printf("M1 矩形数量: %d\n", count)
    printf("M1 矩形总面积: %f\n", total)
  )
)
reportM1Stats()
第 16 章 · 创建图形

dbCreate* 全解

创建函数都叫 dbCreateXxx,第一个参数是 cellview,第二个是图层 lpp

cv = geGetEditCellView()          ; 1. 拿到画布
lpp = list("M1" "drawing")        ; 2. 指定图层

矩形 dbCreateRect

dbCreateRect(cv lpp list(0:0 10:5))
; 左下角 -> 右上角

线 dbCreatePath

dbCreatePath(cv lpp list(0:0 10:0 10:5) 2.0)
; 顶点列表 + 线宽

多边形 dbCreatePolygon

dbCreatePolygon(cv lpp list(0:0 10:0 10:5 0:5))
; 首尾自动闭合

文字 dbCreateLabel

dbCreateLabel(cv list("M1" "text") 0:0 "VSS" "centerLeft" "R0" "roman" 2.0)
; 图层 位置 文字 对齐 旋转 字体 高度

实例 dbCreateInst

master = dbOpenCellViewByType("mylib" "INV" "layout" "" "r")
dbCreateInst(cv master 0:0 "R0")

类比dbCreateInst 相当于按 i 键、选 cell、放下来。所有 dbCreate* 都返回新对象的 dbId,可接住继续操作。

单位提醒:坐标默认以微米为单位,写 10:5 就是 x=10μm、y=5μm。
hiRedraw()   ; 创建完记得刷新显示
第 17 章 · 修改与删除

移动、复制、删除、变换

dbMoveFig(shape 5:0)      ; 沿 x 移动 5 微米(相对位移)
dbDeleteObject(shape)     ; 删掉这个图形
newShape = dbCopyFig(shape 10:0)         ; 复制
newShape = dbCopyFig(shape 10:0 "R90")   ; 复制并旋转
警告:dbDeleteObject 直接改数据库,没有脚本层 undo。写删除脚本前先 dbSave 备份,或先打印清单确认。

改属性

shape~>width = 4.0       ; 部分对象支持直接改

复杂形状建议「删了重建」更保险。

变换 transform

inst~>transform    ; => ((10 20) "R0")
inst~>xy           ; => 10:20
inst~>orient       ; => "R0"

朝向取值:"R0" "R90" "R180" "R270" "MX" "MY" "MXR90" "MYR90"

newPt = dbTransformPoint(pt transform)     ; 对点变换
newBBox = dbTransformBBox(bBox transform)  ; 对 bBox 变换

实战:M1 图形整体右移 10 微米

procedure( shiftM1Right()
  let((cv m1Shapes)
    cv = geGetEditCellView()
    m1Shapes = setof(s cv~>shapes  s~>lpp == list("M1" "drawing"))
    foreach(s m1Shapes
      dbMoveFig(s 10:0)
    )
    hiRedraw()
  )
)
dbSave(cv)     ; 改完保存
第 18 章 · 实例与层次

instance 与层次化

实例是「引用别的 cell 的复制品」。顶层放 100 个 INV,其实只存了一份 INV 版图(master),放的是 100 个「指向 INV 的引用 + 各自位置朝向」。改 master 一次,100 个实例全跟着变。

读实例信息

inst~>cellName      ; 引用单元名
inst~>name          ; 实例名,如 "I0"
inst~>xy            ; 位置
inst~>orient        ; 朝向
inst~>transform     ; 完整变换

打开实例的 master

master = dbOpenCellViewByType(cv~>libName inst~>cellName "layout" "" "r")
foreach(shape master~>shapes ...)

层次化遍历(递归)

procedure( printAllShapes(cv)
  foreach(shape cv~>shapes
    println(shape~>objType)
  )
  foreach(inst cv~>insts
    master = dbOpenCellViewByType(cv~>libName inst~>cellName "layout" "" "r")
    when(master
      printAllShapes(master)   ; 递归进入实例内部
    )
  )
)

层次坐标换算

topPt = dbTransformPoint(1:2 inst~>transform)      ; 局部 -> 顶层
invT = dbInvertTransform(inst~>transform)
localPt = dbTransformPoint(topPt invT)             ; 顶层 -> 局部
第 19 章 · 坐标与 bBox

几何计算

pt = 10:5
xCoord(pt)       ; => 10
yCoord(pt)       ; => 5

bBox 是「图形的最小外接矩形」,表示成左下角 + 右上角两个坐标点。

bBox = shape~>bBox     ; => (0:0 10:5)
leftEdge(bBox)         ; => 0
rightEdge(bBox)        ; => 10
bottomEdge(bBox)       ; => 0
topEdge(bBox)          ; => 5
lowerLeft(bBox)        ; => 0:0
upperRight(bBox)       ; => 10:5
centerBox(bBox)        ; => 5:2.5

计算宽高面积

procedure( bboxWidth(bBox)
  rightEdge(bBox) - leftEdge(bBox)
)
procedure( bboxHeight(bBox)
  topEdge(bBox) - bottomEdge(bBox)
)
procedure( bboxArea(bBox)
  bboxWidth(bBox) * bboxHeight(bBox)
)

常见场景

; 两个图形间距
gap = leftEdge(shape2~>bBox) - rightEdge(shape1~>bBox)
; 给 bBox 四周加 5 微米边距
bBox = shape~>bBox
expanded = list((leftEdge(bBox)-5):(bottomEdge(bBox)-5)
                (rightEdge(bBox)+5):(topEdge(bBox)+5))
Part 04 · 金属 2 实战进阶
第 20 章 · 输出与文件

printf / sprintf / 文件读写

println("固定文本")                          ; 打印换行
printf("面积 = %f\n", area)                  ; 带格式
msg = sprintf(nil "面积 = %.2f", area)       ; 只生成字符串
占位符含义
%d整数
%f小数
%.2f保留 2 位小数
%e科学计数
%s字符串
%L列表(带括号)
%n数字(自动判断类型)

写文件(生成报表)

procedure( writeShapeReport(cv filePath)
  let((p)
    p = outfile(filePath)
    fprintf(p "objType  layer  bBox\n")
    foreach(s cv~>shapes
      fprintf(p "%s  %s  %L\n" s~>objType (car s~>lpp) s~>bBox)
    )
    close(p)
    printf("已写入 %s\n" filePath)
  )
)

读文件

p = infile("/tmp/data.txt")
line = gets(p)
while(line
  println(line)
  line = gets(p)
)
close(p)
第 21 章 · 调试与错误

常见错误、errset、排查

报错 / 现象原因解决
syntax error括号没配对数 ( 和 ) 数量
中文引号 “”用了中文标点换英文 "
unbound variable - xxx变量没定义先赋值,查拼写大小写
nil 报错对空对象操作操作前 when(obj ...)
没反应没图形忘 hiRedraw加 hiRedraw
结果差一位nth 从 0 数nth(0 ...) 是第一个
整数但想要小数整数除法写成 x/2.0

两条铁律

loadi("/path/to/file.il")   ; 1. 用 loadi,报错定位到行
printf("cv = %L\n" cv)      ; 2. 不确定就打印,别靠猜

主动报错与警告

error("这里出错了")          ; 抛错终止
warn("这里有个警告")         ; 警告不终止

错误拦截 errset

result = errset( riskyOperation() )
if(result == nil then
  println("操作失败,已跳过")
else
  println("成功,结果: " car(result))   ; errset 返回装结果的列表

排查思路:1) loadi 看第几行 → 2) 打印可疑变量 → 3) help 查参数 → 4) 删成最小版本逐步加回来。

第 22 章 · 交互与 UI

选取、点击、快捷键、菜单

选中对象

sel = geGetSelSet()   ; 当前选中的对象列表
foreach(s sel ...)

鼠标点击

pt = enterPoint()    ; 点一下,返回坐标
box = enterBox()     ; 拖一个框,返回 bBox

快捷键 bindkey

hiSetBindKey("Layout" "None<Key>F2" "makeViaArray(0 0 4 3 2 2 0.5 0.5)")
; 按 F2 就执行命令

菜单与表单(了解)

hiCreateMenuItem(
  ?name 'myMenu
  ?itemText "我的工具"
  ?callback "myTool()"
)

表单(hiCreateAppForm 等)能做出「弹出窗口、填参数、点确定执行」的工具,是进阶专题。

第 23 章 · Pcell 入门

参数化单元

Pcell = Parameterized cell(参数化单元)。写一个 SKILL 函数,声明参数(宽、长、层),每次传不同参数就生成对应版图。Virtuoso 里很多器件就是 pcell。

最简单的 Pcell

pcDefinePCell(
  list(ddGetObj("mylib") "myRect" "layout")   ; 库、单元名、视图
  ((width 10.0) (height 5.0))                  ; 参数及默认值
  let((cv lpp)
    cv = pcCellView                            ; pcell 里的"画布"
    lpp = list("M1" "drawing")
    dbCreateRect(cv lpp list(0:0 width:height))
  )
)

更实用:带中心、可调层的电阻

pcDefinePCell(
  list(ddGetObj("mylib") "res" "layout")
  ((l 10.0) (w 2.0) (layer "M1"))
  let((cv lpp x1 y1 x2 y2)
    cv = pcCellView
    lpp = list(layer "drawing")
    x1 = -l/2.0
    y1 = -w/2.0
    x2 = l/2.0
    y2 = w/2.0
    dbCreateRect(cv lpp list(x1:y1 x2:y2))
  )
)

pcell 是 SKILL 的「毕业设计」。先把前面基础打牢,再来攻 pcell 会轻松很多。

第 24 章 · 性能与规范

代码规范与性能

规范

  • 命名见名知意:makeViaArraya 好一万倍。
  • 一个函数只干一件事。
  • 局部变量进 let,别到处写全局。
  • 关键处写注释,缩进对齐括号。

性能

; 慢:遍历全部再判断
setof(s cv~>shapes  s~>lpp == list("M1" "drawing"))
; 快:直接按范围 + 层抓
dbGetOverlaps(cv cv~>bBox list("M1" "drawing"))
  • 避免循环里反复 dbOpenCellViewByType,先打开一次存起来。
  • 能用 foreach/setof 就别递归。
  • 批量操作后统一 hiRedraw,不要每个图形刷新一次。

健壮性

  • 操作前判断非 nil;用 errset 包裹可能出错的操作。
  • 删改前先 dbSave 备份;函数开头校验参数。
第 25 章 · 实战项目集

六个可直接改用的脚本

A. via 阵列

procedure( makeViaArray(x0 y0 cols rows pitchX pitchY viaW viaH)
  let((x y)
    for(r 0 rows-1
      for(c 0 cols-1
        x = x0 + c * pitchX
        y = y0 + r * pitchY
        dbCreateRect(geGetEditCellView() list("M1" "drawing")
                     list(x:y (x+viaW):(y+viaH)))
      )
    )
    hiRedraw()
  )
)
makeViaArray(0 0 4 3 2.0 2.0 0.5 0.5)

B. guard ring

procedure( drawGuardRing(x1 y1 x2 y2 ringW)
  let((cv lpp)
    cv = geGetEditCellView()
    lpp = list("M1" "drawing")
    dbCreateRect(cv lpp list(x1:y1 x2:(y1+ringW)))           ; 下
    dbCreateRect(cv lpp list(x1:(y2-ringW) x2:y2))           ; 上
    dbCreateRect(cv lpp list(x1:y1 (x1+ringW):y2))           ; 左
    dbCreateRect(cv lpp list((x2-ringW):y1 x2:y2))           ; 右
    hiRedraw()
  )
)
drawGuardRing(0 0 50 30 2.0)

C. 列出所有 instance

procedure( listInstances()
  let((cv)
    cv = geGetEditCellView()
    foreach(inst cv~>insts
      printf("instance: %s @ %L\n" inst~>cellName inst~>xy)
    )
  )
)

D. 按坐标放 instance 阵列

procedure( placeInstArray(lib cell cols rows pitchX pitchY)
  let((cv master x y)
    cv = geGetEditCellView()
    master = dbOpenCellViewByType(lib cell "layout" "" "r")
    for(r 0 rows-1
      for(c 0 cols-1
        x = c * pitchX
        y = r * pitchY
        dbCreateInst(cv master x:y "R0")
      )
    )
    hiRedraw()
  )
)
placeInstArray("mylib" "INV" 3 2 20.0 15.0)

E. 把某层图形写到文件

procedure( reportLayerShapes(layerName filePath)
  let((cv shapes p)
    cv = geGetEditCellView()
    shapes = setof(s cv~>shapes  s~>lpp == list(layerName "drawing"))
    p = outfile(filePath)
    fprintf(p "objType  bBox\n")
    foreach(s shapes
      fprintf(p "%s  %L\n" s~>objType s~>bBox)
    )
    close(p)
  )
)

F. 给 M1 矩形标尺寸 label

procedure( labelM1Rects()
  let((cv rects w h text)
    cv = geGetEditCellView()
    rects = setof(s cv~>shapes
                  s~>objType == "rect" && s~>lpp == list("M1" "drawing"))
    foreach(r rects
      w = rightEdge(r~>bBox) - leftEdge(r~>bBox)
      h = topEdge(r~>bBox) - bottomEdge(r~>bBox)
      text = sprintf(nil "%.2fx%.2f" w h)
      dbCreateLabel(cv list("M1" "text") upperRight(r~>bBox) text
                    "centerLeft" "R0" "roman" 1.0)
    )
    hiRedraw()
  )
)
Part 05 · 通孔 参考篇
第 26 章 · 速查表

贴墙用的速查

; ── 基础 ──
println("x")                 ; 打印换行
x = 5                        ; 赋值
list(1 2 3)   '(1 2 3)      ; 列表
car/cdr/nth/length/append/cons/member/memberp/reverse/last
if(c then a else b)          ; 判断
when(c ...)  unless(c ...)   ; 单分支
cond((c1 a1)(c2 a2)(t a3))  ; 多分支
case(v (v1 a1)(t an))        ; 精确匹配
for(i 0 9 ...)               ; 循环
foreach(x L ...)             ; 遍历
while(c ...)                 ; 条件循环
procedure(f(a) ...)          ; 定义函数
let((v) ...)  prog((v) ...)  return(x)
(+ - * / max min abs mod)    ; 算术
(== != > < >= <=)          ; 比较
(and or !)   &&  ||  !       ; 逻辑
lambda((x) ...)  mapcar  apply

; ── 版图 ──
cv = geGetEditCellView()          ; 当前 cellview
cv~>libName cv~>cellName cv~>shapes cv~>insts
s~>objType  s~>lpp  s~>bBox  s~>points  s~>width  s~>xy  s~>text
inst~>cellName  inst~>transform  inst~>orient  inst~>name
dbCreateRect(cv lpp bbox)         ; 矩形
dbCreatePath(cv lpp pts width)    ; 线
dbCreatePolygon(cv lpp pts)       ; 多边形
dbCreateLabel(cv lpp pt text ...) ; 文字
dbCreateInst(cv master pt "R0")   ; 实例
dbMoveFig(s delta)  dbCopyFig  dbDeleteObject(s)
dbGetOverlaps(cv bbox lpp)        ; 抓某层图形
setof(s cv~>shapes 条件)  exists  forall
geGetSelSet()  hiRedraw()  dbSave(cv)  dbClose(cv)
dbOpenCellViewByType(lib cell view "" "r")  ddGetObj(lib [cell])

; ── 坐标 / 几何 ──
xCoord  yCoord
leftEdge/rightEdge/bottomEdge/topEdge
lowerLeft/upperRight/centerBox
dbTransformPoint  dbTransformBBox  dbInvertTransform

; ── 输出 / 调试 ──
printf("x=%f\n" x)  sprintf(nil "x=%.2f" x)  strcat  atoi  atof
load/loadi("path.il")  errset(expr)  error  warn  help("f")
outfile/infile/close/gets/fprintf

; ── 交互 ──
enterPoint()  enterBox()
hiSetBindKey("Layout" "None<Key>F2" "cmd()")
附录 A · 练习

配套练习题

先自己做,卡住再展开答案。答案只是其中一种写法,能跑就行。

练习 1 · 变量与算术

矩形宽 12.5、高 8.0,打印面积和周长。

参考答案
w = 12.5
h = 8.0
printf("面积 = %f\n" w * h)
printf("周长 = %f\n" 2.0 * (w + h))

练习 2 · 列表与 nth

L = list(3 1 4 1 5),取出第 3 个元素(从 0 计数),打印长度。

参考答案
L = list(3 1 4 1 5)
printf("第3个 = %d\n" nth(2 L))
printf("长度 = %d\n" length(L))

练习 3 · 函数

rectPerimeter(w h) 返回周长。

参考答案
procedure( rectPerimeter(w h)
  let((p)
    p = 2.0 * (w + h)
    p
  )
)
printf("周长 = %f\n" rectPerimeter(12.5 8.0))

练习 4 · 循环 + 条件

打印 1 到 10 里所有偶数。

参考答案
for(i 1 10
  when(mod(i 2) == 0
    println(i)
  )
)

练习 5 · 遍历 + 过滤

统计当前版图 M2 drawing 层的 path 数量。

参考答案
cv = geGetEditCellView()
m2Paths = setof(s cv~>shapes
                s~>objType == "path" && s~>lpp == list("M2" "drawing"))
printf("M2 path 数量 = %d\n" length(m2Paths))

练习 6 · 创建

在当前版图创建 20x10 的 M1 矩形,中心在 (0,0)。

参考答案
procedure( createCenteredRect(cx cy w h)
  let((cv lpp x1 y1 x2 y2)
    cv = geGetEditCellView()
    lpp = list("M1" "drawing")
    x1 = cx - w/2.0
    y1 = cy - h/2.0
    x2 = cx + w/2.0
    y2 = cy + h/2.0
    dbCreateRect(cv lpp list(x1:y1 x2:y2))
    hiRedraw()
  )
)
createCenteredRect(0 0 20 10)

练习 7 · 缩放

stretchM1(factor):把 M1 矩形以左下角为锚点,宽高都乘 factor。

参考答案
procedure( stretchM1(factor)
  let((cv m1Shapes bBox ll w h newBBox)
    cv = geGetEditCellView()
    m1Shapes = setof(s cv~>shapes
                     s~>objType == "rect" && s~>lpp == list("M1" "drawing"))
    foreach(s m1Shapes
      bBox = s~>bBox
      ll = lowerLeft(bBox)
      w = rightEdge(bBox) - leftEdge(bBox)
      h = topEdge(bBox) - bottomEdge(bBox)
      newBBox = list(ll (xCoord(ll)+w*factor):(yCoord(ll)+h*factor))
      dbDeleteObject(s)
      dbCreateRect(cv s~>lpp newBBox)
    )
    hiRedraw()
  )
)

练习 8 · 文件输出

把当前版图所有图形的类型、图层、bBox 写到文本文件,每行一个。

参考答案
procedure( dumpShapes(filePath)
  let((cv p)
    cv = geGetEditCellView()
    p = outfile(filePath)
    foreach(s cv~>shapes
      fprintf(p "%s %s %L\n" s~>objType (car s~>lpp) s~>bBox)
    )
    close(p)
  )
)
dumpShapes("/tmp/shapes.txt")
附录 B · FAQ

常见问题

Q1 · geGetEditCellView() 返回 nil?
当前没有正在编辑的版图窗口,先打开一个 layout 并确保它是当前编辑窗口。

Q2 · 报 illegal top level form
procedure 定义后多了一个右括号,或文件里有孤立的 )。数括号。

Q3 · 重新 load 还是旧行为?
load 覆盖同名函数,但改函数名的话旧函数还在;或加载了错误路径。

Q4 · printfprintln 区别?
println 只打印字符串、自动换行;printf 支持 %d %f %L,需手动写 \n

Q5 · 坐标单位?
界面默认微米,底层是 DB 网格。不放心就打印 cv~>bBox 和屏幕对比。

Q6 · 删错图形怎么撤销?
dbDeleteObject 无脚本层 undo,删除前先 dbSave 备份或打印清单确认。

Q7 · 为什么用 == 不用 =
= 是赋值,== 是比较。

Q8 · 字符串和数字能直接加吗?
不能。用 strcat("M" sprintf(nil "%d" 1))

Q9 · 怎么知道对象支持哪些属性?
shape~>? 列出全部,或 help("rect") 查类文档。

Q10 · car / cdr 名字很怪?
来自 Lisp 历史。记「car=车头=第一个,cdr=剩下的」即可。

Q11 · 学了 SKILL 够不够?
SKILL 是版图自动化主力,后续可学 pcell、SKILL++。

Q12 · 脚本跑得慢?
dbGetOverlaps 代替全遍历;避免循环里反复 open;批量操作后统一 hiRedraw

附录 C · 索引

常用函数分类索引

基础语言

println print printf sprintf strcat strlen substring atoi atof list car cdr cons append nth length member memberp reverse last setq = let prog return procedure lambda mapcar apply if when unless cond case for foreach while and or not setof exists forall sort type errset error warn

版图数据访问

geGetEditCellView geGetWindowCellView dbOpenCellViewByType ddGetObj dbSave dbClose dbIsId dbIsShape dbIsInst dbGetq

版图创建 / 修改

dbCreateRect dbCreatePath dbCreatePolygon dbCreateLabel dbCreateInst dbCopyFig dbMoveFig dbDeleteObject dbTransformPoint dbTransformBBox dbInvertTransform

版图查询 / 几何

dbGetOverlaps dbFindAnyInstByName geGetSelSet xCoord yCoord leftEdge rightEdge bottomEdge topEdge lowerLeft upperRight centerBox

显示 / 交互 / 文件 / Pcell

hiRedraw hiSetBindKey enterPoint enterBox hiCreateMenuItem hiDisplayForm outfile infile gets fprintf close pcDefinePCell pcCellView load loadi help

学 SKILL 最快的路径,永远是带着一个真实版图任务去写,而不是背语法。挑一个你每周重复做的操作,从第 25 章的脚本开始改吧。