Vivado工程RTL源码提取:Python自动化脚本实现 FPGA 项目交接或者代码归档的时候最头疼的一件事就是拿到一个 Vivado 工程想快速把里面的 RTL 源码捞出来单独看结果发现 .xpr 工程文件里全是路径引用源码散落在十几个不同的目录里还夹杂着 IP 核自动生成的 wrapper、约束文件、仿真文件。手动一个个找、一个个复制一个中等规模的工程能耗掉你大半天时间还容易漏文件。我前阵子接手一个别人离职留下的项目工程目录里光是 srcs 下面就有七八层嵌套IP 核生成的代码和手写 RTL 混在一起靠手工整理基本不现实。后来花了一个下午写了个 Python 脚本把整个提取流程自动化了现在处理一个工程只需要几秒钟。这篇就聊聊这个工具的设计思路、实现细节以及我在实际使用中踩过的那些坑。1. 先搞清楚 Vivado 工程到底把源码藏在哪1.1 .xpr 文件不是普通的文本文件很多人第一反应是直接去读 .xpr 文件觉得它是个 XML 或者类似格式解析一下就能拿到文件列表。我一开始也是这么想的结果打开一看就懵了。.xpr 确实是个 XML 结构的文件但它里面记录的主要是工程的配置信息、part 型号、综合策略这些真正的源文件列表并不直接以明文路径的形式存在里面。你会在 .xpr 里看到一些类似FileSet的引用但具体的文件路径信息是分散在工程目录下的各个子文件里的。具体来说Vivado 工程目录结构大概是这样的根目录下有 .xpr 文件然后有 .srcs 目录、.runs 目录、.gen 目录、.ip_user_files 目录等等。.srcs 目录下面又会按照 sources_1、constrs_1、sim_1 这样的分类来组织每个分类下面还有 imports、bd 等子目录。手写 RTL 通常在 sources_1 下面但 IP 核相关的代码可能在 .gen 或者 .srcs/sources_1/ip 下面。1.2 三种可行的提取路径对比在实际动手之前我调研了几种方案各有优劣方案实现方式优点缺点直接解析 .xpr用 XML 解析库读 .xpr不依赖 Vivado 环境文件列表不完整IP 核路径拿不到调用 Tcl 脚本通过 Vivado 的 batch 模式执行 Tcl信息最全官方接口需要装 Vivado启动慢遍历工程目录按文件扩展名扫描整个工程目录实现简单不依赖环境会混入非工程文件需要过滤我最后选的是第二种和第三种结合的方式优先用 Tcl 脚本从 Vivado 里导出准确的文件列表如果环境里没有 Vivado就退回到目录遍历加规则过滤的方案。这样既能保证准确性又能在没有 Vivado 的机器上跑。1.3 Tcl 方案的核心命令Vivado 提供了一组 Tcl 命令可以直接查询工程里的文件信息。最核心的是get_files命令它可以按文件类型过滤# 获取所有 Verilog 源文件 set verilog_files [get_files -filter {FILE_TYPE Verilog}] # 获取所有 VHDL 源文件 set vhdl_files [get_files -filter {FILE_TYPE VHDL}] # 获取所有约束文件 set xdc_files [get_files -filter {FILE_TYPE XDC}]这些命令返回的是文件的绝对路径非常准确。但要注意get_files默认返回的是当前工程里所有 fileset 的文件包括仿真文件和约束文件。如果你只想要综合用的 RTL需要加上-of_objects [get_filesets sources_1]这样的限定。2. Python 脚本的骨架设计与关键模块拆解2.1 整体流程设计整个工具的流程其实不复杂但每个环节都有细节要注意。大致的流程是定位工程文件、提取文件列表、分类过滤、复制到目标目录、生成清单报告。我用一个主控类来串联这些步骤每个步骤独立成方法方便单独调试和替换。class VivadoRtlExtractor: def __init__(self, project_path, output_dir): self.project_path Path(project_path) self.output_dir Path(output_dir) self.file_list [] self.category_map {}这里用 pathlib 而不是 os.path主要是因为 pathlib 在处理跨平台路径和路径拼接时更直观代码可读性也好很多。特别是在 Windows 上处理 Vivado 工程时路径里经常有反斜杠和空格的混合pathlib 能省掉不少麻烦。2.2 定位 .xpr 文件的策略用户传进来的可能是一个目录也可能直接是 .xpr 文件的路径。我写了一个方法来做归一化处理def locate_project_file(self): if self.project_path.is_file() and self.project_path.suffix .xpr: return self.project_path xpr_files list(self.project_path.glob(*.xpr)) if not xpr_files: raise FileNotFoundError(未找到 .xpr 工程文件) if len(xpr_files) 1: # 多个 .xpr 时选修改时间最新的 xpr_files.sort(keylambda f: f.stat().st_mtime, reverseTrue) return xpr_files[0]这里有个实际会遇到的情况一个目录下可能有多个 .xpr 文件比如工程升级后留下的备份。我的处理是选修改时间最新的那个因为通常那才是当前在用的工程。但这个策略不是绝对的所以我在日志里会打印出选中的是哪个文件方便用户确认。2.3 调用 Vivado 执行 Tcl 的封装如果环境里有 Vivado就通过 subprocess 调用它执行 Tcl 脚本。这里的关键是找到 Vivado 的可执行文件路径。在 Windows 上通常是vivado.batLinux 上是vivado。我写了一个查找逻辑先查环境变量再查常见安装路径def find_vivado(self): vivado shutil.which(vivado) if vivado: return vivado common_paths [ rC:\Xilinx\Vivado\2020.2\bin\vivado.bat, rC:\Xilinx\Vivado\2018.3\bin\vivado.bat, /tools/Xilinx/Vivado/2020.2/bin/vivado, ] for p in common_paths: if Path(p).exists(): return p return None调用的时候用-mode batch -source script.tcl参数让 Vivado 在批处理模式下执行 Tcl 脚本不启动图形界面。这样启动速度快很多大概十几秒就能跑完比开 GUI 快得多。2.4 Tcl 脚本的输出格式约定Tcl 脚本和 Python 之间的数据交换我用的是最简单的文本格式每行一个文件路径前面加上分类标签用竖线分隔。这样 Python 端解析起来很简单也不容易出错。set output_file [open file_list.txt w] foreach f [get_files -of_objects [get_filesets sources_1]] { set ftype [get_property FILE_TYPE $f] puts $output_file $ftype|$f } close $output_file用竖线做分隔符是因为文件路径里基本不会出现竖线而空格、逗号这些在路径里很常见用它们做分隔符容易出问题。3. 文件分类与过滤哪些该拿哪些该扔3.1 按文件类型分类的规则Vivado 里的文件类型比想象中多。除了常见的 Verilog、VHDL、XDC还有 SystemVerilog、Verilog Header、Memory File、Coefficient File 等等。我根据实际需要把文件分成了几大类RTL 源码.v、.sv、.vhd、.vh约束文件.xdc、.sdcIP 相关.xci、.xcix 以及 IP 生成的 wrapper仿真文件testbench 相关的 .v/.sv其他.coe、.mif、.mem 等分类的目的是让用户在提取的时候可以选择只要 RTL还是连约束和 IP 一起拿。实际用下来大部分场景下用户只想要手写的 RTL 源码IP 生成的代码和约束文件并不需要。3.2 IP 核代码的处理策略IP 核生成的代码是最容易让人纠结的部分。一方面IP 的 wrapper 文件通常是xxx_wrapper.v或者xxx_wrapper.vhd是工程编译必须的另一方面IP 核内部生成的代码在 .gen 目录下通常不需要手动修改提取出来意义不大。我的策略是默认只提取 IP 的 wrapper 文件不提取 IP 内部生成的代码。判断依据是文件路径里是否包含.gen或者.srcs/sources_1/ip/这样的特征。如果用户明确需要完整的 IP 代码可以通过一个参数来开启。def is_ip_generated(self, file_path): path_str str(file_path).replace(\\, /) ip_patterns [/.gen/, /ip/] return any(p in path_str for p in ip_patterns)这里要注意一个坑有些工程里用户自己写的代码也可能放在名为 ip 的目录下。所以不能只看路径里有没有 ip还要结合文件是否在 .gen 目录下来判断。.gen 目录是 Vivado 自动生成的里面的代码基本可以确定是工具生成的。3.3 重复文件的去重逻辑Vivado 工程里经常出现同一个文件被多个 fileset 引用的情况。比如一个 Verilog 文件既在 sources_1 里又在 sim_1 里。如果不做去重提取出来的文件会有重复。去重的时候不能简单地按文件名去重因为不同目录下可能有同名文件。我的做法是按文件的绝对路径去重同时保留文件在不同 fileset 里的引用信息。这样既避免了重复复制又不会丢失文件的使用上下文。def deduplicate(self, file_entries): seen {} for entry in file_entries: real_path str(Path(entry[path]).resolve()) if real_path not in seen: seen[real_path] entry else: seen[real_path][filesets].append(entry[fileset]) return list(seen.values())用resolve()是为了处理符号链接和相对路径的情况确保同一个文件的不同路径表示能被正确识别为同一个文件。4. 复制与目录结构重建的实操细节4.1 目标目录的组织方式提取出来的文件怎么放这个看似简单的问题其实有好几种方案。我试过全部平铺在一个目录里也试过按原工程的目录结构重建。最后发现按原结构重建是最实用的因为 RTL 代码里的include语句和相对路径引用都依赖目录结构平铺之后这些引用就全断了。重建目录结构的时候我以工程根目录为基准计算每个文件相对于工程根目录的路径然后在输出目录下创建相同的层级。这样提取出来的代码可以直接用编辑器打开include 路径也不会出问题。def copy_with_structure(self, src_file, project_root): rel_path Path(src_file).relative_to(project_root) dst_file self.output_dir / rel_path dst_file.parent.mkdir(parentsTrue, exist_okTrue) shutil.copy2(src_file, dst_file) return dst_file用copy2而不是copy是因为copy2会保留文件的修改时间和权限信息方便后续追溯文件的原始状态。4.2 处理路径中的中文和空格Vivado 对中文路径的支持一直不太好但实际项目中确实会遇到路径里有中文的情况。Python 的 shutil 在处理中文路径时一般没问题但在调用 Vivado 的 Tcl 脚本时中文路径可能会导致 Tcl 报错。我的处理方式是在 Tcl 脚本里对路径做转义把反斜杠替换成正斜杠同时用花括号把路径包起来set f [file normalize {C:/Users/张三/project/src/top.v}]如果路径里确实有中文建议在提取之前先把工程复制到一个纯英文路径下这样能避免很多莫名其妙的问题。这个坑我在一个客户现场踩过折腾了两个小时才发现是路径里的中文导致的。4.3 生成提取清单报告提取完成后生成一份清单报告是很有必要的。报告里包含每个文件的原始路径、提取后的路径、文件类型、所属 fileset、文件大小等信息。这份报告在代码审查和交接的时候特别有用别人拿到提取结果能快速了解每个文件的来源。我用的是 CSV 格式因为 Excel 能直接打开方便非技术背景的同事查看。报告里还会统计各类文件的数量和总大小让人对提取结果有个整体把握。def generate_report(self, entries, report_path): with open(report_path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([原始路径, 提取路径, 文件类型, Fileset, 大小(字节)]) for e in entries: writer.writerow([e[original], e[extracted], e[type], ,.join(e[filesets]), e[size]])编码用utf-8-sig是为了让 Excel 能正确识别中文不然打开 CSV 会乱码。这个细节很小但实际用起来体验差别很大。5. 实测中遇到的几个典型问题和解决思路5.1 Vivado 版本差异导致的 Tcl 命令不兼容不同版本的 VivadoTcl 命令的行为有细微差别。比如get_files命令在 2018.3 和 2020.2 里返回的文件类型名称就不完全一样。2018.3 里 Verilog 文件的类型是 Verilog而某些版本里可能是 verilog大小写不同。我的处理方式是在 Tcl 脚本里对文件类型做统一的大小写转换然后在 Python 端再做一次归一化。这样不管 Vivado 返回的是什么格式最终都能正确分类。set ftype [string tolower [get_property FILE_TYPE $f]]另外get_filesets命令在不同版本里返回的 fileset 名称也可能不同。有的版本返回 sources_1有的返回 sources_1 加上一些后缀。我在 Python 端用模糊匹配来处理只要包含 sources 就认为是源文件 fileset。5.2 工程路径过长导致的复制失败Windows 系统对路径长度有 260 个字符的限制。Vivado 工程本身的路径就可能很长再加上重建目录结构后的相对路径很容易超过这个限制。我遇到过一个工程原始路径就有 180 多个字符提取后的路径直接超限复制时报错。解决方案有两个一是开启 Windows 的长路径支持需要修改注册表二是在提取时对过长的路径做截断处理。我倾向于第二种因为修改注册表对普通用户来说门槛太高。截断的策略是保留文件名把中间的目录层级用哈希值替代。def shorten_path(self, path, max_len240): path_str str(path) if len(path_str) max_len: return path # 保留文件名和最后两级目录中间用哈希替代 parts Path(path_str).parts if len(parts) 3: hash_part hashlib.md5(str(parts[1:-2]).encode()).hexdigest()[:8] new_path Path(parts[0]) / hash_part / Path(*parts[-2:]) return new_path return path5.3 大工程提取时的性能问题一个大型 FPGA 工程可能有上千个源文件如果逐个文件复制速度会比较慢。我测试过一个有 1500 多个文件的工程逐个复制花了将近 30 秒。后来改成用线程池并发复制速度提升到了 5 秒左右。from concurrent.futures import ThreadPoolExecutor def batch_copy(self, file_pairs, max_workers8): with ThreadPoolExecutor(max_workersmax_workers) as executor: futures [executor.submit(self.copy_with_structure, src, root) for src, root in file_pairs] for future in futures: future.result()线程数设成 8 是个经验值再高的话磁盘 IO 会成为瓶颈提升不明显。如果是机械硬盘建议降到 4 左右避免磁头频繁寻道反而变慢。5.4 提取后代码无法直接编译的问题提取出来的 RTL 代码如果直接拿去综合大概率会报错。原因主要有两个一是缺少 IP 核的生成文件二是缺少约束文件。这个不是工具的问题而是提取本身的局限性——IP 核的代码是工具生成的脱离了 Vivado 环境确实没法直接用。我的建议是提取工具主要用于代码阅读、审查和归档不要指望提取出来的代码能直接综合。如果确实需要可编译的代码应该用 Vivado 的 Archive Project 功能那个会打包所有必要的文件。提取工具的价值在于快速获取可读的源码而不是替代工程归档。6. 工具的实际使用场景与扩展思路6.1 代码审查和交接场景这个工具最直接的使用场景就是代码审查。接手别人的工程时先用工具把 RTL 提取出来然后用编辑器或者代码分析工具做静态检查看看有没有明显的编码问题、命名不规范、模块划分不合理的地方。提取出来的代码可以放到 Git 里做版本管理方便后续追踪修改。交接的时候把提取结果和清单报告一起打包给对方对方拿到后能快速了解工程里有哪些模块、每个模块大概多少行代码、用了哪些 IP。这比直接扔一个几十 GB 的 Vivado 工程目录要友好得多。6.2 与代码统计工具的配合提取出来的 RTL 代码可以配合代码统计工具使用。比如用cloc统计各类文件的行数用pylint或者verible做 Verilog 代码的 lint 检查。我通常会在提取完成后自动跑一遍 cloc把统计结果附在报告里。cloc --by-file --include-langVerilog,SystemVerilog,VHDL output_dir/这样一份报告里既有文件清单又有代码量统计对项目评估很有参考价值。6.3 批量处理多个工程的思路如果有多个 Vivado 工程需要处理可以写一个批处理脚本遍历目录下的所有 .xpr 文件逐个调用提取工具。我在一个项目里需要处理 12 个相关的 Vivado 工程就是用一个简单的循环搞定的。for xpr in Path(base_dir).rglob(*.xpr): extractor VivadoRtlExtractor(xpr, output_base / xpr.stem) extractor.run()注意rglob会递归查找所有子目录如果工程目录里有备份或者历史版本可能会找到多个 .xpr。这时候需要加一些过滤条件比如排除路径里包含 backup 或 old 的目录。6.4 后续可以扩展的方向这个工具目前的功能还比较基础后续有几个方向可以扩展。一是增加对 SystemVerilog 的更好支持包括解析package和interface的依赖关系二是增加代码依赖分析功能自动生成模块之间的例化关系图三是支持导出为其他格式比如把提取结果直接生成一个 Markdown 的代码索引文档。不过说实话工具够用就行不用追求大而全。我现在的用法就是提取加报告满足日常的代码审查和交接需求已经足够了。真正复杂的代码分析还是交给专业的 EDA 工具更靠谱。最后分享一个我在使用中总结的小技巧提取之前先看一眼工程的 part 型号和顶层模块名这两个信息在 .xpr 文件里能直接找到。知道 part 型号能帮你判断代码的目标平台知道顶层模块名能帮你在提取出来的文件里快速定位入口。这个习惯让我在审查代码时能更快地建立起整体认知不至于在一堆文件里迷失方向。