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.
Columns are rising edges. The monitor that looked only at valid counted 32 and 48 three times each.
Two observations, one checking path
- 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
- Waiting: Input pins — waiting for acceptance
- 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
- data x = 16: Input pins → Incrementer
- data x = 16: Input pins → Input observation
- data x = 16: Input observation → Checking path
- Released: Input pins
- 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
- data y = 17: Incrementer → Output pins
- Waiting: Output observation — next sampling edge
- 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
- data y = 17: Output pins → Output observation
- data y = 17: Output observation → Checking path
- 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.
| Placement | Sees | Typical checks | Feeds |
|---|---|---|---|
| Agent monitor | One interface, the one its agent handles | That protocol's handshake and field rules | Scoreboards and coverage for that interface |
| Bus monitor | Shared traffic at the environment level, across several agents | Arbitration, ordering and traffic that spans agents | System-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.
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
endThe 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.
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); // alwaysKeep 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.
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 : 0Check yourself
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
validandreadyare 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.