4、VCS仿真执行:vcs与simv的关系,运行仿真,仿真波形生成(VPD/VCD),仿真时间控制。

好,咱们接着聊。前面我们把编译环境搭好了,也把UVM验证平台的结构理清了。现在,终于到了真正跑仿真的环节。

说实话,很多刚入行的朋友,包括我当年,都觉得这一步很简单——不就是敲个命令吗?但实际项目中,仿真执行这块坑特别多。你想想看,一个大型SoC项目,仿真动辄跑几天,如果因为命令用错了、波形没抓到、时间没控制好,导致重跑,那真是欲哭无泪。

所以这一章,我重点讲讲vcs和simv的关系、怎么跑仿真、怎么抓波形、怎么控制仿真时间。这些都是基本功,但也是决定你工作效率的关键。

4.1 vcs与simv:编译与运行的两步走

先搞清楚一个核心概念:vcs是编译器,simv是仿真器

vcs负责把你的RTL代码、验证环境、UVM库等,全部编译成一个可执行的二进制文件。这个文件默认就叫simv。然后你运行simv,才是真正的仿真执行。

为什么这么设计?说白了,就是为了效率。你想想看,如果每次改一点点代码都要重新编译整个环境,那得多慢。所以VCS把编译和运行分开了:

  • vcs编译阶段:把Verilog/VHDL、SystemVerilog、UVM库、DPI-C等,全部链接成一个可执行文件。这个过程比较耗时,但只需要做一次。
  • simv运行阶段:直接执行编译好的simv文件,加载测试用例,跑仿真。这个过程快得多,而且可以反复跑不同的测试用例,不需要重新编译。

我在项目中遇到过一种情况:有个同事每次改完激励都要重新跑vcs编译,一等就是半小时。我告诉他,其实你只需要编译一次,后面改testbench或者测试用例,直接跑simv就行。他当时就愣住了——原来之前白等了那么多次。

核心命令示例:

# 第一步:编译
vcs -sverilog -debug_access+all -l vcs.log \
    -f filelist.f \
    -top top_tb

# 第二步:运行
./simv -l simv.log +UVM_TESTNAME=my_test

这里有个细节:-debug_access+all这个选项,我建议你加上。它允许你在仿真过程中查看内部信号、抓波形、甚至做调试。如果不加,simv跑起来会快一点,但出了问题你根本没法查。嗯,这个坑我踩过。

4.2 运行仿真:从命令行到批处理

跑仿真看起来简单,就是敲./simv。但实际项目中,我们通常不会这么裸跑。因为你需要传递各种参数、控制随机种子、指定测试用例名等等。

我个人习惯写一个简单的shell脚本,比如run.sh

#!/bin/bash
# 编译
vcs -sverilog -debug_access+all -l vcs.log \
    -f filelist.f \
    -top top_tb

# 运行
./simv -l simv.log \
    +UVM_TESTNAME=$1 \
    +ntb_random_seed=$2 \
    +UVM_VERBOSITY=UVM_MEDIUM

然后调用的时候:./run.sh my_test 12345。这样既方便,又不容易出错。

你可能会问:为什么要指定随机种子?因为UVM的随机化是基于种子的。同一个种子,跑出来的随机序列是一样的。如果仿真出了问题,你可以用同一个种子复现,方便调试。这个技巧,我在定位一个随机化导致的死锁问题时,帮了大忙。

小技巧: 如果你想让每次跑的种子都不一样,可以用+ntb_random_seed=$RANDOM,或者用时间戳作为种子。

4.3 仿真波形生成:VPD vs VCD

仿真跑完了,怎么知道结果对不对?看波形。波形是验证工程师的眼睛。

VCS支持两种主要的波形格式:VCDVPD

特性 VCD VPD
全称 Value Change Dump VCD Plus Dump
文件大小 很大(文本格式) 较小(二进制格式)
生成速度
兼容性 所有EDA工具都支持 主要VCS/DVE/Verdi
调试功能 基本信号查看 支持UVM事务级调试

我个人强烈推荐使用VPD。为什么?

首先,VPD是二进制格式,文件大小只有VCD的十分之一甚至更小。你想想看,一个大型SoC仿真,如果生成VCD,动辄几十GB甚至上百GB,磁盘根本扛不住。而VPD就友好得多。

其次,VPD支持UVM的事务级调试。你可以看到sequence、driver、monitor之间的交互,而不仅仅是信号跳变。这对于定位协议级问题,简直不要太方便。

生成VPD波形的方法很简单:

// 在testbench中
initial begin
    $vcdpluson();  // 开启VPD波形
    $vcdplusmemon(); // 记录UVM事务
end

// 或者通过命令行
./simv +vcdpluson=1 +vcdplusmemon=1

我曾经遇到过一个bug:仿真跑了一整天,结果发现波形没抓到。原因是我忘了在testbench里加$vcdpluson()。从那以后,我都在编译脚本里默认加上-debug_access+all,并且在run脚本里默认开启波形。宁可多占点磁盘,也不能白跑。

注意: VPD文件虽然小,但如果仿真时间很长、信号很多,它也会变得很大。建议只dump你关心的模块的信号,用$vcdpluson(0, dut.u_core)这样的方式指定范围。

4.4 仿真时间控制:跑多久?怎么停?

仿真时间控制,说白了就是两个问题:跑多久?怎么停?

跑多久,由你的测试用例决定。UVM中,我们通常用#timeout或者set_timeout来设置最大仿真时间。比如:

class my_test extends uvm_test;
    function void build_phase(uvm_phase phase);
        set_timeout(1ms); // 如果1ms内没结束,就超时退出
    endfunction
endclass

怎么停?UVM提供了多种方式:

  • 自然结束:所有sequence都跑完了,所有objection都被撤销了,仿真自动结束。
  • 强制结束:用$finish或者uvm_phase::drop_objection
  • 超时结束:超过设定的时间,仿真强制退出。

这里有个常见的坑:objection管理不当导致仿真不结束。比如你创建了一个sequence,但忘了raise_objection,仿真会瞬间结束,啥也没测到。或者你raise了但没drop,仿真永远跑不完。

我记得有一次,仿真跑了整整一个周末,回来发现还在跑。一查,原来是某个monitor里raise了objection,但条件判断写错了,一直没drop。从那以后,我养成了一个习惯:在每个组件的final_phase里打印一下objection计数,确保都归零了。

命令行控制仿真时间:

# 设置最大仿真时间
./simv +UVM_TIMEOUT=100us

# 设置仿真停止时间
./simv +vcs+stop+100us

# 设置仿真运行时间
./simv +vcs+finish+200us

这些命令行选项,在调试阶段特别有用。比如你怀疑某个bug出现在100us之后,你可以先让仿真跑到100us停下来,看看波形,再决定要不要继续跑。

4.5 实战经验总结

好了,这一章的内容差不多就这些。最后我总结几个实战中容易忽略的点:

  1. 编译和运行分开:不要每次改激励都重新编译,浪费时间。
  2. 默认开启波形:宁可多占磁盘,不能白跑仿真。
  3. 用VPD别用VCD:除非你有特殊需求,否则VPD是更好的选择。
  4. 管理好objection:这是UVM仿真结束的关键,出问题先查它。
  5. 善用超时机制:防止仿真卡死,浪费计算资源。

下一章,我们会深入讲讲仿真调试与覆盖率收集。到时候我会分享一些我调试复杂bug的实战案例,保证让你有收获。