2. 搭建你的第一个UVM环境:从零开始搭建一个简单的UVM验证平台,理解uvm_component与uvm_object

好,咱们直接动手。这一章的目标很明确——从零开始,搭一个能跑起来的UVM验证平台。别怕,它很简单,但能帮你搞清楚两个最核心的概念:uvm_componentuvm_object

我记得刚学UVM那会儿,看了好多理论,什么树形结构、phase机制,头都大了。后来我发现,与其死磕概念,不如先写一个最小系统。跑通了,自然就懂了。你想想看,是不是这个理?

2.1 先搞清楚:component 和 object 到底有啥区别?

这是UVM里第一个绕不开的坎。说白了,uvm_componentuvm_object 都是UVM世界里的“基类”,但它们的定位完全不同。

特性 uvm_component uvm_object
生命周期 贯穿整个仿真,有phase机制 临时创建,用完即销毁
树形结构 有parent,构成层次化树 没有parent,独立存在
典型代表 driver, monitor, agent, env transaction, sequence_item, config
创建方式 使用 type_id::create() 并指定parent 使用 type_id::create()new()

核心记忆点: 有“生命”的、需要被组织起来的,用component。临时传递数据的“包裹”,用object。

我在项目中遇到过一位同事,把transaction写成了component,结果仿真器里生成了无数个永远不会被销毁的实例,内存直接爆了。嗯,这就是没分清两者的代价。

2.2 搭建最小验证平台:一个driver就够了

咱们的目标是:写一个driver,它能从sequencer拿到transaction,然后驱动到DUT的接口上。为了简化,咱们先不连DUT,只让driver打印出它收到的数据。

先定义一个transaction,它是个object:

class my_transaction extends uvm_sequence_item;
    rand bit [7:0] addr;
    rand bit [7:0] data;
    rand bit       wr_en;

    `uvm_object_utils_begin(my_transaction)
        `uvm_field_int(addr, UVM_ALL_ON)
        `uvm_field_int(data, UVM_ALL_ON)
        `uvm_field_int(wr_en, UVM_ALL_ON)
    `uvm_object_utils_end

    function new(string name = "my_transaction");
        super.new(name);
    endfunction
endclass

注意看,这里用的是 uvm_sequence_item,它继承自 uvm_transaction,而 uvm_transaction 又继承自 uvm_object。所以它本质上还是个object。

接下来,写driver。driver是个component:

class my_driver extends uvm_driver #(my_transaction);
    `uvm_component_utils(my_driver)

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

    virtual task run_phase(uvm_phase phase);
        my_transaction tr;
        forever begin
            seq_item_port.get_next_item(tr);
            `uvm_info("DRV", $sformatf("Got transaction: addr=%0d, data=%0d, wr=%0d",
                       tr.addr, tr.data, tr.wr_en), UVM_LOW)
            seq_item_port.item_done();
        end
    endtask
endclass

这里有个细节:run_phase 是UVM自动调用的。你不需要手动去启动它。这就是component的phase机制在起作用。

个人习惯: 我写driver时,喜欢在 run_phase 开头加一句 phase.raise_objection(),在结尾加 phase.drop_objection()。虽然这个例子没用到,但养成习惯后,以后做复杂场景时不容易漏掉。

2.3 把它们组装起来:写一个简单的test

有了driver和transaction,咱们还需要一个test来把它们串起来。test本身也是个component:

class my_test extends uvm_test;
    `uvm_component_utils(my_test)

    my_driver drv;

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

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        drv = my_driver::type_id::create("drv", this);
    endfunction

    task run_phase(uvm_phase phase);
        my_transaction tr;
        phase.raise_objection(this);
        // 手动创建一个transaction,发给driver
        tr = my_transaction::type_id::create("tr");
        tr.randomize();
        drv.seq_item_port.put(tr);
        #100;
        phase.drop_objection(this);
    endtask
endclass

注意 build_phase 里的 create 函数。第二个参数 this 就是parent。这行代码执行后,UVM会自动把 drv 挂到 my_test 下面,形成树形结构。

最后,在顶层模块里启动UVM:

module top;
    initial begin
        run_test("my_test");
    end
endmodule

就这么简单。跑一下仿真,你会看到driver打印出类似这样的信息:

UVM_INFO @ 0: drv [DRV] Got transaction: addr=123, data=45, wr=1

2.4 避坑指南:新手最容易犯的三个错误

  • 忘记注册宏: 不管是component还是object,都必须用 `uvm_component_utils`uvm_object_utils 注册。我曾经有一次漏掉了,结果仿真器报了一堆奇怪的错误,查了半天才发现是宏没写。
  • component的new函数参数写错: component的new函数必须有两个参数:string nameuvm_component parent。少一个都不行。object的new函数只需要一个 string name
  • 在component里手动new另一个component: 永远不要用 new() 去创建component。必须用 type_id::create()。否则UVM的phase机制和树形结构都不会生效。

我曾经... 在刚转UVM时,为了图省事,直接在driver的new函数里用 new() 创建了一个monitor。结果那个monitor的 build_phase 根本没执行,数据全丢了。从那以后,我养成了习惯:只要是component,一律用 create

2.5 小结:你学到了什么?

这一章,咱们从零开始搭了一个能跑的UVM环境。虽然它很简陋,但你已经亲手实践了两个最核心的概念:

  • uvm_object:用来描述数据,比如transaction。它没有生命周期管理,用完就扔。
  • uvm_component:用来描述验证组件,比如driver。它有phase机制,有parent-child关系,是UVM树形结构的节点。

说白了,object是“数据”,component是“处理数据的模块”。这个区分搞清楚了,后面学agent、env、scoreboard这些,就会顺畅很多。

下一章,咱们会引入sequencer和monitor,让这个平台真正具备“激励生成”和“数据监测”的能力。到时候,你就能看到一个更完整的UVM验证环境了。