cisen / blog

Time waits for no one.
132 stars 20 forks source link

TCL脚本 #838

Open cisen opened 4 years ago

cisen commented 4 years ago

http://www.elecfans.com/emb/fpga/20171118581789_a.html http://m.elecfans.com/article/973606.html https://reborn.blog.csdn.net/article/details/84977359

描述

Xilinx的新一代设计套件Vivado相比上一代产品ISE,在运行速度、算法优化和功能整合等很多方面都有了显著地改进。但是对初学者来说,新的约束语言XDC以及脚本语言Tcl的引入则成为了快速掌握Vivado使用技巧的最大障碍,以至于两年多后的今天,仍有很多用户缺乏升级到Vivado的信心。

本文介绍了Tcl在Vivado中的基础应用,希望起到抛砖引玉的作用,指引使用者在短时间内快速掌握相关技巧,更好地发挥Vivado在FPGA设计中的优势。

Tcl的背景介绍和基础语法

Tcl(读作tickle)诞生于80年代的加州大学伯克利分校,作为一种简单高效可移植性好的脚本语言,目前已经广泛应用在几乎所有的EDA工具中。Tcl 的最大特点就是其语法格式极其简单甚至可以说僵化,采用纯粹的 [命令 选项 参数] 形式,是名副其实的“工具命令语言”( 即Tcl的全称Tool Command Language)。

实际上Tcl的功能可以很强大,用其编写的程序也可以很复杂,但要在Vivado或大部分其它EDA工具中使用,则只需掌握其中最基本的几个部分。

注: 在以下示例中,% 表示Tcl的命令提示符,执行回车后,Tcl会在下一行输出命令执行结果。 // 后是作者所加注释,并不是例子的一部分。

设置变量

Tcl在Vivado中的基础应用 s_d385465f89404d7bade7a170f01e8eb7

打印主要通过puts语句来执行,配合特殊符号,直接决定最终输出内容。

文件I/O

Tcl在Vivado中的基础应用 035J2D34_0

可以看到Tcl对文件的操作也是通过设置变量,改变属性以及打印命令来进行的。上述写文件的例子中通过puts命令在my_file.txt文件中写入两行文字,分别为“Hello World!” 和myVar变量的值,然后在读文件操作中逐行读取同一文件的内容。

控制流和循环命令

Tcl语言中用于控制流程和循环的命令与C语言及其它高级语言中相似,包括if、while、for和foreach等等。

具体使用可以参考如下示例: 035G12213_0

Tcl在Vivado中的基础应用

子程序/过程

Tcl中的子程序也叫做过程(Procedures),Tcl正是通过创建新的过程来增强其内建命令的能力,提供更强的扩展性。具体到Vivado的使用中,用户经常可以通过对一个个子程序/过程的创建来扩展或个性化Vivado的使用流程。 035F114H_0

Tcl在Vivado中的基础应用

一些特殊符号 03564045E_0

Tcl在Vivado中的基础应用

Tcl语言的基本语法相对简单,但要熟练掌握仍需日常不断练习。Xilinx网站上有很多相关资料,这里推荐两个跟Tcl相关的文档 UG835 和 UG894 ,希望对大家学习Vivado和Tcl有所帮助。

在Vivado中使用Tcl定位目标

在Vivado中使用Tcl最基本的场景就是对网表上的目标进行遍历、查找和定位,这也是对网表上的目标进行约束的基础。要掌握这些则首先需要理解Vivado对目标的分类。

目标的定义和定位

Tcl在Vivado中的基础应用 035610P03_0

如上图所示,设计顶层的I/O称作ports,其余底层模块或是门级网表上的元件端口都称作pins。而包括顶层在内的各级模块,blackbox以及门级元件,都称作cells。连线称作nets,加上XDC中定义的clocks,在Vivado中一共将网表文件中的目标定义为五类。要选取这五类目标,则需用相应的get_*命令,例如get_pins等等。 get_ports

ports仅指顶层端口,所以get_ports的使用相对简单,可以配合通配符“* ”以及Tcl语言中处理list的命令一起使用。如下所示, 035540DQ_0

Tcl在Vivado中的基础应用

get_cells/get_nets

不同于ports仅指顶层端口,要定位cells和nets则相对复杂,首先需要面对层次的问题。这里有个大背景需要明确:Vivado中Tcl/XDC对网表中目标的搜索是层次化的,也就是一次仅搜索一个指定的层次current_instance,缺省值为顶层。

以下图所示设计来举例,若要搜索A(不含a1,a2)层次内的所有cells和名字中含有nt的nets, 有两种方法: 0356015416_0

Tcl在Vivado中的基础应用

若要将搜索层次改为A+B+b1,则可以写一个循环,逐一用current_instance将搜索层次指向A,B和b1,再将搜索到的cells或nets合成一个list输出即可。

若要将搜索层次改为当前层次以及其下所有子层次,可以使用 -hierarchical (在Tcl中可以简写为-hier )。 0355104360_0

Tcl在Vivado中的基础应用

在使用-hierarchical时有一点需要特别留意,即后面所跟的搜索条件仅指目标对象的名字,不能含有代表层次的“/” 。 下面列出的写法便是一种常见的使用误区,并不能以此搜索到A及其下子层次内所有的cells。

get_pins

Tcl在Vivado中的基础应用 0354341315_0

pins在Vivado数据库中有个独特的存在形式,即 / 。这里的“/”不表示层次,而是其名字的一部分,表示这个pin所属的实体。也就是说,在使用get_pins 配合-hier来查找pins时,“/”可以作为名字的一部分,出现在搜索条件内(注意与上述get_cells和get_nets的使用区别)。 035403V48_0

Tcl在Vivado中的基础应用

目标之间的关系

Tcl在搜索网表中的目标时,除了上述根据名字条件直接搜索的方式,还可以利用目标间的关系,使用-of_objects(在Tcl中可以简写为-of)来间接搜索特定目标。Vivado中定义的五类目标间的关系如下页左图所示。 035401I29_0

Tcl在Vivado中的基础应用

以上示右图的设计来举例, 035333N04_0

Tcl在Vivado中的基础应用

下图是一个更复杂的示例,涉及跨层次搜索。可以看到在get_pins时,要加上-leaf才能准确定位到门级元件(或blackbox)的端口q。另外,在实际操作中,使用get_nets和get_pins时,需要视情况而加上其它条件(-filter)才能准确找到下述例子中的cells (i2)。 1G01TR5-13

Tcl在Vivado中的基础应用

高级查找功能

在使用get_*命令查找网表中的目标时,除了名字这一直接条件,往往还需要辅以其它更复杂的条件判断,这就需要用到高级查找功能:-filter 结合Tcl支持的各种关系和逻辑运算符(==, !=, =~, !~, <=, >=, >, <, &&, ||)甚至是正则表达式来操作。 035301C48_0

Tcl在Vivado中的基础应用

在创建子程序时也常常用到-filter,例如下述get_p的子程序/过程就可以用来返回指定管脚的方向属性,告诉用户这是一个输入管脚还是一个输出管脚。

Tcl在Vivado中的延伸应用

Tcl在Vivado中的应用还远不止上述所列,其它常用的功能包括使用预先写好的Tcl脚本来跑设计实现流程,创建高级约束(XDC不支持循环等高级Tcl语法)以及实现复杂的个性化设计流程等等。Tcl所带来的强大的可扩展性决定了其在版本控制、设计自动化流程等方面具有图形化界面不能比拟的优势。

Vivado在不断发展更新的过程中,还有很多新的功能,包括ECO、PR、HD Flow等等都是从Tcl脚本方式开始支持,然后再逐步放入图形化界面中实现。这也解释了为何高端FPGA用户和熟练的Vivado用户都更偏爱Tcl脚本。

cisen commented 3 years ago

https://blog.csdn.net/long_fly/article/details/78897158

TCL脚本语言 注:TCL在VIVADO中的具体使用将写在VIVADO 之 TCL脚本工具 [下]中,本文只有TCL语法

Tcl(Tool Command Language)是一种很通用的脚本语言,它几乎在所有的平台上都可以解释运行,而且VIVADO也提供了TCL命令行。最近发现TCL脚本貌似比GUI下操作VIVADO效率高一些,方便一些。而且最近跟着官网文档做SDSOC的flatform,发现xilinx官网的文档里都是用TCL命令来完成操作,于是决心学习一下TCL的语法。

应用程序(如VIVADO)使用Tcl作为它的命令语言的好处: 1 Tcl提供了标准语法,一旦用户掌握了Tcl就可以很容易的发布命令给基于Tcl的程序。 2 Tcl实现了很多的功能,使你的工作变得很方便。 3 TCl可作为程序间通信的接口。

命令格式 一条Tcl的命令串包含了多条命令时,用换行符或分号来隔开 而每一条命令包含了一个域的集合,域使用空白分开的,第一个域是一个命令的名字,其它的是作为参数来传给它

数据类型 Tcl只支持一种数据结构:字符串。所有的命令、命令里的所有的参数、命令的结果、变量全部都是都是字符串。

简单实例:

set i 123 将123这个字符串赋值给i变量

unset i 清除变量

set i hi 将hi这个字符串赋值给i变量

set i "hi hello" hi hello中有空格,所以加引号

set i 123;#开始注释 注意注释前,要先用分号,把命令结束掉,或者换行注释

基本语法和基础命令 在VIVADO中的TCL命令行里,学习这些基本语法 (Windows下 –> 开始 –> 所有程序 –> Xilinx Design Tools –> Vivado xxx –> Vivado xxx Tcl Shell)

1>使用$符号引用变量

其中puts是打印命令

2>使用[]将命令返回值,作为新命令的参数

set j 232命令会返回值232 新命令就成了 set i 232

这里稍微复杂一点点的例子: set i a[set j b][set k c] 最后的结果就是:j=b ; k=c ; i=abc

3>数组 数组不需要声明,直接赋值即可,也不必按照顺序来: set i(1) 123 ; set i(16) hi

当然也支持任意维数的数组: set i(1,2,3) hi 引用的时候直接$i(1,2,3)即可

· parray命令 可以打印出一个数组的全部信息:

· array命令 命令格式:array option arrayName

option 是 操作选项,有如下可选:   name : 返回数组的所有元素的名称   size : 返回数组的长度   startsearch : 初始化一次遍历,返回一个遍历标识符(searchId),这个searchId在下面用到,(是可以多个遍历同时进行的)   下面的命令格式为:array option arrayName searchId   ->nextelement : 返回数组中下一个元素,如果没有返回空   ->anymore : 如果接下来还有元素,返回1,否则返回0   ->donesearch : 结束遍历

4>字符串命令

· string命令

命令格式:string option string1 string2

option 是 操作选项,有如下可选:   compare : 按照字母的排序方式比较,string1 <,=,>string2,分别返回-1,0,1   match : 判断string1和string2是否匹配   first : 检索string2中第一次出现string1的位置,如果没有出现string1则返回-1   last : 和first相反   trim : 从string1中删除开头和结尾的,string2的字符

命令格式:string option string   tolower : 返回string中的所有字符被转换为小写字符后的新字符串   toupper : 返回string中的所有字符串转换为大写后的字符串   trimleft : ,去除string左空白,类似的还有trimright   length : 返回string1的长度

  range :    string range abcdef 1 2,返回输出结果为bc

· append命令 字符串追加,可以无限拼接

set i a append i b c d puts $i 1 2 3 i变量的值就成了 abcd,注意append i b c d命令,而不是append $i b c d

· split命令 命令格式:split 字符串 分割符,将字符串转换为列表

5>数字操作 tcl中只有string类型的变量,所以当进行数字运算的时候,需要用到incr和expr操作命令

· incr命令 a变量自加-3:incr a -3 a变量自加1 : incr a

· expr命令 类似C语言中的算术操作符有(在Tcl 中的逻辑:真为1,假为0): !、* 、/、 %、+、-、<<、 >> 、< 、> 、<= 、>= 、== 、!=、& 、^ 、|、&&、 || 、x ? y : z 除此之外,expr还能够识别一些函数及其返回值: abs(x) 、round(x) 、sin(x)、cos(x) 等 使用方法:expr 表达式

6>list列表 类似python中的列表,比如:{abc {def {jkl ccc}}}是一个有两个元素的列表 abc和{def {jkl ccc}},Tcl中对list的命令有: (首先set l {abc {def {jkl ccc}}},下面实例中将对这个l列表进行操作)

命令 命令格式 功能 实例 concat concat 列表1 列表2 等 列表拼接
list list 列表1 列表2 等 同上
lindex lindex 列表 索引 索引列表
lrange lrange列表 索引1 索引2 索引列表的一部分
llength llength 列表 返回长度
linsert linsert 列表 索引 对象 列表新增元素
lreplace lreplace 列表 索引1 索引2 对象 把列表对应位置的元素替换
lsearch lsearch (-mode) 列表 对象 在列表中搜索元素的位置(可以加具体mode)
lsort lsort (-mode) 列表 列表中元素排序(可以加具体mode)
join join 列表 (连接符) 将列表中的元素连接成字符串
lappend lappend 列表 对象 向列表追加元素(需要注意如下) 需要注意的是:大部分命令都是对$l进行处理,也把就是l的内容字符串取出来,再处理,并不会对l列表的内容造成影响 需要注意的是lappend命令,lappend $l abcd是无效的,必须lappend l abcd才能实现列表内容的更新,而且是直接更改列表的内容

7>proc自定义函数 proc:

proc hello {str} { puts hello:$str } 1 2 3

需要注意的是,如果不能一行写完,那建议按照如下格式来定义(主要是要将“{”放到第一行的末尾): 第一行: proc+(空格)+函数名+(空格)+{参数}+(空格)+{ 中间行: 逻辑运算 最后行: }

全局变量global: 用于将过程中的局部变量变成外界可操作的全局变量

proc hello {} { global x set x hi set i hello} 1 2 3 4 上述代码,执行结果:

return命令:

proc hello {} {return world} set i [hello] 1 2 return命令没啥好说的,上述代码的结果是,将i变量赋值为world字符串

8>流控制

if 流控制 这个同样建议按照格式来: 第一行: if+(空格)+{表达式}+(空格)+{ 中间行: 逻辑运算 第N行: }+(空格)+else+(空格)+{ 中间行: 逻辑运算 最后行: }

switch流控制 例子如下,一目了然:

switch 2 { 1 {puts 111}
2 {puts 222}
3 {puts 333}
default {puts xxx}
} 1 2 3 4 5 6 case流控制 case abcd in a {puts 111} bc {puts 333} default {puts xxx}
1 上述程序对字符串abcd进行判断: 条件一 : 字符串为a 条件二 : 不管字符串的前后字符是啥,只要中间有bc子字符串即可 条件三 : default

9>循环控制

foreach循环: 假如想要将0,3,2,1按照顺序分别放到上述switch的判决条件(列表)里,输出四个结果,那就需要这个foreach了: foreach i {0 3 2 1} { switch $i { 1 {puts 111}
2 {puts 222}
3 {puts 333}
default {puts xxx}
} } 1 2 3 4 5 6 7 8 for循环: TCL的for循环也是很类似C语言的: for {set i 0} {$i < 10} {incr i} { puts $i } 1 2 3 初始化i=0,范围 i<10 ,循环i=i+1

while循环: set i 10 while {$i!=5} { puts $i incr i -1 } 1 2 3 4 5 运行的结果,自己就可以想象了

10>字符串转为命令 eval命令: set a set ; set b i ; set c hello ; eval $a $b $c 上述代码就等效于:set i hello eval将字符串的内容,作为命令,执行

11>打印输出 之前的那个puts命令也是可以打印到命令行,但是,也只是能打印出来而已,而这个format类似于C中的sprintf(用于格式化输出): format命令: format可以这样用:

format "%s %d" hello 666 set i [format "%s %d" hello 666] 1 2 scan命令: 说到format,刚好一起把scan说了,这两个命令可以看做是相反的一对, 前者组合成字符串,后者把字符串拆分后赋值给变量 scan 12.34.56.78 %d.%d.%d.%d a b c d 将12.34.56.78拆分,并分别赋值给a b c d四个变量,命令返回赋值成功的变量的个数

puts命令: puts当然也可以打印到文件中

set f [open test.txt w] puts -nonewline $f "hello\n" puts $f "world" close $f 1 2 3 4 puts -nonewline $f "hello\n"表示的是强制不换行打印,否则自动追加一个换行符

文件系统 基本常用操作: gets –> 一次读一行文件 puts –> 写入文件 open –> 打开文件 close –> 关闭文件 flush –> 刷新缓冲区

cd命令 和shell中的cd一样

pwd命令 用于查看当前所在的目录

open命令 打开文件,返回文件描述符 命令格式:open 文件名 模式,支持6种模式,和其他编程语言中的文件IO,也是很相似的,模式如下: r 模式: 打开只读文件(文件必须存在) r+ 模式: 打开可读写文件[r+和a+模式可以类比] w 模式: 打开只写文件,若文件存在则清空内容;若文件不存在则创建文件。 a 模式: 以追加方式打开只写文件,若文件不存在,则创建;如果文件存在,则会在文件内容最后面追加写入的数据

xxxx 理论上说open |文件名 模式,在文件名前加个“|”符号,可以以管道的模式打开文件,但是测试一直没有成功,之后用到的话再回来解决吧 xxxx

read命令

set f [open test.txt r] read $f 6 close $f 1 2 3 可以使用eof命令,判断文件是否读完了,eof $f,读完返回1,否则返回0 上述代码直接从文件中读6个字节;如果想把文件内容全部读出,则直接read $f;如果想一行一行读则使用gets命令:gets $f

source命令 命令格式:source $f 从对应的文件中读出内容,并传给Tcl解释执行

tell命令 返回文件的指针位置,命令格式:tell $f

file命令 命令格式:file option name option操作选项较多,就直接列个表了,表示如下:

option名称 功能 dirname 返回最后一个“/”之前的部分(目录部分) tail 返回最后一个“/”以后的部分 executable 返回文件是否可被执行 mkdir 在当前目录下创建文件夹 owned 判断文件是否属于当前目录 exists 返回1 表示文件存在,0 表示文件不存在 extension 返回文件的扩展名 rootname 返回去除文件扩展名后的name isdirectory 判断是否为目录 isfile 判断是否为文件 writable 判断文件是否可写 readable 判断文件是否可读 size 返回文件的大小 split 把Windows格式的路径拆分 join 组合成标准路径 normalize 返回标准化路径 nativename 返回原生格式路径 pathtype 判断路径为相对路径还是绝对路径 type 返回文件类型(有file、directory、characterSpecial、blockSpecial、fifo、link、socket) delete 删除指定文件,当需要强制删除非空目录时(命令格式:file delete -force 目录名) copy 复制文件(命令格式:file copy 源 目标),同样的-force可以强行覆盖同名文件(目标名冲突时) rename 重命名,同样可以强制

除此之外,file 的 stat 状态操作选项: 命令格式:file stat name k,结果存在数组k里

glob命令 1)查看当前目录下的文件(类似shell中的ls) glob *

2)查看当前目录下特定后缀的文件 glob .txt .tcl

3)查看当前目录下的txt、txl、tcl和tct文件: glob {*t[xc][tl]}

4)查看当前目录下的子目录里查看txt、txl、tcl和tct文件: 用“\”分割路径,格式为:glob {{目录1,目录2等}\*.后缀}

5)-type选择查看类型: 命令格式:glob -type {类型1 类型2 等} 目标目录 类型有:

类型 含义 b 块设备 c 字符设备 d 代表目录 f 文件 l 代表符号链接 p 代表命名管道 s 代表套接字 r 读 w 写 x 可执行

seek命令 用于调整文件指针 命令seek $f 2,文件指针定位到序号为2,现在有一个文件名为s1.txt,内容为hello字符串,那么,设计一个程序实现从第三个字符串开始读文件内容:

info命令获取信息 假如创建了一个过程:proc hello { a b c } {puts hi} 执行命令:info args hello,则返回a b c,参数列表 执行命令:info body hello,则返回puts hi,函数体 info procs,返回所有的过程的列表 info procs hello,如果存在hello过程则返回hello字符串,不存在则不返回

info commands,则列出解释器支持的所有命令 info commands create_ip,create_ip是vivado支持的tcl命令,所以这个info返回的值是create_ip,如果不支持该命令的话,则不返回值

info exists kkk,判断kkk变量是否存在

info vars,返回当前变量名的列表 info vars i,如果存在该i变量则返回i字符串,不存在则不返回

info globals,返回全局变量的列表 info globals env,如果存在该env全局变量则返回env,不存在则不返回

info locals,返回local变量列表 info locals i,如果存在该i局部变量则返回i,不存在则不返回

info hostname,返回主机名 info cmdcount,则返回当前解释器已经执行的命令个数 info tclversion,返回解释器版本号

info level,返回当前的在栈中的绝对位置 info level 1,如果加了参数数字,则返回该层的命令和参数

注:uplevel命令(连接参数) 既然说了level那就把uplevel命令说了,level值为0代表顶层,level代表在栈中的绝对位置,过程调用的时候,一层比一层的level值高1,被调用的过程中若想在上一层的环境中执行操作,那么就需要uplevel命令了

proc hello {} { uplevel set a "helloworld" } set a hi ; hello ; puts $a 1 2

注:upvar命令(连接变量) 既然说了uplevel那就把upvar命令也说了吧,其类似于uplevel命令,但是其侧重的是在不同层之间连接单一变量

proc hello {a} {upvar $a x ; set x helloworld} set i hi ; hello i ; puts $i 1 2

系统异常、系统监视 catch命令 用于阻止因错误而导致的中断执行,类似python中的异常,执行成功返回0,否则返回1

unknown命令 我将这个指令归为异常指令 使用方法:首先定义一个unknown过程,这个过程的参数为cwd(命令)和args(参数)

proc unknown {cwd args} { puts commend:$cwd puts args:$args } 1 2 3 4

这样的话,当有未知命令或者打错了代码的话,就可以通过unknown过程,控制错误

time命令 time "set i 10",该命令将计算执行的时间

trace命令 监视变量的存储的命令,感觉暂时用不到,需要用到的时候再看

命名空间namespace 命名空间是命令和变量的集合,通过命名空间的封装,来保证他们不会影响其它命名空间的变量和命令

设置新命名空间 首先定义两个hello过程,其中一个在hlf命名空间内,然后测试

namespace eval hlf {pro hello {} {puts hello_hlf}} pro hello {} {puts hello_all} 1 2

设置新变量 直接通过set hlf::i 888,就可以对hlf空间的i进行设置

删除命名空间 命令:namespace delete hlf

不同命名空间共享变量和过程 通过export和import命令,完成一个命名空间导出过程,另一个命名空间将其导入,完成过程共享

对命名空间的变量进行设置或访问 variable命令,以例子说明:

namespace eval hlf { variable i 5 proc next {} {variable i;return [incr i]} proc reset {} {variable i;set i 0} } 1 2 3 4 5

目前的理解就是可以在同一命名空间内的不同过程中传递变量,也就不深究了

到此为止算是对TCL的基本使用有了一个大致的理解,里面还有很多具体的函数和函数选项没有涉及到,如果之后用的到的话再做补充吧,但是我觉得,应对VIVADO的TCL的语法,这些基础语法应该足够了的,接下来就是对VIVADO自带的TCL的库里的函数,进行一个了解了

之后遇到不懂的命令,就直接输入命令 -help,就可以看到一堆帮助了

cisen commented 2 years ago

总结:

vavado添加vh文件:

set file "default/common_defines.vh"
set file_obj [get_files -of_objects [get_filesets sources_1] [list "*$file"]]
set_property -name "file_type" -value "Verilog Header" -objects $file_obj
set_property -name "is_enabled" -value "1" -objects $file_obj
set_property -name "is_global_include" -value "0" -objects $file_obj
set_property -name "library" -value "xil_defaultlib" -objects $file_obj
set_property -name "path_mode" -value "RelativeFirst" -objects $file_obj
set_property -name "used_in" -value "synthesis simulation" -objects $file_obj
set_property -name "used_in_simulation" -value "1" -objects $file_obj
set_property -name "used_in_synthesis" -value "1" -objects $file_obj

set file "$origin_dir/../../../../Downloads/FPGA_SWRV/SweRV_on_FPGA/SweRV_on_FPGA.srcs/sources_1/imports/default/common_defines.vh"
set file [file normalize $file]
set file_obj [get_files -of_objects [get_filesets sim_1] [list "*$file"]]
set_property -name "file_type" -value "Verilog Header" -objects $file_obj
set_property -name "is_enabled" -value "1" -objects $file_obj
set_property -name "is_global_include" -value "0" -objects $file_obj
set_property -name "library" -value "xil_defaultlib" -objects $file_obj
set_property -name "path_mode" -value "RelativeFirst" -objects $file_obj
set_property -name "used_in" -value "synthesis simulation" -objects $file_obj
set_property -name "used_in_simulation" -value "1" -objects $file_obj
set_property -name "used_in_synthesis" -value "1" -objects $file_obj

打印日志:

# 可以带变量
puts "aaaa"