UVM-30: Complete UVM Testbench — VLSI Trainers
VLSI Trainers UVM Series · UVM-30
UVM Series · UVM-30

Complete UVM Testbench

Every component of a production-ready UVM testbench assembled end-to-end — the GPIO DUT, APB interface, sequence item, driver, monitor, agent, scoreboard, coverage collector, register model, environment, virtual sequence, and tests. All 12 pieces wired together with every connection shown.

📋 01 DUT and Interface

The DUT is a 32-bit GPIO block with four APB-mapped registers: DATA (0x00), DIR (0x04), IEN (0x08), ISR (0x0C). The interface wraps all APB signals and the GPIO data bus.

GPIO DUT — Complete Testbench Architecture gpio_tb_top APB Agent driver monitor sequencer apb_if gpio_env Scoreboard Coverage Predictor gpio_reg_block GPIO DUT DATA 0x00 DIR 0x04 IEN 0x08 ISR 0x0C gpio[31:0] output ap apb_if→DUT vlsitrainers.com
Figure 1 — Complete GPIO testbench architecture. The APB agent (driver + monitor + sequencer) is the sole interface to the DUT. The monitor’s analysis port broadcasts to the Scoreboard, Coverage collector, and Predictor. The gpio_reg_block provides protocol-independent register access. All live inside gpio_env managed by the test.
// ── APB interface ─────────────────────────────────────────
interface apb_if(input logic PCLK, PRESETn);
  logic [11:0] PADDR;
  logic        PSELx, PENABLE, PWRITE, PREADY, PSLVERR;
  logic [31:0] PWDATA, PRDATA;
  logic [3:0]  PSTRB;
  logic [2:0]  PPROT;
endinterface

📋 02 Sequence Item

class apb_seq_item extends uvm_sequence_item;
  `uvm_object_utils(apb_seq_item)

  // Stimulus fields
  rand logic [11:0] addr;
  rand logic        read_not_write;
  rand logic [31:0] write_data;
  rand logic [3:0]  byte_en;
  rand logic [2:0]  pprot;

  // Response fields (populated by driver)
  logic [31:0] read_data;
  logic        error;

  constraint valid_addr_c {
    addr inside {12'h000, 12'h004, 12'h008, 12'h00C};
  }
  constraint strobe_c {
    if (!read_not_write) byte_en != 4'b0000;
  }

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

  function void do_copy(uvm_object rhs);
    apb_seq_item rhs_;
    $cast(rhs_, rhs); super.do_copy(rhs);
    addr = rhs_.addr; read_not_write = rhs_.read_not_write;
    write_data = rhs_.write_data; byte_en = rhs_.byte_en;
    read_data = rhs_.read_data; error = rhs_.error;
  endfunction

  function bit do_compare(uvm_object rhs, uvm_comparer c);
    apb_seq_item rhs_;
    $cast(rhs_, rhs);
    return (addr == rhs_.addr && read_not_write == rhs_.read_not_write &&
            byte_en == rhs_.byte_en &&
            (read_not_write ? read_data  == rhs_.read_data
                            : write_data == rhs_.write_data));
  endfunction

  function string convert2string();
    return $sformatf("[%s @0x%03h data=0x%08h be=%04b err=%0b]",
      read_not_write ? "RD" : "WR", addr,
      read_not_write ? read_data : write_data, byte_en, error);
  endfunction
endclass

📋 03 Driver

class apb_driver extends uvm_driver #(apb_seq_item);
  `uvm_component_utils(apb_driver)
  virtual apb_if vif;

  function new(string n, uvm_component p); super.new(n,p); endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db #(virtual apb_if)::get(
          this,"","vif",vif))
      `uvm_fatal("DRV","No vif")
  endfunction

  task run_phase(uvm_phase phase);
    apb_seq_item req;
    vif.PSELx <= 0; vif.PENABLE <= 0;
    @(posedge vif.PRESETn);
    forever begin
      seq_item_port.get_next_item(req);
      drive_transfer(req);
      seq_item_port.item_done();
    end
  endtask

  task drive_transfer(apb_seq_item req);
    @(posedge vif.PCLK);
    vif.PADDR <= req.addr; vif.PWDATA <= req.write_data;
    vif.PWRITE <= ~req.read_not_write;
    vif.PSTRB  <= req.byte_en; vif.PPROT  <= req.pprot;
    vif.PSELx <= 1; vif.PENABLE <= 0;
    @(posedge vif.PCLK);
    vif.PENABLE <= 1;
    do @(posedge vif.PCLK); while (!vif.PREADY);
    req.error     = vif.PSLVERR;
    if (req.read_not_write) req.read_data = vif.PRDATA;
    vif.PSELx <= 0; vif.PENABLE <= 0;
  endtask
endclass

📋 04 Monitor

class apb_monitor extends uvm_monitor;
  `uvm_component_utils(apb_monitor)
  virtual apb_if                    vif;
  uvm_analysis_port #(apb_seq_item) ap;

  function new(string n, uvm_component p); super.new(n,p); endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ap = new("ap",this);
    if (!uvm_config_db #(virtual apb_if)::get(
          this,"","vif",vif))
      `uvm_fatal("MON","No vif")
  endfunction

  task run_phase(uvm_phase phase);
    @(posedge vif.PRESETn);
    forever collect_transfer();
  endtask

  task collect_transfer();
    apb_seq_item txn;
    @(posedge vif.PCLK);
    if (vif.PSELx && vif.PENABLE && vif.PREADY) begin
      txn = apb_seq_item::type_id::create("txn");
      txn.addr           = vif.PADDR;
      txn.read_not_write = ~vif.PWRITE;
      txn.byte_en        = vif.PSTRB;
      txn.error          = vif.PSLVERR;
      if (vif.PWRITE) txn.write_data = vif.PWDATA;
      else            txn.read_data  = vif.PRDATA;
      ap.write(txn);
    end
  endtask
endclass

📋 05/06 Agent Config and Agent

// ── Agent config ──────────────────────────────────────────
class apb_agent_cfg extends uvm_object;
  `uvm_object_utils(apb_agent_cfg)
  virtual apb_if   vif;
  uvm_active_passive_enum is_active    = UVM_ACTIVE;
  bit              has_coverage        = 1;
  function new(string n="apb_agent_cfg"); super.new(n); endfunction
endclass

// ── Agent ─────────────────────────────────────────────────
class apb_agent extends uvm_agent;
  `uvm_component_utils(apb_agent)

  apb_driver                  m_drv;
  apb_monitor                 m_mon;
  uvm_sequencer #(apb_seq_item) m_seqr;
  apb_agent_cfg               m_cfg;
  uvm_analysis_port #(apb_seq_item) ap;  // forwarded from monitor

  function new(string n, uvm_component p); super.new(n,p); endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db #(apb_agent_cfg)::get(
          this,"","cfg",m_cfg))
      `uvm_fatal("AGT","No cfg")
    ap    = new("ap",this);
    m_mon = apb_monitor::type_id::create("m_mon",this);
    uvm_config_db #(virtual apb_if)::set(
      this,"*","vif",m_cfg.vif);
    if (m_cfg.is_active == UVM_ACTIVE) begin
      m_seqr = uvm_sequencer #(apb_seq_item)::type_id::
                  create("m_seqr",this);
      m_drv  = apb_driver::type_id::create("m_drv",this);
    end
  endfunction

  function void connect_phase(uvm_phase phase);
    m_mon.ap.connect(this.ap);    // forward monitor ap through agent boundary
    if (m_cfg.is_active == UVM_ACTIVE)
      m_drv.seq_item_port.connect(m_seqr.seq_item_export);
  endfunction
endclass

📋 07 Scoreboard

`uvm_analysis_imp_decl(_expected)
`uvm_analysis_imp_decl(_actual)

class gpio_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(gpio_scoreboard)

  uvm_analysis_imp_expected #(apb_seq_item, gpio_scoreboard) expected_export;
  uvm_analysis_imp_actual   #(apb_seq_item, gpio_scoreboard) actual_export;

  tlm_analysis_fifo #(apb_seq_item) m_exp_fifo, m_act_fifo;
  int m_matches = 0, m_mismatches = 0;

  function new(string n, uvm_component p); super.new(n,p); endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    expected_export = new("expected_export",this);
    actual_export   = new("actual_export",  this);
    m_exp_fifo = new("m_exp_fifo",this);
    m_act_fifo = new("m_act_fifo",this);
  endfunction

  function void write_expected(apb_seq_item t);
    m_exp_fifo.write(t);
  endfunction
  function void write_actual(apb_seq_item t);
    m_act_fifo.write(t);
  endfunction

  task run_phase(uvm_phase phase);
    apb_seq_item exp_t, act_t;
    forever begin
      m_exp_fifo.get(exp_t); m_act_fifo.get(act_t);
      if (exp_t.compare(act_t)) m_matches++;
      else begin
        m_mismatches++;
        `uvm_error("SB", $sformatf(
          "MISMATCH\n  exp: %s\n  act: %s",
          exp_t.convert2string(), act_t.convert2string()))
      end
    end
  endtask

  function void phase_ready_to_end(uvm_phase phase);
    if (phase.get_name() != "run") return;
    if (m_exp_fifo.size() > 0 || m_act_fifo.size() > 0) begin
      phase.raise_objection(this,"SB draining");
      fork begin
        wait(m_exp_fifo.size()==0 && m_act_fifo.size()==0);
        phase.drop_objection(this);
      end join_none
    end
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("SB", $sformatf(
      "Result: %0d matches, %0d mismatches — %s",
      m_matches, m_mismatches,
      m_mismatches==0 ? "PASSED" : "FAILED"), UVM_LOW)
  endfunction
endclass

📋 08 Coverage Collector

class apb_cov_collector extends uvm_subscriber #(apb_seq_item);
  `uvm_component_utils(apb_cov_collector)
  apb_seq_item txn;

  covergroup apb_xfer_cg;
    cp_rw:   coverpoint txn.read_not_write {
      bins rd={1}; bins wr={0};
    }
    cp_addr: coverpoint txn.addr {
      bins data_reg={12'h000}; bins dir_reg={12'h004};
      bins ien_reg={12'h008};  bins isr_reg={12'h00C};
    }
    cp_err:   coverpoint txn.error;
    cx_rw_addr: cross cp_rw, cp_addr;
  endgroup

  function new(string n, uvm_component p);
    super.new(n,p);
    apb_xfer_cg = new();
  endfunction

  function void write(apb_seq_item t);
    txn = t; apb_xfer_cg.sample();
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("COV", $sformatf(
      "APB coverage: %.1f%%", apb_xfer_cg.get_coverage()), UVM_LOW)
  endfunction
endclass

📋 09 Register Model (abbreviated)

The full register model is detailed in UVM-24 and UVM-25. Below is the adapter and the key build snippet for reference in the env wiring:

// ── APB adapter (full implementation in UVM-25) ───────────
class reg2apb_adapter extends uvm_reg_adapter;
  `uvm_object_utils(reg2apb_adapter)
  function new(string n="reg2apb_adapter");
    super.new(n);
    supports_byte_enable = 0;
    provides_responses   = 0;
  endfunction

  virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
    apb_seq_item apb = apb_seq_item::type_id::create("apb");
    apb.read_not_write = (rw.kind == UVM_READ);
    apb.addr = rw.addr[11:0];
    apb.write_data = rw.data;
    apb.byte_en = 4'b1111;
    return apb;
  endfunction

  virtual function void bus2reg(uvm_sequence_item bus_item,
                                  ref uvm_reg_bus_op rw);
    apb_seq_item apb;
    $cast(apb, bus_item);
    rw.kind   = apb.read_not_write ? UVM_READ : UVM_WRITE;
    rw.addr   = apb.addr;
    rw.data   = apb.read_data;
    rw.status = apb.error ? UVM_NOT_OK : UVM_IS_OK;
  endfunction
endclass

📋 10 Environment

class gpio_env_cfg extends uvm_object;
  `uvm_object_utils(gpio_env_cfg)
  apb_agent_cfg   m_apb_cfg;
  gpio_reg_block  gpio_rm;
  bit             has_scoreboard = 1;
  bit             has_coverage   = 1;
  function new(string n="gpio_env_cfg"); super.new(n); endfunction
endclass

class gpio_env extends uvm_env;
  `uvm_component_utils(gpio_env)

  apb_agent           m_agent;
  gpio_scoreboard     m_sb;
  apb_cov_collector   m_cov;
  gpio_predictor      m_predictor;
  uvm_reg_predictor #(apb_seq_item) m_reg_predictor;
  reg2apb_adapter     m_adapter;
  gpio_env_cfg        m_cfg;

  function new(string n, uvm_component p); super.new(n,p); endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db #(gpio_env_cfg)::get(
          this,"","cfg",m_cfg))
      `uvm_fatal("ENV","No cfg")

    uvm_config_db #(apb_agent_cfg)::set(
      this,"m_agent","cfg",m_cfg.m_apb_cfg);
    uvm_config_db #(gpio_reg_block)::set(
      this,"*","gpio_rm",m_cfg.gpio_rm);

    m_agent        = apb_agent::type_id::create("m_agent",       this);
    m_adapter      = reg2apb_adapter::type_id::create("m_adapter");
    m_reg_predictor = uvm_reg_predictor #(apb_seq_item)::type_id::
                        create("m_reg_predictor",this);
    if (m_cfg.has_scoreboard) begin
      m_sb        = gpio_scoreboard::type_id::create("m_sb",     this);
      m_predictor = gpio_predictor::type_id::create("m_predictor",this);
    end
    if (m_cfg.has_coverage)
      m_cov = apb_cov_collector::type_id::create("m_cov",      this);
  endfunction

  function void connect_phase(uvm_phase phase);
    // ── RAL stimulus path ─────────────────────────────────
    m_cfg.gpio_rm.APB_map.set_sequencer(
      m_agent.m_seqr, m_adapter);
    // ── RAL mirror path ───────────────────────────────────
    m_reg_predictor.map     = m_cfg.gpio_rm.APB_map;
    m_reg_predictor.adapter = m_adapter;
    m_agent.ap.connect(m_reg_predictor.bus_in);
    // ── Analysis: scoreboard and coverage ─────────────────
    if (m_cfg.has_scoreboard) begin
      m_predictor.expected_ap.connect(m_sb.expected_export);
      m_agent.ap.connect(m_sb.actual_export);
      m_agent.ap.connect(m_predictor.analysis_export);
    end
    if (m_cfg.has_coverage)
      m_agent.ap.connect(m_cov.analysis_export);
  endfunction
endclass

📋 11 Sequences

// ── Base register sequence ────────────────────────────────
class gpio_base_reg_seq extends uvm_sequence #(uvm_sequence_item);
  `uvm_object_utils(gpio_base_reg_seq)
  gpio_reg_block rm;
  uvm_status_e   status;
  function new(string n="gpio_base_reg_seq"); super.new(n); endfunction
  virtual task body();
    if (!uvm_config_db #(gpio_reg_block)::get(
          m_sequencer,"","gpio_rm",rm))
      `uvm_fatal("SEQ","No reg model")
  endtask
endclass

// ── Register initialisation sequence ──────────────────────
class gpio_reg_init_seq extends gpio_base_reg_seq;
  `uvm_object_utils(gpio_reg_init_seq)
  function new(string n="gpio_reg_init_seq"); super.new(n); endfunction
  task body();
    super.body();
    rm.dir_reg.write(status,  32'hFFFF_0000, .parent(this));
    rm.ien_reg.write(status,  32'h0000_000F, .parent(this));
    rm.data_reg.write(status, 32'h0000_0000, .parent(this));
  endtask
endclass

// ── Randomised read-write sequence ────────────────────────
class gpio_rand_seq extends gpio_base_reg_seq;
  `uvm_object_utils(gpio_rand_seq)
  rand int num_ops = 20;
  function new(string n="gpio_rand_seq"); super.new(n); endfunction
  task body();
    super.body();
    repeat(num_ops) begin
      uvm_reg_data_t data;
      if ($random % 2)
        rm.data_reg.write(status, $urandom, .parent(this));
      else
        rm.data_reg.read(status, data, .parent(this));
    end
  endtask
endclass

📋 12 Base Test and Tests

// ── Base test: builds config tree ─────────────────────────
class gpio_base_test extends uvm_test;
  `uvm_component_utils(gpio_base_test)
  gpio_env        m_env;
  gpio_env_cfg    m_cfg;

  function new(string n, uvm_component p); super.new(n,p); endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    m_cfg = gpio_env_cfg::type_id::create("m_cfg");
    m_cfg.m_apb_cfg = apb_agent_cfg::type_id::create("m_apb_cfg");
    m_cfg.m_apb_cfg.is_active = UVM_ACTIVE;
    if (!uvm_config_db #(virtual apb_if)::get(
          this,"","vif",m_cfg.m_apb_cfg.vif))
      `uvm_fatal("TEST","No vif")
    m_cfg.gpio_rm = gpio_reg_block::type_id::create("gpio_rm");
    m_cfg.gpio_rm.build();
    uvm_config_db #(gpio_env_cfg)::set(
      this,"m_env","cfg",m_cfg);
    m_env = gpio_env::type_id::create("m_env",this);
  endfunction
endclass

// ── Derived test: register init + random stimulus ─────────
class gpio_rand_test extends gpio_base_test;
  `uvm_component_utils(gpio_rand_test)
  function new(string n, uvm_component p); super.new(n,p); endfunction

  task run_phase(uvm_phase phase);
    gpio_reg_init_seq init_seq;
    gpio_rand_seq     rand_seq;
    phase.raise_objection(this);

    init_seq = gpio_reg_init_seq::type_id::create("init_seq");
    init_seq.start(m_env.m_agent.m_seqr);

    rand_seq = gpio_rand_seq::type_id::create("rand_seq");
    rand_seq.randomize() with { num_ops == 50; };
    rand_seq.start(m_env.m_agent.m_seqr);

    phase.drop_objection(this);
  endtask
endclass

// ── Built-in register test ────────────────────────────────
class gpio_reg_hw_reset_test extends gpio_base_test;
  `uvm_component_utils(gpio_reg_hw_reset_test)
  function new(string n, uvm_component p); super.new(n,p); endfunction

  task run_phase(uvm_phase phase);
    uvm_reg_hw_reset_seq hw_reset_seq;
    phase.raise_objection(this);
    hw_reset_seq = uvm_reg_hw_reset_seq::type_id::create("hw_reset_seq");
    hw_reset_seq.model = m_cfg.gpio_rm;
    hw_reset_seq.start(m_env.m_agent.m_seqr);
    phase.drop_objection(this);
  endtask
endclass

📋 13 tb_top Module

module gpio_tb_top;
  import uvm_pkg::*;
  `include "uvm_macros.svh"
  import gpio_test_pkg::*;   // ← must import test package for factory

  logic PCLK, PRESETn;

  // Clock: 10ns period
  initial PCLK = 0;
  always #5ns PCLK = ~PCLK;

  // Reset: deassert after 80ns
  initial begin
    PRESETn = 0;
    #80ns;
    PRESETn = 1;
  end

  apb_if APB(.PCLK(PCLK), .PRESETn(PRESETn));

  gpio_dut dut(
    .PCLK(PCLK), .PRESETn(PRESETn),
    .PADDR(APB.PADDR), .PWDATA(APB.PWDATA),
    .PRDATA(APB.PRDATA), .PWRITE(APB.PWRITE),
    .PSELx(APB.PSELx), .PENABLE(APB.PENABLE),
    .PREADY(APB.PREADY), .PSLVERR(APB.PSLVERR),
    .PSTRB(APB.PSTRB)
  );

  initial begin
    uvm_config_db #(virtual apb_if)::set(
      null, "uvm_test_top", "vif", APB);
    run_test();
  end
endmodule

📋 Complete Wiring Table

ConnectionWhere madeCode
vif → driver + monitoragent build_phaseuvm_config_db::set(this, "*", "vif", m_cfg.vif)
driver ↔ sequenceragent connect_phasem_drv.seq_item_port.connect(m_seqr.seq_item_export)
monitor.ap → agent.apagent connect_phasem_mon.ap.connect(this.ap)
RAL map → sequencerenv connect_phasegpio_rm.APB_map.set_sequencer(m_agent.m_seqr, m_adapter)
agent.ap → RAL predictorenv connect_phasem_agent.ap.connect(m_reg_predictor.bus_in)
agent.ap → scoreboard actualenv connect_phasem_agent.ap.connect(m_sb.actual_export)
agent.ap → gpio predictorenv connect_phasem_agent.ap.connect(m_predictor.analysis_export)
predictor.expected_ap → scoreboard expectedenv connect_phasem_predictor.expected_ap.connect(m_sb.expected_export)
agent.ap → coverageenv connect_phasem_agent.ap.connect(m_cov.analysis_export)
reg_predictor.map + adapterenv connect_phasem_reg_predictor.map = APB_map; m_reg_predictor.adapter = m_adapter
vif → config_dbtb_top initialuvm_config_db::set(null, "uvm_test_top", "vif", APB)
Test package in factorytb_topimport gpio_test_pkg::*
Scroll to Top