Monitor the event the DUT accepts

Write a monitor that turns pin activity into one transaction per real transfer, sampled at the edge where the design takes the value.

Lesson
U7.1
Time
25 min
Runs on
Verilator with UVM
Check
6 questions

The scoreboard counts seven inputs and three results. The third comparison fails, 49 against an expected 33, and four predictions are never answered. The sequence sent three values: 16, 32 and 48. The monitor published a transaction at every rising edge where valid was 1, but the design was busy at four of those edges and took nothing.

Seven edges with valid high, three transfers
0123456edge (ns)15253545556575validreadyx163248monitor saw163248design took16-32-48valid but busy: no transfer

Columns are rising edges. The monitor that looked only at valid counted 32 and 48 three times each.

Two observations, one checking path

Observe the accepted input and the actual outputThe monitor reads both sides of the design. It does not use the driver’s item as evidence that a transfer happened.
Observe the accepted input and the actual outputThe monitor reads both sides of the design. It does not use the driver’s item as evidence that a transfer happened. pins Input pins valid, ready and x atthe edge design Incrementer Accepts 16; laterproduces 17 pins Output pins out_valid and y atthe edge monitor Input observation A fresh item with x =16 consumer Checking path Predict from input;compare output monitor Outputobservation A fresh result with y= 17 accepted xresult pinssample inputinput itemsample outputresult item
  1. Offer10 ns (illustrative)

    The driver presents 16 at a falling edge and holds valid high. An offer alone creates no input transaction.

    More context

    On this bus, both valid and ready must be high at a rising edge.

    Follow the calls, values and waits
    1. Waiting: Input pins — waiting for acceptance
  2. Accept and record15 ns (illustrative)

    Both handshake signals are high. The design accepts 16, and the input monitor publishes the value sampled on that same edge.

    More context

    The motion separates the explanations; acceptance and input observation occur at the same simulated time.

    Follow the calls, values and waits
    1. data x = 16: Input pins → Incrementer
    2. data x = 16: Input pins → Input observation
    3. data x = 16: Input observation → Checking path
    4. Released: Input pins
  3. Drive the resultJust after 25 ns (illustrative)

    The design’s nonblocking updates put 17 and out_valid = 1 on the pins. At the 25 ns sampling edge, the monitor still saw the old out_valid = 0.

    More context

    Driving a value and observing it are different events in this bench.

    Follow the calls, values and waits
    1. data y = 17: Incrementer → Output pins
    2. Waiting: Output observation — next sampling edge
  4. Record the output35 ns (illustrative)

    The output monitor now samples out_valid = 1 and y = 17. It publishes the observed result for comparison with the independent prediction.

    More context

    Neither observation reads the sequence’s request object. The pins are the evidence.

    Follow the calls, values and waits
    1. data y = 17: Output pins → Output observation
    2. data y = 17: Output observation → Checking path
    3. Released: Output observation
Read the diagram as text
  • Input pins: valid, ready and x at the edge
  • Incrementer: Accepts 16; later produces 17
  • Output pins: out_valid and y at the edge
  • Input observation: A fresh item with x = 16
  • Checking path: Predict from input; compare output
  • Output observation: A fresh result with y = 17
  • Input pins → Incrementer: accepted x
  • Incrementer → Output pins: result pins
  • Input pins → Input observation: sample input
  • Input observation → Checking path: input item
  • Output pins → Output observation: sample output
  • Output observation → Checking path: result item

Arrows carry sampled values or transaction data, not connection calls. The two monitor boxes are the two observation branches of one incr_monitor component.

A monitor turns pin activity into transactions. Here one component observes both sides of the incrementer: the accepted input feeds prediction, and the observed output feeds comparison. Neither observation uses the driver's request object.

The pins are the evidence. A driver bug can make the accepted value differ from the intended one, and only a comparison with the planned item catches that.

A missing transfer is a different failure. If three items were planned and two transferred, the input/output check still balances; U7.3 adds a planned count for that case. A count alone cannot catch a wrong value that did transfer.

The monitor never drives these pins, so the same code can watch traffic that another block generates, in a passive agent with no driver or sequencer. U11.1 builds that agent.

PlacementSeesTypical checksFeeds
Agent monitorOne interface, the one its agent handlesThat protocol's handshake and field rulesScoreboards and coverage for that interface
Bus monitorShared traffic at the environment level, across several agentsArbitration, ordering and traffic that spans agentsSystem-level checkers

Both extend uvm_monitor; the names describe placement, not different base classes. When both observe the same transfer, choose which stream feeds each checker, or the transfer is counted twice.

Identify the acceptance event

On this bus, a rising edge with valid && ready is one accepted transfer. Holding valid while ready is low is a legal stall. Holding both high across several edges means several transfers, even when the data value stays the same.

Separate if statements allow an input and an output on the same edge. This output interface has no ready signal: out_valid alone marks an output transfer.
forever begin
  @(posedge vif.clk);
  if (vif.valid && vif.ready) begin
    incr_item t = incr_item::type_id::create("t");
    t.x = vif.x;
    in_ap.write(t);
  end
  if (vif.out_valid) begin
    incr_result r = incr_result::type_id::create("r");
    r.y = vif.y;
    out_ap.write(r);
  end
end

The monitor publishes an observation with write. U7.2 follows that transaction into consumers. It does not wait for item_done, count sequence iterations, or infer acceptance from a change in x.

In the log, accepted inputs occur at 15, 45 and 75 ns; observed outputs occur at 35, 65 and 95 ns. The seven valid-high edges in the opening trace therefore describe three accepted requests.

Choose the sampling point

This example reads immediately after @(posedge vif.clk). That works because the driver changes inputs on falling edges and the design changes registers with nonblocking assignments. The monitor reads before those rising-edge updates take effect.

Reads after the update

@(posedge vif.clk);
#1;
if (vif.valid && vif.ready)
  publish(vif.x);

Misses 16. It later reports 32 and 48 one edge before acceptance, when the busy counter has just cleared.

Reads the accepted values

@(posedge vif.clk);
if (vif.valid && vif.ready)
  publish(vif.x);

Correct for this bench: the same stable values drive the design's acceptance decision.

A clocking block with the default input skew, #1step, preserves values sampled just before its clock event. Read vif.cb.valid, vif.cb.ready and vif.cb.x after @(vif.cb). An input skew of #0 samples in the Observed region, after this edge's nonblocking updates (IEEE 1800-2017 §14.4), so it repeats the #1 mistake in the compare. [Lesson 6.3](6-3-clocking-blocks.html) covers skews.

Why “at the edge” is not a universal recipe

A bare edge event does not make all signal reads race-free. If another process changes a sampled signal with a blocking assignment at the same edge, process order can affect the value read. Choose the clocking event and input skew to match the interface timing.

The output timing is equally deliberate: 16 is accepted at 15 ns, the design drives 17 just after 25 ns, and this monitor observes it at 35 ns. A protocol using a different sampling event needs a corresponding monitor. Adding an arbitrary delay changes the event being observed.

Switch checks and coverage without losing observations

A reusable monitor offers checks_enable for its local protocol checks and coverage_enable for coverage sampling, so a test can turn off a redundant check or expensive coverage. The monitor author adds both fields; uvm_monitor has neither.

The switches gate optional work. The analysis write sits outside both.
bit checks_enable = 1, coverage_enable = 1;
function void build_phase(uvm_phase phase);
  void'(uvm_config_db #(bit)::get(this, "", "checks_enable", checks_enable));
  void'(uvm_config_db #(bit)::get(this, "", "coverage_enable", coverage_enable));
endfunction
// for each transfer t:
if (checks_enable)   check_protocol(t);
if (coverage_enable) cg.sample();
in_ap.write(t);      // always

Keep in_ap.write and out_ap.write outside those conditions. A monitor that stops publishing when checks are off starves the scoreboard, and one that stops when coverage is off changes the functional verdict.

The example's monitor has both switches. Its check is a real protocol rule: while the design stalls, valid must stay high and x must hold its value. The test turns coverage off with uvm_config_db #(bit)::set(this, "mon", "coverage_enable", 0). The run reports 4 stall checks and 0 coverage samples, and still 3 transfers and 3 results. U8.4 S6 shows the scoreboard honouring its own checks_enable on both sides of its ledger; the coverage module develops the separate coverage consumer.

Keep a record before the pins change

Create a fresh object and copy the sampled fields into it. A later checker needs the recorded values, not a virtual interface handle that would expose today's pins. Include identifiers, status and timestamps when the protocol or diagnostics need them.

Keep accepted traffic and protocol violations distinguishable. A stalled offer is not another accepted request. A stalled cycle creates no accepted transaction. If the protocol requires valid and payload to hold while stalled, as the example's stall check does, report a hold violation separately. If the design accepts a value on that same edge, still publish the transfer it actually took.

When a transaction spans several beats

A burst monitor keeps assembly state: which request is active, the accepted beats, and the termination condition. Count accepted beats, including repeated values, and publish the completed transaction when the protocol says it is complete.

If a checker needs early visibility, publish a separate start or beat event. Define what reset, an error or a missing last beat does to the partial transaction; silently discarding it can hide lost traffic. U10.2 covers reset.

Try it

The monitor prints one line per published transaction, and the report counts them. Seven edges had valid high, but only three were transfers. The stall check ran at the four edges after a stall (35, 45, 65 and 75 ns); coverage was off, and every transfer was still published. The fixed logging delay here is replaced by counted completion in U7.3.

In the monitor only, change if (vif.valid && vif.ready) to if (vif.valid): seven input transactions now appear. Restore it, then insert #1; after the monitor's edge wait and explain the early observations.

Transfer exercise: valid, ready and x = 7 stay high/constant for three rising edges. Sketch the transactions the monitor must publish. Then add one stalled edge and explain which count changes.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 15 ns monitor transfer x=16 35 ns monitor result y=17 45 ns monitor transfer x=32 65 ns monitor result y=33 75 ns monitor transfer x=48 95 ns monitor result y=49 transfers 3, results 3, edges with valid high 7 stall checks 4, coverage samples 0 (coverage_enable=0) UVM_ERROR : 0

Check yourself

Predict the outputThis bench uses the example's classes, but the design is busy for one edge after each accept instead of two (BUSY = 1), and the sequence sends 5, 6 and 7. What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

// The incrementer from U4.3 with a result port. It captures x at a rising edge k where valid and
// ready are both 1 and is busy for the next BUSY edges. It drives y = x + 1 and out_valid after
// edge k+1, for one cycle, so a monitor sampling at rising edges sees the result at edge k+2.
interface incr_if (input logic clk);
  logic       valid = 0;
  logic [7:0] x = 0;
  logic       ready;
  logic       out_valid = 0;
  logic [7:0] y = 0;
endinterface

module incr_pipe #(parameter int BUSY = 2) (incr_if bus);
  logic [1:0] busy = 0;
  logic       staged = 0;
  logic [7:0] stage = 0;
  assign bus.ready = (busy == 0);
  always @(posedge bus.clk) begin
    bus.out_valid <= staged;
    bus.y         <= stage;
    staged        <= bus.valid && bus.ready;
    stage         <= bus.x + 1;
    if (bus.valid && bus.ready) busy <= BUSY;
    else if (busy != 0)         busy <= busy - 1;
  end
endmodule

// What the sequence sends, and what the monitor sees going in.
class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  bit [7:0] x;
  function new(string name = "incr_item"); super.new(name); endfunction
endclass

// What the monitor sees coming out.
class incr_result extends uvm_sequence_item;
  `uvm_object_utils(incr_result)
  bit [7:0] y;
  function new(string name = "incr_result"); super.new(name); endfunction
endclass

// Sends the values in the queue, one item each.
class incr_seq extends uvm_sequence #(incr_item);
  `uvm_object_utils(incr_seq)
  bit [7:0] values[$];
  function new(string name = "incr_seq"); super.new(name); values = '{16, 32, 48}; endfunction
  task body();
    foreach (values[i]) begin
      incr_item it = incr_item::type_id::create("it");
      start_item(it);
      it.x = values[i];
      finish_item(it);
    end
  endtask
endclass

// The driver from U4.3: offers an item at a falling edge, holds it until an edge with ready.
class incr_driver extends uvm_driver #(incr_item);
  `uvm_component_utils(incr_driver)
  virtual incr_if vif;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(negedge vif.clk);
      seq_item_port.try_next_item(req);
      if (req == null) begin
        vif.valid <= 0;
        continue;
      end
      vif.valid <= 1;
      vif.x     <= req.x;
      do @(posedge vif.clk); while (!vif.ready);
      seq_item_port.item_done();
    end
  endtask
endclass

// Watches the pins and publishes one transaction per transfer. It never drives anything.
class incr_monitor extends uvm_monitor;
  `uvm_component_utils(incr_monitor)
  virtual incr_if vif;
  uvm_analysis_port #(incr_item)   in_ap;
  uvm_analysis_port #(incr_result) out_ap;
  int t_in[$], t_out[$];                   // times of each transfer and each result
  function new(string name, uvm_component parent);
    super.new(name, parent);
    in_ap  = new("in_ap", this);
    out_ap = new("out_ap", this);
  endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);                      // values just before the edge
      if (vif.valid && vif.ready) begin        // a transfer: the design takes x
        incr_item t = incr_item::type_id::create("t");
        t.x = vif.x;
        t_in.push_back($time);
        in_ap.write(t);
      end
      if (vif.out_valid) begin                 // a result: the design shows y
        incr_result r = incr_result::type_id::create("r");
        r.y = vif.y;
        t_out.push_back($time);
        out_ap.write(r);
      end
    end
  endtask
endclass

class predict_test extends uvm_test;
  `uvm_component_utils(predict_test)
  uvm_sequencer #(incr_item) sqr;
  incr_driver  drv;
  incr_monitor mon;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual incr_if vif;
    sqr = uvm_sequencer #(incr_item)::type_id::create("sqr", this);
    drv = incr_driver::type_id::create("drv", this);
    mon = incr_monitor::type_id::create("mon", this);
    if (!uvm_config_db #(virtual incr_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "no virtual interface")
    drv.vif = vif;
    mon.vif = vif;
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
  task run_phase(uvm_phase phase);
    incr_seq seq = incr_seq::type_id::create("seq");
    seq.values = '{5, 6, 7};
    phase.raise_objection(this);
    seq.start(sqr);
    #40;
    phase.drop_objection(this);
  endtask
  function void report_phase(uvm_phase phase);
    $display("transfers at %0d %0d %0d, results at %0d %0d %0d",
             mon.t_in[0], mon.t_in[1], mon.t_in[2], mon.t_out[0], mon.t_out[1], mon.t_out[2]);
  endfunction
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  incr_if bus (clk);
  incr_pipe #(.BUSY(1)) dut (bus);
  initial begin
    uvm_root::get().set_timeout(2000ns, 1);
    uvm_config_db #(virtual incr_if)::set(null, "uvm_test_top", "vif", bus);
    run_test("predict_test");
  end
endmodule
Spot the bugThe sequence sends 16, 32 and 48 to the stalling design. Each option is the body of the input monitor's forever loop. Which one publishes the wrong transfers?
ConceptThe driver has a bug: it sometimes calls item_done although the design never took the value. The scoreboard predicts from the monitor's input transactions and keeps no count of planned items. What does it report for a skipped value?
Read the waveformA correct monitor watches this bus. How many input transactions does it publish?
Rising edges of a ready/valid bus
012345edge (ns)152535455565validreadyx794

Values just before each rising edge.

ConceptTwo agents each have a monitor on their own port. An environment-level bus monitor also watches the shared bus that both ports drive. Each monitor reports every accepted transfer once, and all three streams are connected to the scoreboard's single prediction input, which expects one observation per transfer. What goes wrong?
ConceptA reusable monitor has local protocol checks and coverage, controlled by user-defined checks_enable and coverage_enable flags. Both flags are off, but the scoreboard is still required. What must the monitor continue doing?

Remember

  • A monitor watches pins, never drives them, and publishes a fresh transaction per transfer, filled from pin values.
  • Publish once per transfer: on a ready/valid bus, at an edge where valid and ready are both 1.
  • Match sampling to the protocol: this bench relies on falling-edge drive and rising-edge nonblocking updates; a clocking block states the input skew.
  • Checks and coverage switches gate optional work inside the monitor; the analysis writes always happen.
  • An agent monitor watches one interface, a bus monitor shared traffic; when both see a transfer, feed a given checking input from only one of them, or that transfer counts twice.