UVM Monitor
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.
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.
3. Virtual interface handle
Since the monitor observes DUT signals, it also needs access to the interface.
Just like the driver, the monitor needs a virtual interface handle to access the DUT signals. This handle is set through
uvm_config_dbfrom 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
Sending transactions via analysis port
The analysis port provides a write() method which is used to broadcast the transaction.
Unlike the driver's
seq_item_portwhich is blocking and requires a handshake with the sequencer, the analysis port'swrite()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:
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.
| Aspect | UVM Driver | UVM Monitor |
|---|---|---|
| Role | Active -- drives signals to the DUT | Passive -- samples signals from the DUT |
| Communication | Pulls transactions from sequencer via seq_item_port | Broadcasts transactions via uvm_analysis_port |
| Handshake | Blocking handshake with sequencer (get_next_item/item_done) | Non-blocking broadcast (write()) |
| Subscribers | One -- the sequencer | Many -- scoreboard, coverage collectors, etc. |
| Signal access | Drives input signals of DUT | Samples 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.
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).
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 sinceresis declared asinputinside themonmodport.
Also, note that the
drvmodport has only the signals that need to be driven by the driver. As the driver only needsclkas 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.
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:
- In the testbench hierarchy, we can see a new component
monitorwith namemonnow appears alongside thedriver. - At every positive clock edge, the monitor samples the DUT output signals (res, cout, err, oflow, egl).
- The sampled values are packaged into a
transactionobject and broadcast viaitem_collected_port. - The
uvm_infomessage logs every collected transaction, helping with debug visibility. - The monitor operates independently of the driver and sequencer -- it does not participate in any handshake.
- 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.