UVM-06: Block-Level Testbench — VLSI Trainers
VLSI Trainers UVM Series · UVM-06
UVM Series · UVM-06

Block-Level Testbench

A complete step-by-step walkthrough of building a block-level UVM testbench from scratch — SV top module, sequence item, driver, monitor, agent config, agent, environment config, environment, base test, and the sequence that drives it all.

📋 What We Are Building

This post builds a complete block-level UVM testbench for a simple 32-bit APB GPIO DUT. Every component is shown in the order it should be written. By the end you will have a working testbench that you can adapt to any DUT by changing the sequence item fields and the driver/monitor signal assignments.

1
SV Top Module
2
Sequence Item
3
Driver
4
Monitor
5
Agent Config
6
Agent
7
Env Config
8
Environment
9
Base Test
10
Sequence
Block-Level UVM Testbench — Complete Component Map tb_top.sv — DUT instantiation + interface + clock gen + run_test() gpio_base_test (uvm_test) — build: create configs, get vifs, set cfg_db, create env / run: start sequence gpio_env (uvm_env) — build: get env_cfg, create agent + scoreboard / connect: wire analysis ports gpio_agent (uvm_agent) Sequencer gpio_sequencer seq_item_export Driver gpio_driver seq_item_port Monitor gpio_monitor analysis_port Scoreboard gpio_scoreboard write() — check actual vs expected ap.write(txn) DUT (gpio_dut) — driven via virtual apb_if Seq Item gpio_seq_item rand addr, data rd/wr, strobe vlsitrainers.com
Figure 1 — Complete block-level testbench component map. Every box corresponds to one class file. The arrows show data flow: sequence items flow from sequencer→driver→DUT; observed transactions flow from monitor→analysis_port→scoreboard. Config objects (not shown) flow through config_db from test down to each component.

📋 Step 1 — SV Top Module

The SV top module is static — it is not a UVM component. It instantiates the DUT, the SystemVerilog interface, generates the clock, and calls run_test(). The only UVM-specific job it has is placing the virtual interface into config_db before simulation time starts.

module tb_top;
  import uvm_pkg::*;
  `include "uvm_macros.svh"
  `include "gpio_test_pkg.sv"   // imports all testbench classes

  // ① Clock generation
  logic clk;
  initial clk = 0;
  always #5 clk = ~clk;   // 100 MHz

  // ② Interface instantiation
  apb_if apb_bus(.PCLK(clk), .PRESETn(rst_n));

  // ③ DUT instantiation
  logic rst_n;
  gpio_dut dut(
    .PCLK   (clk),
    .PRESETn(rst_n),
    .PADDR  (apb_bus.PADDR),
    .PWRITE (apb_bus.PWRITE),
    .PWDATA (apb_bus.PWDATA),
    .PSELx  (apb_bus.PSELx),
    .PENABLE(apb_bus.PENABLE),
    .PRDATA (apb_bus.PRDATA),
    .PREADY (apb_bus.PREADY),
    .PSLVERR(apb_bus.PSLVERR)
  );

  // ④ Pass virtual interface into config_db BEFORE run_test()
  initial begin
    uvm_config_db #(virtual apb_if)::set(
      null, "uvm_test_top", "apb_vif", apb_bus);
    rst_n = 0;
    #20ns;
    rst_n = 1;
    run_test();   // ⑤ Start UVM — blocks until all objections dropped
  end
endmodule
The config_db::set() in tb_top must come before run_test(). The null as the first argument means “from the top level” — no component context. The path "uvm_test_top" makes it accessible to the test and all its descendants. If you call run_test() before setting the virtual interface, the test’s build_phase will fail to retrieve it.

📋 Step 2 — Sequence Item

The sequence item represents one atomic transaction on the bus. It carries all the information the driver needs to generate one bus transfer, and all the information the monitor needs to report one observed transaction.

class gpio_seq_item extends uvm_sequence_item;
  `uvm_object_utils_begin(gpio_seq_item)
    `uvm_field_int(addr,   UVM_ALL_ON)
    `uvm_field_int(data,   UVM_ALL_ON)
    `uvm_field_int(we,     UVM_ALL_ON)
    `uvm_field_int(strobe, UVM_ALL_ON)
  `uvm_object_utils_end

  // Randomisable fields
  rand logic [11:0]  addr;    // 12-bit APB address
  rand logic [31:0]  data;    // 32-bit data (write data or captured read data)
  rand logic         we;      // 1=write  0=read
  rand logic [3:0]   strobe;  // PSTRB byte enables

  // Status populated by driver / monitor on completion
  logic slverr;               // PSLVERR from DUT

  // Constraints
  constraint valid_addr_c {
    addr inside {12'h000, 12'h004, 12'h008, 12'h00C, 12'h010};
  }
  constraint strobe_write_c {
    we -> (strobe != 4'b0000);   // writes must have at least one strobe
  }
  constraint strobe_read_c  {
    !we -> (strobe == 4'b0000);  // reads must have zero strobes
  }

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

📋 Step 3 — Driver

The driver pulls sequence items from the sequencer using the get_next_item() / item_done() handshake and drives the DUT signals via the virtual interface. The driver’s run_phase runs forever in a loop.

class gpio_driver extends uvm_driver #(gpio_seq_item);
  `uvm_component_utils(gpio_driver)

  virtual apb_if vif;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  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 virtual interface!")
  endfunction

  task run_phase(uvm_phase phase);
    gpio_seq_item item;
    // Drive idle / reset state on interface
    vif.PSELx   <= 0;
    vif.PENABLE <= 0;
    @(posedge vif.PRESETn);          // wait for reset to deassert
    forever begin
      seq_item_port.get_next_item(item);  // block until item available
      drive_transfer(item);               // APB Setup + Access phases
      seq_item_port.item_done();          // signal completion to sequencer
    end
  endtask

  task drive_transfer(gpio_seq_item item);
    // SETUP phase (1 cycle)
    @(posedge vif.PCLK);
    vif.PADDR   <= item.addr;
    vif.PWDATA  <= item.data;
    vif.PWRITE  <= item.we;
    vif.PSTRB   <= item.strobe;
    vif.PSELx   <= 1;
    vif.PENABLE <= 0;
    // ACCESS phase
    @(posedge vif.PCLK);
    vif.PENABLE <= 1;
    do @(posedge vif.PCLK); while (!vif.PREADY);  // wait for PREADY
    item.slverr  = vif.PSLVERR;
    if (!item.we) item.data = vif.PRDATA;           // capture read data
    vif.PSELx   <= 0;
    vif.PENABLE <= 0;
  endtask
endclass

📋 Step 4 — Monitor

The monitor passively observes the DUT interface and assembles complete transactions. It never drives any signals. When a transfer completes (PSEL+PENABLE+PREADY=1), it builds a sequence item from the observed signals and broadcasts it via the analysis port.

class gpio_monitor extends uvm_monitor;
  `uvm_component_utils(gpio_monitor)

  virtual apb_if                    vif;
  uvm_analysis_port #(gpio_seq_item) ap;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  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 virtual interface!")
  endfunction

  task run_phase(uvm_phase phase);
    gpio_seq_item txn;
    forever begin
      // Wait for transfer completion: PSEL & PENABLE & PREADY
      @(posedge vif.PCLK);
      if (vif.PSELx && vif.PENABLE && vif.PREADY) begin
        txn        = gpio_seq_item::type_id::create("txn");
        txn.addr   = vif.PADDR;
        txn.we     = vif.PWRITE;
        txn.strobe = vif.PSTRB;
        txn.slverr = vif.PSLVERR;
        if (vif.PWRITE) txn.data = vif.PWDATA;
        else            txn.data = vif.PRDATA;
        ap.write(txn);   // broadcast to all subscribers
      end
    end
  endtask
endclass

📋 Step 5 — Agent Config

class gpio_agent_cfg extends uvm_object;
  `uvm_object_utils(gpio_agent_cfg)

  virtual apb_if            vif;
  uvm_active_passive_enum   is_active = UVM_ACTIVE;
  bit                       has_coverage = 1;

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

📋 Step 6 — Agent

typedef uvm_sequencer #(gpio_seq_item) gpio_sequencer;

class gpio_agent extends uvm_agent;
  `uvm_component_utils(gpio_agent)

  gpio_driver    m_drv;
  gpio_sequencer m_seqr;
  gpio_monitor   m_mon;
  gpio_agent_cfg m_cfg;

  uvm_analysis_port #(gpio_seq_item) ap;

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

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db #(gpio_agent_cfg)::get(
          this, "", "cfg", m_cfg))
      `uvm_fatal("AGT", "No agent config!")
    ap    = new("ap", this);
    m_mon = gpio_monitor::type_id::create("m_mon", this);
    if (m_cfg.is_active == UVM_ACTIVE) begin
      m_seqr = gpio_sequencer::type_id::create("m_seqr", this);
      m_drv  = gpio_driver::type_id::create("m_drv", this);
    end
    uvm_config_db #(virtual apb_if)::set(this, "m_mon", "vif", m_cfg.vif);
    if (m_cfg.is_active == UVM_ACTIVE)
      uvm_config_db #(virtual apb_if)::set(this, "m_drv", "vif", m_cfg.vif);
  endfunction

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

📋 Step 7 — Environment Config

The environment config nests the agent config inside it. The test creates both, populates them, assigns the agent config handle into the env config, then puts the env config into config_db. The env retrieves it and distributes the nested agent config to the agent.

class gpio_env_cfg extends uvm_object;
  `uvm_object_utils(gpio_env_cfg)

  // Nested agent config — test populates and assigns this
  gpio_agent_cfg  m_agent_cfg;

  // Enable/disable analysis components
  bit has_scoreboard = 1;
  bit has_coverage   = 1;

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

📋 Step 8 — Environment

class gpio_env extends uvm_env;
  `uvm_component_utils(gpio_env)

  gpio_agent      m_agent;
  gpio_scoreboard m_sb;
  gpio_env_cfg    m_cfg;

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

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // ① Get env config from config_db
    if (!uvm_config_db #(gpio_env_cfg)::get(
          this, "", "cfg", m_cfg))
      `uvm_fatal("ENV", "No env config!")

    // ② Push agent config down to agent
    uvm_config_db #(gpio_agent_cfg)::set(
      this, "m_agent", "cfg", m_cfg.m_agent_cfg);

    // ③ Create agent (always) and optional analysis components
    m_agent = gpio_agent::type_id::create("m_agent", this);
    if (m_cfg.has_scoreboard)
      m_sb = gpio_scoreboard::type_id::create("m_sb", this);
  endfunction

  function void connect_phase(uvm_phase phase);
    // Connect agent analysis port to scoreboard
    if (m_cfg.has_scoreboard)
      m_agent.ap.connect(m_sb.analysis_export);
  endfunction
endclass

📋 Step 9 — Base Test

The test is the top of the UVM hierarchy. Its build_phase is responsible for creating all config objects, retrieving virtual interfaces from config_db, nesting configs, placing the env config into config_db, and creating the env. Its run_phase starts the root sequence.

class gpio_base_test extends uvm_test;
  `uvm_component_utils(gpio_base_test)

  gpio_env     m_env;
  gpio_env_cfg m_env_cfg;

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

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    // ① Create env config
    m_env_cfg = gpio_env_cfg::type_id::create("m_env_cfg");

    // ② Create agent config, get virtual interface from tb_top
    m_env_cfg.m_agent_cfg = gpio_agent_cfg::type_id::create("m_agent_cfg");
    if (!uvm_config_db #(virtual apb_if)::get(
          this, "", "apb_vif", m_env_cfg.m_agent_cfg.vif))
      `uvm_fatal("TEST", "No virtual interface in config_db!")

    // ③ Configure env and agent defaults
    m_env_cfg.has_scoreboard         = 1;
    m_env_cfg.m_agent_cfg.is_active  = UVM_ACTIVE;

    // ④ Place env config into config_db for env to retrieve
    uvm_config_db #(gpio_env_cfg)::set(
      this, "m_env", "cfg", m_env_cfg);

    // ⑤ Create env — triggers env's build_phase
    m_env = gpio_env::type_id::create("m_env", this);
  endfunction

  task run_phase(uvm_phase phase);
    gpio_write_seq seq;
    phase.raise_objection(this);
    seq = gpio_write_seq::type_id::create("seq");
    seq.start(m_env.m_agent.m_seqr);   // start on agent's sequencer
    phase.drop_objection(this);
  endtask
endclass

📋 Step 10 — Sequence

The sequence generates the actual stimulus. Its body() task creates sequence items, randomises them, and sends them to the sequencer using the start_item() / finish_item() handshake.

class gpio_write_seq extends uvm_sequence #(gpio_seq_item);
  `uvm_object_utils(gpio_write_seq)

  int num_txns = 10;

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

  task body();
    gpio_seq_item item;
    repeat(num_txns) begin
      item = gpio_seq_item::type_id::create("item");
      start_item(item);             // request access to sequencer
      if (!item.randomize() with { we == 1; })
        `uvm_fatal("SEQ", "Randomisation failed")
      finish_item(item);            // send to driver, block until done
    end
  endtask
endclass

📋 How It All Wires Together

Config Flow — How virtual interface reaches driver and monitor tb_top config_db::set(vif) config_db test.build_phase get vif → agent_cfg.vif set env_cfg env.build_phase get env_cfg → set agent_cfg set agent_cfg agent.build_phase get cfg → set vif to drv/mon config_db Driver Monitor TLM Connection — Sequence items from sequencer to driver Sequencer seq_item_port .connect( seq_item_export) Driver drives signals DUT vlsitrainers.com
Figure 2 — Two wiring flows. Top: config_db chain from tb_top through test→env→agent, delivering the virtual interface handle to the driver and monitor. Bottom: TLM connection made in agent’s connect_phase, wiring the driver’s seq_item_port to the sequencer’s seq_item_export.
What gets wiredWhereHow
Virtual interface → config_dbtb_top initial blockconfig_db::set(null, "uvm_test_top", "apb_vif", apb_bus)
Virtual interface → agent_cfg.viftest build_phaseconfig_db::get(this, "", "apb_vif", m_env_cfg.m_agent_cfg.vif)
env_cfg → envtest build_phaseconfig_db::set(this, "m_env", "cfg", m_env_cfg)
agent_cfg → agentenv build_phaseconfig_db::set(this, "m_agent", "cfg", m_cfg.m_agent_cfg)
vif → driveragent build_phaseconfig_db::set(this, "m_drv", "vif", m_cfg.vif)
vif → monitoragent build_phaseconfig_db::set(this, "m_mon", "vif", m_cfg.vif)
seq_item_port → seq_item_exportagent connect_phasem_drv.seq_item_port.connect(m_seqr.seq_item_export)
monitor.ap → agent.apagent connect_phasem_mon.ap.connect(this.ap)
agent.ap → scoreboardenv connect_phasem_agent.ap.connect(m_sb.analysis_export)

📋 Quick Reference

ItemKey fact
tb_top responsibilityDUT + interface instantiation, clock gen, config_db::set(vif), run_test()
Virtual interface in config_dbSet with null context before run_test() — path “uvm_test_top” makes it global
Sequence item fieldsAll bus signals needed by driver to drive one transfer + monitor to report one transfer
Driver loopget_next_item() → drive signals → item_done() — forever in run_phase
Monitor triggerPSEL & PENABLE & PREADY all HIGH on rising PCLK = transfer complete
Monitor outputap.write(txn) — broadcasts completed transaction to all subscribers
Config nesting ordertest creates agent_cfg → assigns into env_cfg → sets env_cfg into config_db → env gets it → pushes agent_cfg to agent
Agent config key fieldvif handle + is_active — all other fields are optional
Sequencer typedeftypedef uvm_sequencer #(my_item) my_sequencer; — no extra code needed
Sequence body()create item → start_item() → randomize() → finish_item() per transfer
Test run_phaseraise_objection → seq.start(m_env.m_agent.m_seqr) → drop_objection
TLM wired inagent connect_phase — seq_item_port.connect(seq_item_export)
Analysis port wired inagent connect_phase (forward) + env connect_phase (subscribe scoreboard)
File naming conventionOne class per .svh file; all collected in a _pkg.sv package
Scroll to Top