UVM Monitor

20 Jun 2026
14 mins

In the last article, we learnt about UVM Driver which receives transactions from the sequencer and drives signals to the DUT. But how do we observe what the DUT is doing in response to these signals? This is where the UVM Monitor comes into the picture.

UVM provides UVM Monitor which is a passive component that observes the pin-level activity on the DUT interface and converts it back into transaction-level objects. These transactions can then be sent to other components like scoreboard or reference model for analysis. Let's deep dive into UVM Monitor in this article.

What is UVM Monitor

UVM Monitor is a UVM component that samples DUT interface signals and translates them back into transaction items (uvm_sequence_item). Unlike the driver which is active and drives signals, the monitor is passive -- it only observes and never drives.

For example, continuing with our ALU testbench:

  • The driver drives OPA, OPB, cin, cmd, mode, and ce signals to the DUT.
  • The monitor samples the DUT's output signals (res, cout, err, oflow, egl) and assembles them into a transaction object. We can also sample DUT's input signals, i.e., TB's output to create end-to-end scoreboards.
  • This transaction is then broadcast via an analysis port to the scoreboard for checking.

Why might we need to sample TB's output signals? This is a very common question that can arise when starting to write test benches. We have a sequence which creates an output packet and a driver which drives the packet into signals. If we already have the output packet, why sample again? This is because the scoreboard needs to act on the actual signal driven by the TB and not on the intended packets. This also helps end-to-end checkers synchronise with the TB's actual output before doing any check.

Defining a UVM Monitor

Like driver, we cannot use the base uvm_monitor class directly. We need to define our own monitor which is a sub-class of uvm_monitor. Let's see how monitors are defined.

1. Declaring monitor class

We define monitor class by extending uvm_monitor class. Unlike uvm_driver or uvm_sequencer, the uvm_monitor base class is not parameterized.

class my_monitor extends uvm_monitor;

The transaction type does not flow through the monitor class. Instead, it is bound to the analysis port which is parameterized with the transaction type it broadcasts.

2. Factory registration and constructor definition

We need to register the monitor with the UVM factory and define the constructor.

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

3. Virtual interface handle

Since the monitor observes DUT signals, it also needs access to the interface.

virtual my_interface vif_handle;

Just like the driver, the monitor needs a virtual interface handle to access the DUT signals. This handle is set through uvm_config_db from the top module.

Monitor's Analysis Port

The key difference between a monitor and a driver lies in how they communicate with other components. While the driver uses seq_item_port to pull transactions from the sequencer, the monitor uses an analysis port to broadcast transactions to multiple subscribers.

What is an analysis port?

An analysis port (uvm_analysis_port) is a TLM port that supports one-to-many communication. A monitor can have multiple subscribers (like scoreboards, coverage collectors, reference models) connected to its analysis port. The monitor does not know or care how many subscribers are connected -- it simply broadcasts every observed transaction.

We will learn more about TLM, Transaction Level Modelling ports later in the UVM track.

Declaring an analysis port

uvm_analysis_port#(transaction) item_collected_port;

Sending transactions via analysis port

The analysis port provides a write() method which is used to broadcast the transaction.

item_collected_port.write(item);

Unlike the driver's seq_item_port which is blocking and requires a handshake with the sequencer, the analysis port's write() is non-blocking. The monitor broadcasts and moves on -- it does not wait for subscribers to consume the transaction.

Monitor Operation in run_phase

The monitor's main activity happens inside the run_phase. Here, it continuously samples the interface signals, assembles them into a transaction object, and broadcasts it via the analysis port.

A typical monitor run_phase follows this pattern:

task run_phase(uvm_phase phase);
    super.run_phase(phase);
    forever begin
        transaction trans;
        trans = transaction::type_id::create("trans");
        // Wait for the clock edge where signals are valid
        @(posedge vif.clk);
        // Sample DUT output signals into the transaction
        trans.res = vif.res;
        trans.cout = vif.cout;
        trans.err = vif.err;
        trans.oflow = vif.oflow;
        trans.egl = vif.egl;
        // Broadcast the collected transaction
        item_collected_port.write(trans);
    end
endtask

The monitor should never drive any signal on the interface. It must only sample. This is what makes the monitor a passive component and ensures it does not interfere with the DUT or the driver.

Adding any non-blocking or blocking assignments to DUT signals inside the monitor violates the passive nature of the monitor and can lead to unexpected behavior.

Monitor vs Driver

It is important to understand the distinction between the monitor and the driver as they sit on opposite sides of the DUT interface.

AspectUVM DriverUVM Monitor
RoleActive -- drives signals to the DUTPassive -- samples signals from the DUT
CommunicationPulls transactions from sequencer via seq_item_portBroadcasts transactions via uvm_analysis_port
HandshakeBlocking handshake with sequencer (get_next_item/item_done)Non-blocking broadcast (write())
SubscribersOne -- the sequencerMany -- scoreboard, coverage collectors, etc.
Signal accessDrives input signals of DUTSamples signals of interface

Example

Let's continue building our ALU testbench by adding a monitor component. The monitor will observe the DUT output signals and broadcast the collected data as a transaction.

import uvm_pkg::*;
 
class monitor extends uvm_monitor;
    `uvm_component_utils(monitor)
 
    virtual alu_if vif;
    uvm_analysis_port#(transaction) item_collected_port;
 
    function new(string name, uvm_component parent);
        super.new(name, parent);
        item_collected_port = new("item_collected_port", this);
    endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual alu_if)::get(this, "", "vif", vif))
            `uvm_fatal("NOVIF", "Virtual interface not found")
    endfunction
 
    task run_phase(uvm_phase phase);
        super.run_phase(phase);
        forever begin
            transaction trans;
            trans = transaction::type_id::create("trans");
            @(posedge vif.mon.clk);
            trans.res = vif.mon.res;
            trans.cout = vif.mon.cout;
            trans.err = vif.mon.err;
            trans.oflow = vif.mon.oflow;
            trans.egl = vif.mon.egl;
            trans.OPA = vif.mon.OPA;
            trans.OPB = vif.mon.OPB;
            trans.cin = vif.mon.cin;
            trans.mode = vif.mon.mode;
            trans.ce = vif.mon.ce;
            trans.cmd = vif.mon.cmd;
            `uvm_info(get_name(), $sformatf("Collected: %s", trans.convert2string()), UVM_HIGH)
            item_collected_port.write(trans);
        end
    endtask
endclass

Interface

Let's take this opportunity to update our interface to use modports. Modports as we know define different views of the interface based on the direction of signals. This ensures that the monitor can only sample signals and never accidentally drive them -- a perfect enforcement of its passive nature.

We will update our interface to have two modports: mon for the monitor (all signals as input) and drv for the driver (DUT input signals as output).

interface alu_if(input logic clk);
    ...
    logic cout, err, oflow;
 
+    modport drv(output OPA, OPB, cin, mode, ce, cmd,
+                input clk);
 
+    modport mon(input clk, OPA, OPB, cin, mode, ce, cmd,
+                res, cout, err, oflow, egl);
endinterface

With modports, accessing a signal in the wrong direction results in a compile-time error. This prevents accidental signal driving from the monitor -- using vif.mon.res = some_value; would be illegal since res is declared as input inside the mon modport.

Also, note that the drv modport has only the signals that need to be driven by the driver. As the driver only needs clk as input, we exclude the DUT output signals from its modport.

TB Top

Let's update our test bench top to include the monitor and see how the TB hierarchy now looks.

`include "package.svh"
`include "uvm_macros.svh"
`include "interface.sv"
 
class env extends uvm_component;
    `uvm_component_utils(env)
 
    sequencer custom_seqr;
    uvm_sequencer#(transaction) base_seqr;
    random_seq rnd_seq1, rnd_seq2, rnd_seq3;
    int use_arbitratoin_method;
    driver drv;
    monitor mon;
 
    // Constructor:new
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction: new
 
    // function: build
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        custom_seqr = sequencer::type_id::create("custom_seqr", this);
        base_seqr = uvm_sequencer#(transaction)::type_id::create("base_seqr", this);
        drv = driver::type_id::create("drv", this);
        mon = monitor::type_id::create("mon", this);
        rnd_seq1 = random_seq::type_id::create("rnd_seq1");
        rnd_seq2 = random_seq::type_id::create("rnd_seq2");
        rnd_seq3 = random_seq::type_id::create("rnd_seq3");
    endfunction: build_phase
 
    // function: connect_phase
    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        drv.seq_item_port.connect(custom_seqr.seq_item_export);
    endfunction: connect_phase
 
    // function: end_of_elaboration_phase
    function void end_of_elaboration_phase(uvm_phase phase);
        super.end_of_elaboration_phase(phase);
        uvm_top.print_topology();
    endfunction: end_of_elaboration_phase
 
    // task: run_phase
    task run_phase(uvm_phase phase);
        super.run_phase(phase);
        phase.raise_objection(this);
        fork
            rnd_seq1.start(custom_seqr, null, 20);
            rnd_seq2.start(custom_seqr, null, 10);
            rnd_seq3.start(custom_seqr, null, 20);
        join
        phase.drop_objection(this);
    endtask: run_phase
 
    // function: start_of_simulation_phase
    function void start_of_simulation_phase(uvm_phase phase);
        super.start_of_simulation_phase(phase);
 
        // set the below variable (1-6) to try different arbitration methods
        use_arbitratoin_method = 6;
        // ---------------------------------------
 
        case(use_arbitratoin_method)
            1: custom_seqr.set_arbitration(UVM_SEQ_ARB_FIFO);
            2: custom_seqr.set_arbitration(UVM_SEQ_ARB_RANDOM);
            3: custom_seqr.set_arbitration(UVM_SEQ_ARB_WEIGHTED);
            4: custom_seqr.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
            5: custom_seqr.set_arbitration(UVM_SEQ_ARB_STRICT_RANDOM);
            6: custom_seqr.set_arbitration(UVM_SEQ_ARB_USER);
            default: custom_seqr.set_arbitration(UVM_SEQ_ARB_FIFO);
        endcase
    endfunction: start_of_simulation_phase
endclass: env
 
module temp_top();
    bit clk;
    alu_if alu_if_inst(clk);
 
    always #5 clk = ~clk;
 
    initial begin
        run_test("env");
    end
 
    // Set the virtual interface for driver and monitor
    initial begin
        uvm_config_db#(virtual alu_if)::set(null, "*", "vif", alu_if_inst);
    end
endmodule: temp_top
Try this code in EDA Playground
Output

# KERNEL: UVM_INFO ./uvm-1.2/src/base/uvm_root.svh(583) @ 0: reporter [UVMTOP] UVM testbench topology:
# KERNEL: ------------------------------------------------------------
# KERNEL: Name                     Type                    Size  Value
# KERNEL: ------------------------------------------------------------
# KERNEL: uvm_test_top             env                     -     @336 
# KERNEL:   base_seqr              uvm_sequencer           -     @490 
# KERNEL:     rsp_export           uvm_analysis_export     -     @499 
# KERNEL:     seq_item_export      uvm_seq_item_pull_imp   -     @617 
# KERNEL:     arbitration_queue    array                   0     -    
# KERNEL:     lock_queue           array                   0     -    
# KERNEL:     num_last_reqs        integral                32    'd1  
# KERNEL:     num_last_rsps        integral                32    'd1  
# KERNEL:   custom_seqr            sequencer               -     @353 
# KERNEL:     rsp_export           uvm_analysis_export     -     @362 
# KERNEL:     seq_item_export      uvm_seq_item_pull_imp   -     @480 
# KERNEL:     arbitration_queue    array                   0     -    
# KERNEL:     lock_queue           array                   0     -    
# KERNEL:     num_last_reqs        integral                32    'd1  
# KERNEL:     num_last_rsps        integral                32    'd1  
# KERNEL:   drv                    driver                  -     @627 
# KERNEL:     rsp_port             uvm_analysis_port       -     @646 
# KERNEL:     seq_item_port        uvm_seq_item_pull_port  -     @636 
# KERNEL:   mon                    monitor                 -     @656 
# KERNEL:     item_collected_port  uvm_analysis_port       -     @665 
# KERNEL: ------------------------------------------------------------
# KERNEL: 
# KERNEL: UVM_INFO /home/runner/sequencer.sv(20) @ 0: uvm_test_top.custom_seqr [custom_seqr] running custom sequencer...
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(20) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Requesting Grant
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(20) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Requesting Grant
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(20) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Requesting Grant
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(22) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Received Grant
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Sent packet
# KERNEL: UVM_INFO /home/runner/driver.sv(28) @ 0: uvm_test_top.drv [driver] Driving packet
# KERNEL: UVM_INFO /home/runner/monitor.sv(37) @ 5: uvm_test_top.mon [mon] Collected: Packet ID: 1
# KERNEL: Input: OPA = xxxxxxxx, OPB = xxxxxxxx, cin = x, cmd = xxxx, mode = x, ce = x, 
# KERNEL: Ouput: result = xxxxxxxxx, cout = x, err = x, oflow = x, EGL = xxx
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(22) @ 5: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Received Grant
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 5: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Sent packet
# KERNEL: UVM_INFO /home/runner/driver.sv(28) @ 5: uvm_test_top.drv [driver] Driving packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(20) @ 7: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Requesting Grant
# KERNEL: UVM_INFO /home/runner/monitor.sv(37) @ 15: uvm_test_top.mon [mon] Collected: Packet ID: 2
# KERNEL: Input: OPA = 10101000, OPB = 11101011, cin = 1, cmd = 1011, mode = 0, ce = 1, 
# KERNEL: Ouput: result = xxxxxxxxx, cout = x, err = x, oflow = x, EGL = xxx
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(22) @ 15: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Received Grant
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 15: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Sent packet
# KERNEL: UVM_INFO /home/runner/driver.sv(28) @ 15: uvm_test_top.drv [driver] Driving packet
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(20) @ 17: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Requesting Grant
# KERNEL: UVM_INFO /home/runner/monitor.sv(37) @ 25: uvm_test_top.mon [mon] Collected: Packet ID: 3
# KERNEL: Input: OPA = 00011011, OPB = 11000110, cin = 1, cmd = 0110, mode = 0, ce = 1, 
# KERNEL: Ouput: result = xxxxxxxxx, cout = x, err = x, oflow = x, EGL = xxx
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(22) @ 25: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Received Grant
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 25: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Sent packet
# KERNEL: UVM_INFO /home/runner/driver.sv(28) @ 25: uvm_test_top.drv [driver] Driving packet
# KERNEL: UVM_INFO /home/runner/monitor.sv(37) @ 35: uvm_test_top.mon [mon] Collected: Packet ID: 4
# KERNEL: Input: OPA = 10100001, OPB = 11110000, cin = 0, cmd = 0010, mode = 1, ce = 1, 
# KERNEL: Ouput: result = xxxxxxxxx, cout = x, err = x, oflow = x, EGL = xxx
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(22) @ 35: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Received Grant
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 35: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Sent packet
# KERNEL: UVM_INFO /home/runner/driver.sv(28) @ 35: uvm_test_top.drv [driver] Driving packet
# KERNEL: UVM_INFO /home/runner/monitor.sv(37) @ 45: uvm_test_top.mon [mon] Collected: Packet ID: 5
# KERNEL: Input: OPA = 00010100, OPB = 11001011, cin = 0, cmd = 0110, mode = 0, ce = 1, 
# KERNEL: Ouput: result = xxxxxxxxx, cout = x, err = x, oflow = x, EGL = xxx
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(20) @ 47: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Requesting Grant
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(22) @ 47: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Received Grant
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(27) @ 47: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Sent packet
# KERNEL: UVM_INFO /home/runner/driver.sv(28) @ 47: uvm_test_top.drv [driver] Driving packet
# KERNEL: UVM_INFO /home/runner/monitor.sv(37) @ 55: uvm_test_top.mon [mon] Collected: Packet ID: 6
# KERNEL: Input: OPA = 01110000, OPB = 00111001, cin = 1, cmd = 0100, mode = 1, ce = 1, 
# KERNEL: Ouput: result = xxxxxxxxx, cout = x, err = x, oflow = x, EGL = xxx

Let's decode the output:

  1. In the testbench hierarchy, we can see a new component monitor with name mon now appears alongside the driver.
  2. At every positive clock edge, the monitor samples the DUT output signals (res, cout, err, oflow, egl).
  3. The sampled values are packaged into a transaction object and broadcast via item_collected_port.
  4. The uvm_info message logs every collected transaction, helping with debug visibility.
  5. The monitor operates independently of the driver and sequencer -- it does not participate in any handshake.
  6. Even though there is no scoreboard connected yet, the monitor continues to broadcast transactions. The analysis port's write() simply passes the data to any connected subscriber.

In the current setup, the monitor's analysis port has no connected subscribers. The data is broadcast but not consumed anywhere. In the next article, we will connect the monitor to a scoreboard which will compare the collected transactions against expected values.

Conclusion

In this article we went through how to declare UVM Monitor using uvm_monitor base class. We saw how the monitor differs from the driver by being a passive observer that samples DUT signals and broadcasts transaction-level data via uvm_analysis_port. The monitor forms the eyes of the testbench, capturing what the DUT produces so that other components can verify correctness. In the next article we will look at how to create a scoreboard that receives transactions from the monitor and checks them against expected results.