1. 为什么还要折腾 Vivado 与 VCS 的联合仿真
如果你平时用 Vivado 自带的仿真器跑 RTL,小规模设计还能忍,一旦设计里塞进 DDR 控制器、PCIe 硬核、或者带 Memory 的 SoC 子系统,仿真速度会慢到让人怀疑人生。Vivado Simulator 在编译大型仿真模型时,编译时间动辄十几分钟,跑一个后仿真的 Memory 初始化流程,可能一晚上都出不来波形。这时候把仿真任务交给 VCS,再用 Verdi 看波形,是数字前端和 FPGA 验证工程师比较常见的组合。
Vivado 2025.1 是 Xilinx 较新的版本,VCS 2024.SP1 是 Synopsys 的仿真工具版本,两者联合仿真的核心思路并不复杂:用 Vivado 生成仿真所需的库文件,把 Xilinx 的 IP 仿真模型编译成 VCS 能识别的库,然后在 VCS 里调用这些库跑仿真,最后用 Verdi 加载 FSDB 波形进行调试。听起来简单,但实际操作中,编译库的选项、仿真精度、Memory 初始化文件的路径、Verdi 的加载方式,每一步都有坑。
这篇文章适合两类人:一类是刚接触 FPGA 验证、想从 Vivado Simulator 迁移到 VCS 的工程师;另一类是被后仿真 Memory 初始化问题折磨过、想找一套稳定流程的从业者。我会把整个流程拆开,从库编译的底层逻辑讲到 Verdi 波形调试的实操细节,尽量把每个参数为什么这么设讲清楚。
提示:本文涉及的 Vivado 和 VCS 版本组合是 Vivado 2025.1 与 VCS 2024.SP1,其他版本在库编译选项上可能有差异,但整体思路一致。
2. 联合仿真的底层逻辑:Vivado 编译库到底在编译什么
2.1 仿真库的本质是预编译的 Verilog/VHDL 模型
Xilinx 的 IP 核和原语在仿真时,并不是直接拿综合用的网表去跑,而是使用行为级或结构级的仿真模型。这些模型分散在 Vivado 安装目录下的data/verilog、data/vhdl等路径里。Vivado 自带的仿真器在启动时会自动编译这些模型,但 VCS 不认识 Vivado 的工程结构,所以需要提前把这些模型编译成 VCS 的库格式。
编译库的过程,本质上就是调用 VCS 的vlogan和vhdlan命令,把 Xilinx 的仿真源文件编译成 VCS 的库文件,存放在指定的库目录中。编译完成后,VCS 在仿真时通过-y或-v选项引用这些库,就能找到对应的模块定义。
这里有个关键点:仿真库的编译选项必须和后续仿真时的选项一致,尤其是-sverilog、-full64、-timescale这些。如果编译库时用了-sverilog,仿真时没用,或者反过来,都会导致模块找不到或者参数不匹配的错误。
2.2 为什么不用 Vivado 自动生成的仿真脚本
Vivado 在导出仿真时,会生成一个compile.sh或simulate.sh脚本,里面包含了仿真库的路径和编译选项。很多人直接拿这个脚本改一改就用,结果发现 VCS 报一堆错。原因在于,Vivado 生成的脚本默认是给 Vivado Simulator 用的,里面的库路径和编译选项并不完全适配 VCS。
比如,Vivado 生成的脚本里可能会引用xsim相关的库,而 VCS 需要的是unisims_ver、simprims_ver、secureip这些库。另外,Vivado 2025.1 的 IP 仿真模型可能依赖一些 SystemVerilog 的包,如果编译库时没有加-sverilog,这些包就无法解析。
所以,比较稳妥的做法是:用 Vivado 的compile_simlib命令手动编译库,而不是依赖自动生成的脚本。compile_simlib是 Vivado 提供的一个 Tcl 命令,专门用来为第三方仿真器编译仿真库。
2.3 compile_simlib 的关键参数拆解
compile_simlib的常用参数如下:
compile_simlib -directory <库输出目录> \ -simulator vcs \ -simulator_exec_path <VCS安装路径>/bin \ -family all \ -language all \ -library all \ -verbose-directory:指定编译后的库文件存放路径。建议放在一个独立的目录,比如/home/user/vivado_lib/vcs_2024,不要放在 Vivado 工程目录里,避免工程清理时误删。-simulator vcs:指定目标仿真器为 VCS。-simulator_exec_path:VCS 的可执行文件路径,通常是<VCS安装目录>/bin。这个路径必须正确,否则 Vivado 找不到 VCS 的编译器。-family all:编译所有器件系列的库。如果只针对特定器件,可以指定-family zynq或-family kintex7等,减少编译时间。-language all:同时编译 Verilog 和 VHDL 库。如果设计里只有 Verilog,可以只编译 Verilog,节省时间。-library all:编译所有库,包括unisims_ver、simprims_ver、secureip、xpm等。
编译时间取决于器件系列和语言数量,全系列全语言编译可能需要 30 分钟到 1 小时。如果只是跑某个具体器件的仿真,可以只编译该器件系列的库,时间会缩短到 10 分钟左右。
注意:编译库时,Vivado 会调用 VCS 的
vlogan和vhdlan,如果 VCS 的环境变量没有配置好,编译会中途失败。建议先在终端里执行which vlogan确认 VCS 命令可用。
3. 从零搭建联合仿真环境:库编译与工程配置
3.1 环境准备:VCS 和 Verdi 的环境变量
在 Linux 环境下,VCS 和 Verdi 的安装通常由 IT 或 CAD 部门完成,但环境变量需要自己配置。典型的配置如下:
export VCS_HOME=/opt/synopsys/vcs/2024.06-SP1 export VERDI_HOME=/opt/synopsys/verdi/2024.06-SP1 export PATH=$VCS_HOME/bin:$VERDI_HOME/bin:$PATH export LD_LIBRARY_PATH=$VCS_HOME/linux64/lib:$VERDI_HOME/linux64/lib:$LD_LIBRARY_PATH配置完成后,执行vcs -ID和verdi -version确认版本信息。如果vcs -ID报错,说明 License 或者安装路径有问题,需要先解决。
Vivado 2025.1 的安装路径也需要加入 PATH,方便调用vivado和compile_simlib:
export XILINX_VIVADO=/opt/Xilinx/Vivado/2025.1 export PATH=$XILINX_VIVADO/bin:$PATH3.2 用 compile_simlib 编译 VCS 库的完整命令
假设 VCS 安装在/opt/synopsys/vcs/2024.06-SP1,库输出目录为/home/user/vivado_lib/vcs_2024,编译 Zynq UltraScale+ 系列的库,命令如下:
compile_simlib -directory /home/user/vivado_lib/vcs_2024 \ -simulator vcs \ -simulator_exec_path /opt/synopsys/vcs/2024.06-SP1/bin \ -family zynqultrascaleplus \ -language verilog \ -library all \ -verbose执行后,Vivado 会输出编译日志,显示每个库的编译进度。编译完成后,库目录下会生成unisims_ver、simprims_ver、secureip、xpm等子目录,每个子目录里包含编译好的库文件和synopsys_sim.setup文件。
synopsys_sim.setup是 VCS 的库映射文件,里面定义了逻辑库名和物理路径的对应关系。在仿真时,需要通过-synopsys_sim.setup选项指定这个文件,或者在当前工作目录下创建一个同名的文件。
3.3 仿真工程目录结构的设计
一个清晰的仿真工程目录结构能省去很多路径问题。我通常这样组织:
sim_project/ ├── rtl/ # RTL 源码 ├── tb/ # 测试平台 ├── ip/ # Vivado IP 生成的仿真模型 ├── lib/ # 编译好的 Vivado 仿真库 ├── waves/ # 波形输出目录 ├── filelist.f # 仿真文件列表 ├── run_vcs.sh # VCS 编译和仿真脚本 └── synopsys_sim.setup # 库映射文件filelist.f里列出所有需要编译的 RTL 和 TB 文件,以及 IP 仿真模型文件。IP 仿真模型通常在 Vivado 工程的<project>.srcs/sources_1/ip/<ip_name>/simulation目录下,需要把这些文件路径加入filelist.f。
synopsys_sim.setup的内容示例:
unisims_ver : /home/user/vivado_lib/vcs_2024/unisims_ver simprims_ver : /home/user/vivado_lib/vcs_2024/simprims_ver secureip : /home/user/vivado_lib/vcs_2024/secureip xpm : /home/user/vivado_lib/vcs_2024/xpm这样 VCS 在编译时就能通过逻辑库名找到对应的物理路径。
3.4 VCS 编译命令的选项拆解
一个典型的 VCS 编译命令如下:
vcs -full64 -sverilog -debug_access+all \ -timescale=1ns/1ps \ -f filelist.f \ -y /home/user/vivado_lib/vcs_2024/unisims_ver \ +libext+.v+.sv \ -synopsys_sim.setup ./synopsys_sim.setup \ -l compile.log \ -o simv-full64:编译 64 位仿真器,处理大规模设计时必须加。-sverilog:支持 SystemVerilog,Xilinx 的 IP 仿真模型大量使用 SV 语法。-debug_access+all:开启调试功能,Verdi 需要这个选项才能加载波形和查看信号。-timescale=1ns/1ps:指定时间精度,必须和 RTL 中的 timescale 一致,否则时序会错乱。-f filelist.f:指定文件列表。-y:指定库搜索路径,VCS 会在这些路径下查找模块定义。+libext+.v+.sv:指定库文件的扩展名。-synopsys_sim.setup:指定库映射文件。-l compile.log:输出编译日志。-o simv:指定输出可执行文件名。
编译成功后,会生成simv可执行文件。运行./simv即可启动仿真。
提示:如果编译时报
module not found,先检查-y路径是否正确,以及synopsys_sim.setup里的库映射是否匹配。
4. 后仿真 Memory 初始化:最容易翻车的环节
4.1 后仿真为什么需要 Memory 初始化
后仿真(Post-Synthesis/Post-Implementation Simulation)使用的是综合或实现后的网表,网表里的 Memory(如 BRAM、URAM)通常没有初始值。如果设计里有一个从 Memory 读取数据的启动流程,仿真时 Memory 里全是 X,读出来的数据也是 X,导致仿真结果无意义。
解决方法是:在仿真开始时,通过$readmemh或$readmemb把初始化文件加载到 Memory 中。但在后仿真中,Memory 被映射成了 Xilinx 的原语(如RAMB36E2),这些原语没有直接的$readmemh接口,需要通过defparam或者initial块来加载。
4.2 用-g选项传递 Memory 初始化文件路径
VCS 支持通过-g选项在编译时传递参数,可以在 RTL 或网表中用$value$plusargs获取。但在后仿真中,更常见的做法是修改网表,在 Memory 原语的initial块里加入$readmemh。
不过,直接修改网表不是好习惯,因为每次综合后网表都会变。更好的做法是:在测试平台里用force或者deposit的方式,在仿真开始时把数据写入 Memory。但这种方法对大型 Memory 效率很低。
我比较推荐的做法是:在 Vivado 生成网表时,勾选“Write Memory Initialization File”选项,生成.mem文件,然后在测试平台里用$readmemh加载到对应的 Memory 信号上。具体操作是:
- 在 Vivado 的 Implementation 设置里,找到
Write Memory Initialization File选项,设置为true。 - 实现完成后,Vivado 会在
<project>.runs/impl_1/目录下生成.mem文件。 - 在测试平台里,用
$readmemh("path/to/memory.mem", dut.memory_instance.mem_array)加载。
但这里有个问题:后仿真网表里的 Memory 实例名可能和 RTL 不一样,需要先在网表里找到对应的实例路径。可以用grep在网表里搜索RAMB或URAM关键字,找到实例名。
4.3 Memory 初始化文件的格式陷阱
.mem文件的格式必须和 Memory 的位宽、深度匹配。如果 Memory 是 32 位宽、1024 深度,.mem文件里每行应该是一个 8 位十六进制数(32 位 = 8 个十六进制字符)。如果格式不对,$readmemh会报错或者加载错误的数据。
另外,.mem文件里的地址顺序也很重要。有些 Memory 原语的地址是线性的,有些是交织的(interleaved),需要根据原语的数据手册确认。如果加载后发现数据错位,大概率是地址映射搞错了。
注意:后仿真中 Memory 初始化失败,最常见的报错是
$readmemh: not enough data in file或$readmemh: too much data in file,前者说明文件行数不够,后者说明行数过多。检查.mem文件的行数和 Memory 深度是否一致。
4.4 用 Verdi 的 nWave 查看 Memory 内容
Verdi 的 nWave 支持直接查看 Memory 数组的内容。在 nWave 里,选中 Memory 信号,右键选择Memory->View Memory,可以以表格形式查看 Memory 的每个地址和对应的数据。如果发现某些地址是 X,说明初始化没有覆盖到这些地址。
另外,Verdi 的fsdbDumpvars系统任务可以指定 dump Memory 的深度。如果 Memory 很大,全 dump 会导致 FSDB 文件巨大,仿真速度变慢。可以用fsdbDumpMDA只 dump 需要的 Memory 部分。
5. Verdi 波形调试:从 FSDB 生成到信号追踪
5.1 在测试平台里加入 FSDB dump 代码
VCS 默认生成 VCD 波形,但 VCD 文件大、加载慢,Verdi 更推荐 FSDB 格式。要在仿真中生成 FSDB,需要在测试平台里加入以下代码:
initial begin $fsdbDumpfile("waves/tb.fsdb"); $fsdbDumpvars(0, tb); $fsdbDumpMDA(0, tb); end$fsdbDumpfile:指定 FSDB 文件名和路径。$fsdbDumpvars(0, tb):dumptb模块下所有层次的信号,0表示不限深度。$fsdbDumpMDA(0, tb):dump Memory 数组,后仿真中查看 Memory 内容必须加这个。
编译时,需要链接 Verdi 的 FSDB 库。VCS 的命令里加上:
vcs -full64 -sverilog -debug_access+all \ -P $VERDI_HOME/share/PLI/VCS/LINUX64/novas.tab \ $VERDI_HOME/share/PLI/VCS/LINUX64/pli.a \ ...-P选项指定 Verdi 的 PLI 接口文件,pli.a是静态库。如果 VCS 版本和 Verdi 版本不匹配,PLI 接口可能会报错,需要确认两个工具的版本兼容性。
5.2 Verdi 加载 FSDB 的两种方式
仿真运行结束后,FSDB 文件会生成在指定目录下。用 Verdi 打开有两种方式:
方式一:命令行直接打开
verdi -ssf waves/tb.fsdb &-ssf选项直接加载 FSDB 文件,Verdi 启动后会自动打开 nWave 窗口。
方式二:先启动 Verdi,再加载 FSDB
verdi &然后在 Verdi 的 nWave 窗口里,选择File->Open->FSDB,选择对应的 FSDB 文件。
两种方式效果一样,但方式一更适合脚本化流程。如果仿真和调试是分开的,比如仿真在服务器上跑,调试在本地做,可以把 FSDB 文件拷贝到本地,再用方式一打开。
5.3 用 nWave 做信号追踪和调试
Verdi 的 nWave 功能很强,但很多人只用了基础的波形查看。以下几个功能在后仿真调试中特别有用:
- Signal Search:在 nWave 里按
Ctrl+F,可以搜索信号名。后仿真网表里的信号名可能被优化过,用搜索功能可以快速定位。 - Trace Driver/Load:选中一个信号,右键选择
Trace->Driver,可以追踪这个信号的驱动源。后仿真中信号被优化时,这个功能能帮你找到实际的驱动路径。 - Event Search:在 nWave 里选择
Tools->Event Search,可以搜索特定的事件,比如某个信号从 0 变 1 的时刻。 - Memory View:前面提到的 Memory 查看功能,在 nWave 里选中 Memory 信号,右键选择
Memory->View Memory。
另外,Verdi 的Hierarchy窗口可以查看设计的层次结构。后仿真网表的层次可能和 RTL 不一样,用 Hierarchy 窗口可以快速了解网表的模块结构。
5.4 FSDB 文件过大的处理技巧
后仿真中,如果 dump 了所有信号和 Memory,FSDB 文件可能达到几十 GB,加载和查看都很慢。几个优化技巧:
- 只 dump 需要的信号:用
$fsdbDumpvars(1, tb)只 dump 一层信号,或者用$fsdbDumpvars(0, tb.u_dut)只 dump DUT 内部的信号。 - 设置 dump 起始时间:用
$fsdbDumpoff和$fsdbDumpon控制 dump 的时间窗口,只在关键时间段 dump。 - 压缩 FSDB:Verdi 支持 FSDB 压缩,在
$fsdbDumpfile里加上-compress选项,可以减小文件大小,但会增加仿真时的 CPU 开销。
提示:如果 FSDB 文件已经生成但太大,可以用 Verdi 的
fsdbedit工具裁剪,只保留需要的信号和时间段。
6. 联合仿真中那些让人抓狂的报错与解决思路
6.1module not found的排查链路
这是联合仿真中最常见的报错。排查顺序如下:
- 确认
synopsys_sim.setup里的库映射路径是否正确。可以用ls命令检查库目录下是否有对应的.v或.sv文件。 - 确认 VCS 命令里的
-y路径是否指向库目录。-y路径应该指向包含库文件的目录,而不是库文件的父目录。 - 确认
+libext选项是否包含了库文件的扩展名。Xilinx 的库文件通常是.v或.sv,如果只写了+libext+.v,SV 文件就找不到。 - 确认编译库时的选项和仿真时的选项是否一致。比如编译库时用了
-sverilog,仿真时也必须用。
如果以上都确认无误,但还是报module not found,可以在 VCS 命令里加上-debug_pp或者-v选项,查看详细的库搜索过程。
6.2timescale不一致导致的时序错乱
Vivado 的 IP 仿真模型通常有自己的timescale,如果测试平台的timescale和 IP 模型不一致,仿真时序会错乱。比如测试平台是1ns/1ps,IP 模型是1ps/1ps,仿真时 IP 内部的延迟会被放大 1000 倍。
解决方法:在 VCS 命令里用-timescale=1ns/1ps统一指定,或者在测试平台里用`timescale 1ns/1ps覆盖。但注意,-timescale选项只对没有指定timescale的模块生效,如果模块里已经指定了,以模块内的为准。
6.3 Verdi 加载 FSDB 时报PLI错误
如果 Verdi 加载 FSDB 时报PLI相关错误,通常是 VCS 和 Verdi 的版本不匹配,或者 PLI 库路径不对。检查以下几点:
- VCS 和 Verdi 的版本是否来自同一个 Synopsys 发布版本。比如 VCS 2024.06-SP1 应该搭配 Verdi 2024.06-SP1。
-P选项指定的novas.tab和pli.a路径是否正确。路径通常在$VERDI_HOME/share/PLI/VCS/LINUX64/下。LD_LIBRARY_PATH是否包含了 Verdi 的库路径。
如果版本确实不匹配,可以尝试用-P指定其他版本的 PLI 库,但兼容性无法保证。最好的办法还是统一版本。
6.4 后仿真中 Memory 读出 X 的排查
后仿真中 Memory 读出 X,原因可能有:
- Memory 初始化文件没有加载成功。检查
$readmemh的路径是否正确,文件是否存在。 - Memory 实例路径不对。后仿真网表里的实例名可能和 RTL 不一样,需要在网表里搜索确认。
- Memory 的地址映射不对。有些 Memory 原语的地址是交织的,需要根据数据手册调整。
- 初始化文件格式不对。检查文件的行数和每行的数据宽度是否匹配。
排查时,可以在测试平台里加入$display语句,打印 Memory 的初始值,确认初始化是否成功。
6.5 仿真速度慢的优化方向
联合仿真速度慢,除了设计本身规模大之外,还有几个优化方向:
- 减少 dump 的信号数量:只 dump 关键信号,避免全量 dump。
- 使用
-debug_access+all的替代选项:如果不需要 Verdi 的调试功能,可以用-debug_access+pp或者-debug_access,减少调试信息的生成。 - 关闭不必要的库编译:只编译设计用到的器件系列和语言,减少库文件数量。
- 使用 VCS 的增量编译:如果只修改了少量文件,可以用
-Mupdate选项做增量编译,避免全量重新编译。
7. 一些让流程更顺手的个人经验
联合仿真这套流程,我踩过的坑主要集中在库编译和 Memory 初始化上。库编译最怕的是 VCS 环境变量没配好,compile_simlib跑到一半报错,日志里只显示vlogan: command not found。所以每次编译库之前,我都会先在终端里执行which vlogan和which vhdlan,确认命令可用。
Memory 初始化这块,我现在的习惯是:在 Vivado 里生成.mem文件后,先用 Python 脚本检查一下文件的行数和数据宽度,确认和 Memory 的配置匹配,再放到仿真目录里。这个检查脚本很简单,就是读文件、统计行数、检查每行的字符数,但能省去很多仿真跑了一半才发现数据不对的时间。
Verdi 的 FSDB dump 代码,我通常放在测试平台的initial块里,但会加一个ifdef开关,方便在不需要波形时关闭 dump。比如:
`ifdef DUMP_FSDB initial begin $fsdbDumpfile("waves/tb.fsdb"); $fsdbDumpvars(0, tb); $fsdbDumpMDA(0, tb); end `endif编译时用+define+DUMP_FSDB开启,不定义就关闭。这样同一套测试平台可以用于快速回归和详细调试两种场景。
最后说一个 Verdi 的小技巧:在 nWave 里,按Shift+鼠标滚轮可以横向缩放波形,按Ctrl+鼠标滚轮可以纵向缩放。后仿真波形通常很长,用这两个快捷键可以快速定位到感兴趣的区间。另外,Verdi 的Marker功能可以标记多个时间点,方便对比不同时刻的信号状态。