3、仿真调试基础:VCS/Verilog仿真器使用、波形查看技巧、断言(Assertion)基础

3.1 VCS仿真器:从编译到运行

做数字验证,VCS 是绕不开的工具。我个人习惯把仿真流程拆成三步:编译、运行、调试。

编译阶段,说白了就是把你的 RTL 代码和 testbench 翻译成可执行的二进制文件。我常用的命令长这样:

vcs -full64 -sverilog +v2k -debug_access+all \
    -f filelist.f -l compile.log \
    -timescale=1ns/1ps \
    -o simv

这里有几个关键点:

  • -debug_access+all:打开全部调试能力,波形 dump 全靠它
  • -sverilog:支持 SystemVerilog 语法,断言和覆盖率都靠它
  • -timescale:定义时间精度,我建议统一用 1ns/1ps

编译通过后,运行就简单了:

./simv -l run.log +UVM_TESTNAME=my_test

嗯,这里要注意:+UVM_TESTNAME 是 UVM 的标配,如果你用纯 Verilog 做验证,直接跑 ./simv 就行。

我的小技巧: 编译时加 -j8 可以并行编译,大型设计能省一半时间。我曾经有个项目,编译一次要 40 分钟,加了并行后缩到 12 分钟。

3.2 波形查看:别只会看波形

波形是验证工程师的眼睛。但很多人只会拖拽信号看高低电平,这远远不够。

我常用的波形查看流程:

  1. 先看顶层接口信号(时钟、复位、握手信号)
  2. 再看关键状态机跳转
  3. 最后深入数据通路

在 VCS 里 dump 波形,我推荐用 VPD 格式:

initial begin
    $vcdpluson(0, tb_top);
    $vcdplusmemon;
end

或者用更现代的 FSDB 格式(配合 Verdi 使用):

initial begin
    $fsdbDumpfile("wave.fsdb");
    $fsdbDumpvars(0, tb_top);
    $fsdbDumpMDA();  // dump 多维数组
end
避坑指南: 我曾经 debug 一个数据错位问题,看了三天波形没找到原因。后来发现是没 dump 内部数组信号。加上 $fsdbDumpMDA() 后,十分钟就定位到了问题。

波形查看的几个实用技巧:

  • 设置标记点: 在关键时间点按 M 键标记,方便来回跳转
  • 信号分组: 把地址、数据、控制信号分不同颜色组
  • 使用公式: 在 Verdi 里可以直接写表达式,比如 data_in === data_out 来快速比对
  • 查找上升沿: 用 @(posedge clk) 快速定位时钟沿

3.3 断言(Assertion)基础:让仿真自己发现问题

断言是什么?说白了就是给仿真器装上一双眼睛,让它自己盯着关键行为。出错了立刻报,不用你一行行看波形。

断言分两种:

  • 立即断言: 像 if 语句一样,在仿真时刻立即检查
  • 并发断言: 基于时钟沿,检查跨周期的时序关系

先看一个简单的立即断言:

always_comb begin
    assert (data_valid && data_ready) else
        $error("握手信号同时为高时出错");
end

再看并发断言,这才是重头戏:

property p_req_ack;
    @(posedge clk)
    req |=> ##[1:3] ack;
endproperty

assert property (p_req_ack)
    else $fatal("请求后 1-3 个周期内必须有应答");

这里 |=> 表示「下一个时钟沿」,##[1:3] 表示 1 到 3 个周期内。你想想看,这种跨周期的检查,用手动看波形得累死。

注意: 断言不是万能的。我曾经在一个项目里写了 200 多条断言,结果仿真跑得巨慢。后来发现是某个断言里用了 $past() 函数,每次时钟沿都要回溯 100 个周期。优化后速度提升了 3 倍。

3.4 实战:断言 + 波形联动调试

光写断言不够,你得知道断言失败时怎么快速定位。我的做法是三步走:

  1. 看断言失败信息: 仿真日志里会打印失败时间、信号值
  2. 打开波形定位: 在 Verdi 里按 F 键直接跳转到失败时间点
  3. 回溯原因: 从失败点往前推,看是什么导致了违规

举个例子,假设断言报 req_ack 失败:

# 仿真日志输出
"test.sv":45: (vlog-2110) Assertion failed: p_req_ack
    Time: 1200 ns, Scope: tb_top.u_dut
    Values: req=1, ack=0

看到这个,我直接打开波形,定位到 1200ns。发现 req 在 1100ns 拉高,但 ack 一直没反应。再往前看,发现复位信号在 1050ns 被拉低了——原来是复位期间发了请求,逻辑没处理。

我的经验: 断言失败后,别急着改代码。先问三个问题:
1. 是设计 bug 还是断言写错了?
2. 这个场景在 spec 里有没有定义?
3. 其他模块有没有类似问题?
我曾经因为一个断言失败,顺藤摸瓜找到了三个关联模块的 bug。

3.5 本章知识体系

下面这张图总结了仿真调试的核心逻辑,从编译到波形再到断言,环环相扣:

仿真调试核心流程 VCS 编译 vcs -sverilog -debug 波形查看 VPD / FSDB 格式 断言检查 assert property 调试反馈循环 编译选项配置 -debug_access+all 波形信号分组 颜色标记 + 公式 断言失败定位 时间 + 信号值 核心思想:让工具替你发现问题 编译通过 → 波形确认 → 断言自动检查 → 快速迭代

这张图展示了仿真调试的完整闭环。你从编译开始,跑出波形后用断言自动检查,发现问题再回到编译阶段修改。循环越快,验证效率越高。

最后说一句: 仿真调试不是看谁波形看得快,而是看谁能用工具把问题自动揪出来。断言就是你的得力助手,别嫌麻烦,多写几条,后面 debug 能省十倍时间。

可可精品资料库