Nothing in a UVM environment is conceptually new after a hand-built class testbench. Read each row as the hand-written version versus the standardized one.
Hand-built | UVM equivalent |
|---|---|
Env class with |
|
Generator class pushing into a mailbox |
|
|
|
Monitor fanning out into one mailbox per consumer |
|
Done counter or event ending the test |
|
|
|
Logger class with levels and scopes |
|
Config object threaded through constructors |
|
A hand-built generator does two jobs: it decides what stimulus to create and it delivers that stimulus to the driver. UVM splits the jobs: the uvm_sequence holds the stimulus code while the uvm_sequencer delivers items and arbitrates between competing sequences.
Arbitration is the genuine addition. Several sequences can share one sequencer and interleave, which is painful to hand-roll with mailboxes. Sequencer detail lives in the UVM course under sequencer arbitration.
// snippet: hand-built push handoff, fire and forget
mailbox#(bus_txn) out_mb = new(8);
task automatic gen_run(int count);
for (int i = 0; i < count; i++) begin
bus_txn t = new();
void'(t.randomize());
out_mb.put(t); // returns as soon as the mailbox accepts
end
endtaskA mailbox handoff is producer-push: the generator calls put() and moves on. The UVM handshake is consumer-pull with completion: the driver calls get_next_item(), drives the pins, then calls item_done().
The sequence blocks in finish_item() until then, so it knows when each item finished. That completion signal is what makes reactive stimulus natural in UVM and clumsy with a plain mailbox.
// snippet: UVM pull handshake with completion feedback
task automatic seq_body(int count);
for (int i = 0; i < count; i++) begin
req = bus_txn::type_id::create("req"); // a fresh item each time
start_item(req); // wait until the driver is ready
void'(req.randomize());
finish_item(req); // unblocks only after item_done()
end
endtask
// driver side: get_next_item(req); drive_pins(req); item_done();The same architecture appears twice, once hand-wired and once standardized. Trace one transaction from test to verdict on either side.
HAND-BUILT STACK UVM STACK
──────────────── ─────────
test_lib.sv my_test (uvm_test)
│ case(+TESTNAME) │ +UVM_TESTNAME → factory
▼ ▼
env ─────────────────────────── uvm_env
├─ generator ├─ sequence ─► sequencer
│ │ mailbox.put │ │ seq_item_port
├─ driver ◄──┘ ├─ driver ◄──────┘ (pull + done)
│ │ virtual if │ │ virtual if (config db)
├─ monitor ├─ monitor
│ │ mailbox per consumer │ │ analysis_port.write
├─ scoreboard ◄┘ ├─ scoreboard (analysis_imp)
└─ done event / counter └─ objections raised/dropped
│ │
▼ ▼
verdict banner + $finish run_phase ends → report_phasefinish_item that the generator never learns?The generator can run all three items ahead because depth 8 holds them, and it learns nothing about completion. A sequence blocked in finish_item learns that each item completed, and it can read back a response before choosing the next item.
The mailbox version needs two boxes and two puts per transaction, and the monitor knows its consumers. The analysis-port version calls write() once, and subscribers connect themselves without the monitor changing.