一、搭建第一个UVM环境:从零开始

说实话,我第一次接触UVM的时候,看着那一堆类名——uvm_component、uvm_sequence、uvm_driver——头都大了。但后来我发现,只要理解了UVM的"骨架",搭建环境其实就是往骨架上填肉。

这一章,我们就来搭建第一个完整的UVM环境。我会带着你,从DUT开始,一步步把Transaction、Sequence、Driver、Monitor、Agent、Env、Test这些组件串起来。

1.1 DUT简介:我们要验证什么?

先说说DUT(Design Under Test)。说白了,就是被验证的设计。我习惯用一个简单的FIFO作为入门例子,因为它逻辑清晰,接口也典型。

DUT功能描述:

  • 同步FIFO,深度16
  • 数据位宽8bit
  • 支持写使能、读使能
  • 输出空标志、满标志

嗯,这里要注意:DUT的接口定义直接决定了你的验证环境怎么搭。我在项目中见过有人把DUT接口搞错了,结果整个验证环境重写了一遍。所以,先花10分钟把DUT的接口搞清楚,绝对值得。

1.2 Transaction和Sequence:数据长什么样?

Transaction,就是你要发给DUT的数据包。在UVM里,它继承自uvm_sequence_item

class fifo_trans extends uvm_sequence_item;
    rand bit [7:0] data_in;
    rand bit       wr_en;
    rand bit       rd_en;
    
    `uvm_object_utils_begin(fifo_trans)
        `uvm_field_int(data_in, UVM_ALL_ON)
        `uvm_field_int(wr_en,   UVM_ALL_ON)
        `uvm_field_int(rd_en,   UVM_ALL_ON)
    `uvm_object_utils_end
    
    function new(string name = "fifo_trans");
        super.new(name);
    endfunction
endclass

Sequence呢?它负责生成一串Transaction。我习惯把Sequence想象成"剧本",它告诉Driver:接下来该发什么数据了。

class fifo_sequence extends uvm_sequence #(fifo_trans);
    `uvm_object_utils(fifo_sequence)
    
    function new(string name = "fifo_sequence");
        super.new(name);
    endfunction
    
    virtual task body();
        fifo_trans tr;
        repeat(10) begin
            tr = fifo_trans::type_id::create("tr");
            start_item(tr);
            // 随机化,生成数据
            assert(tr.randomize());
            finish_item(tr);
        end
    endtask
endclass

你想想看,Sequence和Transaction的关系,就像工厂和产品。Sequence是流水线,Transaction是流水线上一个个的零件。

1.3 Driver和Monitor:数据的搬运工和观察者

Driver,负责把Transaction里的数据,按照接口时序,驱动到DUT的管脚上。Monitor,负责从DUT的管脚上采集数据,打包成Transaction。

我曾经在一个项目中,Driver和Monitor的时序写反了,结果仿真波形看起来都对,但Scoreboard就是报错。查了两天才发现,原来是Monitor采样点差了半个时钟周期。

class fifo_driver extends uvm_driver #(fifo_trans);
    `uvm_component_utils(fifo_driver)
    
    virtual fifo_if vif;
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    virtual task run_phase(uvm_phase phase);
        forever begin
            seq_item_port.get_next_item(req);
            drive_item(req);
            seq_item_port.item_done();
        end
    endtask
    
    virtual task drive_item(fifo_trans tr);
        @(posedge vif.clk);
        vif.wr_en <= tr.wr_en;
        vif.rd_en <= tr.rd_en;
        vif.data_in <= tr.data_in;
    endtask
endclass

个人经验:Driver里一定要加时序检查。我习惯在drive_item里加一个assert,确保驱动时序和DUT的spec一致。否则,时序错了,验证环境跑得再欢也是白搭。

Monitor的代码和Driver有点像,但方向相反:

class fifo_monitor extends uvm_monitor;
    `uvm_component_utils(fifo_monitor)
    
    virtual fifo_if vif;
    uvm_analysis_port #(fifo_trans) mon_ap;
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
        mon_ap = new("mon_ap", this);
    endfunction
    
    virtual task run_phase(uvm_phase phase);
        fifo_trans tr;
        forever begin
            @(posedge vif.clk);
            if (vif.wr_en || vif.rd_en) begin
                tr = fifo_trans::type_id::create("tr");
                tr.wr_en   = vif.wr_en;
                tr.rd_en   = vif.rd_en;
                tr.data_in = vif.data_in;
                mon_ap.write(tr);
            end
        end
    endtask
endclass

1.4 Agent和Env:打包和组装

Agent,就是把Driver、Monitor、Sequencer打包在一起。为什么要有Agent?说白了,就是为了复用。一个Agent对应一种接口协议。比如,你有一个AXI Agent,一个APB Agent,以后换个项目,直接拿来用就行。

class fifo_agent extends uvm_agent;
    `uvm_component_utils(fifo_agent)
    
    fifo_driver    driver;
    fifo_monitor   monitor;
    uvm_sequencer #(fifo_trans) sequencer;
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    virtual function void build_phase(uvm_phase phase);
        driver    = fifo_driver::type_id::create("driver", this);
        monitor   = fifo_monitor::type_id::create("monitor", this);
        sequencer = uvm_sequencer #(fifo_trans)::type_id::create("sequencer", this);
    endfunction
    
    virtual function void connect_phase(uvm_phase phase);
        driver.seq_item_port.connect(sequencer.seq_item_export);
    endfunction
endclass

Env,就是环境。它把所有的Agent、Scoreboard、Reference Model都组装在一起。我习惯把Env想象成"总装车间",所有组件在这里完成最后的连接。

class fifo_env extends uvm_env;
    `uvm_component_utils(fifo_env)
    
    fifo_agent    agent;
    fifo_scoreboard sb;
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    virtual function void build_phase(uvm_phase phase);
        agent = fifo_agent::type_id::create("agent", this);
        sb    = fifo_scoreboard::type_id::create("sb", this);
    endfunction
    
    virtual function void connect_phase(uvm_phase phase);
        agent.monitor.mon_ap.connect(sb.analysis_export);
    endfunction
endclass

1.5 Test和Testbench:启动一切

Test,是UVM环境的入口。它负责配置、启动Sequence。Testbench,是顶层模块,它实例化DUT和接口,然后调用run_test

class fifo_test extends uvm_test;
    `uvm_component_utils(fifo_test)
    
    fifo_env env;
    fifo_sequence seq;
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    virtual function void build_phase(uvm_phase phase);
        env = fifo_env::type_id::create("env", this);
    endfunction
    
    virtual task run_phase(uvm_phase phase);
        phase.raise_objection(this);
        seq = fifo_sequence::type_id::create("seq");
        seq.start(env.agent.sequencer);
        #100;
        phase.drop_objection(this);
    endtask
endclass

Testbench顶层:

module top;
    logic clk;
    logic rst_n;
    
    fifo_if vif(clk, rst_n);
    fifo dut(.clk(clk), .rst_n(rst_n), .*);
    
    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end
    
    initial begin
        uvm_config_db #(virtual fifo_if)::set(null, "uvm_test_top.env.agent.*", "vif", vif);
        run_test("fifo_test");
    end
endmodule

避坑指南:我曾经忘记在Test里raise_objection,结果仿真一启动就结束了,Sequence根本没执行。记住:只要你的Test里有动态行为(比如发Sequence),就必须raise_objection,告诉UVM:"别急着结束,我还有活要干!"

1.6 知识体系总览

下面这张图,是我自己总结的UVM环境搭建路线图。每次搭新环境,我都会对照着检查一遍,确保没有遗漏。

UVM验证环境搭建路线图 Testbench (top) Test Env Agent Sequencer Driver Monitor DUT 图例 顶层模块 测试用例 验证环境 代理 待测设计

从这张图可以看得很清楚:Testbench在最顶层,它实例化DUT和接口,然后启动Test。Test里创建Env,Env里包含Agent,Agent里又包含Sequencer、Driver、Monitor。Driver和Monitor直接和DUT打交道。

我个人习惯,每次搭新环境时,都从这张图出发,先确定DUT的接口,然后定义Transaction,接着写Driver和Monitor,再打包成Agent,最后组装成Env和Test。按这个顺序来,思路特别清晰。

好了,第一个UVM环境就搭好了。虽然看起来组件很多,但每个组件各司其职,结构非常清晰。你想想看,如果没有UVM,你得自己写多少重复的代码?有了这套框架,以后换个DUT,改改Transaction和Driver的时序,其他组件基本不用动。


专注资料整理