基于Python的Vivado工程RTL代码提取工具:原理与实现
发布时间:2026/9/29 10:39:47来源:尧图网络
1. 这个工具到底解决什么问题做FPGA开发的朋友大概率都遇到过这种场景接手一个前人留下的Vivado工程打开之后发现里面塞了几十个IP核、上百个源文件想找某个模块的RTL代码得在Hierarchy窗口里一层层点开点到手酸。更麻烦的是有时候你只想把某个子模块的代码单独拎出来看看或者做代码审查、做版本对比结果发现Vivado的工程结构把文件路径藏得很深手动一个个复制粘贴效率极低。这就是我写这个基于Python的Vivado工程RTL代码提取工具的初衷。说白了它干的事情就是你给它一个Vivado工程文件.xpr它自动解析工程结构把所有RTL源文件Verilog/VHDL/SystemVerilog按模块层级提取出来复制到你指定的目录下并且保留原始的目录结构或者按模块名重新组织。整个过程不需要打开Vivado GUI纯命令行操作几秒钟搞定。这个工具适合谁用第一类是FPGA工程师尤其是经常需要做代码迁移、代码审查、工程清理的人第二类是做FPGA相关EDA工具开发的需要批量处理Vivado工程第三类是对Python自动化感兴趣、想拿一个实际项目练手的初学者。不管你属于哪一类只要你对Python基础语法有了解能看懂文件读写和字符串处理就能跟着这篇文章把工具跑起来并且理解每一步为什么要这么做。我先把核心思路说清楚Vivado的.xpr文件本质上是一个XML格式的工程描述文件里面记录了工程包含的所有源文件路径、文件类型、所属模块等信息。我们要做的就是解析这个XML找到所有RTL文件然后按需复制。听起来简单但实际操作中有几个坑路径可能是相对路径也可能是绝对路径文件可能分布在不同的目录层级有些文件是IP核自动生成的比如.xci对应的综合文件这些要不要提取、怎么处理都需要根据实际需求做取舍。2. 核心思路与方案选型拆解2.1 为什么选择解析.xpr而不是调用Vivado TclVivado本身提供了Tcl接口你可以用get_files命令获取工程中的所有文件。那为什么不直接写Tcl脚本而是用Python解析.xpr呢原因有三点。第一速度。启动Vivado本身就要几十秒甚至几分钟哪怕只是跑一个简单的Tcl脚本也得等Vivado完整加载。而Python解析XML文件几百毫秒就完事了。对于需要批量处理几十个工程的场景这个时间差距是致命的。第二环境依赖。调用Vivado Tcl意味着你的机器上必须装Vivado而且版本要匹配。但解析.xpr只需要Python和标准库任何装了Python的机器都能跑甚至可以在没有Vivado的服务器上做代码提取。第三可控性。Vivado的Tcl接口返回的文件列表有时候会包含一些你不需要的东西比如仿真文件、约束文件、IP核的中间产物。用Python自己解析你可以精确控制提取哪些文件、忽略哪些文件、怎么组织输出目录。当然解析.xpr也有代价你需要理解.xpr的XML结构而且不同Vivado版本的.xpr格式可能有细微差异。但根据我的实测从Vivado 2018.3到2023.1核心的文件列表结构基本一致兼容性很好。2.2 .xpr文件结构快速剖析一个典型的.xpr文件用文本编辑器打开后你会看到类似这样的结构简化版Project Version7 Minor0 PathC:/project/test.xpr FileSets FileSet Namesources_1 TypeDesignSrcs RelSrcDir$PSRCDIR/sources_1 File Path$PSRCDIR/sources_1/top.v FileInfo Attr NameUsedIn Valsynthesis/ Attr NameUsedIn Valimplementation/ /FileInfo /File File Path$PSRCDIR/sources_1/sub_module.v ... /File /FileSet /FileSets /Project关键信息在FileSet和File标签里。FileSet的Type属性标识了这组文件的用途DesignSrcs就是设计源文件也就是我们需要的RTL代码。File标签的Path属性就是文件路径里面可能包含$PSRCDIR这样的变量需要替换成实际路径。注意不同版本的Vivado$PSRCDIR的展开规则可能不同。通常它指向工程目录下的srcs文件夹但如果你在创建工程时选择了“将源文件复制到工程目录”那路径就是工程目录下的相对路径如果选择的是“引用外部文件”那路径可能是绝对路径。这个细节后面会详细讲。2.3 工具的整体架构设计整个工具我拆成了三个模块解析模块、过滤模块、提取模块。解析模块负责读取.xpr文件提取出所有文件路径和元信息过滤模块根据文件扩展名和FileSet类型筛选出真正的RTL文件提取模块负责复制文件到目标目录并处理路径冲突和目录结构。为什么这么拆因为实际使用中不同的人对“提取”的定义不一样。有人只想提取Verilog文件有人想把VHDL也带上有人想保留原始目录结构有人想按模块名扁平化存放。把过滤和提取分开方便后续扩展。比如你以后想加一个“只提取某个模块及其子模块”的功能只需要在过滤模块里加逻辑提取模块不用动。3. 核心细节解析与实操要点3.1 路径变量展开的坑前面提到$PSRCDIR这是Vivado工程文件里的一个变量。在.xpr中常见的变量还有$PRJDIR工程目录、$SRCDIR源文件目录等。这些变量不会自动展开需要你手动替换。我的做法是先解析.xpr文件所在的目录作为工程根目录。然后根据Vivado的约定$PSRCDIR通常对应工程目录/工程名.srcs/sources_1$PRJDIR对应工程目录本身。但这不是绝对的最稳妥的方式是读取.xpr中Project标签的Path属性结合FileSet的RelSrcDir属性来计算。举个例子如果.xpr文件在D:/fpga/my_project/my_project.xprRelSrcDir是$PSRCDIR/sources_1那么实际路径就是D:/fpga/my_project/my_project.srcs/sources_1。这个规则在大多数情况下成立但如果你在Vivado中手动修改过源文件目录可能会不一样。所以我的工具里加了一个校验步骤展开路径后检查文件是否存在如果不存在就打印警告让你知道哪个文件没找到。实操心得建议在提取之前先用Vivado打开工程确认所有源文件都能正常显示。如果Vivado里都显示文件丢失那工具肯定也找不到。另外Windows和Linux的路径分隔符不同Python的os.path模块可以自动处理但如果你在Windows上解析Linux创建的工程可能需要手动替换反斜杠。3.2 文件类型过滤策略Vivado工程里的文件类型很多常见的有扩展名类型是否提取.vVerilog是.svSystemVerilog是.vhdVHDL是.vhVerilog头文件可选.xciIP核配置否但需要记录.xdc约束文件否.tcl脚本否.coe系数文件否我的默认策略是只提取.v、.sv、.vhd三种。.vh头文件看情况如果你的代码里用了include那最好一起提取否则编译会报错。.xci文件虽然不直接是RTL但它对应的IP核会生成RTL代码这些代码通常在工程目录的.gen文件夹下。如果你需要提取IP核的RTL得单独处理。这里有个细节Vivado的FileSet里DesignSrcs类型的文件才是设计源文件SimulationSrcs是仿真文件Constrs是约束文件。我的工具默认只处理DesignSrcs但你可以通过参数指定是否包含仿真文件。3.3 目录结构保留与扁平化提取出来的文件怎么放我提供了两种模式保留原始结构按照文件在工程中的相对路径在目标目录下重建相同的目录树。这样做的好处是提取出来的代码可以直接用Vivado重新建工程或者用其他仿真工具编译路径关系不变。扁平化存放所有文件都放在同一个目录下文件名冲突时自动加前缀。这种模式适合做代码审查所有文件一目了然不用一层层点文件夹。实现上保留原始结构用shutil.copy2配合os.makedirs就行。扁平化稍微麻烦一点需要维护一个文件名到路径的映射遇到重名时在文件名前加上父目录名作为区分。注意如果两个不同目录下的文件同名扁平化时一定要处理冲突否则后复制的会覆盖先复制的。我的做法是第一次遇到重名时把两个文件都重命名为父目录名_原文件名并在日志里记录。4. 完整实操过程与核心代码实现4.1 环境准备与依赖安装这个工具只依赖Python标准库不需要额外安装任何第三方包。Python版本建议3.6以上因为用到了pathlib和xml.etree.ElementTree的一些特性。如果你用的是Python 2需要改一些语法但我不建议毕竟Python 2已经停止维护了。检查Python版本python --version如果显示3.6以上就可以直接用了。不需要安装Vivado不需要配置任何环境变量。4.2 解析.xpr文件的核心代码先上代码再解释import xml.etree.ElementTree as ET import os from pathlib import Path def parse_xpr(xpr_path): 解析.xpr文件返回工程根目录和文件列表 xpr_path Path(xpr_path).resolve() project_dir xpr_path.parent tree ET.parse(xpr_path) root tree.getroot() # 获取工程路径属性 project_path root.get(Path, ) files [] for fileset in root.iter(FileSet): fileset_type fileset.get(Type, ) if fileset_type ! DesignSrcs: continue rel_src_dir fileset.get(RelSrcDir, ) for file_elem in fileset.iter(File): file_path file_elem.get(Path, ) if not file_path: continue # 展开路径变量 expanded expand_path(file_path, project_dir, rel_src_dir) files.append({ path: expanded, fileset: fileset_type, original: file_path }) return project_dir, files def expand_path(file_path, project_dir, rel_src_dir): 展开Vivado路径变量 # 替换常见变量 replacements { $PSRCDIR: str(project_dir / f{project_dir.name}.srcs / sources_1), $PRJDIR: str(project_dir), $SRCDIR: str(project_dir / f{project_dir.name}.srcs), } result file_path for var, value in replacements.items(): if var in result: result result.replace(var, value) # 处理相对路径 if not os.path.isabs(result): result str(project_dir / result) return os.path.normpath(result)这段代码的核心逻辑是用ElementTree解析XML遍历所有FileSet只处理DesignSrcs类型。对于每个File元素提取Path属性然后调用expand_path展开变量。expand_path里我硬编码了$PSRCDIR的展开规则。为什么硬编码因为Vivado的变量展开规则没有官方文档我是通过对比多个工程的.xpr文件和实际目录结构总结出来的。如果你发现某个工程的路径展开不对可以在这里加新的变量映射。4.3 文件过滤与提取逻辑解析出文件列表后下一步是过滤和复制import shutil RTL_EXTENSIONS {.v, .sv, .vhd, .vh} def extract_rtl(files, output_dir, preserve_structureTrue): 提取RTL文件到指定目录 output_dir Path(output_dir) output_dir.mkdir(parentsTrue, exist_okTrue) extracted [] skipped [] name_map {} for f in files: src_path Path(f[path]) ext src_path.suffix.lower() if ext not in RTL_EXTENSIONS: skipped.append(f) continue if not src_path.exists(): print(f警告文件不存在 {src_path}) skipped.append(f) continue if preserve_structure: # 保留原始结构 rel_path src_path.name dst_path output_dir / rel_path else: # 扁平化 dst_path output_dir / src_path.name # 处理重名 if dst_path.exists(): parent_name src_path.parent.name new_name f{parent_name}_{src_path.name} dst_path output_dir / new_name print(f重名处理{src_path.name} - {new_name}) shutil.copy2(src_path, dst_path) extracted.append((src_path, dst_path)) return extracted, skipped这里有几个细节值得说。preserve_structure模式下我实际上没有保留完整的目录结构而是只保留了文件名。为什么因为Vivado工程里的源文件路径往往很深比如project.srcs/sources_1/new/sub_module/xxx.v如果完整保留提取出来的目录会嵌套很多层反而不好用。我的做法是只保留文件名但如果你需要完整结构可以把rel_path改成src_path.relative_to(project_dir)。扁平化模式下的重名处理我用的是“父目录名_文件名”的规则。这个规则简单有效但有个小问题如果父目录名也很长文件名会变得很长。你可以根据自己的需求改成加数字后缀或者用哈希值。4.4 命令行入口与参数设计为了方便使用我加了一个命令行入口import argparse def main(): parser argparse.ArgumentParser(descriptionVivado工程RTL代码提取工具) parser.add_argument(xpr, help.xpr文件路径) parser.add_argument(-o, --output, default./rtl_extract, help输出目录) parser.add_argument(--flat, actionstore_true, help扁平化存放) parser.add_argument(--include-vh, actionstore_true, help包含.vh头文件) args parser.parse_args() global RTL_EXTENSIONS if args.include_vh: RTL_EXTENSIONS.add(.vh) project_dir, files parse_xpr(args.xpr) print(f工程目录{project_dir}) print(f找到 {len(files)} 个设计源文件) extracted, skipped extract_rtl(files, args.output, not args.flat) print(f提取了 {len(extracted)} 个RTL文件) print(f跳过了 {len(skipped)} 个非RTL文件) if __name__ __main__: main()用法很简单python extract_rtl.py my_project.xpr -o ./output --flat这条命令会解析my_project.xpr把所有RTL文件扁平化提取到./output目录下。5. 常见问题与排查技巧实录5.1 文件找不到怎么办这是最常见的问题。工具报“文件不存在”通常有三种原因路径变量展开错误。前面说了$PSRCDIR的展开规则不是官方文档化的不同Vivado版本可能有差异。排查方法打开.xpr文件找到报错的文件路径看看里面有哪些变量然后手动在文件系统里找一下实际位置对比工具的展开结果。工程使用了外部引用。如果你在创建Vivado工程时选择了“引用外部文件”而不是“复制到工程目录”那源文件可能散落在各个地方甚至在不同的盘符。这种情况下$PSRCDIR可能不适用路径可能是绝对路径。工具会尝试直接使用绝对路径但如果文件被移动过就会找不到。文件被删除或重命名。这个不用多说Vivado工程里的文件列表是创建时记录的如果后来手动删了文件.xpr里可能还有记录。排查技巧在工具里加一个--verbose参数打印每个文件的原始路径和展开后的路径对比一下就能快速定位问题。5.2 IP核的RTL代码怎么提取Vivado的IP核比如FIFO、乘法器、BRAM在工程里通常以.xci文件的形式存在。.xci本身不是RTL但它会在工程目录的.gen文件夹下生成对应的RTL代码。这些代码默认不会被DesignSrcs的FileSet包含所以我的工具默认提取不到。如果你需要提取IP核的RTL有两个方案。方案一在Vivado中打开工程找到IP核右键选择“Generate Output Products”然后在.gen目录下手动复制。方案二修改工具在解析.xpr时同时扫描.gen目录把里面的.v文件也加入提取列表。方案二更自动化但需要处理IP核版本兼容性问题因为不同Vivado版本生成的IP核RTL可能不同。我个人的做法是工具只负责提取用户自己写的RTLIP核的代码单独处理。因为IP核代码通常不需要修改提取出来意义不大反而会让输出目录变得很乱。5.3 中文路径与编码问题Vivado对中文路径的支持一直不太好但实际项目中确实有人把工程放在中文目录下。Python 3默认用UTF-8编码处理路径在Windows上通常没问题但如果遇到乱码可以尝试用sys.getfilesystemencoding()查看系统编码必要时手动指定。另外.xpr文件本身的编码可能是UTF-8也可能是GBK。用ElementTree.parse时它会自动检测编码但偶尔会失败。如果遇到解析错误可以用open手动读取文件内容指定编码后再用ET.fromstring解析。5.4 常见问题速查表问题现象可能原因解决方法报“文件不存在”路径变量展开错误检查expand_path中的变量映射提取的文件为空FileSet类型不匹配确认工程使用的是DesignSrcs文件名乱码系统编码问题指定encodingutf-8或gbkIP核代码缺失.xci不在DesignSrcs中手动从.gen目录提取重名文件被覆盖扁平化模式冲突启用重名处理逻辑解析XML报错.xpr文件损坏用Vivado重新保存工程5.5 性能优化与批量处理如果你需要处理几十个工程可以写一个批处理脚本import glob for xpr in glob.glob(D:/projects/*/*.xpr): print(f处理{xpr}) project_dir, files parse_xpr(xpr) output f./extract/{Path(xpr).stem} extract_rtl(files, output, preserve_structureFalse)这个脚本会遍历D:/projects下的所有.xpr文件分别提取到以工程名命名的目录下。实测下来处理一个中等规模的工程约200个源文件只需要不到1秒比打开Vivado快太多了。实操心得批量处理时建议加上异常捕获避免某个工程解析失败导致整个脚本中断。另外输出目录最好按工程名分开否则不同工程的文件会混在一起。6. 工具扩展与进阶玩法6.1 按模块层级提取现在的工具是提取所有RTL文件但有时候你只想要某个顶层模块及其子模块。这个功能可以通过解析Verilog的模块实例化关系来实现。简单来说就是先找到顶层模块的文件然后递归查找它实例化的子模块直到没有新的模块为止。实现思路用正则表达式匹配module和实例化语句构建模块依赖图然后从顶层模块开始做深度优先遍历。这个功能我还在完善中主要难点是Verilog的语法比较复杂正则表达式容易漏匹配。如果你有更好的解析方案欢迎交流。6.2 代码统计与质量检查提取出来的RTL代码可以顺便做一些统计每个文件的行数、模块数量、注释比例等。这些数据对于代码审查和工程评估很有用。Python的re模块可以轻松实现这些统计比如用re.findall(r^\s*module\s(\w), content, re.MULTILINE)来提取模块名。6.3 与版本控制工具结合提取出来的代码可以直接放到Git仓库里做版本管理。我的做法是每次提取到一个新目录然后用git init初始化仓库提交一次之后每次提取都对比差异。这样就能清楚地看到工程代码的演变过程。最后分享一个小技巧如果你经常需要提取同一个工程的代码可以把工具的配置写成一个JSON文件包括输出目录、是否扁平化、是否包含头文件等参数。这样每次只需要运行python extract_rtl.py --config my_config.json就行不用重复输入参数。这个工具我从2022年开始用前后改了七八个版本踩过的坑基本都写在上面了。最开始只是想省去手动复制粘贴的麻烦后来越用越顺手现在已经成为我处理Vivado工程的标配工具。如果你也在做FPGA开发建议花半个小时把代码跑起来后面能省下大量时间。
网站建设高端定制企业官网