Vivado/Tcl零基础入门与案例实战
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第1部分 基础部分

第1章 Tcl基础知识

1.1 什么是Tcl

安装Vivado(以Vivado 2020.1版本为例)之后,在GUI(Graphical User Interface)界面中会看到Tcl Console。在这里可以输入Tcl命令,同时,还会发现Vivado 2020.1 Tcl Shell,表示可以在Tcl模式下使用Vivado。通常情况下,借助GUI的操作都有对应的Tcl命令,但不是每个Tcl命令都可以通过GUI完成。从这个角度而言,用户可以编写自己的Tcl命令来扩展Vivado的功能,从而让Vivado更强大。熟练使用Tcl,将会显著提升Vivado的使用效率。事实上,很多EDA厂商都把Tcl作为标准的API(Application Programming Interface),控制和扩展工具的应用。

那么,什么是Tcl呢?Tcl(Tool Command Language)是一种脚本语言,一种基于字符串的命令语言,一种解释性语言。所谓解释性语言,是指其不像其他高级语言一样需要通过编译和联结,而是与其他Shell语言一样,可直接对每条语句进行顺序解释、执行。

Tcl具有两大特征:

● 所有结构都是一条命令,包括语法结构(如for、if等)。

● 所有数据类型都可被视为字符串(基于字符串的命令语言)。

总结:在Tcl中,一切都是字符串。

基于以上两大特征再次理解什么是解释性语言。

代码1-1

以代码1-1为例,在处理if命令时,Tcl解释器只知道这个命令包括三个单词,第一个单词是命令名if。此时,Tcl解释器并不知道if命令的第一个参数是表达式,也不知道第二个参数是Tcl脚本。在完成对这个命令的解析之后,Tcl解释器才会把if命令中的两个单词都传给if。此时if命令会把第一个参数作为表达式,把第二个参数作为Tcl脚本进行处理。如果表达式的值非0,那么if命令就会把第二个参数传回Tcl解释器进行处理。此时,解释器会把第二个参数作为脚本对待。事实上,命令名后面的两组大括号并无不同,其目的都是让Tcl解释器把括号内的字符原封不动地传给if命令,不进行任何置换操作。

学习Tcl的工具如下:

● 如果安装了Vivado,则Vivado自带的Vivado Tcl Shell就足够了。

● 用于学习Tcl的其他工具可在https://www.tcl.tk/software/tcltk/中浏览。

1.2 Tcl脚本的构成

一条Tcl脚本是由一个或多个单词构成的。单词之间以空格或Tab键隔开。第一个单词为命令名,其余单词为该命令的参数,如图1-1所示。该命令由3个单词构成。命令名为set,包含两个参数:第1个参数为变量名;第2个参数为变量值。

图1-1

Tcl脚本可以只包含一条命令,也可以包含多条命令。命令之间可以由分号隔开,也可以直接采用换行方式,如图1-2所示。

图1-2

在采用分号或换行方式作为命令之间的分隔符时,两者的区别在于分号促使其左侧命令不会显示输出结果,如代码1-2所示。由此可见,尽管以分号作为命令之间的分隔符可使代码更为紧凑,但也降低了调试过程中命令结果的可视性。从代码风格的角度而言,换行方式可提升代码的可读性。

代码1-2

总结:从代码风格的角度而言,在书写Tcl脚本时,对于独立的命令,最好使用换行方式隔开不同的命令,有助于调试后续代码。

1.3 变量赋值

在Tcl中,对变量赋值时需要用到set命令。该命令后跟随两个参数:第一个参数是变量名;第二个参数是变量值。C语言要求变量名的开头必须是字母或下画线,不能是数字,而Tcl对于变量的命名没有任何限制,变量名可以以下画线开头,也可以以数字开头,还可以以空格符开头,如代码1-3所示。

代码1-3

总结:尽管Tcl对于变量的命名没有实质性的限制,但从代码风格的角度而言,仍然建议变量名中只包含字母、数字、下画线,从而避免因变量名带来歧义。同时,如果使用字母,则需要注意,Tcl对大小写是敏感的,例如,a和A是不同的变量名。

也可以通过命令incr对变量进行赋值。在该命令后可跟随一个或两个参数,其中的第一个参数始终是变量名。如果没有第二个参数,则执行第一个参数加1的操作,如果有第二个参数,则执行第一个参数与第二个参数相加的操作,如代码1-4所示:第2行代码不仅定义了变量v,还将其初始值设置为0;第10行和第11行代码表明,incr后的第一个参数必须是整型数据,实际上,incr的第二个参数也必须是整型数据。

代码1-4

总结:incr命令后的两个参数都必须是整型数据。

Tcl对命令的求值过程分为两步:解析和执行。

● 在解析阶段,Tcl解释器运用规则把命令分解为一个个独立的单词,同时进行必要的置换(Substitution,关于置换的内容将在下一节介绍)。

● 在执行阶段,Tcl解释器会把第一个单词作为命令名,并查看该命令是否有定义,同时查找完成该命令功能的命令过程。如果有定义,则Tcl解释器调用该命令过程,并把命令中的全部单词传递给该过程。命令过程会根据自己的需求来分辨这些单词的具体含义。每个命令对所需参数都有一些自身的要求,如果不满足要求,则会报错。Tcl会把错误信息保存在全局变量errorInfo中,如代码1-5所示。在执行代码1-5时,先执行代码的第2行和第4行,待第5行输出错误信息后,再执行代码的第6行。其中,命令puts用于向控制台或文件输出信息;“$”是变量置换符,将在下一节中介绍。

代码1-5

总结:Tcl会把当前命令的错误信息保存在全局变量errorInfo中,对于代码调试而言非常有用,因为可以通过puts$errorInfo的方式输出变量值,进而查看错误信息。

unset命令的功能与set命令的功能相反。该命令将取消变量定义并释放该变量所占用的内存空间。需要注意的是,取消未定义的变量是不合法的,如代码1-6所示。

代码1-6

如果需要判断指定变量是否已经被定义,则可以使用命令“info exists变量名”进行判断,如代码1-7所示。若变量存在,则返回1,否则返回0。

代码1-7

info命令的主要功能是查看Tcl解释器的相关信息,如代码1-8所示:选项tclversion用于返回Tcl解释器的版本信息;选项hostname用于返回主机名。

代码1-8

1.4 变量置换

除了可以直接给变量赋值,还可以把某个变量的值赋给另一个变量。例如,变量x的值为1,期望变量y的值为x,则期望变量y的值也为1。若采用代码1-9所示的方式,则最终发现y的值为x(Tcl解释器把字符x赋值给y),并不是期望值1。这里就涉及变量置换的内容。在Tcl中,变量置换通过$(美元符号)完成,如代码1-9中的第6行所示。通过这样的方式可成功地将变量x的值赋给变量z。

代码1-9

那么,是不是所有的变量,只要通过符号“$”都可以完成变量置换呢?下面看一个简单的例子,如代码1-10所示。变量名为a-b-c,变量值为字符串“Hello”,这里需要把变量str的值设置为变量a-b-c的值。但在通过“$”进行变量置换时,显示变量a不存在。由此可见,Tcl将“-”当作字符串分割符了。此时,可通过“{}”把变量名a-b-c括起来,使Tcl解释器把它当作一个整体对待,从而正确实现变量置换,如代码1-10中的第6行所示。

代码1-10

类似地,Tcl解释器也会将“.”当作字符串分割符,但对于下画线或以数字开头的变量名则不会出现这样的问题,如代码1-11所示。如果并不清楚Tcl解释器是否会把变量名作为整体对待,则谨慎起见,可用“{}”把变量名括起来。

代码1-11

总结:Tcl解释器会将“-”“.”视为字符串分隔符,因此,应尽可能避免在变量名中出现这两个字符。若不可避免需要使用这两个字符,则在变量置换时,必须将变量名放在“{}”中。

借助变量置换,可以很容易地完成字符串拼接。例如,变量a为5,变量b为6,给变量c赋值56,则可通过$a$b完成,如代码1-12所示。此功能也可通过命令append完成。在该命令后跟随两个参数:第一个参数是变量名;第二个参数是新添加的字符串。此命令较$a$b的方式更为高效。因为下画线不是字符串分隔符,所以,Tcl解释器不会认为存在名为x_的变量,如代码1-12中的第16行和第17行所示。在这种情况下,可依然采用将变量名放入花括号“{}”中的方式,如代码1-12中的第14行所示(第14行中的第二个“{}”可以省略不要)。

代码1-12

例如,变量x为LUT,需要把变量y设置为LUT6,也就是把字符串LUT和字符6拼接在一起。如果直接使用$x6的方式,则会报错,Tcl解释器会将x6视为一个变量,但该变量并未事先定义,所以会显示该变量不存在。此时,需要通过“{}”把变量x括起来,并通过“$”符号完成变量置换,如代码1-13所示。

代码1-13

在满足上述变量置换规则的前提下,Tcl是否能够完成两次变量置换呢?如代码1-14所示,变量a的值为5,变量var的值为a,期望$$var为5,也就是进行两次变量置换,但从代码1-14中第6行的输出结果来看,最终输出的是$a。这是因为Tcl解释器在同一层级下遇到变量置换符“$”时,只会发生一次置换。在执行变量置换时,Tcl解释器会先找到“$”,然后从“$”之后的第一个字符开始算起,直到遇到非法字符(字母、数字、下画线之外的其他字符)为止,这其中的所有字符被视为变量名。在代码1-14的第6行,对于$$var,第一个“$”符号之后的字符仍是“$”,为非法字符,故Tcl解释器没有找到合法的变量名,也就没有发生变量置换。紧接着,Tcl解释器开始从第二个“$”符号后的第一个字符开始扫描,很快找到了合法的变量名var,并进行了变量置换,故最终输出$a,而不是5。

代码1-14

如果期望执行两次变量置换,则可采用代码1-15的方式(第6行中的“[]”用于命令置换,将在下一节中介绍)。“[]”中的set命令只跟随一个参数$var,$var用于进行变量置换,故set $var等效于set a,而set a将返回变量a的值。因此,从本质上讲,$var是[set var]的缩写版本。通过代码1-15中的第10行和第12行可以看出,如果set后只有一个参数,而这个参数又是一个已经定义的变量名,那么该命令就直接返回该变量的变量值,与$var等价。

代码1-15

除了可使用代码1-15所示的方式执行两次变量置换,还可以采用命令subst实现此目的,如代码1-16所示。

代码1-16

结合代码1-17再次理解两次变量置换。在这里用到了foreach命令(此命令将在第4章中进行详细介绍),读者可重点关注代码1-17中的第9行。

代码1-17

总结:在执行变量置换时,Tcl解释器会从“$”后的第一个字符开始扫描,直至遇到除字母、数字、下画线之外的字符,并将其中所有字符构成的字符串视为变量名。同时,$var与set var等效,可认为$var是set var的缩写版本。此外,变量可在单词的任意位置置换。

1.5 命令置换

命令置换是Tcl的第二种置换形式,以“[]”的形式体现(“[]”中是另一个Tcl命令)。从这个角度而言,命令置换实际上就是命令的嵌套。命令置换会导致某一个命令的所有或部分单词被另一个命令的结果所代替,如代码1-18所示。代码1-18中第6行的expr命令会在解析set的单词时执行,expr的结果,即字符串16,将成为set命令的第二个参数。

代码1-18

在命令置换时,“[]”中的脚本可以包含任意多条命令,命令之间用换行符或分号隔开,但是,最终的返回值为最后一条命令的返回值。如代码1-19所示,“[]”中有两个命令expr和set,通过分号隔开,最终y的值为最后一条命令set x的返回值。从代码风格的角度而言,并不建议在“[]”中通过换行符或分号分隔多条命令。

代码1-19

另外,命令置换是可以嵌套的,即在一个命令置换中还可以包含另一个命令置换,如代码1-20所示。在代码1-20的第6行命令set中嵌套了命令expr,而expr中又嵌套了string length(该命令用于返回字符串的长度)。因此,在解析set命令时,会先解析expr,在解析expr时又会先解析并执行string length。

代码1-20

总结:从代码风格的角度而言,在以命令置换为目的的“[]”中,尽可能只放一条Tcl命令,以增强代码的可读性,同时便于后期代码调试。

1.6 反斜线置换

最后一种置换是反斜线置换。与C语言中的反斜线用法类似,Tcl中的反斜线主要用于在单词中插入被Tcl解释器当成特殊符号的字符,如换行符、“[”、空格、“$”等。

以代码1-21为例,需要将变量str1赋值为hello world(注意hello与world之间有空格),如果没有反斜线,则Tcl解释器会认为这里的空格是分隔符,从而认为set命令的参数多于两个,故报错。在添加反斜线后,空格不再被当作分隔符,hello world被当作一个整体,即作为一个单词。在代码1-21中的第4行,需要将变量str2赋值为“$5”,由于“$”是变量置换符,如果直接写成“$5”,则Tcl解释器会认为“$”后跟的是变量名,但5作为变量名并不存在,故报错。在添加反斜线后,“$”不再被认为是变量置换符。在代码1-21中的第8行,需要给变量net赋值reg[x],而“[”是命令置换符,但x不是合法命令,故报错。在添加反斜线后,“[”不再被当作命令置换符进行处理。

代码1-21

如果希望反斜线本身也成为变量值的一部分,那么仍需要通过反斜线置换完成。以代码1-22为例,若想给变量str3赋值“\”,则需要描述为“\\”的形式;若想给变量str4赋值“\b”,但“\b”实际上表示Backspace,故需要通过反斜线置换才可得到“\b”。

代码1-22

总结:反斜线“\”本身也被Tcl解释器认为是特殊字符。

1.7 深入理解Tcl中的置换

在Tcl语言中有三类置换:变量置换、命令置换和反斜线置换。可以说置换是Tcl的灵魂,同时也是一个让初学者感到困惑的难点。很多初学者常会碰到这样的情形:不希望发生置换时却发生了,或者希望发生置换时却没有发生,加之一些Tcl解释器的调试功能欠佳,往往让初学者觉得自己的脚本发生了“诡异”行为。实际上,Tcl的置换机制很简单,其行为也很容易预测,只需记住如下两条规则。

● 规则1:Tcl在解析一条命令时,只从左向右解析一次,进行一轮置换,每一个字符只会被扫描一次。

● 规则2:每一个字符只会发生一层置换,而不会对置换后的结果再进行一次扫描置换。

以代码1-23为例,变量x被赋值为10,变量a被赋值为字符x,变量b被赋值为$$a。根据上述规则,Tcl从左向右对命令“set b$$a”进行解析,即扫描所有的字符,在发现“$$a”时,执行变量置换,得到“$x”,同时只发生一层置换,不会对置换后的结果“$x”再进行扫描置换(否则“$$a”中最左侧的“$”,也就是第一个“$”将被扫描两次,与规则1冲突)。因此,最左侧的“$”并不会触发变量置换,最终变量b的值将会是“$x”,而不是10。

代码1-23

从Tcl代码风格的角度看,应尽可能地将置换简单化,这就意味着尽可能地将多层次的嵌套置换分解为简单的层次置换,可通过命令分解实现,同时,应避免在同一条命令中出现太多的置换,尤其避免出现太多复杂的不同类型的置换,这对代码维护十分不利。值得考虑的方法是建立“过程”,将复杂的操作隔离开来,从而增强代码的可读性和可维护性。以代码1-24为例,在计算两个字符串总长度时,用到了三个命令:set、expr和string length。在计算str_len时,使用了变量置换和命令置换,同时出现了命令嵌套。对比另一种写法,如代码1-25所示,将嵌套拆分,代码的可读性便跃然纸上。尤其是当后续需要使用代码1-25中第6行或第7行的结果时,这种方法可有效降低代码的冗余性。

代码1-24

代码1-25

1.8 双引号与花括号

在Tcl中,可通过双引号“""”和花括号“{}”将多个单词,包括分隔符(例如,换行符和空格)和置换符(例如,美元符号“$”、方括号“[]”和反斜线)等特殊字符组成一组,作为一个参数处理,这实际上是一种引用操作。双引号与花括号的区别在于双引号内的置换可正常进行,而花括号内的置换可能会被阻止,如代码1-26所示:变量s被赋值为Hello Tcl,注意这里通过双引号避免了空格被当作分隔符处理;第4行的puts命令使用了双引号,可以看到所有的置换都随之发生;第6行的puts命令使用了花括号,相应的内部置换均被阻止。

代码1-26

双引号的另一常用情形是出现在嵌套命令中,并且嵌套的命令是外层命令参数的一部分。例如,代码1-26中第4行的puts命令,内部嵌套了string length命令,而string length命令的返回值是puts命令参数的一部分。如果仅仅是命令嵌套,则不需要双引号,如代码1-27所示。

代码1-27

在给变量赋值时,也可以通过花括号使特殊字符被当作普通字符处理,如代码1-28所示。在这个例子中,第2行的花括号阻止了“$”表征的变量置换。如果将花括号替换为双引号,则系统会报错。

代码1-28

如果在一个脚本中同时使用双引号和花括号会是什么结果呢?以代码1-29为例,在给变量b赋值时使用了反斜线置换(第4行);在给变量c赋值时使用了双引号加花括号,其中双引号在最外层(第6行);在给变量d赋值时使用了花括号加双引号,其中花括号在最外层(第8行)。由此可以得出这样的结论:在同时使用双引号和花括号时,最外层的起主导作用。

代码1-29

对于花括号,如前文所述“花括号内的置换可能会被阻止”,这是因为花括号的功能稍微复杂一些,但总体来说遵循两个原则:第一个原则是如果花括号用于置换操作,则其内部的置换操作会被阻止;第二个原则是如果花括号作为界限符,例如,在过程定义时用作过程体的边界,if语句、循环语句(for和while)、switch语句等的边界,以及数学表达式时,其内部的置换操作不会被阻止。

如果需要双引号或花括号作为普通字符出现在字符串中,则可通过反斜线置换,或者通过双引号和花括号的嵌套使用实现特定功能,如代码1-30所示。

代码1-30

1.9 注释与续行

Tcl中的注释符为“#”,但“#”的位置是有规定的,即它必须为命令的第一个字符。从这个角度而言,Tcl的注释和命令处于同一层次,这就意味着一个注释符要占用一个命令的位置。以代码1-31为例,第1行和第2行的注释独自占据一行并以“#”开头,因此该注释是合法的;尽管第3行的注释和set命令位于同一行,但在set命令后紧跟分号,表明命令结束,故该注释也是合法的;在第5行的注释中,“#”出现在set命令中间,被当作set命令的一部分,从而造成set命令参数设置不合理的问题。

代码1-31

如果在注释语句中出现了反斜线,那么即便另起一行,该行仍被认为是注释的一部分,如代码1-32所示。这也表明了反斜线具有续行的功能。

代码1-32

如果需要注释大段的代码块,则可采用如下三种方法。

方法1:if命令

这种方法被普遍接受,如代码1-33所示。由于if条件的判断条件始终为0,故花括号中的代码块不会被执行,从而起到了注释的作用。

代码1-33

方法2:花括号

由于Tcl中的花括号具有阻止内部置换的功能,故可利用此特性实现大段代码块的注释,如代码1-34所示:第9行和第10行代码是第4行代码的返回结果,从第12行代码看到,x的值仍是100,并没有发生变化,说明第5行代码没有被执行,从而达到了注释的目的。

代码1-34

方法3:proc过程

Tcl中的proc过程类似于C语言中的函数,只有当函数被调用时,才会被执行。同样地,只有该过程proc被调用,才会被作为命令去执行,如代码1-35所示:commented_out没有参数(过程名后的花括号为空),并且该过程在后续脚本中没被调用,从而达到了注释的目的。

代码1-35

在Tcl中可采用反斜线实现续行功能。需要注意的是,反斜线后的同一行不能跟随任何字符,包括空格和制表符,否则续行功能将无效,如代码1-36所示:在第2行代码的反斜线后直接回车换行,故变量a的值为hello;在第5行的反斜线后有空格,变量x被赋值为空格,此时反斜线起到置换的作用,而不是续行的功能,因此,第6行代码中的hello被认为是一个命令,故系统报错。

代码1-36

1.10 本章小结

本章小结如表1-1所示。

表1-1