本文源自UG894,主要介绍如何在vivado中使用tcl脚本

1.vivado中如何获取tcl help

vivado中任何自带的命令都可以通过“-help”获取帮助信息。
也可以直接输入“help”取得vivado命令合集,并通过“help -category (tools)”来获取某类操作的所有命令简介。

2.工程模式下编译和报告示例脚本

该过程可以通过运行GUI vivado自动产生的记录文件vivado.jou查看,该文件中记录了自打开vivado后运行的所有命令。
1.通过create_project命令建立工程。
2.通过add_files或import_files添加工程文件。
3.通过launch_runs和wait_on_run依次进行综合实现。

3.在vivado中加载并运行自己的tcl脚本

3.1 vivado启动时自动初始化脚本

vivado会在启动时自动加载Vivado_init.tcl文件中定义的脚本。可将自己写的proc写入该文件中,或者直接source自己的tcl脚本。
Vivado_inti.tcl路径为%APPDATA%/Xilinx/Vivado//Vivado_init.tcl

3.2 通过source运行tcl脚本

可以通过source将tcl脚本加载进入vivado中

source <filename>

使用source会默认打印脚本代码内容,可通过“-notrace”隐藏代码内容

source <filename> -notrace

3.3 在vivado编译流程中添加脚本

在Setting中,找到综合、实现、生成比特流设置,都可以添加tcl.pre和tcl.post的路径,即可在该流程之前或者之后插入自己的tcl脚本。

4.tcl脚本编写方法

原文档这部分主要简介tcl脚本语法及经典问题,此处只挑出其中的经典问题进行记录

4.1 proc参数数量不定时的解决方法

当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 <listOfPorts>]
                [-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分析列表的方法,但是没有看懂,这里先不写了

4.2 局部变量和全局变量

局部变量是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。

4.3 namespace的使用

为了避免变量和函数名重复,可以将函数和变量定义在更独立的空间。命名空间可以嵌套,通过::来调用命名空间中的变量和函数。

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::*

5.访问设计对象

vivado提供了非常丰富的get_*命令用于访问设计对象,这些命令会以列表形式返回所需内容。

5.1 对象特性

每种设计对象(net,pin,port……)都有不同种类的特性参数,可以通过get_* filter来找到某种特性的对象,可以通过report_property来展示某个对象的所有特性。特性也可以增加和删除。

5.2 对象检索

通过get_*、-hierarchical、filter、-regxp、-of_objects等来检索相关对象,此处不在赘述。

6.处理对象列表

通过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
...

7.保存命令输出

很多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 ]

7.1 对文件进行操作

tcl提供了一系列命令对文件进行操作,这些都是tcl基本语法,此处不再赘述。

7.2 对字符串进行操作

tcl提供了一系列命令对字符串进行处理,此处不再赘述。

8.error处理

写脚本时,一个好的习惯:使用某个参数前先检查参数状态,比如打开文件前检查文件是否存在,使用报告列表时先检查是否有元素存在,这样会避免很多脚本运行错误。

8.1 检查变量状态

# 打开文件前检查文件是否存在
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
}

8.2 处理tcl错误

一些内置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
}

9.调用外部程序

使用exec调用外部程序,使用时要确保外部程序可以适配当前操作系统。

set result [exec /bin/perl <path_to>/my_perl_script.pl]

10.自定义设计规则检查(DRC)

DRC定义了一系列设计规则,并检查当前设计是否符合这些规则,打印找到的错误和冲突部分。
可以通过report_drc执行DRC,检查并报告当前设计中的违例部分。
当发现设计违背设计规则时,可以通过create_dec_violation定义且标识违例行为。
可以通过create_dec_check命令创建用户自定义DRC。
可以通过create_dec_ruledeck命令创建drc集合,集合中可以同时有自定义DRC和官方DRC,可以通过add_drc_checks在集合中新增DRC。

10.1 创建tcl检查函数

创建一个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

10.2 新建DRC检查

建立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时不会检查这一条。

10.3 新建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属性。

10.4 单独运行自定义DRC

用户自定义的DRC、DRC集合可以单独运行。

report_drc -check {RAMW-1}
report_drc -check {RAMW-1 RAMW-2}
report_drc -ruledecks myrules

10.5 DRC属性设置

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]

11.自定义GUI按钮

Tools > Custom Commands > Customize Commands.

12.提高tcl运行效率的方法

以下几种方法可以提高tcl脚本的运行效率。

12.1运用嵌套

一条命令在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]]

12.2 缓存对象

当有一些运行结果需要多次使用时,最好先将其设置为一个变量,避免每次使用时都要重新检索或计算一遍。

12.3 vivado的object可以直接当作string作为其他命令的参数

当一些命令需要输入字符串时,vivado的设计对象可以直接输入,而无需再调用设计对象的NAME属性。

if {[regexp {.*enable.*} $MyObject]} { ... }
if {[regexp {.*enable.*} [get_property NAME $MyObject]]} { ... }

第一种写法不仅比第二种易读,同时运行时间更短。

12.4 写更高效的代码

提高运行时间的一种方法是高效地编写代码,以便构建一个容器,并在整个容器上运行一个命令,而不是在一个循环中对作为容器一部分的每个项目运行命令。(即将循环中的数据整合成一个列表,之后对列表进行统一操作)。

# 低效写法
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]

12.5 获取用户输入

在通常的tclsh环境下,通过stdin获取用户输入:

gets stdin answer

而在vivado环境下,tcl脚本会在不同系统环境下运行,stdin很难兼容所有情况。因此除非确定脚本只在某些情况下运行,并且stdin可以全部兼容时,才使用这个命令。
vivado环境下,通常建议将所有输入整合成一个tcl脚本文件,之后通过source来执行输入文件,即可完成参数输入。用户需要更改配置时,只需要更改文件中的数据即可。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐