本文源自UG894,主要介绍如何在vivado中使用tcl脚本
vivado中任何自带的命令都可以通过“-help”获取帮助信息。
也可以直接输入“help”取得vivado命令合集,并通过“help -category (tools)”来获取某类操作的所有命令简介。
该过程可以通过运行GUI vivado自动产生的记录文件vivado.jou查看,该文件中记录了自打开vivado后运行的所有命令。
1.通过create_project命令建立工程。
2.通过add_files或import_files添加工程文件。
3.通过launch_runs和wait_on_run依次进行综合实现。
vivado会在启动时自动加载Vivado_init.tcl文件中定义的脚本。可将自己写的proc写入该文件中,或者直接source自己的tcl脚本。
Vivado_inti.tcl路径为%APPDATA%/Xilinx/Vivado//Vivado_init.tcl
可以通过source将tcl脚本加载进入vivado中
source
使用source会默认打印脚本代码内容,可通过“-notrace”隐藏代码内容
source -notrace
在Setting中,找到综合、实现、生成比特流设置,都可以添加tcl.pre和tcl.post的路径,即可在该流程之前或者之后插入自己的tcl脚本。
原文档这部分主要简介tcl脚本语法及经典问题,此处只挑出其中的经典问题进行记录
当proc参数数量不定时,即proc有0-n个参数都可以正常运行时,参数可以以链表形式给出,在proc中,依次对输入参数列表元素进行分析处理,每处理一个就删除一个,直至所有参数处理完毕。
# 将第一个元素从列表中移除
proc lshift listVar {
upvar 1 $listVar L
set r [lindex $L 0]
set L [lreplace $L [set L 0] 0]
return $r
}
proc myproc { args } {
#-------------------------------------------------------
# Process command line arguments
#-------------------------------------------------------
set error 0
set help 0
set verbose 0
set ports {}
# if {[llength $args] == 0} { incr help }; # Uncomment if necessary
# 以此分析参数元素,每次循环将第一个元素删除
while {[llength $args]} {
set flag [lshift args]
switch -exact -- $flag {
-p -
-ports {
set ports [lshift args]
}
-v -
-verbose {
set verbose 1
}
-h -
-help {
incr help
}
default {
if {[string match "-*" $flag]} {
puts " ERROR - option '$flag' is not a valid option."
incr error
} else {
puts "ERROR - option '$flag' is not a valid option."
incr error
}
}
}
}
if {$help} {
set callerflag [lindex [info level [expr [info level] -1]] 0]
# <-- HELP
puts [format {
Usage: %s
[-ports|-p ]
[-verbose|-v]
[-help|-h]
Description: xxxxxxxxxxxxxxxxxxx.
xxxxxxxxxxxxxxxxxxx.
Example:
%s -port xxxxxxxxxxxxxxx
} $callerflag $callerflag ]
# HELP -->
return -code ok {}
}
# Check validity of arguments. Increment $error to generate an error
if {$error} {
return -code error {Oops, something is not correct}
}
# Do something
return -code ok {}
}
【注意】原文档中还给出了另一种用switch分析列表的方法,但是没有看懂,这里先不写了
局部变量是proc内部变量,该变量只在proc内使用,与外部变量相互独立(当与外部变量名字相同时,他们相互独立互不影响)。
全局变量是在proc外部定义的变量,属于全局命名空间变量。如果要在proc内部调用全局变量,需要在变量名前用global关键词;或者在变量名前加“::”。
proc printEnv {} {
global env
foreach var [lsort [array names env]] { puts " $var = $env($var)" }
}
proc printEnv {} {
foreach var [lsort [array names ::env]] { puts " $var = $::env($var)" }
}
【注意】通常不建议用在proc内用global调用全局变量,global通常用于避免向proc传递太大的列表。通常优先考虑用upvar。
为了避免变量和函数名重复,可以将函数和变量定义在更独立的空间。命名空间可以嵌套,通过::来调用命名空间中的变量和函数。
01 namespace eval foo {
02 variable stack [list]
03 variable count 0
04 variable params
05 array set params [list var1 value1 var2 value2 var3 value3]
06
07 namespace export push pop
08
09 proc push { args } {
10 variable stack
11 variable count
12 lappend stack $args
13 incr count
14 }
15
16 proc pop {} {
17 variable stack
18 variable count
19 if {[llength $stack] > 0} {
20 set value [lindex $stack end]
21 set stack [lrange $stack 0 end-1]
22 incr count -1
23 return $value
24 } else {
25 error " no more element in the stack"
26 }
27 }
28
29 }
30
31 proc foo::dump {} {
32 variable stack
33 variable count
34 if {[llength $stack] > 0} {
35 puts " There are $count element(s) in the stack:"
36 foreach element $stack {
37 puts " $element"
38 }
39 return 0
40 } else {
41 error " no element in the stack"
42 }
43 }
44
45 namespace import foo::*
vivado提供了非常丰富的get_*命令用于访问设计对象,这些命令会以列表形式返回所需内容。
每种设计对象(net,pin,port……)都有不同种类的特性参数,可以通过get_* filter来找到某种特性的对象,可以通过report_property来展示某个对象的所有特性。特性也可以增加和删除。
通过get_*、-hierarchical、filter、-regxp、-of_objects等来检索相关对象,此处不在赘述。
通过get_*都以tcl链表形式返回对象,因此可以通过tcl内置的列表命令进行处理。
foreach X [lsort -decreasing [get_cells]] {puts $X}
wbArbEngine
usb_vbus_pad_1_i_IBUF_inst
usb_vbus_pad_0_i_IBUF_inst
usbEngine1
usbEngine0
...
很多vivado命令都支持将输出保存至指定文件中,通过-file选项;或将输出结果以string形式保存,通过-return_string,以便后面的函数调用。
# 新建文件并写入内容
report_timing -delay_type max -file setup_violations.rpt
report_timing -delay_type min -file hold_violations.rpt
# 通过-append在原有文件中新增内容
report_timing -delay_type max -file all_violations.rpt
report_timing -delay_type min -file -append all_violations.rpt
文件的绝对或者相对路径可以当作文件名的一部分。
report_*命令也可以通过-return_string将结果以字符串形式输出,并可以赋给tcl变量。
set timeLines [split [report_timing -return_string -max_paths 10] \n ]
tcl提供了一系列命令对文件进行操作,这些都是tcl基本语法,此处不再赘述。
tcl提供了一系列命令对字符串进行处理,此处不再赘述。
写脚本时,一个好的习惯:使用某个参数前先检查参数状态,比如打开文件前检查文件是否存在,使用报告列表时先检查是否有元素存在,这样会避免很多脚本运行错误。
# 打开文件前检查文件是否存在
if {[file exists $filename]} {
set FH [open $filename r]
if {$FH != {}} {
# The file is opened, do something
# …
close $FH
} else {
puts " File $filename could not be opened"
}
} else {
puts " File $filename does not exist"
}
# 在使用get_*之后,检查对象是否存在
proc get_pin_dir { pinName } {
if {$pinName == {}} {
puts " Error - no pin name provided"
return {}
}
set pin [get_pins $pinName]
if {$pin == {}} {
puts " Error - pin $pinName does not exist"
return {}
}
set direction [get_property DIRECTION $pin]
return $direction
}
一些内置tcl命令出错时,如果没有对错误进行处理,可能会中断命令执行。例如当执行file命令时发现文件无法打开。
为了处理命令出错的情况,tcl内置了catch命令,当发现错误时返回1,否则返回0。
# catch语法
catch script [varname]
# script为一个或一个命令集,varname为报错内容存储变量名。示例如下:
If {[catch script errorstring]} {
# A low-level TCL_ERROR happened
puts " Error - $errorstring "
} else {
# No TCL_ERROR was generated
puts " The code completed successfully "
}
# 当使用file命令时发现文件无法开始时,通过cathc处理
if {[file exists $filename]} {
if {[catch { set FH [open $filename r] } errorstring]} {
puts " File $filename could not be opened : $errorstring"
} else {
# The file is opened, do something
# …
close $FH
}
} else {
puts " File $filename does not exist"
}
error命令可用于产生TCL_ERROR。(这里没看懂,error是用于增加报错内容的吗?)
proc get_file_content { filename } {
if {[catch {
set FH [open $filename r]
set content [read $FH]
close $FH
} errorstring]} {
error " File $filename could not be opened : $errorstring "
}
return $content
}
使用exec调用外部程序,使用时要确保外部程序可以适配当前操作系统。
set result [exec /bin/perl /my_perl_script.pl]
DRC定义了一系列设计规则,并检查当前设计是否符合这些规则,打印找到的错误和冲突部分。
可以通过report_drc执行DRC,检查并报告当前设计中的违例部分。
当发现设计违背设计规则时,可以通过create_dec_violation定义且标识违例行为。
可以通过create_dec_check命令创建用户自定义DRC。
可以通过create_dec_ruledeck命令创建drc集合,集合中可以同时有自定义DRC和官方DRC,可以通过add_drc_checks在集合中新增DRC。
创建一个proc对设计中感兴趣的对象进行检查,最终以DRC违例报告形式返回一个明确定义的错误。新增的脚本必须在report_drc执行之前导入。
# 示例中dataWidthCheck检查WRITE_B总线宽度是否符合要求。
# This is a simplistic check -- report BRAM cells with WRITE_WIDTH_B wider than 36.
proc dataWidthCheck {} {
# list to hold violations
set vios {}
# iterate through the objects to be checked
foreach bram [get_cells -hier -filter {PRIMITIVE_SUBGROUP == bram}] {
set bwidth [get_property WRITE_WIDTH_B $bram]
if { $bwidth > 36} {
# define the message to report when violations are found
set msg "On cell %ELG, WRITE_WIDTH_B is $bwidth"
set vio [ create_drc_violation -name {RAMW-1} -msg $msg $bram ]
lappend vios $vio
}; # End IF
}; # End FOR
if {[llength $vios] > 0} {
return -code error $vios
} else {
return {}
}; # End IF
} ; # End PROC
建立tcl检查函数之后,还需要在vivado中将其定义为DRC,并加入到系统DRC报告内容中。
首先需要通过create_drc_check声明建立新的检查规则,该命令需要用户自定义一个检查名字(向DRC集合中添加该检查,或在DRC报告中找到该检查,会用到名字),在上面的示例中,DRC名为RAMW-1。除此以外,create_drc_check还需要添加proc名字,作为-rule_body,上述例子中,proc名为dataWidthCheck。
DRC检查规则有is_enable特性,可以通过set_property进行配置,当其为fulse时,执行report_drc时不会检查这一条。
可以手动将多个DRC检查加入DRC检查集,通过create_drc_ruledesk创建检查集,通过add_drc_checks新增检查,通过remove_drc_checks删除检查。
create_drc_ruledeck myrules
add_drc_checks -ruledeck myrules {RAMW-1 RAMW-2 RAMW-3}
remove_drc_checks {RAMW-2} -ruledeck myrules
DRC检查集也有is_enable属性。
用户自定义的DRC、DRC集合可以单独运行。
report_drc -check {RAMW-1}
report_drc -check {RAMW-1 RAMW-2}
report_drc -ruledecks myrules
DRC就像其他对象一样可以通过get_property查看属性、set_property设置属性。
Vivado% report_property [get_drc_checks RAMW-1]
Property Type Read-only Visible Value
ARCHITECTURES string* true true
CLASS string true true drc_check
DESCRIPTION string true true Block RAM Data Width Check
GROUP string true true RAMW
HIERNAME string true true RAMB Checks
IS_ENABLED bool false true 1
IS_USER_DEFINED bool true true 1
MESSAGE string true true
MSG_ID int true true 1
NAME string true true RAMW-1
SEVERITY enum false true Advisory
DRC属性只有IS_ENABLE和SEVERITY可以设置。
set_property IS_ENABLED false [get_drc_checks RAMW-1]
set_property SEVERITY {Critical Warning} [get_drc_checks RAMW-1]
# 将设置属性初始化
reset_drc_check [get_drc_checks]
Tools > Custom Commands > Customize Commands.
以下几种方法可以提高tcl脚本的运行效率。
一条命令在tcl控制台执行时,首先通过tcl编译器对命令进行解析,之后进入C++层继续执行这条命令,执行结束后返回正确的值给tcl编译器,之后执行下一条命令,tcl编译器到C++层会消耗一定时间,因此如果可以将两个命令通过嵌套变为一个命令,则系统只需在tcl编译器和C++层变化一次,节省了一定时间。举例来说:
set nets [get_nets -hier]
set pins [get_pins -of_objects $nets
这样执行时间将会长于
set pins [get_pins -of_objects [get_nets -hier]]
当有一些运行结果需要多次使用时,最好先将其设置为一个变量,避免每次使用时都要重新检索或计算一遍。
当一些命令需要输入字符串时,vivado的设计对象可以直接输入,而无需再调用设计对象的NAME属性。
if {[regexp {.*enable.*} $MyObject]} { ... }
if {[regexp {.*enable.*} [get_property NAME $MyObject]]} { ... }
第一种写法不仅比第二种易读,同时运行时间更短。
提高运行时间的一种方法是高效地编写代码,以便构建一个容器,并在整个容器上运行一个命令,而不是在一个循环中对作为容器一部分的每个项目运行命令。(即将循环中的数据整合成一个列表,之后对列表进行统一操作)。
# 低效写法
foreach bram [get_cells -hier -filter {PRIMITIVE_SUBGROUP == bram}] {
set bwidth [get_property WRITE_WIDTH_B $bram]
if { $bwidth > 36} {
highlight_object -color red [get_cells $bram]
}; # End IF
}; # End FOR
# 高效写法
foreach bram [get_cells -hier -filter {PRIMITIVE_SUBGROUP == bram}] {
set bwidth [get_property WRITE_WIDTH_B $bram]
if { $bwidth > 36} {
lappend bram_list $bram
}; # End IF
}; # End FOR
highlight_object -color red [get_cells $bram_list]
在通常的tclsh环境下,通过stdin获取用户输入:
gets stdin answer
而在vivado环境下,tcl脚本会在不同系统环境下运行,stdin很难兼容所有情况。因此除非确定脚本只在某些情况下运行,并且stdin可以全部兼容时,才使用这个命令。
vivado环境下,通常建议将所有输入整合成一个tcl脚本文件,之后通过source来执行输入文件,即可完成参数输入。用户需要更改配置时,只需要更改文件中的数据即可。