
如果你手上有一个 Xilinx FPGA 工程代码已经在 Vivado 里写好、综合和实现也都正常但到了仿真这一步越做越别扭——xsim 编译大工程慢跑回归缺少顺手的命令行体验波形窗口用起来也不够灵活那你大概率会想换一套更顺手的仿真调试工具链。接下来要聊的就是把 Vivado 当作前端设计入口把 Synopsys VCS 当作仿真编译和运行引擎再用 Synopsys Verdi 做波形调试这三者组合起来干活的标准流程、配置细节以及我在实际环境里踩过的坑和解决办法。这套联合仿真方案在数字 IC 验证领域几乎是标配很多从 FPGA 转过来做芯片验证的工程师也会沿用这个习惯。对于 FPGA 工程师来说它最大的价值在于仿真速度更快、命令行可控性更强、波形调试体验更好。这篇文章不扯理论只讲怎么落地目标是一步步把环境搭到能跑通你的第一个外仿RTL 仿真和后仿门级时序仿真中途遇到问题也能自己判断原因。1. 为什么要把 Vivado 和 VCS/Verdi 搭在一起1.1 三个工具各管哪一段先把这个组合的分工说清楚避免后面看命令时一头雾水。Vivado 是 Xilinx FPGA 的完整设计套件负责工程管理、RTL 编辑、综合、布局布线、比特流生成这些环节是 VCS 替代不了的。VCS 作为仿真编译器和仿真器负责把我们写的 Verilog/SystemVerilog 代码连同 testbench 一起编译成可执行的 simv然后跑出仿真结果。Verdi 则是波形和调试工具读 fsdb 格式的波形文件做源文件跟踪、状态机查看、信号比较让排查逻辑问题效率更高。一句话理解Vivado 管前端设计VCS 管仿真运行Verdi 管波形调试。我们之所以要手动把三者串起来是因为 VCS 和 Verdi 不是 Xilinx 的工具默认不知道你的 FPGA 库在哪儿也不认识 Vivado 的 IP 仿真模型所以需要你先做一次“库编译”和“文件列表整理”把 Vivado 工程里的东西翻译成 VCS 能认识的形式。1.2 xsim 在大型工程里的短板Vivado 自带的 xsim 不是不能用小模块、快速功能验证完全没问题。但工程规模一旦上来你会很快撞到几个明显的天花板编译速度慢。SystemVerilog 文件多、IP 核多的时候xsim 的 elaboration 时间会明显拉长改两行代码重新编译一次等待感很强烈。回归脚本不好写。xsim 虽然也支持命令行和 xvlog/xelab/xsim 三件套但整体生态和 Makefile/CI 的配合不如 VCS 自然。你做大规模随机测试、跑几百个用例的时候VCS 的 UCLI 和编译选项明显更成熟。波形调试能力弱。xsim 自带的波形窗口用来看看基本信号没问题但一旦要跨层次追踪逻辑、对比多组波形、看状态机转换体验就比较局促了Verdi 在这方面是长项。1.3 适合用这套组合的场景不是所有项目都值得搭这套环境。我的经验是下面这几类场景收益最大工程里挂了多个 Xilinx IP比如 AXI 接口、PCIe、DDR、EthernetIP 的仿真模型在 xsim 里调理起来比较费劲用 VCS 跑反而更稳定。需要大量回归测试或者要把验证用例交给团队其他人复用命令行脚本化比 GUI 点按钮更可控。你不只想验证 RTL 功能还要做综合后、实现后的时序仿真这时候 VCS 对门级网表 SDF 的支持比 xsim 更顺手。你正在从 FPGA 开发向数字 IC 验证方向转提前熟悉 VCS Verdi 这套业界标准流程后面上手 UVM、回归环境会平滑很多。反过来说如果只是写个几十行的状态机或者验证一个很小的接口模块用 Vivado 自带工具就足够了没必要为了搭环境折腾半天。2. 开工前的环境检查与版本匹配2.1 版本组合怎么选版本匹配是这套环境里第一个容易踩坑的地方。Vivado 在编译仿真库时会检测仿真器的版本VCS 在编译 Xilinx 库时也会检查自身的兼容性。如果版本跨度太大经常出现编译错误或者库文件不兼容的问题。我目前长期使用的是 Vivado 2020.2 搭配 VCS 2018.06 和 Verdi 2018.06这套组合运行很稳定社区里的用户也最多遇到问题相对好搜到答案。如果你的 Vivado 版本更新比如 2022.2 或者 2023.1建议 VCS 也选同期的 2020 之后版本尽量避免用特别老的 VCS 去编译新库。以下是一个大概的匹配参考Vivado 版本建议 VCS 版本区间备注2019.x2017 ~ 2019较老除非工程需要不建议新项目用2020.x2018 ~ 2020我测试过的 2020.2 2018.06 组合稳定2021.x2019 ~ 2021需要确认 compile_simlib 支持列表2022.x2020 ~ 2023新版本工具链注意 Linux 系统 glibc 版本操作系统方面优先选 64 位的 CentOS 7、Rocky Linux 8 或者 Ubuntu 18.04/20.04。VCS 对系统库有依赖太激进的新系统版本有时候反而会碰到 glibc 兼容性问题。2.2 环境变量与 License 检查环境变量决定工具能不能正常找到安装路径和许可证。我一般会在~/.bashrc里写这样一段export VIVADO_HOME/opt/Xilinx/Vivado/2020.2 export VCS_HOME/opt/synopsys/vcs_2018.06 export VERDI_HOME/opt/synopsys/verdi_2018.06 export PATH$VCS_HOME/bin:$VERDI_HOME/bin:$VIVADO_HOME/bin:$PATH export LM_LICENSE_FILE/opt/synopsys/license.dat alias vcsvcs -full64注意PATH的顺序让 VCS 和 Verdi 的 bin 目录排在 Vivado 前面避免同名命令冲突。另外如果你用 Synopsys 的 Common Licensing可以直接指定 license 文件路径如果用的是 FlexLM 服务器LM_LICENSE_FILE要写成porthost的格式。配置完环境变量后用下面几条命令验证工具是否就绪which vcs vcs -ID which verdi verdi -V which vivado如果 vcs 和 verdi 都能正常打印版本信息说明安装和环境变量没问题。如果vcs提示找不到命令先确认$VCS_HOME/bin路径是否正确如果报 license 错误优先检查LM_LICENSE_FILE和 license 服务器状态。2.3 快速自测跑一个最小的 VCS 例程正式搭建 Xilinx 库之前建议先跑一个和 Vivado 无关的最小仿真确认 VCS Verdi 本身链路是通的。方法很简单随便新建两个文件// hello.v module hello; initial begin $display(hello vcs); #100 $finish; end endmodule编译运行vcs -full64 -sverilog hello.v -o simv_hello -l compile_hello.log ./simv_hello -l run_hello.log能正常打印hello vcs说明 VCS 编译和运行没问题。再试一下 Verdi 能不能正常打开verdi -sv hello.v 能弹出 Verdi 主界面说明 Verdi 和 license 也没问题。这一步的自测很值它把“工具链本身的问题”和“之后 FPGA 库的问题”隔离开来后面再报错你会更容易定位是哪个环节出了岔子。3. 把 Vivado 仿真库编译给 VCS 用3.1 GUI 方式compile_simlib 到底在做什么Vivado 自带的仿真器 xsim 认识 Xilinx 的 unisim 库、secureip 库以及各种 IP 的仿真模型这些库文件在安装目录里都有。但 VCS 不认 Vivado 的库格式所以需要先把这些 Verilog 库文件编译成 VCS 能直接读取的形式这一步就是 compile_simlib 完成的。GUI 操作路径启动 Vivado打开任意一个工程或者直接在主界面点击菜单栏Tools - Compile Simulation Libraries。在弹出的对话框里把Simulator选成VCSLanguage选成Verilog或SystemVerilogFamily按需选择如果你的工程只用 7 Series 或 UltraScale可以只勾选对应系列省编译时间。Output location设置一个独立目录比如/opt/xilinx_lib/vcs。点击 Compile 后Vivado 会调用 VCS 把 unisim_ver、secureip_ver 等库逐一编译。这个过程视机器性能可能需要十几分钟到半小时耐心等编译日志末尾出现Compile completed successfully就说明成功了。3.2 命令行方式与产物检查不喜欢 GUI 的可以走命令行在 Vivado Tcl Console 里执行类似命令compile_simlib -simulator vcs \ -family all \ -language verilog \ -dir /opt/xilinx_lib/vcs这条命令的效果和 GUI 一致。编译完成后检查输出目录正常情况下会看到以vcs为前缀的目录或文件常见的有/opt/xilinx_lib/vcs/ ├── vcs/ │ ├── unisim_ver/ │ ├── secureip_ver/ │ ├── xpm/ │ └── ...关键就是要确认unisim_ver和secureip_ver这两个目录存在并且里面有编译好的.v文件或者 VCS 库索引文件。后续 VCS 编译的时候通过-y 库路径 libext.v选项去认识它们。3.3 库编译常见的坑库编译看着简单实际上翻车率不低结合我的经验说几个典型问题32 位 vs 64 位库不匹配。VCS 默认可能走 32 位模式而 Vivado 生成的库是 64 位的编译时务必给 vcs 加-full64否则库加载会报错。Family 勾选过多导致编译时间太长。如果你只做 7 Series 或 UltraScale不要全选否则等于白等半小时。License 不完整。compile_simlib 需要调用 VCS 编译而 VCS 又需要对应 feature如果 license 只配置了 Verdi 没配置 VCS这里会直接失败。版本太老导致报错。VCS 版本和 Vivado 支持列表不匹配时compile_simlib 会提示 simulator version not supported这时候要么换 VCS 版本要么换 Vivado 版本。4. 联合仿真完整流程从 filelist 到 Verdi 波形4.1 文件列表手动写还是用 export_simulation环境准备好之后进入最核心的联合仿真流程。第一步是整理仿真文件列表也就是让 VCS 知道要编译哪些 RTL 文件、哪些 testbench、哪些 IP 仿真模型、哪些库路径。有两种做法。第一种是手动写 filelist适合工程里 RTL 文件结构比较清晰的场景。文件列表长这样incdir../rtl incdir../tb defineFSDB_DUMP ../rtl/axis_pkg.sv ../rtl/axis_fifo.sv ../rtl/top_axis.sv ../tb/test_top.sv /opt/Xilinx/Vivado/2020.2/data/verilog/src/glbl.v -y /opt/xilinx_lib/vcs/unisim_ver libext.v -y /opt/xilinx_lib/vcs/secureip_ver libext.v这里说明几个细节。incdir用于指定 include 目录define用于全局宏定义glbl.v是 Xilinx 的全局信号文件很多 IP 仿真必须要它否则会出现 GSR、PULLUP 之类的信号未定义问题。-y告诉 VCS 把某个目录当作库来搜索libext.v指定库文件的扩展名。第二种是使用 Vivado 的export_simulation工程里挂了很多 IP 时更省心。在 Vivado Tcl Console 里执行export_simulation -simulator vcs -directory ./sim_vcs这个命令会把当前工程里的 RTL、IP 仿真模型、约束相关文件和 glbl.v 自动组织成一份 VCS 可用的目录里面有filelist.f、编译脚本等。执行完进入./sim_vcs/vcs目录就能看到导出的文件列表。我个人建议小工程、文件少直接手写 filelist大工程、IP 多用 export_simulation 打底然后根据实际需要再手动调整 filelist。4.2 VCS 编译命令逐条拆解拿到 filelist 后执行 VCS 编译。我的常用命令长这样vcs -full64 -sverilog \ -debug_accessall \ -f filelist.f \ -timescale1ns/1ps \ -o simv \ -l compile.log \ -P $VERDI_HOME/share/PLI/VCS/LINUX64/novas.tab $VERDI_HOME/share/PLI/VCS/LINUX64/pli.a逐条解释一下选项的作用-full64使用 64 位模式处理大工程更稳定也避免和 Vivado 库的位数不匹配。-sverilog使能 SystemVerilog 语法。如果你的 testbench 用到了 interface、class、assertion这个选项必须加。-debug_accessall生成完整的调试数据库Verdi 才能做 nWave 波形和源文件追踪。如果只是要 fsdb 波形也可以搭配-debug_pp使用但直接all最省心。-f filelist.f导入文件列表。-timescale1ns/1ps设置全局时间单位和精度防止有些文件没有写 timescale 导致仿真时间不对。-o simv指定输出的可执行文件名默认是 simv加这个明确一下。-l compile.log编译日志排查错误必备。-P .../novas.tab .../pli.a把 Verdi 的 PLI 接口挂载到 VCS 里这是生成 fsdb 波形的关键。novas.tab是任务映射表pli.a是接口库路径要根据实际安装的 Verdi 版本调整。编译完成后目录下会出现simv可执行文件同时还有csrc、simv.daidir等中间产物这些都是正常的。4.3 UCLI 运行与 fsdb 波形生成编译通过后运行仿真。我习惯写一个 UCLI 脚本把运行参数和停止条件都放在里面// ucli.do run 100us quit执行命令./simv -ucli -do ucli.do -l run.log-ucli -do是让 VCS 进入交互/脚本模式读入 ucli.do跑 100us 后自动退出。日志留在 run.log 里。但到现在为止仿真还没有 dump 波形。要在 testbench 里加一段系统任务initial begin $fsdbDumpfile(wave.fsdb); $fsdbDumpvars(0, test_top, all); end$fsdbDumpfile指定波形文件名$fsdbDumpvars指定要记录的层次和信号范围。第一个参数 0 表示 dump 整个设计all表示包含所有信号类型。如果你只关心某个模块可以把 0 换成具体层次路径减少波形文件大小。如果你不想在代码里加 dump 任务也可以运行时加fsdbautoflush参数并配合相应的任务但最直观的还是代码里写清 dump 逻辑。加完 dump 任务后重新编译运行仿真结束后会生成wave.fsdb可以检查文件大小确认波形真的写出来了。4.4 用 Verdi 打开波形开始调试波形生成后用 Verdi 打开verdi -f filelist.f -top test_top -ssf wave.fsdb -f filelist.f让 Verdi 也读同一份文件列表这样它知道源码在哪儿-top test_top指定硬件顶层-ssf wave.fsdb指定波形文件。打开后左侧是源文件树下方是 nWave 波形窗口。你可以把关心的信号拖进波形窗口也可以在源文件里点击信号右键Add to Waveform。Verdi 最爽的地方在于从源文件到波形可以交叉高亮点波形里的信号跳变源文件里对应逻辑会被同步显示排查逻辑问题非常直观。如果你只想快速看波形不加-f和-top只加载 fsdb 也可以但源代码联动追踪会不好使建议还是把文件列表带上。4.5 用 Makefile 把流程固化命令行敲多了容易手误我习惯把整套编译、运行、开波形流程写进 Makefileall: simv simv: filelist.f vcs -full64 -sverilog \ -debug_accessall \ -f filelist.f \ -timescale1ns/1ps \ -o simv \ -l compile.log \ -P $(VERDI_HOME)/share/PLI/VCS/LINUX64/novas.tab $(VERDI_HOME)/share/PLI/VCS/LINUX64/pli.a run: simv ./simv -ucli -do ucli.do -l run.log wave: run verdi -f filelist.f -top test_top -ssf wave.fsdb clean: rm -rf simv csrc simv.daidir ucli.key nova.* verdiLog *.fsdb *.log *.vpd *.vcd rm -rf inter.vpd以后每次改代码只需要make run就能编译跑仿真出波形后make wave打开调试。make clean一键清理中间文件避免增量编译的缓存干扰。5. 后仿真场景网表、SDF 和 memory 初始化5.1 从 Vivado 导出网表和 SDF联合仿真不只适用于 RTL 功能验证后仿真时序仿真也是刚需。后仿用的不是 RTL 源码而是综合或布局布线后生成的网表再加上 SDF 格式的时序反标文件。Vivado 里可以直接导出open_run impl_1 write_verilog -mode sim_netlist ./post_impl_netlist.v write_sdf -mode setup_hold ./post_impl.sdf也可以走 GUI打开 Implemented DesignFile - Export - Export Simulation选择Verilog和SDF。导出的网表文件里Xilinx 的 primitive 会被例化成IBUF、BUFG、FDRE、BRAM_TDP_MACRO等元件这些元件的仿真模型来自 unisim 库所以后仿编译依然要指定库路径。后仿的 testbench 通常不会大改但需要在 initial 里声明 SDF 反标initial begin $sdf_annotate(../sim/post_impl.sdf, tb_top.dut, , sdf_annotate.log, MAXIMUM); end$sdf_annotate的第一个参数是 SDF 文件路径第二个参数是被反标模块的层次路径一定要和网表里 DUT 的例化层次对应。5.2 后仿编译的三个关键点后仿编译命令和 RTL 仿真相差不多但有三个点需要特别注意。第一网表里没有 testbench 里常见的always (posedge clk)写法全部是门级和时序单元仿真事件数量会暴涨同样 100us 的仿真时间后仿可能比 RTL 仿慢一个数量级要有心理准备。第二必须正确指定 unisim 库和 secureip 库。很多时序原语在 unisim_ver 里而一些高速接口的仿真模型在 secureip_ver 里漏掉任何一个都会报模块找不到的错误。第三关于 X 态问题。门级仿真里未初始化的寄存器会显示 X可能导致很多逻辑判断直接跑飞。RTL 仿真里 X 态不一定致命但后仿中 X 的传播范围往往更大。必要时可以在编译命令里加notimingcheck跳过时序检查避免 setup/hold 违例引入大量 X 态干扰功能判断但这只是排查手段不能替代真正的时序收敛。5.3 memory 初始化问题怎么破后仿最常见的拦路虎之一是 memory 初始化。BRAM 等存储单元在网表里通常被例化成BRAM_TDP_MACRO或RAMB36之类的原语RTL 里写的initial $readmemh在综合后很可能已经不存在了所以后仿时 memory 内容全是 X。如果算法逻辑里依赖 RAM 初始值仿真波形会一塌糊涂。解决思路是在 testbench 里直接对网表层次中的 memory 做初始化。先打开网表确认 BRAM 例化实例名比如tb_top.dut.u_ram.ram_instance然后在 testbench 里加initial begin $readmemh(mem.hex, tb_top.dut.u_ram.ram_instance.mem); end但这里有个现实问题BRAM 原语内部结构是 Xilinx 加密过的模型内部 memory 变量名不一定能直接引用。更可靠的做法是如果在 Vivado 里对 RAM IP 配置了Coefficient File或Memory Init File综合时应该手动把初始化属性传递下去确保网表模块通过INIT_xx参数完成初始化。对于纯 RTL 实现的 memory比如自己用 reg 数组写的 RAM综合后往往能被推断成分布式 RAM 或 BRAM后仿时能不能直接引用内部数组取决于网表保留程度需要逐个工程验证。如果确实遇到内部不可见的情况还有一种实用技巧在后仿编译时定义一个宏把 testbench 里对 memory 的读写改成对网表端口做条件初始化用defineINIT_MEM控制编译路径这样 RTL 仿真和门级仿真共用一套 tb只在宏开启时执行初始化逻辑。6. 高频报错与排错经验汇总6.1 编译阶段常见报错VCS 编译阶段报错是环境搭建时最普遍的下面是我遇到最多的问题错误现象原因解决办法Error-[ITFCS] Interface protocol assertion in ...缺少 glbl.v 或者 unisim 库没加对在 filelist 中加入 glbl.v并检查-y unisim_ver libext.vError-[VCSSCAN] SystemVerilog keyword ...文件后缀是 .v 但用了 SV 语法或者没加-sverilog给 vcs 加-sverilog必要时把文件后缀改成 .svCannot find the file glbl.vglbl.v 路径错误检查$VIVADO_HOME/data/verilog/src/glbl.vCannot find the library unisim_ver-y后面的库路径不对确认 compile_simlib 输出目录检查 unisim_ver 是否真的存在Error-[P L I]或者 fsdb 系统任务未定义Verdi 的 PLI 没挂载检查-P后面的 novas.tab 和 pli.a 路径是否正确./simv: error while loading shared librariesLinux 系统缺少 VCS 依赖的兼容库安装对应的 32 位库或兼容库常见的是 libelf、libX11 相关包其中最坑的是ITFCS错误很多人死活找不到原因最后发现只是 glbl.v 没加进文件列表。Xilinx 的很多 IP 仿真模型依赖全局复位和全局置位信号这些信号定义在 glbl.v 里不加它仿真编译时那些接口协议检查就会失败。6.2 运行阶段常见报错编译过了不代表能顺利跑完运行阶段同样有坑fsdb 文件生成为 0KB。多半是 PLI 没生效或者 dump 任务没执行。确认编译命令里有-P参数确认 tb 里的$fsdbDumpfile确实被 initial 块覆盖到。仿真停不下来一直跑。检查 UCLI 脚本里的run时间是否合理或者 testbench 里有没有 forever 循环没设置结束条件。X 态满天飞。RTL 仿真阶段先查时钟和复位有没有正常拉起来后仿阶段再考虑时序违例和 memory 初始化问题。波形文件太大磁盘爆掉。把$fsdbDumpvars的层级从 0 改成指定模块只 dump 你关心的那部分信号。Verdi 打开后无法交叉定位源码。通常是打开波形时没有带-f filelist.f和-top重新用带参数的命令打开。license 报错。运行阶段才报 license 问题核心是 VCS 和 Verdi 各要自己的 feature检查LM_LICENSE_FILE是否同时覆盖两个工具。后仿 SDF 反标失败。确认$sdf_annotate中的层次路径和网表例化层次一致同时确认 SDF 文件路径在当前运行目录下能访问到。6.3 排错思路与好习惯排查联合仿真问题我的经验是先分阶段再分工具。报错信息出来第一时间判断是 VCS 编译阶段的错还是 simv 运行阶段的错还是 Verdi 加载波形的错。这三个阶段互相独立不要混在一起猜。保留好所有日志文件持续加-l选项把编译日志、运行日志分别落盘。编译报错时往前翻日志VCS 通常会把第一个错误的原因写在前面后面的错误往往是第一个错误的连锁反应只需要修第一个。另外每改一个配置就重新编译不要一次堆很多修改。我见过太多人同时改了 filelist、版本、库路径、宏定义报错之后完全不知道是谁造成的最后一层层回退排查反而更浪费时间。还要养成方便切换的环境变量或脚本模板。同一台机器上不同工程可能需要不同 Vivado 版本或不同库路径把这些变量定义在工程级的 env.sh 里用source env.sh切换别都写死在全局 bashrc 里。我自己的工程目录习惯是这样的project/ ├── rtl/ ├── tb/ ├── sim/ │ ├── filelist.f │ ├── ucli.do │ ├── Makefile │ ├── env.sh │ └── wave.fsdb把仿真相关的东西全部丢到 sim 目录里和 RTL 源码、Vivado 工程文件分开这样不管工程怎么 Exported、怎么归档仿真环境都有一份独立可复现的配置。最后分享一个我实际使用中的小技巧第一次搭环境时建议先跑一个最简单的 LED 闪烁模块整个链路只要能跑通后面把真正的工程往这套流程里套就能省很多心。这个验证流程走通之后再往工程里逐步加 IP、加各种复杂度一旦报错你也知道是新增内容带来的问题而不是环境本身的问题。踩过几次坑之后你会发现这套联合仿真环境真正的价值不在“搭好”而在“可维护”这三个字上——配置清晰、脚本可控、日志完整比任何花哨的调试技巧都更能救你的交付时间。