How to Build UVM Environment Part – 3

In short: This is part three of building a UVM environment for the pipe design. Here we add the monitor that watches the pins, wrap the driver, sequencer, and monitor into an agent, and build a scoreboard that checks the results against a small reference model. We then group everything into an environment. After this part, the testbench can not only drive stimulus but also observe and check it. All code follows the IEEE 1800.2 rules.

In part two we finished the driver, added the sequencer, and wrote sequences, so stimulus now flows into the pipe. But driving is only half of verification. We still need to watch what the design does and decide whether it is right. That is the job of the monitor and the scoreboard, which we build now.

A simple way to picture it

Imagine a delivery service. The driver drops off parcels: that is our UVM driver. A separate camera at the door records every parcel that arrives, without touching them: that is the monitor. A manager compares the delivery log against the list of what was ordered and flags anything wrong: that is the scoreboard. The camera never carries parcels, and the manager never drives the van. Keeping these jobs separate is what makes the checking trustworthy.

The monitor

The monitor watches the interface and rebuilds each transaction it sees, then publishes it through an analysis port. It never drives a signal. That rule matters: if the monitor also drove pins, your checks would depend on the very thing being tested, and a bug could hide itself.

class pipe_monitor extends uvm_monitor;
  `uvm_component_utils(pipe_monitor)

  virtual pipe_if vif;
  uvm_analysis_port #(data_packet) item_collected_port;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    item_collected_port = new("item_collected_port", this);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db #(virtual pipe_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "virtual interface not set for pipe_monitor")
  endfunction

  virtual task run_phase(uvm_phase phase);
    forever begin
      data_packet tr = data_packet::type_id::create("tr");
      @(posedge vif.clk iff vif.enable);
      // capture inputs and outputs seen on the pins
      tr.cf        = vif.cf;
      tr.data_in1  = vif.data_in1;
      tr.data_in2  = vif.data_in2;
      tr.data_out1 = vif.data_out1;
      tr.data_out2 = vif.data_out2;
      item_collected_port.write(tr);   // broadcast to listeners
    end
  endtask
endclass

The iff vif.enable means the monitor only samples on clocks where the design is actually active, so it does not capture idle cycles. The captured packet is then broadcast through item_collected_port, which the scoreboard will subscribe to.

The agent

The agent is a container that groups the driver, sequencer, and monitor for one interface. Since we already built those three, the agent is mostly wiring. It also chooses a mode: an active agent builds all three parts and drives, while a passive agent builds only the monitor and just observes.

Agent modeBuildsUsed for
Activedriver, sequencer, monitorAn interface the testbench drives
Passivemonitor onlyAn interface already driven elsewhere, that we only watch
class pipe_agent extends uvm_agent;
  `uvm_component_utils(pipe_agent)

  pipe_driver    driver;
  pipe_sequencer sequencer;
  pipe_monitor   monitor;

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

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    monitor = pipe_monitor::type_id::create("monitor", this);
    if (get_is_active() == UVM_ACTIVE) begin
      driver    = pipe_driver::type_id::create("driver", this);
      sequencer = pipe_sequencer::type_id::create("sequencer", this);
    end
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    if (get_is_active() == UVM_ACTIVE)
      driver.seq_item_port.connect(sequencer.seq_item_export);
  endfunction
endclass

The pipe has an input side and an output side, so later we will use two agents: an active one for driving the inputs and a passive one for watching the outputs. Building the mode into the agent means the same class serves both roles.

The scoreboard

The scoreboard is the judge. It receives packets from the monitor and checks them against what the design should have produced. To know the expected result, it needs a small reference model: a copy of the specification written in plain code. For the pipe, the reference model applies the same operation that i_cf selects.

The scoreboard subscribes to the monitor using an analysis implementation. The macro uvm_analysis_imp_decl lets one scoreboard receive from more than one port if needed.

class pipe_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(pipe_scoreboard)

  uvm_analysis_imp #(data_packet, pipe_scoreboard) item_collected_export;
  int matches, mismatches;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    item_collected_export = new("item_collected_export", this);
  endfunction

  // Called automatically for each packet the monitor broadcasts
  virtual function void write(data_packet tr);
    bit [15:0] exp1, exp2;
    // reference model: reproduce what the spec says the pipe does
    case (tr.cf)
      2'b00: begin exp1 = tr.data_in1;            exp2 = tr.data_in2;            end // pass through
      2'b01: begin exp1 = tr.data_in2;            exp2 = tr.data_in1;            end // swap
      2'b10: begin exp1 = tr.data_in1 + tr.data_in2; exp2 = tr.data_in1 - tr.data_in2; end // add / sub
      default: begin exp1 = 16'h0;               exp2 = 16'h0;                 end
    endcase

    if (tr.data_out1 === exp1 && tr.data_out2 === exp2) begin
      matches++;
      `uvm_info("SB", "packet matched", UVM_HIGH)
    end
    else begin
      mismatches++;
      `uvm_error("SB", $sformatf("mismatch: got %0h/%0h exp %0h/%0h",
                 tr.data_out1, tr.data_out2, exp1, exp2))
    end
  endfunction

  virtual function void report_phase(uvm_phase phase);
    `uvm_info("SB", $sformatf("matches=%0d mismatches=%0d", matches, mismatches), UVM_LOW)
  endfunction
endclass

The === comparison, rather than ==, catches unknown (x) values, which is exactly what you want in a checker. The report phase then prints a clean tally at the end of the run.

The environment

The environment groups the agents and the scoreboard, and connects the monitor to the scoreboard. We build it in two layers, which matches how real projects scale. First a small pipe_env that holds one agent:

class pipe_env extends uvm_env;
  `uvm_component_utils(pipe_env)

  pipe_agent agent;

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

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agent = pipe_agent::type_id::create("agent", this);
  endfunction
endclass

Then a top level dut_env that holds two of them, one for the input side and one for the output side, plus the scoreboard. It connects each monitor to the scoreboard so every observed packet is checked.

class dut_env extends uvm_env;
  `uvm_component_utils(dut_env)

  pipe_env        penv_in;    // active: drives inputs
  pipe_env        penv_out;   // passive: watches outputs
  pipe_scoreboard sb;

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

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // make the output-side env observe only: its agent must be passive
    uvm_config_db #(uvm_active_passive_enum)::set(this, "penv_out.agent", "is_active", UVM_PASSIVE);
    penv_in  = pipe_env::type_id::create("penv_in", this);
    penv_out = pipe_env::type_id::create("penv_out", this);
    sb       = pipe_scoreboard::type_id::create("sb", this);
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    // send observed output packets to the scoreboard
    penv_out.agent.monitor.item_collected_port.connect(sb.item_collected_export);
  endfunction
endclass

This two layer shape, small env inside a bigger env, is how a chip level testbench reuses block level pieces. The same pipe_env serves both the input and output sides just by changing its agent mode.

How the pieces connect

Following one packet end to end: a sequence creates it, the sequencer hands it to the driver, the driver drives the input pins, the design responds, the output monitor captures the result, and the scoreboard compares it against the reference model. Each arrow points one way, which keeps the flow easy to follow.

Common mistakes at this stage

  • Driving from the monitor. Keep the monitor sample only. If it drives, your checks become circular.
  • Forgetting to connect the analysis port. If the monitor port is not connected to the scoreboard, packets are broadcast to nobody and nothing is checked.
  • Using == instead of ===. Plain equality misses x values. Use === in the checker so unknowns are caught.
  • A wrong reference model. If the scoreboard model does not match the specification, it will flag correct behaviour as a bug. Keep the model and the spec in step.

Where we are now

The testbench can now drive, observe, and check. You have a monitor, an agent with active and passive modes, a scoreboard with a reference model, and a two layer environment. In part four we add the test, pass the interface in through the config database, and run the first simulation.

Keep learning

For the concepts behind this part, browse the UVM guides for the monitor, TLM connections, and reporting, the SystemVerilog guides for the class basics, and the Verification Fundamentals for the wider flow. Preparing for an interview? See the interview questions.

Frequently asked questions

Why must the monitor never drive signals?

Because it is the source of truth for checking. If it also drove pins, your checks would depend on the thing being tested, and a real bug could be masked.

What is the difference between an active and a passive agent?

An active agent builds the driver, sequencer, and monitor and drives the interface. A passive agent builds only the monitor and just observes, which suits an interface driven elsewhere.

What does the scoreboard need a reference model for?

To know the expected output. The reference model reproduces the specification in plain code, so the scoreboard can compare the design output against what should have happened.

Why use === instead of == in the checker?

=== also compares unknown (x) and high impedance (z) values, so it catches an x on an output that == would miss. That makes the checker stricter and safer.

Why build a small env inside a bigger env?

It lets you reuse the small block level environment across a larger testbench. The same pipe_env serves the input and output sides just by changing its agent mode.

What connects the monitor to the scoreboard?

The monitor analysis port connects to the scoreboard analysis implementation in the environment connect phase. The monitor calls write, and the scoreboard receives each packet to check it.

Similar Posts