3. 测试用例创建:如何创建一个基本的uvm_test、注册到factory、设置仿真超时

好,咱们今天聊聊测试用例的创建。说白了,就是怎么在UVM里写一个最基础的test,然后把它注册到工厂里,再顺手把仿真超时给设了。

我记得刚接触UVM那会儿,总觉得test是个很神秘的东西。后来做多了才发现,它其实就是整个验证环境的“总指挥”。你想想看,所有的sequence、driver、monitor,最后都得靠test来调度。

3.1 创建一个基本的uvm_test

创建一个test,其实就两步:继承uvm_test,然后实现run_phase。我习惯把test写得尽量简洁,只做“调度”的工作,具体的激励生成交给sequence去做。

来看一个最简单的例子:

class my_test extends uvm_test;

    // 组件声明
    my_env env;

    // 标准构造函数
    function new(string name = "my_test", uvm_component parent = null);
        super.new(name, parent);
    endfunction

    // build_phase:创建子组件
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        env = my_env::type_id::create("env", this);
    endfunction

    // run_phase:启动测试
    task run_phase(uvm_phase phase);
        my_sequence seq;
        phase.raise_objection(this);
        seq = my_sequence::type_id::create("seq");
        seq.start(env.agent.sequencer);
        #100ns;
        phase.drop_objection(this);
    endtask

endclass

嗯,这里要注意几个点。第一,构造函数里的name参数,我建议给它一个默认值,这样在创建实例时如果不传参也能跑。第二,build_phase里创建env时,一定要用type_id::create,别直接用new。为什么?因为这样才能享受到工厂的覆盖机制。

我的小习惯: 我一般会在test的build_phase里把整个环境的配置都设好。比如interface的配置、是否启用覆盖率收集、打印等级等等。这样test就像一个“总配置文件”,一目了然。

3.2 注册到factory

注册到工厂,说白了就是告诉UVM:“嘿,我这个类存在,你可以用名字来创建它。” 这一步太重要了,不注册的话,后面想用override替换组件?门儿都没有。

注册的方式很简单,就一行宏:

class my_test extends uvm_test;
    `uvm_component_utils(my_test)
    // ... 其余代码
endclass

这个`uvm_component_utils宏,会自动帮你生成type_id相关的代码。我见过有人手写这些注册代码,说实话,没必要,还容易出错。

注册完之后,你就可以在仿真命令里指定要跑哪个test了:

// 命令行方式
+UVM_TESTNAME=my_test

// 或者用工厂的set_inst_override
set_inst_override("my_test", "your_test");
我曾经踩过的坑: 有一次我写了一个test,忘了加`uvm_component_utils宏。结果仿真器报错说找不到这个test。我查了半天,最后发现就是少了一行宏。从那以后,我每写一个component,第一件事就是加上这个宏,养成肌肉记忆。

3.3 设置仿真超时

仿真超时,说白了就是给仿真加一个“闹钟”。万一你的test跑飞了,或者死循环了,超时机制能帮你自动结束仿真,不至于让服务器一直空转。

UVM里设置超时有两种常用方式:

方式 代码示例 说明
全局超时 set_global_timeout(1ms) 整个仿真最多跑1ms
phase超时 phase.phase_done.set_timeout(100us) 当前phase最多跑100us

我个人习惯在test的build_phase里设置全局超时:

function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // 设置全局超时:5ms
    set_global_timeout(5ms);
    env = my_env::type_id::create("env", this);
endfunction

为什么设5ms?因为我的设计在正常仿真下,最长的testcase也就跑3ms左右。留点余量,但也不能太大,否则死循环了要等很久才能超时。

避坑指南: 设置超时的时候,注意单位。UVM默认的时间单位是仿真器的最小时间精度。如果你设了set_global_timeout(100),它可能只超时了100个时间单位,而不是100ns。我建议显式写上单位,比如1ms、100us,这样代码更清晰。

3.4 知识体系总览

为了让你更直观地理解test创建的整体流程,我画了一张图:

UVM Test创建核心流程 1. 继承uvm_test class my_test extends uvm_test 2. 注册到factory `uvm_component_utils(my_test) 3. 设置超时 set_global_timeout() build_phase:创建env等组件 run_phase:启动sequence 最终效果 一个可被UVM调度的、带超时保护的测试用例 可通过 +UVM_TESTNAME 命令行指定运行

这张图把整个流程串起来了。从继承开始,到注册,再到设置超时,最后在build_phase和run_phase里做具体的事情。每一步都很关键,缺一不可。

3.5 完整示例

最后,给你一个完整的test模板,可以直接拿来用:

class base_test extends uvm_test;
    `uvm_component_utils(base_test)

    my_env env;

    function new(string name = "base_test", uvm_component parent = null);
        super.new(name, parent);
    endfunction

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        // 设置超时
        set_global_timeout(10ms);
        // 创建环境
        env = my_env::type_id::create("env", this);
    endfunction

    task run_phase(uvm_phase phase);
        phase.raise_objection(this);
        // 启动默认sequence
        run_default_sequence();
        phase.drop_objection(this);
    endtask

    virtual task run_default_sequence();
        // 子类重写此方法
    endtask

endclass

这个模板里,我把run_default_sequence声明成了virtual task。这样,不同的testcase只需要继承base_test,然后重写这个task就行了。代码复用性一下子就上来了。

我的建议: 每个项目都建一个base_test,把公共的东西放进去。比如超时设置、公共配置、日志控制等。这样后续写具体testcase时,只需要关注测试逻辑本身,省心不少。

好了,关于test的创建、注册和超时设置,就聊到这儿。记住这三个核心点:继承、注册、设超时。一个都不能少。