来源:eetop bbs 作者:天牛不唱歌 注:skill相关的一些脚本已经整理在脚本区: /////////example 1//////// ZOOM.zip (235 Bytes) 一、简介 1.1 Skill语言起源 Skill语言是Cadence公司开发的,但是Skill语言的前身可以追溯到LISP语言,在延续了LISP语言的特性的同时,Skill语言也融合了先进C语言的优点。所以Skill语言的语法结合了这二者的有点,同时有的语法也产生了两种书写风格。譬如(注:这两个程序先不用去理解,注意代码风格即可): 程序一: procedure(fibonacci(n) if( (n == 1 || n == 2) then 1 else fibonacci(n-1) + fibonacci(n-2) ) ) 程序二: (defun fibonacci(n) (cond ((or (equal n 1) (equal n 2)) 1) (t (plus (fibonacci (difference n 1)) (fibonacci (difference n 2)))) ) ) 第一种是代数表示法,这种方法的形式如 func( arg1 arg2 ...),该表示方法在大多语言里都被采用。另外一种是前缀表示法,类似于LISP语言,即 (func arg1 arg2 ...)这样的表述形式。这里可以看到类似Lisp语言的表示法后面有很多右括号,而且函数和参数容易混淆,所以一般推荐还是用常用的类C语言代数表示法 1.2 Skill语言的作用 Cadence提供给用户丰富的Skill语言接口,用户可以使用Skill来控制Cadence软件实现各种功能,譬如定制自己的cadence软件设置,进行Cadence批量操作,甚至根据自己需要开发Cadence相关插件或者工具软件(譬如:我们经常用的Calibre,Assura等软件就有大量Skill脚本与Cadence软件来通信)。 Skill可以大,当然可以小(譬如书写一条简单的Skill语句就可以实现设置某个快捷键的目的)。Skill功能的强大有时候超出我们的想象,但Skill的入门又非常的简单,学习Skill对于一般工程师来讲,经常用到的就是用它来帮助我们快速完成一些批量小任务,这极大提高了我们工作的效率。仅此一点Skill也是值得我们学习一下的,更不要说通过进一步的学习我们还能完成那么多高大上的功能。 1.3 Skill脚本的简单使用 Cadence对于skill语言的支持是无所不在的,我们可以从菜单栏,快捷键,程序函数,表格,以及CIW窗口等不同的渠道来触发执行已加载的Skill脚本程序。 一般加载自己的SKILL代码文件一般有两种方式: ① 在家目录里的 .cdsinit 这样,Cadence在启动后就会自动加载该Skill代码了。 ② 在CIW窗口输入 load "***.il"。 这种手动的加载,一般只对当前起作用,会在下一次启动Cadence就失效了。当然我们也可以把 ***.il内的内容直接复制到CIW窗口并回车,效果是一样的。 注:双引号内的文件路径要设置正确才能load成功。 譬如下面的几行skill脚本,我们新建zoom.il文件,并把下面四行代码复制到该文件,然后在Cadence的CIW里按照漆面说的第二种方法load该文件,我们就在原理图和版图里实现了用鼠标中键控制视图缩放的功能。这样我们就算有了自己的第一个非常实用的Skill脚本,并且我们知道如何使用它了。 hiSetBindKey("Layout""<Btn4Down>" "hiZoomInAtMouse()"); hiSetBindKey("Layout""<Btn5Down>" "hiZoomOutAtMouse()"); hiSetBindKey("Schematics""<Btn4Down>" "hiZoomInAtMouse()"); hiSetBindKey("Schematics""<Btn5Down>" "hiZoomOutAtMouse()"); 1.4 Skill脚本的加密 对于某些工程师而言,希望把自己的Skill脚本提供他人使用的同时,也希望保护自己的Skill脚本,不被别人乱改或者把源码传播,那就需要对脚本加密。 Skill脚本一般是用.il和.ile结尾的文件其中.il是普通文本文件,用文本编辑器可以直接打开,.ile结尾的文件是加密的,用文本编辑器是打不开的。 当我们写好自己的Skill脚本后,可以在CIW窗口用encrypt()函数来加密譬如 encrypt(“zoom.il” “zoom.ile” “abc123”) 这样就会把zoom.il 加密成zoom.ile文件了,并且密码是abc123.加密后的文件使用密码也是不能看到源代码的。而且在加载使用zoom.ile的时候是需要提供密码的。譬如:(注:一般最好用文件的绝对路径进行操作,否则可能会找不到文件) load "zoom.ile" “abc123” 注:尽管Linux系统中文件的扩展名一般不影响文件的使用,但是为了区分我们还是在文件命令的时候遵从一定的规则让未加密的文件后缀是.il,让加密的文件以.ile结尾。 对于用encrypt加密的文件,但是如果没有密码的话,是可以用lineread函数配合pprint函数来获得源代码的。load下面函数后使用NlDecrypt(“123.ile”) 就可以恢复出来源码了,这个方法可以用来学习别人的源码。 procedure( NlDecrypt( inputFile @optionaloutputFile "tt") prog( (inp out line) if( inputFile then unless( outputFile outputFile = sprintf(nil"%s.dec" inputFile) ) when( isFile( outputFile ) print( "You must specify a file thatdoesn't exist!!!\n" ) hiGetAttention( ) return() ) unless( inp = infile(inputFile) printf("Unable to open input file:%s\n" inputFile) return() ) unless( out = outfile(outputFile"w") printf("Unable to open output file:%s\n" outputFile) return() ) while( line = lineread(inp) when( line != t pprint(line out) ) ) close(inp) close(out) else printf("You must at least specify aninput file!\n") return() ) ; ** if inputFile ** return(t) ) ; ** let ** ) ; ** procedure ** 1.5 关于.cdsinit和.cdsenv文件 前面说到可以把加载Skill脚本的语句放到.cdsinit文件里,这样每次启动Cadence就能自动加载该脚本了。这里我们要说一下icfb或virtuoso 启动有两个重要文件就是 .cdsinit和.cdsenv文件(文件名以点开始的在Linux下认为是隐藏文件)。我们每次启动Cadence软件,都会加载这两个文件,它们一般负责cadence的初始设置和一些用户自定义设置,其中 .cdsinit主要负责一些加载项的设置, .cdsenv主要负责一些环境变量或者参数的设置, 两者功能有一些交叉,在有的时候都能完成某个设置。譬如:我们点击CIW的退出后,会弹出来是否要真的退出,如果我们不想让软件弹出来这个询问的窗口我们可以在.cdsenv里这样设置, ddserv.ciw promptOnExit boolean nil 同样我们在.cdsinit里也可以进行设置来完成这个功能,如下用.cdsinit的设置和上面用.cdsenv的设置功能是等效的。 envSetVal ("ddserv.ciw" "promptOnExit" 'boolean nil) 注:Skill有个专门设置变量的函数envSetVal()。 关于.cdsenv的设置我们可以在CIW窗口->option->save default里导出当前的cdsenv然后进行分析学习等。(注:在cdsenv我们一般会对字体,格点,加载工艺库,数字的有效位数等进行一些个性化的设置。且cdsenv每行一般分成四部分:大项,小项,类型,值)。 这里我们就不过多说明.cdsenv文件了。把重点放在.cdsinit文件,因为.cdsinit文件里的设置,除了可以完成cdsenv的功能外,还可以完成很多其它功能。另外补充一点,Cadence启动一般会最先在启动目录寻找.cdsinit文件,如果找到就加载,如果没有找到会到用户的家目录去寻找,如果还没有找到,应该会软件安装的某个目录下去寻找。 注:如果当前启动目录能找到.cdsinit文件,软件就不会自动去加载家目录下的.cdsinit文件了。 1.6 Skill语言的学习方法 Skill语言分为两部分,一部分是skill标准语法,另一部分是Cadence内置的关键字和接口函数。
①最简单的方法,看~/CDS.log文件或者CIW的输出区域。首先我们要把Options->LogFilter都选上,这样我们进行任何操作,在CIW窗口或者log文件下都会显示当前操作所用到的函数,譬如添加器件删除器件等操作使用的接口函数,我们一目了然。 ②在CIW里输入命令startFinder,或者在Terminal上输入cdsFinder& . 我们就打开了函数搜索器,如果我们想知道某个函数的大概作用和使用方法,我们可以在这里进行查询,或者我们只知道函数的部分名字,也可以通过匹配搜索,找到包含该字串的函数。 ③上面我们查到的关于函数用法的介绍很简单,如何知道其详细介绍呢。当然如果你知道函数在那个文档里,直接打开就行了,如果不知道,就需要打开help的search功能,这里可以查到详细的功能用法。运行cdsdoc&, 点击Search打开Cadence help的搜索功能。高的版本已经改为cdnshelp& 命令了,但是原理都是一样的。另外有可能端口或者/etc/hosts的设置导致连不上help。或者某些非完整版的Cadence软件,有可能为了缩小软件尺寸会将doc的内容删除了,所以运行cdsdoc命令也会出错。建议安装cadence官方版。另外我们也可以将所有skill的manual下载后,用pdf软件进行查看搜索我们所需的接口函数。 从简入繁,不断用小实例小的成功激励自己,这是快速稳固学习的不二法门,本书也会按照这样的结构和宗旨去书写,所以本书非常适合Skill学习爱好者通读,精读,也是最好的Skill入门教程,没有之一。 二、Skill基本语法 Skill语言的很多地方和C语言差不多,如变量,函数,控制结构,输入输出等,但二者的某些内置函数,操作符等也会有一些区别,学习的时候可以有意识的记忆一下二者的不一样的部分。对于Skill这里只作简单介绍。详情可以参考SKILL Language UserGuide。 2.1 Skill的变量 Skill的变量最大的特点是不需要事先声明,Skill第一次用到是会自动生成变量。变量可以由字符、数字、“_” 和 “?” 组成,注意第一个字符不能是数字和”?”。由于Cadence所开发的Skill中的变量、函数都是第一个字母小写,以_为开头的是Cadence的专用函数,为了避免冲突,所以建议大家函数和变量命名都以大写字母开头。 2.2 Skill的函数的调用 Skill的函数的调用方式有三种,以Skill内置的字串连接函数strcat为例: strcat("Hello" "," " everyone" "!" ) ;常见的类C格式 (strcat "Hello" "," " everyone" "!" ) ;类Lisp语言的格式 strcat"Hello" "," " everyone" "!" ;上面的括号可以省略 上面三种方式的返回的结果都是=> "Hello, everyone!" 推荐使用第一种方式,需要注意的是函数和第一个括号之间没有空格,否则会报错 如: strcat ("Hello" "," " everyone" "!" );会产生如下错误! =>*Error* eval: not a function - "Hello" 2.3 list数据格式 list是Skill继承Lisp(list Processing)而来,它是Skill编程中经常用到的。对于list,它有点类似于C语言中的数组,数据内容是有序的,但是它并不是数组(要知道Skill本身是有数组的类似定义和使用方法和C也是基本一样的),而更像是一个集合,它的数据对象可以是Skill中各种数据格式,所以我们经常给list定义就是“skill数据对象的一个有序集合”。skill数据甚至程序本身都可以看作是一个list,这是C语言中所没有的概念。下面是skill list形式的几个简单例子,对于list的操作我们之后再讲。 (1 2 3) ;这是一个list 包含1 2 3 三个元素 (1) ;这个list只包含一个元素1 ( ) ;一个空的list等同于nil (1 (2 3) 4) ;这个list的第二个元素也是一个list。 2.3.1 创建list的方法 创建list有以下几种基本的方法,譬如:用操作符' 1).用 ' 和 list 定义一个list,注意两者的差别,即list函数会把变量的值以实际的内容代替。 a = 1 => 1 b = 2 => 2 '( a b 3 ) => ( a b 3 ) list( a b 3 ) => ( 1 2 3 ) 2).用cons命令添加一个元素到一个list的头部 result = '( 2 3 ) => ( 2 3 ) result = cons( 1 result ) => ( 1 2 3 ) 3).用append命令合并两个list lista = '( 4 5 6 ) => ( 4 5 6 ) listb = '( 1 2 3 ) => ( 1 2 3 ) listc = append( lista listb) => ( 4 5 6 1 2 3 ) 2.3.2 访问list或list中元素的方法 对于list中元素的访问是,list的基本操作,Skill提供了很多内置的list操作的函数。使用这些函数,不仅让代码简洁,也增加了程序的可读性。 car()函数用于访问list的第一个元素。 numbers = '( 1 2 3 ) => ( 1 2 3 ) car( numbers ) => 1 cdr()函数访问list除了第一个元素外的其他元素,注意返回仍然是个list 。 numbers = '( 1 2 3 ) => ( 1 2 3 ) cdr( numbers ) => ( 2 3 ) nth()函数用索引访问list的某个元素,注意索引从0开始。 numbers = '( 1 2 3 ) => ( 1 2 3 ) nth( 1 numbers ) => 2 member()函数检查指定的元素是否在指定的list中,它只检查顶层元素的元素,返回值是从搜到值开始到结尾的list 。 numbers = '( 1 2 3 ) => ( 1 2 3 ) member( 4 numbers ) => nil member( 2 numbers ) => ( 2 3 ) length()函数计算list所包含元素的个数。 numbers = '( 1 2 3 ) => ( 1 2 3 ) length( numbers ) => 3 2.3.3 关于x、y坐标或者bBox边界list的访问 在原理图和版图设计中,由于是一个二维的笛卡尔坐标系,所以对于坐标list操作经常用到的,它是一组2维的list,常见的表示方法有: 用 :表示一个坐标的list,其结果和list命令一样,用xCoord和yCoord命令可以分别访问x和y坐标。 xValue = 300 yValue = 400 aCoordinate = xValue:yValue => ( 300 400 ) xCoord( aCoordinate ) => 300 yCoord( aCoordinate ) => 400 用list命令或者符号 ' 来表示一个bBox,注意之前讲过二者区别:list命令先计算变量或者表达式,然后赋给list,而操作符' 表示的list和字面的一样,不会计算变量或者表达式的值。 bBox = list( 300:400 500:450 ) ;含有 : 的bBox 如果把两个点的坐标组合起来做bBox的话: 含有变量时要用list()函数: lowerLeft = 300:400 upperRight = 500:450 bBox = list( lowerLeft upperRight ) ' 表示的list严格按字面意思,适合不含有变量的情况 bBox = '(( 300 400 ) ( 500 450 )) 对于已有的bBox,我们可以用car和cdr函数配合使用来得到bBox内的每个元素。而且有相关的简化函数,如下表: list的相关操作还有很多,这里就不详细介绍了,可以参考User Guide里的Advanced list Operations。 2.4 Skill的输入输出 Skill可以把输出写入到屏幕也就是CIW窗口,同时Skill还能对文件进行读入和写入,下面分别说明 2.4.1 输出显示数据: print() 和println() 函数都可以用来显示单个数据,println可以在显示的数据后多加一个回车。 for(i 1 3 print( "hello" )) ;prints hello three times. "hello""hello""hello" for(i 1 3 println( "hello" )) ;prints hello three times. "hello" "hello" "hello" printf ()函数是格式化的输出(对于格式化输出后面会讲),下面的例子是一定格式输出图形层的统计。 printf("\n%-15s %-15s %-10d %-10d %-10d %-10d" layerNamepurpose rectCount labelCount lineCount miscCount ) 对应参数的意义如下,printf需要注意输出类型的对应,一般格式如下: %[-][width][.precision]conversion_code -表示左对齐,width表示位数,.precision表示精度 ,conversion_code部分一般是变量。 对于精度 d表示整数,f表示浮点数,s表示string或者symbol,c表示字符,n表示数字,L表示list,P表示point list,B表示Bounding box。更多请参考下表: 2.4.2 输出数据到一个文件 想输出内容到一个文件,首先用outfile来定义输出接口文件,然后用print println fprintf输出到接口文件, 最后close关闭打开的接口,具体见下面的例子。 例子一: myPort= outfile( "/tmp/myFile1" ) for(i 1 3 println( list( "Number:" i) myPort ) ) close(myPort ) 输出到文件/tmp/myFile1. ("Number:"1) ("Number:"2) ("Number:"3) 例子二: myPort= outfile( "/tmp/myFile2" ) for(i 1 3 fprintf( myPort "Number: %d\n" i ) ;注意printf函数不能输出到port ) close(myPort ) 输出到文件/tmp/myFile2. Number:1 Number:2 Number:3 上面两个例子一个是用的println一个使用fprintf,注意二者的区别,并且注意printf函数不能输出到port。 2.4.3 从文件读取数据 如果想读入文件的内容,首先用intfile定义输入接口文件,然后用gets一次从接口文件读取一行字符串,或者用fscanf根据指定的格式从接口文件读取,最后用close关闭打开的接口。 下面例子实现的功能是:打开~/.cshrc,输出文件的每一行到CIW窗口的目的: inPort= infile( "~/.cshrc" ) when(inPort while( gets( nextLine inPort ) println( nextLine ) ) close(inPort ) ) 下面例子实现的功能是:打开~/.cshrc,输出文件中的每一个字符串到CIW窗口: inPort = infile( "~/.cshrc" ) when( inPort while( fscanf( inPort"%s" word ) println( word ) ) close( inPort ) ) 2.5 Skill的控制结构 2.5.1 关系操作符, 关系操作符和C语言基本差不多主要是用于判断,但是要注意Skill的关系操作符返回值是t或者nil。具体参考下表: 2.5.2 逻辑操作符 逻辑操作符主要是用于完成“与”和“或”运算,注意返回值可以使数值、或者t或者nil。具体参考下表: 注:SKILL中只有nil 是假(FALSE),其余的任何值都是真(TRUE)。 另外要特别注意,与/或逻辑操作只有在需要计算第二个表达式时,才计算第二个表表达式,比如&&操作,当第一个表达时为假时,就不会再计算第二个表达式,|| 操作,当第一个表达时为假时,才会再计算第二个表达式。返回的结果是最后一个计算的表达式,因此与/或逻辑操作可以代替繁琐的if / when等控制语句。 例如:C语言中的操作符, a>b ? c=a : c=b; 即c取a b中较大的一个Skill中没有类似的操作符,可以用下面语句来完成此操作: if( a>b then c=a else c=b ) 也可以用逻辑操作符: c= (a>b)&&a || (a<b)&&b 当然Skill还提供的有max(a b …)的函数,举这个例子是为了说明&& || 可以代替if then else之类的控制语句。所以关系操作符在某种意义上简化了代码。 2.5.3 预定义的函数 Skill还定义了不少预定义的函数,包括基本运算函数,三角函数以及随机产生函数,直接调用它们,不仅方便,而且运算的效率更高,下面是一些预定义函数的列表: 除此之外类型还有很多数据判断和数据类型判断函数,这也是经常用到的,采用这些函数有时候极大的简化了代码,同样避免使用过多的关系运算符和if语句。具体看下表: 2.5.4 控制语句 同c语言类似,Skill语言同样有各种判断和循环控制语句,下面分别举例说明一下。 if语句用于判断,有时候配合else使用,需要注意的是: when语句举例: unless语句举例: case语句:case语句的判断可以是数字和字符串,也可以是它们组成的list,但不支持变量和表达式把最可能出现的情况放在最前面,如果出现的几率都一样,把计算量最大的放在最后面,这样可以有效的提高代码效率。 cond语句:如果有很多判断语句,用cond代替if…then…else组合,代码比较清晰而且执行效率比较高,下面的两种代码是等效的。 上面的cond语句如果用if else表示可以表示成: 举例子说明cond的用法: while语句用于循环,举例如下: for语句用于指定次数的循环,和C语言基本相同,举例如下: foreach语句是skill经常用到的,foreach经常用于对list的每个元素作循环操作,每个循环依次把各个元素的值赋给一个变量举例如下: 上面提到的if(…then…else….),when,unless,case,cond,循环语句for,forecah等,控制语句和C语言类似,都是先判断某个变量或者表达式是否为真,然后执行下面的操作。 2.6 Skill的自定义函数 Skill中,函数大部分都是用procedure定义的,我们可以定义具有函数名的函数如下面例子一,或者用lambda用来定义一个没有函数名的函数,它在一些很小的函数里很方便,如下面例子二。 例子一: procedure(trAdd( x y ) printf( "Adding %d and %d ... %d \n" x y x+y ) x+y )=> trAdd 定义函数trAdd trAdd(6 7 ) => 13 调用函数trAdd 例子二: signallist= '( ( nil strength 1.5 ) ( nil strength 0.4 ) ( nil strength 2.5 ) ) sort(signallist 'lambda( ( a b ) a->strength <=b->strength ) ;定义一个用来按signallist的strength属性排序的内置函数,它没有具体的函数名 ) 函数定义还包括函数的参数,这些会在调用函数实例的时候传递给函数,除了基本的参数传递功能外,Skill还允许我们使用@option的方法控制参数的传递,譬如用@rest,调用函数时允许任意多个参数传递给函数,使用@optional, 可以传递一个可选参数,否则会用默认值,@key,可以指定参数是如何传递的,应此调用是参数的顺序不固定。下面分别用例子说明: 例子一:@rest,调用函数时允许任意多个参数传递给函数, procedure( trTrace( fun @rest args ) let( ( result ) printf("\nCalling %s passing %L" fun args ) result = apply(fun args ) printf("\nReturning from %s with %L\n" fun result ) result ) ; let ); procedure
trTrace('plus 1 2 3 ) => 6 ;返回值 =>Calling plus passing (1 2 3) Returning from plus with 6 例子二:@optional, 可以传递一个可选参数,否则会用默认值, procedure(trBuildBBox( height width @optional (xCoord 0 ) ( yCoord 0 ) ) list( xCoord:yCoord ;;; lower left xCoord+width:yCoord+height ) ;;; upperright ); procedure 调用函数及返回结果: trBuildBBox(1 2 ) => ((0 0) (2 1)) trBuildBBox(1 2 4 ) => ((4 0) (6 1)) trBuildBBox(1 2 4 10) => ((4 10) (6 11)) 例子三:@key,可以指定参数是如何传递的,应此调用是参数的顺序不固定 procedure(trBuildBBox( @key ( height 0 ) ( width 0 ) ( xCoord 0 ) (yCoord 0 ) ) list( xCoord:yCoord ;;; lower left xCoord+width:yCoord+height ) ;;; upper right ); procedure
trBuildBBox()=> ((0 0) (0 0)) trBuildBBox(?height 10 ) => ((0 0) (0 10)) trBuildBBox(?width 5 ?xCoord 10 ) => ((10 0) (15 0)) 注:其中@key和@optional不能同时出现在一个函数的定义中,另外的情况可以组合在一起譬如: 例子一:@optional和@reset共用 procedure(functionname([var1var2 ...] [@optional opt1 opt2 ...] [@rest r]) .. ) 例子二:@key和@reset共用 procedure(functionname([var1var2 ...] [@key key1 key2 ...] [@rest r]) .. )
在Skill中的变量默认都是全局变量,这可能会导致同时调用多个skill的时候,不同人编写的Skill命名可能有相同的,那就会导致冲突,所以要尽量减少全局变量的使用,而采用局部变量。 Skill可以用let和prog定义局部变量,如下面的例子: procedure(trGetBBoxHeight( bBox ) let( ( ( ll car( bBox ) ) ( ur cadr( bBox ) ) lly ury ) lly = cadr( ll ) ury = cadr( ur ) ury - lly ) ; let ); procedure 局部变量默认初始化为nil,当然也可以初始化为别的值或表达式,表达式中不能有别的局部变量,prog和let的区别在于,prog支持函数go和return,可以显示的循环和返回多个值,而let返回值是最后一个表达式的值,譬如上面例子的返回值是ury – lly. 除了有必要用prog,一般用let,更加简洁快速。 2.7 Skill代码书写风格 为了提高代码的可读性,我们一般会加一些注释,和c语言类似,段代码注释我们要/*和*/ 来注释,单行代码用; 来注释。 2.8 Skill 代码Debug 代码中由于我们的疏忽和不严谨,编写的程序难免会存在问题,一般根据在CIW窗口给出错的错误提示,我们就可以找到问题的所在。 例子一: 调用函数名和括号之间有空格
=>*Error* eval: not a function - "Hello"
例子二:软件没响应,比如在CIW中输入段代码,软件没有反映,什么结果也没有。一般是因为( )或者” “不成对造成的,一般可以通过键入 ] 来解决,它表示补充完不对称的括号(可以代替任意多个右括号),如果还没有响应,键入 “] 这时大部分情况下,系统会有响应。 例子三:数据类型不匹配,如: strcat("Mary had a" 5 );会出现如下错误! =>*Error*strcat: argument #2 should be either a string ora symbol (type template = "S") – 5 例子四:函数不合法,一般是我们输入没有的函数会出现illegal function错误,我们要检查是否函数被加载了,或者函数名字写错了。 =>illegalfunction 例子五:无约束变量错误,一般是调用一个变量,而这个变量没有被赋值,或者调用该变量的时候变量名字写错了 =>unboundvariable
|
|
来自: 毕杰lb7q1kq7pr > 《待分类》