ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用Tcl/Tk打造Vivado仿真文件自动导出工具

用Tcl/Tk打造Vivado仿真文件自动导出工具 一直想聊聊这个工具拖了挺久今天把它写明白。FPGA开发干到一定年头你会发现真正耗时间的不是写RTL不是调时序而是处理那些“仿真相关”的杂事。比如要给验证同事导出一份完整的仿真文件清单要把某个版本的仿真环境整个打包归档或者要把几十个IP核的仿真模型、testbench、约束文件按特定结构整理出来。手工在Vivado或者ModelSim里一个个找文件、复制路径、组织目录不仅慢还容易漏。尤其是IP核一多仿真文件散落在各个生成目录里自己看都头疼更别说交付给别人。我做的这个小工具就是用Tcl/Tk写了一个运行在Vivado环境里的交互界面专门解决“把当前工程的仿真文件按需获取、整理、导出”这个需求。它的核心不是帮你写testbench而是把工程里所有跟仿真相关的文件自动捞出来按照你想要的规则组织好一键导出到指定目录顺便还能生成一份文件清单。这个工具本身不复杂但实际用下来有几个设计点我觉得值得拿出来说说包括为什么选Tcl/Tk而不是Python或者别的方案界面逻辑怎么跟Vivado的工程数据库对接以及文件导出时那些容易踩的坑。1. 为什么是Tcl/Tk这个选择背后的现实逻辑先说选型。很多人一看“图形界面”第一反应是Python加PyQt或者C#甚至网页前端。但在FPGA这个场景里Tcl/Tk有不可替代的优势至少对我来说是这样。1.1 和Vivado的无缝集成Vivado本身内嵌了完整的Tcl解释器而且它的工程数据库、IP核配置、综合实现结果全都可以通过Tcl命令来查询和操作。这意味着我可以在Tcl里直接拿到当前打开的工程、工程里所有的文件、每个文件属于哪个IP核、文件的语言类型、文件的作用域仿真还是综合等这些元数据。用Tcl/Tk写GUI可以共享同一个Tcl解释器上下文。界面上点一个按钮背后执行的就是Vivado的Tcl命令数据是“活”的从界面直接穿透到工程内部。如果用Python绕一圈还得通过vivado -mode batch调Tcl脚本数据交换和反馈都有延迟做不到界面上的实时联动。1.2 零额外部署成本Vivado装好以后Tcl/Tk运行环境是自带的。脚本拷过去就能用不需要装Python环境不需要管理pip依赖也不需要考虑验证同事的电脑上有没有PyQt5。这一点在团队协作里尤其省心。1.3 Tcl/Tk语法并不难Tcl的语法确实有点“古早味”大括号和方括号满天飞。但它的核心逻辑简单一切都是命令命令的返回值都是字符串。只要抓住这个思路写界面和写逻辑并不难尤其是字符串处理和文件操作这两块。当然Tcl/Tk也有短板界面美观程度不如现代GUI框架但用于内部工具完全够用。核心诉求是“功能确定、稳定可靠、零部署成本”Tcl/Tk是这四个诉求的交集。2. 界面与逻辑的分层设计入口、检索、导出三区联动这个工具的界面不长这样等我写出来大家印象就清晰了——整体分成三个功能区块工程入口区、文件检索区、导出控制区。2.1 工程入口区支持打开已加载工程或手动指定路径界面上方是工程信息展示区域。如果你在Vivado里已经把工程打开了脚本可以直接通过current_project拿到工程名和路径显示在界面上。如果没有打开工程也可以手动输入一个.xpr文件路径脚本会打开这个工程后再读取文件列表。为什么设计成两种模式因为实际使用中有一种很常见的情况是你手上拿到别人给的一个工程压缩包本地还没有打开过但你就想快速看一下里面有哪些仿真文件。如果非得先打开Vivado再做操作会比较浪费时间。手动指定路径可以直接绕过界面加载用open_project命令在后台打开不弹出GUI拿完文件列表再关掉。proc get_project_info {} { set proj [current_project] if {$proj eq } { return } set proj_dir [get_property DIRECTORY $proj] set proj_name [get_property NAME $proj] return [list $proj_name $proj_dir] }这段代码就是拿当前工程的名字和目录。注意current_project在没打开工程时会返回空字符串所以要先判断。2.2 文件检索区不要全量显示按需筛选文件检索这块我一开始犯过一个错直接把工程里所有文件全部列在一个列表控件里。工程大一点几百个文件列表根本没法看。后来改了思路加了两级筛选第一级是文件类型筛选。只保留仿真相关的文件类型Verilog / SystemVerilog源码.v.svVHDL源码.vhd.vhdl仿真约束文件.xdc中用于仿真的一些时钟约束IP核生成文件的子类型比如.sim目录下的仿真模型内存初始化文件.coe.mem.hex有些仿真模型需要加载这些文件第二级是关键词过滤。在界面上加一个输入框输入关键词后文件列表实时刷新只显示路径里包含这个关键词的文件。比如你只关心axi相关的仿真文件输入axi列表就只剩AXI相关的内容。文件列表控件用的Tk的tk::treeviewTk 8.6以后的表格式控件支持多列显示文件路径、文件类型、所属IP核、作用域。这里贴一下关键代码proc refresh_file_list {} { set filter [string tolower [$txt_filter get]] set file_type [$cb_type get] $tree delete [$tree children ] set proj [current_project] if {$proj eq } { return } set all_files [get_files -all -quiet] foreach f $all_files { set f_lower [string tolower $f] # 类型过滤 if {$file_type ne 全部 ![string match *.$file_type $f_lower]} { continue } # 关键词过滤 if {$filter ne ![string match *$filter* $f_lower]} { continue } # 提取属性 set is_sim [get_property IS_SIM_ONLY $f] set used_in [get_property USED_IN $f] # 插入treeview $tree insert {} end -values [list $f $file_type $used_in $is_sim] } }这里有个细节get_files -all返回的是绝对路径不是相对路径这正好方便后续文件操作和去重。USED_IN属性会告诉你在综合、仿真还是别的流程中用了这个文件这个属性在筛选时非常有价值。2.3 导出控制区手动勾选与自动匹配界面的核心操作是“勾选需要导出的仿真文件点击导出自动复制到目标目录”。为了让用户有足够的控制权我在文件列表的每一行前面都加了一个勾选框用Tk的checkbutton嵌入treeview单元格的方式实现。手动勾选的逻辑不复杂但比较繁琐。更关键的是界面下方提供了“一键全选仿真文件”按钮。点击后会遍历所有文件判断是否属于仿真作用域USED_IN属性包含simulation并且文件存在然后自动勾选这些文件。这个“一键全选仿真文件”不是简单地把所有带sim路径的文件都选上。它做了更细致的判断proc is_sim_file {f} { set used_in catch {set used_in [get_property USED_IN [get_files $f]]} if {[string match *simulation* $used_in]} { return 1 } # 路径中包含sim关键字的文件也认为是仿真文件 if {[string match */sim/* $f] || [string match */simulation/* $f]} { return 1 } return 0 }注意前面的catch。为什么用catch因为有些文件的属性在Vivado里因为某些原因读不到USED_IN直接读会报错用catch把异常吃掉再走路径匹配的兜底逻辑。这是实际调试时踩出来的经验——读属性不能默认一定成功。3. 文件导出的核心机制保持层级关系才能“拿过来就能用”文件选中之后怎么导出最开始我图省事直接把所有选中的文件复制到一个目录里。结果就出问题了仿真的时候vi需要按照原来的相对路径去解析include文件或者IP核的文件相互用相对路径引用。扁平化导出导致一堆include not found最后还得手工调。所以后面我改成“保持层级关系的导出方式”目标目录是用户在界面上指定的导出根目录脚本遍历每个选中的文件读取它在工程中的相对路径相对于工程根目录然后在目标目录下创建相同的目录结构把文件复制过去。3.1 一个具体的例子假设工程路径是D:/projects/uart_test/里面有一个文件D:/projects/uart_test/ip/axi_uart/axi_uart.xci这个IP核的仿真模型在D:/projects/uart_test/ip/axi_uart/sim/axi_uart_sim_netlist.v。如果指定导出目录是D:/export/uart_sim_files/脚本会创建D:/export/uart_sim_files/ip/axi_uart/sim/axi_uart_sim_netlist.v这样文件还是按照原有结构组织的include的相对路径不会断。而且如果工程里有两个不同的IP核都有sim/xx_sim_netlist.v也不会因为文件名冲突而被覆盖。这是扁平导出绝对做不到的也是这个工具“能用”而不是“纸上谈兵”的关键。3.2 符号链接还是物理复制复制文件的时候有个选项是物理复制把文件内容真正拷过去还是创建符号链接在Windows上是快捷方式。为什么提供这个选项因为仿真文件可能很大一个复杂的SoC系统IP核的仿真模型加起来几个GB很正常物理复制很占磁盘空间和时间。但如果创建符号链接目标机器上打开仿真时依赖的文件如果被移动或删除链接就断了不管用。实测下来我一般建议团队里归档用物理复制保证拿到的东西是完整的而需要快速在另一个工程里引用时用符号链接省空间随时可以重新创建。这两种方式在导出时的实现差异不大就是一个参数的事。proc copy_file_to_export {src_file export_root use_link} { set rel_path [file relative $src_file $::env(PWD)] # 如果不在PWD下计算相对于工程的路径 set proj_dir [get_property DIRECTORY [current_project]] set rel_path [file relative $src_file $proj_dir] set dest_path [file join $export_root $rel_path] set dest_dir [file dirname $dest_path] if {![file exists $dest_dir]} { file mkdir $dest_dir } if {$use_link} { file link -symbolic $dest_path $src_file } else { file copy -force $src_file $dest_path } }注意一个坑file relative第二个参数在正常情况下传的是想相对于哪个目录计算。但如果工程里的文件路径是用C:/...这样的绝对路径而当前工作目录Vivado里默认可能不在工程目录下直接计算就会出错。所以一定要用get_property DIRECTORY [current_project]拿到工程目录再基于工程目录计算相对路径。3.3 导出时的文件冲突检测如果目标目录已经存在同名文件是覆盖还是跳过这个细节很容易被忽略但处理不好会带来很多麻烦。我提供的第三个选项是“同名文件处理策略”覆盖直接覆盖适合导出最新版本覆盖旧版本的情况跳过如果目标文件已存在并且内容一致就不动可以大幅加快增量导出的速度重命名在文件名后加_1、_2这样的后缀适用于想把同一工程导出到多个不同版本目录时做对比的场景增量导出这个功能很重要。很多项目是迭代开发的这次跑完仿真过了两周改了几个模块又要导出一次。如果每次都全量复制浪费时间。但如果用增量导出检查文件是否变了只在变化的文件上做复制一次可能从几分钟缩短到几秒钟。proc file_has_changed {src dest} { if {![file exists $dest]} { return 1 } set src_size [file size $src] set dest_size [file size $dest] if {$src_size ! $dest_size} { return 1 } # 大小一样的情况下再比较md5可选为了速度也可以不比 set src_md5 [md5::md5 -hex [read [open $src r]]] set dest_md5 [md5::md5 -hex [read [open $dest r]]] return [expr {$src_md5 ne $dest_md5}] }这段代码是判断源文件和目标文件是否相同先比大小再比md5。md5需要package require md5Tcl的标准库自带不用额外装。如果文件大、数量多只比大小也可以绝大多数情况下够用。md5的计算开销大但更准确你自己权衡。4. 跨工程复用的骨架设计绑定自定义命令与数据传递工具做完了以后我又想到了第二个使用场景不只是导出仿真文件还可以把文件列表发给验证团队或者生成一份“仿真文件清单”的文档让他们填评审表。这个需求本质上和界面强绑定但又不希望每次打开Vivado都手动跑一遍脚本。所以我在设计上做了一个“加载脚本后自动注册一个菜单入口”的逻辑。4.1 把工具做成Vivado菜单项Vivado的GUI支持从Tcl脚本里添加自定义菜单。你可以在init.tclVivado启动时会自动加载的脚本里写入如下内容proc setup_tcl_gui_tools_menu {} { # 在Tools菜单下添加一个子菜单 set tools_menu [dict get [winfo children .] -menu] set top_menu [menu .mTools] $tools_menu add cascade -label Tcl GUI Tools -menu $top_menu $top_menu add command -label Export Simulation Files \ -command [list ::sim_export::open_export_dialog] } ::tcl::tm::path add [file join $::env(HOME) tcllib] package require sim_export if {[catch {setup_tcl_gui_tools_menu}]} { puts Warning: Failed to setup Tcl GUI Tools menu }这样Vivado启动时Tools菜单下就多了一个“Export Simulation Files”选项点开就是我们的主界面。这个功能对于那些团队里经常要用这个工具的同事尤其方便不用每次手动source脚本。4.2 脚本库的package化为了让脚本具备良好的复用性我把核心功能封装成了一个Tcl包packagesim_export。包的结构很简单就是一个目录里面放sim_export.tcl和pkgIndex.tcl。pkgIndex.tcl的内容是package ifneeded sim_export 1.0 [list source [file join $dir sim_export.tcl]]sim_export.tcl里定义了所有核心proc::sim_export::open_export_dialog打开主界面::sim_export::get_sim_files获取仿真文件列表::sim_export::do_export执行导出::sim_export::generate_manifest生成文件清单markdown格式::sim_export::decode_config/::sim_export::encode_config保存和读取配置文件把这套东西做成package好处显而易见多个脚本可以共享同一套核心逻辑以后想加功能比如导出综合文件、导出时序约束只需要往包里加proc界面脚本基本不动。4.3 配置文件记住上次的选择一个好的工具应该在重启后记住自己上次的设置。所以我在导出区加了一个“保存配置”的功能。配置保存为一个简单的文本文件放在~/.sim_export_config。配置内容包括上次选择的导出目录是否启用保持目录结构同名文件处理策略上次使用的关键词过滤保存和读取用Tcl的array配合写入文件非常简单proc save_config {} { global config set fp [open [file join $::env(HOME) .sim_export_config] w] foreach key [array names config] { puts $fp [format {%s%s} $key $config($key)] } close $fp } proc load_config {} { global config set cfg_file [file join $::env(HOME) .sim_export_config] if {![file exists $cfg_file]} { return } set fp [open $cfg_file r] while {[gets $fp line] 0} { if {[regexp {^([^])(.*)$} $line - key value]} { set config($key) $value } } close $fp }配置格式就是简单的keyvalue不用搞什么JSON或者XML。自己用的工具越简单越好省得出解析问题。5. 文件清单生成仿真环境的“说明书”导出文件只是一半工作。很多时候交付仿真环境时还要附上一份文件清单说明每个文件是干什么用的、来自哪个IP核、默认情况下是否参与仿真。这个清单手工写很累而且容易漏我索性在工具里加了自动生成的逻辑。5.1 清单包含什么内容生成的清单是Markdown格式方便直接贴到GitLab或者文档站。主要包含三类信息工程基本信息工程名、Vivado版本、生成时间、导出根目录文件清单表格文件路径相对路径、文件类型、所属IP核、作用域、文件大小、MD5使用说明推荐的文件加载顺序比如先加载IP核仿真模型再加载设计源码再加载testbench里面“推荐文件加载顺序”是我后来加的功能。标准做法是先跑read_verilog加载RTL源码和IP核仿真模型再跑read_verilog -sv testbench.sv加载testbench。文件清单的生成逻辑不复杂但要注意一个细节文件大小的单位。我写的是字节数但如果文件很大全部显示字节数别人看起来费劲。所以我在生成的时候做了格式化超过1MB的显示成xx.x MB否则显示xx.x KB。proc format_file_size {bytes} { if {$bytes 1048576} { return [format %.1f MB [expr {$bytes / 1048576.0}]] } elseif {$bytes 1024} { return [format %.1f KB [expr {$bytes / 1024.0}]] } else { return $bytes B } }5.2 在ModelSim/QuestSim里的自动加载导出清单的同时工具还会生成一个compile.tcl脚本这个脚本就是ModelSim/QuestaSim的编译脚本。打开ModelSim后在命令行里执行source compile.tcl就会自动把文件清单里列出的所有仿真文件按顺序编译好并提示你下一步跑仿真。这个功能其实才是整个工具给验证同学省时间的关键。以前他们收到FPGA工程师的仿真包后还得自己写编译脚本或者手工一个个加文件。现在有了自动生成的compile.tcl拿到就能用。生成compile.tcl的大致逻辑proc generate_compile_script {export_root file_list} { set script_file [file join $export_root compile.tcl] set fp [open $script_file w] puts $fp # Auto-generated compile script puts $fp # Generated at [clock format [clock seconds]] puts $fp # 先编译IP仿真模型 puts $fp # Compile IP simulation models foreach f $file_list { if {[regexp {ip/.*/sim/} $f]} { puts $fp vlog -work xil_defaultlib \$f\ } } # 再编译设计源码 puts $fp # Compile design sources foreach f $file_list { if {![regexp {sim/} $f] [regexp {\.(v|sv)$} $f]} { puts $fp vlog -work xil_defaultlib \$f\ } } # 最后编译testbench puts $fp # Compile testbench foreach f $file_list { if {[string match *tb* $f] [regexp {\.(v|sv)$} $f]} { puts $fp vlog -work xil_defaultlib \$f\ } } puts $fp puts $fp vsim -c work.tb_top close $fp }这里依赖一个假设文件路径里带了ip/xxx/sim/这样的结构。如果工程结构不是标准的IP核生成目录匹配可能会失败。所以生成的时候我还额外留了一个接口允许用户在界面上手动调整哪些文件应该被分到“IP仿真模型”这一步哪些应该分到“设计源码”这一步。这个高级功能通过一个单独的窗口实现这里就不展开了。6. 真实工程中的使用效果与几个易踩的坑工具写完之后在自己负责的UART、SPI、I2C还有几个图像处理的项目里都用了几轮。整体效果是以前手工要花二十分钟整理的仿真文件包现在一分钟以内搞定而且不会漏文件。说几个实际用下来暴露出来的问题算是给同样想写这类工具的同学提个醒。6.1 Vivado不同版本的Tcl命令差异get_files -all、get_property USED_IN这些命令在Vivado 2018.3和2020.1、2022.1这些版本上属性名和行为基本一致。但有个别属性在不同版本里有细微变化比如USED_IN的值老版本可能返回的是simulation新版本可能是SIMULATION.0、SYNTHESIS.0这种带了后缀的格式。所以在读取属性后判断时最好用string first而不是string match避免精确匹配导致的误判。if {[string first simulation [string tolower $used_in]] ! -1} { # 是仿真文件 }同理get_property在某些版本中对于不存在的属性会直接报错而不是返回空值。所以每处读属性的地方都建议用catch包一下或者先用list_property检查一遍。6.2 路径分隔符的坑Tcl在Windows上处理文件路径时用的是正斜杠/还是反斜杠\这个问题会直接影响字符串匹配和file命令的行为。实测下来Vivado返回的get_files -all路径在Windows上用的一直是正斜杠分隔比如C:/projects/uart/uart.srcs/sim_1/new/tb_top.v。所以直接用正斜杠做字符串匹配是没问题的。但是如果用户手动输入路径比如在对话框里粘贴了D:\projects\uart\uart.srcs\sim_1\new\tb_top.vWindows资源管理器复制出来的路径Tcl会把反斜杠当成转义符导致路径解析失败。解决办法是在用户输入路径后统一做一次清洗proc normalize_path {p} { regsub -all {\\} $p / p return $p }输入框、配置文件里的路径都先过一遍这个函数再使用。这个问题我在第一次测试时就被坑到过界面显示路径都正常一导出全是file not found排查了半天才发现是反斜杠的问题。6.3 testbench文件并不总在sim目录下我做“一键全选仿真文件”时最初的判断逻辑是只要路径包含sim关键字的就选上。但实际工程里有些工程师习惯把testbench文件放在工程根目录下命名成tb_top.v这种。路径里根本没有sim。所以后面调整逻辑为文件路径包含sim关键字或者文件名匹配tb_前缀或文件中包含module testbench之类都算仿真文件。虽然这个判断不是100%准确但在我的项目里准确率达到了95%以上。更好的方式其实是到工程的fileset里去查因为Vivado会把testbench文件放在fileset sim_1下面set sim_fileset [get_filesets sim_1] if {$sim_fileset ne } { set sim_files [get_files -of_objects $sim_fileset] }这种方法是最可靠的只要工程里建了sim_1这个fileset一般都会建就能准确拿到所有仿真文件列表。用这种方法后“文件是否属于仿真”这个判断就从路径猜测变成了元数据查询准确率是100%。我最终的工具实现里优先用get_files -of_objects [get_filesets sim_1]拿不到结果才用路径匹配兜底。事实证明这个优先级设计得非常正确。6.4 大工程的导出速度问题当工程特别大比如包含PCIe、DDR控制器、图像处理IP仿真文件可能有几百个总大小到几十个GB。这时候全量物理复制可能要几分钟甚至更久。我实测过一个包含PCIe IP核的工程仿真文件总大小接近15GB物理复制到机械硬盘上花了将近8分钟。这个场景下的优化思路优先用增量导出只复制变化的文件如果目标机器和源机器都支持符号链接可以用符号链接模式如果只是为了生成清单而不需要文件本体可以勾选“仅生成清单”脚本就不复制文件了只输出一个文件清单在界面上我把这三个选项分开放避免用户误操作选了物理复制导致等待时间过长。7. 扩展思路从“仿真文件导出”到“工程交付自动化”最后聊一下这个工具后续可以扩展的方向。现在它只做了仿真文件的获取和导出。但同样的界面和逻辑完全可以扩展成“工程交付包生成器”。选中一个工程一键导出RTL源码去掉仿真文件按目录结构打包约束文件xdcIP核配置xci 生成的产物让接收方不需要重新生成IP仿真文件综合/实现脚本Tcl脚本方便对方重跑流程版本信息和修订说明换句话说这个工具的核心价值不在于“Tcl/Tk界面”本身而在于把FPGA工程师日常重复的、容易出错的“文件整理工作”自动化了。界面只是把这些自动化逻辑串起来变成一个非命令行专业人士也能用的东西。另外如果你想在纯命令行环境CI/CD流程里也用上这部分逻辑核心的sim_export::do_export这些proc可以直接被Tcl脚本调用不需要GUI。我考虑到这一点所有核心逻辑和GUI回调都是分开的GUI只是包装了一层。这个设计带来的好处是同样的功能以后可以接到Jenkins流水线里做自动化交付。从目前的使用反馈看工具给团队带来的最大便利不是“界面好看”而是“少犯错”。以前手工整理文件清单总会出现漏了IP核仿真模型、忘了带某个coe文件、编译顺序不对等问题。有了这个工具所有信息都是从工程数据库里拿到的准确率是100%生成的文件加载顺序虽然不能说能完全替代领域知识但至少覆盖了最常见的场景把“粗心导致的返工”给省掉了。如果你也在用Vivado做FPGA开发经常需要交付仿真环境这个思路值得参考。写一个适配自己项目的Tcl/Tk工具花的时间不会太多但后面每次做版本交付、仿真环境同步时都会感觉到这个决定是划算的。
返回列表