ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用Python解析Vivado工程xpr文件,自动提取RTL代码及依赖

用Python解析Vivado工程xpr文件,自动提取RTL代码及依赖 做FPGA开发的兄弟应该都有过这种经历工程做到后期RTL文件根本不在一处。我手头这个多板卡采集项目源代码分散在公司公共代码库、本地自定义IP、还有从同事那边拷过来的几块代码加起来几十个文件每次做版本交付都要对着Vivado的Sources窗口一个个找。后来我干脆写了个Python小工具直接解析Vivado工程的.xpr文件把里面登记的RTL源文件连同include依赖一起自动提取出来打包成一份带校验清单的目录快照。这就是今天要聊的东西一个基于Python的Vivado工程RTL代码提取工具。它主要解决三类问题一是快速把工程里散落的源码收拢到一个干净目录二是自动补全头文件、package文件这类容易被漏掉的依赖三是生成带MD5的清单文件方便做代码审计、跨机器移植、工程清理前的备份。如果你经常用Vivado做FPGA开发也被“代码散落”“交付缺文件”这种事搞到头大这篇文章可以直接照着用。1. 为什么非要做这个工具四个真实痛点1.1 手动整理源文件的低效与不可靠Vivado的Sources窗口展示的是工程逻辑视图不是文件物理位置。一个RTL文件可能存放在工程目录下的.srcs/sources_1/new/也可能通过外部引用挂在别的磁盘路径下IP核还会有自己的一套生成目录。逻辑视图里看得见文件名真要提取的时候却得一个个右键看属性、复制路径几十个文件操作下来非常烦躁。更麻烦的是这种手动操作不可验证。你觉得自己把所有.v文件都拷出来了实际上某个子模块的源文件放在了两层目录之外或者某个头文件根本没在Sources窗口里显示等到新环境一编译才暴露问题。提取工具的价值就在于把“查找文件”这件事从记忆和目视检查变成可重复、可审计的自动化过程。1.2 依赖文件比源码本身更容易漏Vivado工程里的依赖不只是子模块之间的例化关系还有一套容易被忽略的include机制。比如工程里有个global_defines.vh里面定义了一堆宏和参数几乎所有模块都会在最上面写一句include global_defines.vh 。如果你只手工导出了.v文件忘了带上.vh到了新环境编译直接报一堆“文件未找到”的错误然后就开始漫长的找头文件拉锯战。SystemVerilog的package文件也是重灾区。package_define_pkg.sv、interface_pkg.sv这类文件如果没被完整提取哪怕顶层文件列表是齐的综合仿真也都跑不起来。手动整理根本不会去管这些依赖关系只有脚本能老老实实把引用链扫一遍。1.3 工程清理与代码交付需要“可核验的快照”Vivado工程跑完综合实现之后目录里会堆出.cache、.runs、.hw、.gen等一大堆生成物整个工程动辄几个GB甚至更大。做版本交付、归档老工程、或者给别人做代码评审时不可能把这些生成物一起丢过去通常只想要一份干净的RTL源代码。麻烦的是交付RTL必须保证文件齐全、内容没有被意外改动过。不然对方拿到代码后跑出和你不一致的结果溯源成本极高。这份“可核验的快照”正是工具的核心输出一份带校验和清单的目录结构谁拿到都能确认文件是否完整、是否与原始工程一致。1.4 为什么不直接用TCL脚本提到提取Vivado工程文件列表很多人第一反应是用TCL自带的命令比如get_files -all。确实能拿到完整列表但代价是必须启动Vivado、加载工程。工程稍大一点加载就是几十秒起步占用内存好几个GB而且运行环境必须有可用的Vivado安装和许可配置这在服务器批量处理场景下是很重的依赖。Python直接解析xpr文件就轻量得多几百毫秒出结果不需要任何图形界面不需要Vivado安装机器上有Python就能跑。这个工具做出来之后我把它放到团队公共脚本库里CI流程里也能直接调用批量扫多个历史工程非常顺手。提示如果你的目标是提取RTL代码本身且希望秒级完成纯解析xpr足够如果还想拿到工程里各种非源码的属性配置那就得老老实实开Vivado用TCL。2. xpr文件是突破口Vivado工程信息到底怎么组织的2.1 xpr本质上就是一份带XML结构的文本文件很多工程师没见过.xpr文件内部长什么样。其实Vivado工程文件本身就是一个XML格式的文本文件里面记录了工程的所有元数据器件型号、目标语言、源文件路径、约束文件、仿真文件等。用记事本打开就能看到类似这种结构Project Version7.4 Minor29 PathC:/work/proj/proj.xpr ... File PathC:/work/proj/proj.srcs/sources_1/new/top.v FileInfo Option NameUsedIn Valuesynthesis/ Option NameUsedIn Valuesimulation/ /FileInfo /File File PathC:/work/proj/proj.srcs/sources_1/imports/src/uart_rx.v/ ... /Project关键信息全在File Path...这一行。每个源文件在工程里登记过xpr里就会有对应条目。所以提取工具的核心逻辑可以非常直接从xpr里把所有Path属性取出来再按需求过滤、归类、拷贝。2.2 哪些信息该解析哪些信息不该依赖xpr里有用的主要是源文件的物理路径以及一些文件属性UsedIn、IsIncludeFile等。但这里有个容易误解的地方xpr里的文件条目只解决“有哪些文件”的问题不解决“依赖关系”的问题。Vivado虽然会在xpr里标注某个文件是include文件但include是谁引用的、引用链怎么走xpr并不会完整记录。所以include依赖必须靠扫描RTL源码里的include指令来重建。另外xpr中文件路径可能是绝对路径也可能是相对工程目录的相对路径。工程从一台机器拷贝到另一台机器、或者文件被移动过目录后相对路径和绝对路径混用的现象很常见。解析时必须同时处理这两种情况。2.3 不启动Vivado直接解析的工程价值直接解析xpr文件最大的好处是不依赖Vivado环境。我实际遇到过需要在一台没装Vivado的机器上整理历史工程代码的场景当时就是靠这个脚本秒级完成。对比一下启动Vivado加载一个中大型工程动辄一两分钟批量处理10个老工程就是一个多小时而纯Python解析10个xpr文件也就几秒钟。这个差异在工程治理场景下尤其明显。比如要做全部门的历史代码归档几十个工程逐个启动Vivado去导出根本不现实但脚本扫过去就是一轮循环的事。当然纯解析方式拿不到工程里所有TCL属性的最终值但对RTL提取这个目标来说已经是成本和收益最平衡的路径。3. 核心提取逻辑遍历依赖、归一化路径、归档落盘3.1 第一步从xpr中提取所有文件路径我用的方法是正则匹配。虽然理论上可以用XML解析库但Vivado的xpr里自定义语义太多而且我们要提取的只是所有Path属性正则反而更直接、更不容易被结构变化影响。import re from pathlib import Path XPR_FILE_RE re.compile(rFile\s[^]*Path([^]), re.IGNORECASE) def get_xpr_files(xpr_path: Path) - list[Path]: # utf-8-sig 兼容带BOM的文件 text xpr_path.read_text(encodingutf-8-sig, errorsignore) xpr_dir xpr_path.parent files [] for raw in XPR_FILE_RE.findall(text): # 统一把反斜杠替换成正斜杠避免Windows路径转义问题 raw raw.replace(\\, /).strip() p Path(raw) if not p.is_absolute(): p xpr_dir / p files.append(p.resolve()) return files这里有个细节要说明Path.resolve()不仅是把相对路径变成绝对路径还会把..、.这些多余的路径段消掉。路径经过这一步之后后面做相对路径计算和去重都会干净很多。3.2 第二步按文件类型过滤xpr里登记的不只是RTL源码还有XDC约束文件、XCI的IP核文件、BD文件、以及各种生成目录里的辅助文件。提取工具必须做一层过滤否则会把一堆无关文件带进来。下表是我常用的过滤策略扩展名类型默认是否提取.v / .svVerilog / SystemVerilog源文件提取.vh / .svhVerilog头文件、SystemVerilog头文件提取.vhd / .vhdlVHDL文件提取.xdc管脚/时序约束文件视情况提取.xciIP核配置文件不提取.bdBlock Design文件不提取.dcp综合网表文件不提取.coe / .mem / .hex存储器初始化文件可选提取我自己的脚本默认只保留.v/.sv/.vh/.svh/.vhd/.vhdlXDC约束文件用单独的--with-xdc开关控制。过滤规则最简单的实现就是判断后缀名但为了排除IP核生成文件还需要结合路径关键字做二次过滤。RTL_EXT {.v, .sv, .vh, .svh, .vhd, .vhdl} def is_rtl(path: Path) - bool: return path.suffix.lower() in RTL_EXT def should_skip(path: Path) - bool: parts [p.lower() for p in path.parts] # IP核生成文件通常在 ip 目录下或者文件名带 _stub / _bb if ip in parts: return True name path.name.lower() if _stub in name or _bb in name: return True return False注意ip in parts这个判断比较暴力如果你有工程目录恰好叫ip但里面放的是普通代码就会误伤。更稳妥的做法是结合*.srcs/sources_1/ip/这种Vivado典型目录结构来判断后面避坑章节会细说。3.3 第三步递归扫描include依赖过滤完之后文件清单还是“静态”的没有处理include依赖。这一步需要遍历每个Verilog/SystemVerilog文件找出所有include xxx.vh指令再递归地把被include的文件也纳入提取列表。实现上有三个要点。第一是搜索顺序优先找当前文件同目录下的文件找不到再去已经收集到的RTL文件所在目录里找这样能覆盖大多数工程的include习惯。第二是要处理嵌套include头文件里也可能include另一个头文件。第三是必须用集合记录已处理文件防止循环include导致死循环。INCLUDE_RE re.compile(rinclude\s*([^]), re.IGNORECASE) def expand_includes(file_path: Path, seen: set, candidates: list[Path]) - None: if file_path in seen: return seen.add(file_path) if file_path.suffix.lower() not in (.v, .sv): return try: text file_path.read_text(encodingutf-8-sig, errorsignore) except OSError: return for inc in INCLUDE_RE.findall(text): search_dirs [file_path.parent] candidates found None for d in search_dirs: cand (d / inc).resolve() if cand.exists(): found cand break if found is None: print(f[WARN] include not found: {inc} (from {file_path})) continue expand_includes(found, seen, candidates)这段代码有两个细节值得展开。一是candidates列表来自于已经收集到的所有RTL源文件的父目录集合它的作用是在include文件被放在某个公共目录时能跨目录找到它。二是我用seen集合同时承担了“记录已找到文件”和“防循环include”两个职责A includes B、B includes A这种场景靠它就能直接终止。3.4 第四步路径归一化与目录结构映射文件收集齐了接下来是把它们拷贝到目标目录。这里最大的坑是目录结构问题源文件分散在不同磁盘、不同工程子目录里如果直接全部丢平铺到同一个文件夹同名文件会互相覆盖。我的做法是优先保留相对工程的路径关系。以xpr文件所在目录为基准每个源文件计算relative_to(xpr.parent)如果成功就在目标目录下复刻同样的子目录结构。如果文件在工程目录之外路径计算抛ValueError就统一放到external/下面遇到重名再加序号区分。def map_dest(src: Path, xpr_dir: Path, out_dir: Path, index: int) - Path: try: rel src.relative_to(xpr_dir) except ValueError: rel Path(external) / src.name dst out_dir / rel if dst.exists(): dst dst.with_name(f{dst.stem}_{index}{dst.suffix}) dst.parent.mkdir(parentsTrue, exist_okTrue) return dst这样整理出来的目录顶层文件、子模块、头文件之间的相对关系基本被保留新环境里重新添加进Vivado工程时结构也比较清晰不会出现几十个文件堆在一起的“地狱目录”。3.5 第五步写清单、算MD5、落盘提取工具最后一环是生成manifest清单。每个文件拷贝完成后同时记录原始路径、目标路径、MD5校验值三组信息分别输出成CSV和JSON两个版本。CSV方便Excel打开导入JSON方便后续脚本读取处理。import csv, json, hashlib, shutil manifest [] for i, f in enumerate(sorted(rtl_files)): dst map_dest(f, xpr.parent, out_dir, i) shutil.copy2(f, dst) md5 hashlib.md5(f.read_bytes()).hexdigest() manifest.append({ src: str(f), dst: str(dst.relative_to(out_dir)), md5: md5, }) with open(out_dir / manifest.csv, w, newline, encodingutf-8) as fp: w csv.DictWriter(fp, fieldnames[src, dst, md5]) w.writeheader() w.writerows(manifest) (out_dir / manifest.json).write_text( json.dumps(manifest, ensure_asciiFalse, indent2), encodingutf-8 )MD5的意义在于交付后校验。对方把代码跑出结果后如果怀疑自己拿到手的文件和你的不一致直接比对manifest里的md5字段就能确认。我自己做代码评审时也会先跑一遍md5sum确认文件完整。3.6 完整脚本骨架上面几个片段拼起来就是一个可用版本的脚本。我这里再给一份合并后的骨架方便直接改import argparse import csv import hashlib import json import re import shutil from pathlib import Path XPR_FILE_RE re.compile(rFile\s[^]*Path([^]), re.IGNORECASE) INCLUDE_RE re.compile(rinclude\s*([^]), re.IGNORECASE) RTL_EXT {.v, .sv, .vh, .svh, .vhd, .vhdl} def main(): ap argparse.ArgumentParser(descriptionVivado RTL source extractor) ap.add_argument(xpr, helppath to .xpr project file) ap.add_argument(-o, --output, defaultrtl_snapshot, helpoutput directory) ap.add_argument(--with-xdc, actionstore_true, helpinclude xdc files) args ap.parse_args() xpr Path(args.xpr).resolve() out_dir Path(args.output).resolve() out_dir.mkdir(parentsTrue, exist_okTrue) # 1. 解析 xpr text xpr.read_text(encodingutf-8-sig, errorsignore) entries [] for raw in XPR_FILE_RE.findall(text): raw raw.replace(\\, /).strip() p Path(raw) if not p.is_absolute(): p xpr.parent / p entries.append(p.resolve()) # 2. 过滤 RTL 文件 rtl_files [f for f in entries if f.suffix.lower() in RTL_EXT] rtl_files [f for f in rtl_files if not should_skip_candidate(f)] # 3. 展开 include 依赖 candidates list({f.parent for f in rtl_files}) seen set() for f in sorted(rtl_files): expand_includes(f, seen, candidates) all_files sorted(seen) # 4. 拷贝 manifest manifest [] for i, f in enumerate(all_files): dst map_dest(f, xpr.parent, out_dir, i) shutil.copy2(f, dst) manifest.append({ src: str(f), dst: str(dst.relative_to(out_dir)), md5: hashlib.md5(f.read_bytes()).hexdigest(), }) with open(out_dir / manifest.csv, w, newline, encodingutf-8) as fp: w csv.DictWriter(fp, fieldnames[src, dst, md5]) w.writeheader() w.writerows(manifest) (out_dir / manifest.json).write_text( json.dumps(manifest, ensure_asciiFalse, indent2), encodingutf-8) print(f[INFO] total files extracted: {len(all_files)}) print(f[INFO] output: {out_dir}) if __name__ __main__: main()实际使用时肯定要根据自己的工程结构微调但这个骨架已经把核心流程走通了解析、过滤、依赖展开、拷贝、校验、清单。缺的should_skip_candidate和expand_includes、map_dest三个函数前面小节里都有完整实现拼上去就能跑。4. 实际运行效果与最容易踩的四个坑4.1 命令行运行方式与实测输出脚本用法很简单python rtl_extractor.py C:/work/proj/proj.xpr -o C:/work/rtl_snapshot我在一个中等规模工程上跑过的实际输出大概是这样[INFO] xpr entries: 87 [INFO] filter rtl files: 63 [INFO] include expand: add 5 files [INFO] total files extracted: 68 [INFO] output: C:/work/rtl_snapshot耗时一般在1秒以内。输出目录里会有一个manifest.csv、manifest.json以及按原工程目录结构归档的源码。这个工程有87个xpr条目过滤掉IP核生成文件、仿真文件之后剩63个RTL文件再补上5个头文件最终拿到68个文件整个过程不到一秒钟。4.2 坑一路径中反斜杠混用导致的解析错误Vivado生成的xpr文件路径有时候是正斜杠D:/proj/src/top.v有时候又是Windows习惯的反斜杠D:\proj\src\top.v。一开始我偷懒直接用Path(raw)处理反斜杠路径结果在解析时出现了各种诡异问题后来统一改成先raw.replace(\\, /)再交给Path问题立刻消失。还有个相关坑是xpr里路径前缀可能是file:///D:/...这种URL格式。目前我还没在Vivado生成的文件里遇到过但如果是别人手工改过的工程建议解析前先做一次前缀剥离。4.3 坑二include关键字大小写与跨平台路径大小写敏感include Define.VH这种大小写混用的情况Windows下编译没问题但脚本里的Path.exists()在Linux下会严格大小写匹配找不到文件就会漏提取。跨平台使用时必须注意要么搜索时再尝试一次小写匹配要么提前约定所有include文件名统一小写。更隐蔽的是嵌套include。A.v里include了B.vhB.vh里又include了C.vh如果脚本只做一层扫描C.vh就会漏掉。我一开始就吃过这个亏在Linux服务器上整理完代码跑到Windows上重新编译才报C.vh找不到。所以expand_includes必须设计成递归的并且不能为了省事只处理顶层文件。4.4 坑三xpr编码问题和PowerShell重定向的乱码陷阱xpr文件大概率是UTF-8带BOM。read_text(encodingutf-8-sig)能同时兼容带不带BOM的情况errorsignore是为了防止某些非法字节直接让整个解析崩溃。真正让我意外的是Windows PowerShell环境下的输出重定向问题。有段时间我直接在PowerShell里跑python rtl_extractor.py ... log.txt生成的log文件用记事本打开全是乱码。后来才搞明白PowerShell的重定向默认把stdout转成UTF-16编码和脚本内部的UTF-8完全对不上。解决方式很简单脚本内部直接写文件不要依赖shell重定向如果必须重定向用cmd /c执行或者设置PYTHONIOENCODINGutf-8。4.5 坑四仿真文件、IP核文件混杂在工程目录里Vivado工程中sim_1目录下的testbench文件通常不需要参与RTL代码交付和综合但xpr里它们和被综合的源码一样都被登记为File。如果不过滤提取结果里就会混进一大堆tb文件。我的默认策略是把路径片段中含sim的目录排除掉但注意不能无脑排除所有含sim的路径万一你自己的功能模块目录名字里也有sim就误伤了。更精准的做法是利用Vivado的目录结构特征*.srcs/sources_1/sim/是仿真目录*.srcs/sources_1/ip/是IP核目录。IP核生成的文件名一般还有_bb、_stub这类后缀这些特征都可以写进过滤规则。宁可过滤规则写得清楚一点也不要在脚本里留一堆魔法判断。4.6 一套适用于多数工程的过滤规则参考需要过滤的场景推荐规则仿真testbench排除路径中含sim_1或sim的条目提供--with-sim开关IP核生成代码排除路径中含/ip/的条目以及文件名含_stub、_bb的文件约束文件默认排除.xdc提供--with-xdc开关单独提取工程生成的临时文件排除路径中含.runs、.cache、.gen的条目5. 工具之外的工程治理提取完代码还能做什么5.1 提取后做代码统计与工作量评估拿到一份干净、完整的RTL文件清单后第一件顺手能做的事就是代码量统计。Vivado自带的功能也能看代码行数但无法批量比较多个工程、多个版本之间的差异。我写了个简单函数遍历提取出的文件按扩展名统计文件数、总行数、空行数、注释行数输出成一张表。有了这组数据做工作量汇报、代码审查范围界定就方便得多。比如“这次版本新增了3个SystemVerilog模块共2800行有效代码”这种结论可以直接从提取结果里算出来不需要打开Vivado一个个看。5.2 作为静态检查、Lint工具的文件列表输入很多RTL静态检查工具需要一份完整的文件清单才能运行。提取工具生成的manifest.json恰好就是现成的输入。我配合Verilator做lint时会用Python脚本读取manifest里的dst路径拼成一行verilator --lint-only file1.sv file2.sv ...命令整个lint流程就完全自动化了。这里有个经验include文件也要加进lint命令否则很多宏定义解析不了报错一堆假的。所以提取工具的include展开逻辑不只是为了交付完整代码对后续所有基于文件列表的自动化分析都至关重要。5.3 配合版本管理做代码快照Vivado工程里生成物太多不适合直接丢进Git。比较好的做法是只把RTL代码纳入版本管理但很多老工程最初没有这个规范源文件散落在各个目录里没法一步到位地整理。现在我养成了一个习惯每次迭代开始前用提取工具把当前工程的RTL代码快照导出一份提交到一个独立的代码仓库里。这样即使有人误删了源文件、或者Vivado工程崩溃导致目录结构损坏我手里始终有一份“最近一次可编译状态”的完整代码。快照目录本身就带manifest清单配合Git的提交记录代码演进历史一目了然。5.4 批量处理多个历史工程顺手做工程瘦身脚本只接收一个xpr路径但外面套一层循环就能批量处理。我有一台专门做归档的老机器上面躺着七八个历史工程每个工程都占好几个GB。用一段简单的Python遍历所有.xpr文件逐个提取RTL代码到归档目录代码部分总共才几十MB。提取完成后这些老工程的本地缓存、生成文件就可以放心清理。要复现某个历史版本时不需要翻出一个好几GB的完整工程直接基于提取出的RTL代码重建工程就行。配合“Vivado工程清理”这个常见需求来说流程就是先提取代码快照再删无用缓存最后把快照归档到版本管理。5.5 结合中文路径问题的现实意义Vivado在Windows下对中文路径的支持一直不算友好综合、仿真过程中经常冒出一些莫名其妙的报错网上搜“vivado中文路径”能找到各种经验贴。最彻底的解决办法是把工程放到纯英文路径下但对于已经在中文目录下发展了很久的工程直接迁移代价很大。提取工具在这里能帮上忙先把RTL代码和约束文件提取到一个纯英文路径的目录里再在该目录下新建工程、重新添加源代码这样既绕开了中文路径的坑又确保代码本身没有被遗漏。我处理过两个从中文路径迁移过来的工程提取、重建、验证的流程走完原先的诡异报错基本都消失了。5.6 扩展方向支持VHDL、支持更完整的依赖分析目前的脚本依赖正则扫描include指令对VHDL的支持比较弱。VHDL的依赖机制不是include而是library和use子句不同entity之间的依赖关系没法靠简单正则重建。如果工程以VHDL为主建议先用提取工具拿到文件清单再通过Vivado的TCL命令get_files -all获取完整依赖信息两者结合做二次处理。另外还可以考虑让脚本读取xpr中的IncludeDirs选项把工程自定义的include搜索目录加进expand_includes的搜索路径。这个功能我在新版本里实现了代码不复杂就是比对本节的candidates列表来源额外加上xpr中解析出的目录即可。最后再说一点我个人坚持的习惯无论提取工具多顺手版本管理里的代码快照才是真正的安全网。脚本只是把“收集源码”这个动作从半小时压缩到一秒但如果工程本身没有版本管理习惯提取得再干净也还是会乱。我通常在拿到一个陌生工程时第一件事就是跑这个脚本从交付开始就把代码控制在可追溯的状态下后面综合、上板、回片验证都会省心不少。
返回列表