UVM Testbench Example

A complete, end-to-end UVM verification environment for a Generic Memory Interface.

To truly understand UVM, you need to see how all the components work together. Below is a complete, end-to-end example for a Generic Memory Interface.

Example Specification:

  • Interface: 8-bit Address, 8-bit Data, Write Enable.
  • DUT: A simple synchronous memory.
  • Testbench: Full UVM architecture including Agent, Scoreboard, and Environment.

1. The Design (DUT) & Interface

The DUT is a simple memory, and the interface defines the signals and clocking blocks used for synchronization.

memory_dut.v & interface.sv

// Simple Memory DUT
module memory_dut (
    input logic clk, rst_n,
    input logic [7:0] addr,
    input logic wr_en,
    input logic [7:0] wdata,
    output logic [7:0] rdata
);
    logic [7:0] mem [255:0];
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) rdata <= 8'h00;
        else if (wr_en) mem[addr] <= wdata;
        else rdata <= mem[addr];
    end
endmodule
// UVM Interface
interface mem_if (input logic clk, rst_n);
    logic [7:0] addr, wdata, rdata;
    logic wr_en;
    clocking drv_cb @(posedge clk);
        default input #1ns output #1ns;
        output addr, wr_en, wdata;
        input rdata;
    endclocking
    clocking mon_cb @(posedge clk);
        default input #1ns output #1ns;
        input addr, wr_en, wdata, rdata;
    endclocking
endinterface
                            

2. Sequence Item & Sequence

The sequence item defines the data packet (transaction), and the sequence generates a stream of randomized transactions.

transaction_&_sequence.sv

// Transaction Class
class mem_item extends uvm_sequence_item;
    rand logic [7:0] addr;
    rand logic       wr_en;
    rand logic [7:0] wdata;
         logic [7:0] rdata; // Sampled, not randomized
    `uvm_object_utils_begin(mem_item)
        `uvm_field_int(addr,  UVM_ALL_ON)
        `uvm_field_int(wr_en, UVM_ALL_ON)
        `uvm_field_int(wdata, UVM_ALL_ON)
        `uvm_field_int(rdata, UVM_ALL_ON)
    `uvm_object_utils_end
    function new(string name = "mem_item");
        super.new(name);
    endfunction
endclass
// Stimulus Sequence
class mem_sequence extends uvm_sequence #(mem_item);
    `uvm_object_utils(mem_sequence)
    virtual task body();
        repeat(10) begin
            req = mem_item::type_id::create("req");
            start_item(req);
            if (!req.randomize()) `uvm_error("SEQ", "Randomization failed")
            finish_item(req);
        end
    endtask
endclass
                            

3. Agent, Driver & Monitor

The Agent encapsulates the Sequencer, Driver, and Monitor. The Driver converts transactions to pin-level activity, while the Monitor samples it back.

uvm_agent_components.sv

// Sequencer Typedef
typedef uvm_sequencer #(mem_item) mem_sequencer;
// Driver: Pin-level Handshake
class mem_driver extends uvm_driver #(mem_item);
    `uvm_component_utils(mem_driver)
    virtual mem_if vif;
    function void build_phase(uvm_phase phase);
        if (!uvm_config_db#(virtual mem_if)::get(this, "", "vif", vif))
            `uvm_fatal("DRV", "Virtual interface not found")
    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
    task drive_item(mem_item item);
        @(vif.drv_cb);
        vif.drv_cb.addr  <= item.addr;
        vif.drv_cb.wr_en <= item.wr_en;
        vif.drv_cb.wdata <= item.wdata;
        if (!item.wr_en) begin
            @(vif.drv_cb);
            item.rdata = vif.drv_cb.rdata;
        end
    endtask
endclass
// Monitor: Sampling & Analysis
class mem_monitor extends uvm_monitor;
    `uvm_component_utils(mem_monitor)
    virtual mem_if vif;
    uvm_analysis_port #(mem_item) mon_ap;
    function new(string name, uvm_component parent);
        super.new(name, parent);
        mon_ap = new("mon_ap", this);
    endfunction
    function void build_phase(uvm_phase phase);
        if (!uvm_config_db#(virtual mem_if)::get(this, "", "vif", vif))
            `uvm_fatal("MON", "Virtual interface not found")
    endfunction
    virtual task run_phase(uvm_phase phase);
        forever begin
            mem_item item = mem_item::type_id::create("item");
            @(vif.mon_cb);
            item.addr  = vif.mon_cb.addr;
            item.wr_en = vif.mon_cb.wr_en;
            item.wdata = vif.mon_cb.wdata;
            if (!vif.mon_cb.wr_en) begin
                @(vif.mon_cb);
                item.rdata = vif.mon_cb.rdata;
            end
            mon_ap.write(item);
        end
    endtask
endclass
// Agent: Encapsulating components
class mem_agent extends uvm_agent;
    `uvm_component_utils(mem_agent)
    mem_sequencer sqr;
    mem_driver    drv;
    mem_monitor   monitor;
    function void build_phase(uvm_phase phase);
        sqr = mem_sequencer::type_id::create("sqr", this);
        drv = mem_driver::type_id::create("drv", this);
        monitor = mem_monitor::type_id::create("monitor", this);
    endfunction
    function void connect_phase(uvm_phase phase);
        drv.seq_item_port.connect(sqr.seq_item_export);
    endfunction
endclass
                            

4. Environment & Scoreboard

The Environment instantiates and connects components like the Agent and Scoreboard using TLM ports.

env_&_scoreboard.sv

// Simple Scoreboard
class mem_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(mem_scoreboard)
    uvm_analysis_imp #(mem_item, mem_scoreboard) mon_export;
    logic [7:0] model_mem [255:0];
    function new(string name, uvm_component parent);
        super.new(name, parent);
        mon_export = new("mon_export", this);
    endfunction
    function void write(mem_item item);
        if (item.wr_en) model_mem[item.addr] = item.wdata;
        else if (model_mem[item.addr] !== item.rdata)
            `uvm_error("SCB", $sformatf("Mismatch! Addr:%0h Exp:%0h Got:%0h", 
                        item.addr, model_mem[item.addr], item.rdata))
    endfunction
endclass
// Environment
class mem_env extends uvm_env;
    `uvm_component_utils(mem_env)
    mem_agent agent;
    mem_scoreboard sb;
    function void build_phase(uvm_phase phase);
        agent = mem_agent::type_id::create("agent", this);
        sb = mem_scoreboard::type_id::create("sb", this);
    endfunction
    function void connect_phase(uvm_phase phase);
        agent.monitor.mon_ap.connect(sb.mon_export);
    endfunction
endclass
                            

5. Test & Top (The Harness)

The Test starts the sequence, and the Top module connects the virtual interface and starts the simulation.

test_&_top.sv

// UVM Test
class mem_test extends uvm_test;
    `uvm_component_utils(mem_test)
    mem_env env;
    task run_phase(uvm_phase phase);
        mem_sequence seq = mem_sequence::type_id::create("seq");
        phase.raise_objection(this);
        seq.start(env.agent.sqr);
        phase.drop_objection(this);
    endtask
endclass
// Top Level Module
module top;
    bit clk, rst_n;
    always #5 clk = ~clk;
    mem_if intf(clk, rst_n);
    memory_dut dut (.clk(clk), .rst_n(rst_n), .addr(intf.addr), 
                    .wr_en(intf.wr_en), .wdata(intf.wdata), .rdata(intf.rdata));
    initial begin
        uvm_config_db#(virtual mem_if)::set(null, "*", "vif", intf);
        run_test("mem_test");
    end
endmodule