3、第一个虚拟验证项目:创建项目目录、编写第一个虚拟测试用例、运行并查看结果

好,咱们现在开始动手。

前面聊了那么多概念,说实话,不亲手跑一个用例,那些东西都是飘着的。我个人习惯是,学任何新工具,先让它「动起来」。哪怕只是点亮一个LED,或者打印一行Hello World,都比看十页文档管用。

这一章,我们就来创建你的第一个虚拟验证项目。我会带着你,从零开始搭目录、写用例、跑起来,最后看看结果长什么样。

3.1 项目目录结构:别小看这件事

很多新手上来就写代码,写到一半发现文件乱成一锅粥。我在项目中遇到过好几次,因为目录结构混乱,导致仿真脚本找不到文件,白白浪费半天时间。

所以,第一步先把「家」安好。

我推荐一个比较通用的目录结构,你直接照着建就行:

my_first_virt_project/
├── spec/               # 规格文档,放需求文档、接口定义
├── testbench/          # 测试平台,放顶层连接、时钟复位
│   ├── top.sv
│   └── clk_rst_gen.sv
├── tests/              # 测试用例,每个用例一个文件
│   ├── test_smoke.sv
│   └── test_reg_access.sv
├── vip/                # 虚拟接口、验证IP
│   ├── axi_vip.sv
│   └── i2c_vip.sv
├── scripts/            # 仿真脚本、编译脚本
│   ├── run.f
│   └── compile.tcl
├── logs/               # 仿真日志、波形文件
│   └── (自动生成)
└── Makefile            # 一键编译运行

嗯,这里要注意:tests/vip/ 这两个目录,是虚拟验证项目的核心。测试用例放在 tests/ 里,虚拟接口和驱动放在 vip/ 里。这样分工明确,后期维护起来特别清爽。

我的小技巧: 我习惯在项目根目录放一个 README.md,写上项目用途、依赖工具、运行命令。三个月后你自己回来看,绝对感谢当时的自己。

3.2 编写第一个虚拟测试用例:从「冒烟测试」开始

第一个用例,我建议你写一个「冒烟测试」(Smoke Test)。说白了,就是验证整个验证环境能不能正常启动,时钟有没有、复位有没有、基本通路通不通。

你想想看,如果连冒烟都过不了,后面那些复杂用例根本跑不起来。

下面是一个最简单的虚拟测试用例,用SystemVerilog写的:

// 文件: tests/test_smoke.sv
class test_smoke extends base_test;

    `uvm_component_utils(test_smoke)

    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction

    task run_phase(uvm_phase phase);
        phase.raise_objection(this);

        `uvm_info(get_type_name(), "=== 冒烟测试开始 ===", UVM_LOW)

        // 等待复位释放
        @(posedge vif.rst_n);
        `uvm_info(get_type_name(), "复位已释放,系统启动", UVM_LOW)

        // 等待几个时钟周期,观察基本行为
        repeat (10) @(posedge vif.clk);
        `uvm_info(get_type_name(), "时钟运行正常,已观测10个周期", UVM_LOW)

        // 做一个简单的寄存器读写
        vif.write_reg(0x00, 32'hA5A5_A5A5);
        vif.read_reg(0x00, data);
        if (data == 32'hA5A5_A5A5)
            `uvm_info(get_type_name(), "寄存器读写测试通过", UVM_LOW)
        else
            `uvm_error(get_type_name(), "寄存器读写不匹配!")

        `uvm_info(get_type_name(), "=== 冒烟测试通过 ===", UVM_LOW)
        phase.drop_objection(this);
    endtask

endclass

这段代码干了三件事:

  1. 等待复位释放——确保系统从复位状态正常出来。
  2. 观测时钟——确认时钟在跑,没有卡住。
  3. 读写寄存器——验证最基础的数据通路。
核心要点: 第一个用例不要贪多。能跑通、能看到打印信息,就是胜利。我曾经见过有人第一个用例就写了几百行,结果编译都过不了,心态直接崩了。

3.3 运行并查看结果:让代码真正跑起来

代码写好了,怎么跑?

我一般用Makefile来管理。你可以在项目根目录下放一个简单的Makefile:

# Makefile
SIM_TOOL = vcs
TEST_NAME = test_smoke

run:
    $(SIM_TOOL) -f scripts/run.f +UVM_TESTNAME=$(TEST_NAME) -l logs/run.log

clean:
    rm -rf logs/ *.log *.vcd

然后在终端里敲:

make run TEST_NAME=test_smoke

仿真开始后,你会看到屏幕上刷刷地滚日志。别慌,这是正常的。

跑完之后,去 logs/run.log 里看结果。你应该能看到类似这样的输出:

UVM_INFO @ 0ns: [test_smoke] === 冒烟测试开始 ===
UVM_INFO @ 100ns: [test_smoke] 复位已释放,系统启动
UVM_INFO @ 200ns: [test_smoke] 时钟运行正常,已观测10个周期
UVM_INFO @ 300ns: [test_smoke] 寄存器读写测试通过
UVM_INFO @ 400ns: [test_smoke] === 冒烟测试通过 ===
---
UVM Report: 0 errors, 0 warnings

看到 0 errors 了吗?这就是你想要的。

注意: 如果日志里出现了 UVM_FATAL 或者 UVM_ERROR,别急着改代码。先看错误信息指向哪个文件、哪一行。我曾经有一次折腾了两小时,最后发现是文件名拼错了——大小写没对上。

3.4 避坑指南:新手最容易踩的三个坑

表现 解决办法
路径不对 仿真报错:文件找不到 检查 run.f 里的路径,用相对路径
UVM版本不匹配 编译报一堆语法错误 确认工具链支持UVM 1.2还是1.1d
忘记加 +UVM_TESTNAME 仿真跑起来了,但没执行你的用例 运行命令里一定要指定测试名

嗯,这三个坑我全踩过。尤其是第三个,我第一次跑虚拟验证时,用例没执行,我还以为环境坏了,折腾了半天才发现是参数没传进去。

3.5 小结:你已经迈出了第一步

到这里,你的第一个虚拟验证项目已经跑通了。你创建了目录、写了测试用例、运行了仿真、查看了结果。

说白了,虚拟验证的核心流程就是这三步:搭环境 → 写用例 → 看结果。后面所有的复杂场景,都是在这个基础上叠加的。

下一章,我们会深入虚拟接口的配置,让用例变得更灵活。但今天,先给自己鼓个掌——你已经不是纯新人了。

额外建议: 跑通之后,试着改一下寄存器的期望值,让它故意报错。看看 UVM_ERROR 长什么样。这样以后真遇到错误,你心里就有底了。