UVM
TLM 1.0: Ports, Exports, Imps
Three actors, push and pull models, blocking vs non-blocking, sequencer-driver TLM.
UVM Fundamentals · Module 12
What TLM Solves — Decoupled Communication
Without TLM, a monitor that needs to report transactions to a scoreboard holds a direct handle to the scoreboard. The monitor class must import the scoreboard's package. Changing the scoreboard requires editing the monitor. The monitor cannot be tested without the scoreboard present. Reuse is impossible.
With TLM, the monitor writes to an analysis port — a standard interface it owns. The scoreboard declares an analysis implementation — a standard interface it responds to. They are connected once, in connect_phase, by whoever built both of them. Neither component knows the other's type.
Decoupling
The monitor only knows it has an analysis_port. The scoreboard only knows it implements write(). Neither imports the other's package. Each can be reused independently.
Standard Interfaces
TLM defines standard methods: put(), get(), peek(), write(). Any component implementing the same interface is interchangeable — the factory override then becomes trivially easy.
Type Safety
TLM classes are parameterised by transaction type T. A put_port #(apb_txn) can only connect to a put_imp #(apb_txn, ...). Type mismatch is caught at compile time.
The Three TLM Actors — Port, Export, Implementation
Every TLM connection involves exactly three roles. Understanding what each one is and who owns it is the foundation of all TLM in UVM.
Figure 1 — TLM connection patterns. Simple: port connects directly to imp (most common). Hierarchical: port connects to export which a parent component provides, passing through to an inner subcomponent's imp.
| Role | Declared By | Class | Key Characteristic |
|---|---|---|---|
| Port | The initiator — the component that INITIATES communication | uvm_blocking_put_port #(T) | Must be connected before simulation starts. Calling methods on an unconnected port causes a fatal error at start_of_simulation. |
| Export | A pass-through wrapper — used when the IMP is inside a subcomponent | uvm_blocking_put_export #(T) | Delegates to an IMP. A parent component uses an export to expose its child's IMP to external connections. Not always needed. |
| Imp (Implementation) | The responder — the component that IMPLEMENTS the method | uvm_blocking_put_imp #(T, comp_type) | The second parameter is the component class that will implement the put() / get() method. The method is called on the containing component directly. |
The Push Model — Initiator Drives the Transaction
In the push model, the initiator calls put(txn) on its port. The call flows through the TLM chain and arrives at the implementation's put() task. The initiator controls timing. The target receives and processes.
// ── PRODUCER — declares a put_port ────────────────────────────────────
class apb_monitor extends uvm_monitor;
`uvm_component_utils(apb_monitor)
// Port: "I need someone to receive my transactions"
uvm_blocking_put_port #(apb_seq_item) put_port;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
put_port = new("put_port", this); // ports use new(), not factory create()
endfunction
task run_phase(uvm_phase phase);
apb_seq_item txn;
forever begin
collect_transaction(txn); // observe bus, build transaction object
put_port.put(txn); // push to whoever is connected
end
endtask
task collect_transaction(ref apb_seq_item txn); /* ... */ endtask
endclass
// ── CONSUMER — declares a put_imp and implements put() ────────────────
class my_scoreboard extends uvm_scoreboard;
`uvm_component_utils(my_scoreboard)
// Imp: "I will respond to put() calls"
// Second parameter = this class (so put() is called on THIS component)
uvm_blocking_put_imp #(apb_seq_item, my_scoreboard) put_export;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
put_export = new("put_export", this);
endfunction
// This task is called by the TLM framework when monitor calls put_port.put()
task put(apb_seq_item txn);
`uvm_info("SCB", $sformatf("Received txn: addr=0x%0h", txn.addr), UVM_MEDIUM)
check_transaction(txn);
endtask
task check_transaction(apb_seq_item txn); /* ... */ endtask
endclass
// ── CONNECTION — in the parent component's connect_phase ──────────────
class my_env extends uvm_env;
apb_monitor mon;
my_scoreboard scb;
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
mon.put_port.connect(scb.put_export); // PORT connects to IMP
endfunction
endclassThe Pull Model — Initiator Requests the Transaction
In the pull model, the initiator calls get(txn) on its port. The call blocks until the target has a transaction available. The initiator controls when it wants data. The target produces on demand. The driver-sequencer relationship is the canonical pull model in UVM.
Figure 2 — Push model (left): initiator calls put(), data flows from initiator to target. Pull model (right): initiator calls get(), data flows from target back to initiator. The driver-sequencer connection is always pull.
// ── INITIATOR — calls get() to request a transaction ──────────────────
class my_consumer extends uvm_component;
`uvm_component_utils(my_consumer)
uvm_blocking_get_port #(apb_seq_item) get_port;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
get_port = new("get_port", this);
endfunction
task run_phase(uvm_phase phase);
apb_seq_item txn;
forever begin
get_port.get(txn); // blocks until producer provides a transaction
process(txn);
end
endtask
task process(apb_seq_item txn); /* ... */ endtask
endclass
// ── TARGET — implements get() ──────────────────────────────────────────
class my_producer extends uvm_component;
`uvm_component_utils(my_producer)
uvm_blocking_get_imp #(apb_seq_item, my_producer) get_export;
apb_seq_item txn_queue[$];
function void build_phase(uvm_phase phase);
super.build_phase(phase);
get_export = new("get_export", this);
endfunction
// Called by TLM when consumer calls get_port.get()
task get(output apb_seq_item txn);
wait (txn_queue.size() > 0); // block until item available
txn = txn_queue.pop_front();
endtask
endclassConnection Rules — What Can Connect to What
| From | To | Valid? | Notes |
|---|---|---|---|
uvm_*_port | uvm_*_imp | YES | Most common. Direct connection from initiator to responder. |
uvm_*_port | uvm_*_export | YES | Used when the imp is inside a subcomponent exposed via export. |
uvm_*_port | uvm_*_port | NO | Ports cannot connect to each other. One side must be an imp or export. |
uvm_*_export | uvm_*_imp | YES | Used inside a component to chain export → imp. Called in the parent's connect_phase. |
uvm_*_export | uvm_*_export | YES | For multi-level hierarchical passing through intermediate components. |
uvm_analysis_port | Multiple imps | YES | Analysis ports support fanout — one port, many subscribers. Covered in Module 13. |
// ── connect_phase: correct placement ──────────────────────────────────
class my_env extends uvm_env;
apb_monitor mon;
my_scoreboard scb;
// build_phase: create components
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = apb_monitor::type_id::create("mon", this);
scb = my_scoreboard::type_id::create("scb", this);
endfunction
// connect_phase: wire up TLM — called AFTER all build_phases complete
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
mon.put_port.connect(scb.put_export); // ← correct place
endfunction
endclass
// ── Checking port connectivity ────────────────────────────────────────
// UVM resolves bindings during END_OF_ELABORATION, and uvm_port_base checks
// each port's connection count against its min_size. A shortfall is reported
// as a UVM_ERROR with the id "Connection Error" - not a fatal, and not during
// start_of_simulation:
//
// UVM_ERROR @ 0: reporter [Connection Error] connection count of 0 does not
// meet required minimum of 1
//
// The distinction matters when you go looking for it: grepping for UVM_FATAL
// finds nothing, and the run continues past the error rather than stopping.
//
// CRUCIALLY, this check only fires for ports whose min_size is non-zero.
// uvm_analysis_port is constructed with min_size = 0, because broadcasting to
// zero subscribers is legal - so an unconnected analysis port passes every
// connectivity check UVM performs. That asymmetry is the subject of the Debug
// Lab in section 10b.Blocking vs. Non-Blocking Interfaces
TLM interfaces come in three flavours. The choice determines whether the caller must wait for the operation to complete.
| Interface | Methods | Returns | Use When |
|---|---|---|---|
Blocking uvm_blocking_put_* | task put(T txn) task get(output T txn) | Task — can consume time. Caller blocks until complete. | The default for most connections. Driver blocks waiting for next item. Monitor blocks waiting for bus idle. |
Non-Blocking uvm_nonblocking_put_* | function bit try_put(T txn) function bit try_get(output T txn) function bit can_put() | Function — returns immediately. Returns 1 on success, 0 if busy/empty. | Polling patterns. Try to put without blocking. Check if data is available before committing. |
Combined uvm_put_* | All of the above — both blocking and non-blocking | Provides both task and function interfaces. | When the target needs to support both blocking and non-blocking callers. |
// ── Non-blocking put: try without blocking ────────────────────────────
uvm_nonblocking_put_port #(apb_seq_item) nb_put_port;
task run_phase(uvm_phase phase);
apb_seq_item txn;
forever begin
@(posedge vif.clk);
if (nb_put_port.can_put()) begin // check before trying — function, no time
build_txn(txn);
if (!nb_put_port.try_put(txn)) // returns 0 if target is busy — function
`uvm_warn("TLM", "try_put failed — target busy")
end
end
endtask
// ── Implementation of non-blocking interface ───────────────────────────
// Must implement BOTH can_put() and try_put() when using uvm_nonblocking_put_imp
function bit can_put();
return (queue.size() < 10); // 1 = there is space, 0 = full
endfunction
function bit try_put(apb_seq_item txn);
if (queue.size() >= 10) return 0; // 0 = rejected
queue.push_back(txn);
return 1; // 1 = accepted
endfunctionThe Sequencer-Driver TLM Connection — The Most Important Pattern
The sequencer-driver handshake is a specialised TLM pull connection built into UVM. The driver pulls items from the sequencer using a request-response protocol: get_next_item() blocks until an item is available, and item_done() signals completion.
// ── DRIVER — uses seq_item_port (inherited from uvm_driver) ──────────
class apb_driver extends uvm_driver #(apb_seq_item);
`uvm_component_utils(apb_driver)
// uvm_driver already declares:
// uvm_seq_item_pull_port #(REQ, RSP) seq_item_port;
task run_phase(uvm_phase phase);
apb_seq_item req;
forever begin
// Pull: blocks until sequencer has an item for us
seq_item_port.get_next_item(req);
// Drive the DUT signals based on req
@(posedge vif.clk);
vif.paddr = req.addr;
vif.pwdata = req.data;
vif.pwrite = req.write;
vif.psel = 1;
vif.penable = 1;
@(posedge vif.clk iff vif.pready);
// Signal completion — sequencer can release the item
seq_item_port.item_done();
// Optional: fill response fields before item_done if RSP != REQ
end
endtask
endclass
// ── SEQUENCER — instantiated, connected in the agent ──────────────────
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
apb_driver drv;
uvm_sequencer #(apb_seq_item) seqr;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = apb_driver::type_id::create("drv", this);
seqr = uvm_sequencer#(apb_seq_item)::type_id::create("seqr", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// Connect driver's pull port to sequencer's export
drv.seq_item_port.connect(seqr.seq_item_export);
endfunction
endclass
// ── get_next_item() vs try_next_item() ────────────────────────────────
// get_next_item(req) — blocks until item available (most common)
// try_next_item(req) — returns null if no item available (non-blocking)
// item_done() — must be called after processing (always)
// item_done(rsp) — optional response field back to sequenceQuick Reference — TLM Class Names and Patterns
| Need | Port (initiator) | Imp (target) | Method implemented |
|---|---|---|---|
| Push a transaction | uvm_blocking_put_port #(T) | uvm_blocking_put_imp #(T, comp) | task put(T txn) |
| Pull a transaction | uvm_blocking_get_port #(T) | uvm_blocking_get_imp #(T, comp) | task get(output T txn) |
| Peek without consuming | uvm_blocking_peek_port #(T) | uvm_blocking_peek_imp #(T, comp) | task peek(output T txn) |
| Non-blocking push | uvm_nonblocking_put_port #(T) | uvm_nonblocking_put_imp #(T, comp) | bit try_put(T), bit can_put() |
| Broadcast (one-to-many) | uvm_analysis_port #(T) | uvm_analysis_imp #(T, comp) | function void write(T txn) |
| Sequencer-driver | seq_item_port (in uvm_driver) | seq_item_export (in uvm_sequencer) | Built-in TLM FIFO internally |
// ── TLM Rules ─────────────────────────────────────────────────────────
//
// 1. PORT declares "I initiate" IMP declares "I respond"
// 2. PORT connects to IMP or EXPORT — never PORT to PORT
// 3. All .connect() calls go in connect_phase(), not build_phase()
// 4. Ports with min_connections > 0 must be connected — fatal if not
// 5. Both PORT and IMP are parameterised by T — types must match exactly
// 6. IMP second parameter is the component class that implements the method
// 7. Analysis ports support fanout; all other ports are point-to-point
// 8. For seqr-driver: always call item_done() after get_next_item()
// ── Naming convention (used in all UVM VIPs) ─────────────────────────
// Ports ending in _port are initiator side (driver, monitor's output)
// Ports ending in _export are target side (scoreboard's input face)
// Despite name: uvm_blocking_put_imp IS the export for the scoreboard
// ── Minimum required code to add TLM to a component ──────────────────
// Producer side:
uvm_blocking_put_port #(my_txn) put_port;
// in build_phase:
put_port = new("put_port", this);
// in run_phase:
put_port.put(txn);
// Consumer side:
uvm_blocking_put_imp #(my_txn, my_consumer) put_export;
// in build_phase:
put_export = new("put_export", this);
// implement the method:
task put(my_txn txn); /* process txn */ endtask
// Connection (in parent component's connect_phase):
producer.put_port.connect(consumer.put_export);§9 — Code Examples
Example 1 — Beginner: Push Model, Monitor → Scoreboard
The most common TLM connection in any testbench. Monitor observes the bus, pushes transactions to a scoreboard for checking. Neither component holds a reference to the other after the connection is made.
// ── Transaction type ──────────────────────────────────────────────────
class apb_txn extends uvm_sequence_item;
`uvm_object_utils(apb_txn)
rand logic [31:0] addr;
rand logic [31:0] data;
rand logic write;
function new(string name = "apb_txn"); super.new(name); endfunction
endclass
// ── Monitor: observes bus, pushes transactions ─────────────────────────
class apb_monitor extends uvm_monitor;
`uvm_component_utils(apb_monitor)
uvm_blocking_put_port#(apb_txn) put_port;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
put_port = new("put_port", this); // ports use new(), NOT factory create()
endfunction
task run_phase(uvm_phase phase);
apb_txn txn;
forever begin
@(posedge vif.clk iff (vif.psel && vif.penable && vif.pready));
txn = apb_txn::type_id::create("txn");
txn.addr = vif.paddr;
txn.data = vif.pwrite ? vif.pwdata : vif.prdata;
txn.write = vif.pwrite;
put_port.put(txn); // blocks until scoreboard's put() returns
end
endtask
endclass
// ── Scoreboard: implements put() — receives transactions ──────────────
class apb_scoreboard extends uvm_scoreboard;
`uvm_component_utils(apb_scoreboard)
uvm_blocking_put_imp#(apb_txn, apb_scoreboard) put_export;
// ↑ Second param is THIS class — put() is called on this component
function void build_phase(uvm_phase phase);
super.build_phase(phase);
put_export = new("put_export", this);
endfunction
task put(apb_txn txn); // TLM calls this when monitor does put_port.put()
`uvm_info("SCB", $sformatf("Checking: addr=0x%0h data=0x%0h write=%0b",
txn.addr, txn.data, txn.write), UVM_MEDIUM)
// ... compare against expected ...
endtask
endclass
// ── Env: creates and connects — note connect_phase, NOT build_phase ───
class apb_env extends uvm_env;
`uvm_component_utils(apb_env)
apb_monitor mon;
apb_scoreboard scb;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = apb_monitor::type_id::create("mon", this);
scb = apb_scoreboard::type_id::create("scb", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
mon.put_port.connect(scb.put_export); // port → imp: the only valid connection
endfunction
endclassExample 2 — Intermediate: Hierarchical Export (Passing Through an Agent)
Real VIP structure: the scoreboard is outside the agent. The monitor is inside the agent. The agent needs to expose the monitor's port as its own export, so the env can connect directly to the agent's boundary.
// ── Inside the agent: expose monitor's put_port as an agent-level port ─
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
apb_driver drv;
apb_monitor mon;
uvm_sequencer#(apb_txn) seqr;
// Agent re-exports the monitor's TLM port as its own interface
// Env connects to apb_agent.mon_port — not to mon.put_port directly
uvm_blocking_put_port#(apb_txn) mon_port; // ← this IS the agent's outward face
function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = apb_driver::type_id::create("drv", this);
mon = apb_monitor::type_id::create("mon", this);
seqr = uvm_sequencer#(apb_txn)::type_id::create("seqr", this);
mon_port = new("mon_port", this); // agent's own port wrapping mon's port
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
drv.seq_item_port.connect(seqr.seq_item_export);
// Chain the agent's port to monitor's port
// The actual IMP is in the scoreboard — connected from env
mon.put_port.connect(mon_port); // ← port-to-port OK here? Actually use analysis port for this pattern
endfunction
endclass
// NOTE: The cleaner industry approach is to use uvm_analysis_port for monitors
// (covered in Module 13) — it supports fan-out and no direct connection chain needed.
// The above pattern is valid for blocking put TLM where exactly one subscriber exists.
// ── Env connects agent's exposed port to scoreboard ────────────────────
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// Env sees apb_agent.mon_port — doesn't know about mon directly
apb_agent.mon_port.connect(scb.put_export);
endfunctionExample 3 — Verification: Dual-Monitor Scoreboard with Request-Response
A read-back scoreboard: a write monitor pushes expected results, a read monitor pushes actual results, and the scoreboard uses two separate TLM ports to receive both streams. This is a common pattern in memory controller verification.
// ── Scoreboard with two separate TLM ports (one per channel) ─────────
class mem_scoreboard extends uvm_scoreboard;
`uvm_component_utils(mem_scoreboard)
// Two separate put_imps — one for write transactions, one for read results
// Can't use two uvm_blocking_put_imp with the same name on same class without macro
// Solution: use uvm_tlm_analysis_fifo (Module 14) OR the analysis port pattern
// For blocking put with two ports, use uvm_tlm_*_imp_decl macros:
`uvm_blocking_put_imp_decl(_wr) // declares uvm_blocking_put_imp_wr
`uvm_blocking_put_imp_decl(_rd) // declares uvm_blocking_put_imp_rd
uvm_blocking_put_imp_wr#(mem_txn, mem_scoreboard) wr_export;
uvm_blocking_put_imp_rd#(mem_txn, mem_scoreboard) rd_export;
mem_txn expected_q[$]; // write ops stored as expected reads
function void build_phase(uvm_phase phase);
super.build_phase(phase);
wr_export = new("wr_export", this);
rd_export = new("rd_export", this);
endfunction
// Called when write monitor does wr_port.put(txn)
task put_wr(mem_txn txn);
expected_q.push_back(txn);
`uvm_info("SCB", $sformatf("Expected: addr=0x%0h data=0x%0h",
txn.addr, txn.data), UVM_HIGH)
endtask
// Called when read monitor does rd_port.put(txn)
task put_rd(mem_txn actual);
mem_txn expected;
if (expected_q.size() == 0) begin
`uvm_error("SCB", "Read response received but no pending expected")
return;
end
expected = expected_q.pop_front();
if (actual.data !== expected.data)
`uvm_error("SCB", $sformatf("MISMATCH addr=0x%0h exp=0x%0h got=0x%0h",
actual.addr, expected.data, actual.data))
else
`uvm_info("SCB", "MATCH", UVM_HIGH)
endtask
endclass
// ── Env connection ─────────────────────────────────────────────────────
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
wr_mon.put_port.connect(scb.wr_export); // write channel
rd_mon.put_port.connect(scb.rd_export); // read channel
endfunctionExample 4 — Tricky: get_next_item() Without item_done() — The Deadlock
The single most common TLM bug in new driver implementations. The sequence never advances, the simulation hangs, and the timeout message points nowhere useful.
// ❌ DEADLOCK — item_done() never called ──────────────────────────────
task run_phase(uvm_phase phase);
apb_txn req;
forever begin
seq_item_port.get_next_item(req); // blocks → gets item 1 → continues
@(posedge vif.clk);
vif.paddr <= req.addr;
vif.pwdata <= req.data;
@(posedge vif.clk iff vif.pready);
// FORGOT item_done() here!
// Second call to get_next_item() → sequencer sees item 1 still "in flight"
// → deadlock: sequencer holds item 1 locked, never sends item 2
// → simulation runs until UVM_TIMEOUT fires (typically 1ms later)
end
endtask
// UVM_FATAL timeout: simulation timed out after 1ms with objection held
// No useful stack trace — the real cause is inside get_next_item()
// ✓ CORRECT — item_done() after every get_next_item() ─────────────────
task run_phase(uvm_phase phase);
apb_txn req;
forever begin
seq_item_port.get_next_item(req);
@(posedge vif.clk);
vif.paddr <= req.addr;
vif.pwdata <= req.data;
@(posedge vif.clk iff vif.pready);
seq_item_port.item_done(); // ← mandatory: releases item 1, sequencer sends item 2
end
endtask
// Rule: every get_next_item() must be paired with exactly one item_done()
// Think of it as a lock: get_next_item() acquires, item_done() releases.
// Missing item_done() leaves the lock held forever.§10 — Bugs and Debugging
Bug 1 — TLM Connection in build_phase Instead of connect_phase
// ❌ WRONG — TLM connect in build_phase ───────────────────────────────
class apb_env extends uvm_env;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = apb_monitor::type_id::create("mon", this);
scb = apb_scoreboard::type_id::create("scb", this);
mon.put_port.connect(scb.put_export); // ← WRONG — connect in build_phase
endfunction
endclass
// Possible result 1: null handle if scb.put_export not yet constructed
// Possible result 2: UVM_FATAL: connect called before end_of_elaboration
// Either way, the simulation doesn't run.
// ✓ CORRECT — create in build_phase, connect in connect_phase ─────────
class apb_env extends uvm_env;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = apb_monitor::type_id::create("mon", this);
scb = apb_scoreboard::type_id::create("scb", this);
// NO connect here — just create
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
mon.put_port.connect(scb.put_export); // ← CORRECT place
// By now: ALL build_phases in the hierarchy have completed
// All components and their ports are fully constructed
endfunction
endclassBug 2 — Forgetting the Second Parameter of uvm_blocking_put_imp
// ❌ WRONG — using base class as second parameter ─────────────────────
class my_scoreboard extends uvm_scoreboard;
// Second param = uvm_scoreboard → tries to call put() on uvm_scoreboard
// uvm_scoreboard does not have a put() method → compile error
uvm_blocking_put_imp#(apb_txn, uvm_scoreboard) put_export; // ← WRONG
task put(apb_txn txn); /* ... */ endtask
endclass
// Error: 'put' is not a member of 'uvm_scoreboard'
// ❌ WRONG — trying to create a put_imp for a different class ─────────
uvm_blocking_put_imp#(apb_txn, other_class) put_export;
// TLM will call put() on 'other_class' — but put_export is inside my_scoreboard
// The second parameter must be the class that CONTAINS this imp and implements put()
// ✓ CORRECT — second parameter is THIS class ──────────────────────────
class my_scoreboard extends uvm_scoreboard;
uvm_blocking_put_imp#(apb_txn, my_scoreboard) put_export; // ← CORRECT
// Second param = my_scoreboard → TLM calls my_scoreboard::put()
task put(apb_txn txn); /* called by TLM framework */ endtask
endclass
// Rule: the second parameter of *_imp is always the class that:
// 1. Declares the imp object
// 2. Implements the corresponding method (put/get/peek)Bug 3 — Port-to-Port Connection (Both Sides Are Initiators)
// ❌ WRONG — two ports trying to connect to each other ────────────────
// monitor.sv:
uvm_blocking_put_port#(apb_txn) put_port; // ← port (initiator)
// scoreboard.sv (WRONG — should be imp, not port):
uvm_blocking_put_port#(apb_txn) put_port; // ← also a port — no put() implemented
// env.sv:
mon.put_port.connect(scb.put_port); // ← both initiators — invalid
// Result: UVM_FATAL at start_of_simulation_phase:
// "port 'mon.put_port' has 0 connections to an implementation"
// ✓ CORRECT — scoreboard declares an imp, not a port ─────────────────
// scoreboard.sv:
uvm_blocking_put_imp#(apb_txn, apb_scoreboard) put_export; // ← imp (responder)
task put(apb_txn txn); /* process */ endtask
// env.sv:
mon.put_port.connect(scb.put_export); // ← port → imp: valid
// Memory rule:
// _port = "I start communication" = needs someone to receive
// _imp = "I receive communication" = implements the method
// If you're implementing put()/get()/write() → you need an _imp, not a _port§10b — The Connection UVM Will Not Check For You
Every connectivity check in the previous section rests on min_size. A port
declares how many connections it requires, resolve_bindings counts what it got,
and a shortfall is reported. The gap is that uvm_analysis_port is constructed
with min_size = 0, because broadcasting to nobody is a legal thing to do — so
the one connection most likely to be forgotten is the one UVM is guaranteed not
to complain about.
The scoreboard compared nothing for three weeks and every regression passed
ANALYSIS-PORT-UNCONNECTEDA block-level environment reported clean regressions after a refactor that moved
the monitor from the agent into a wrapper component. Every test passed,
UVM_ERROR was zero, and coverage looked plausible because the coverage
collector had its own connection and was still receiving transactions.
The scoreboard's compare count was zero on every run. Nobody noticed for three weeks, because a scoreboard that compares nothing reports nothing — there is no mismatch to print and no error to raise.
It surfaced when a deliberately-injected DUT bug failed to be caught, and the first hypothesis was that the injection had not worked.
class my_env extends uvm_env;
`uvm_component_utils(my_env)
apb_agent agent;
mon_wrapper wrap; // <- monitor moved in here during the refactor
my_scoreboard scb;
apb_coverage cov;
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// The coverage connection was updated for the new path...
wrap.mon.ap.connect(cov.analysis_export);
// ...and the scoreboard connection still names the OLD path, which no
// longer exists. It was deleted rather than re-pointed.
// agent.mon.ap.connect(scb.analysis_export); <- removed, never restored
endfunction
endclassprint_topology() showed both components present and correctly built, which
ruled out a construction problem. The decisive query was the connection count on
the port itself:
`uvm_info("DBG", $sformatf("mon.ap has %0d subscriber(s)",
wrap.mon.ap.size()), UVM_LOW)
// -> mon.ap has 1 subscriber(s) <-- coverage only; scoreboard missingOne subscriber where two were expected. And critically, no connectivity error
was reported at end of elaboration, because uvm_analysis_port is constructed
with min_size = 0 — zero subscribers is a legal configuration, so one subscriber
is legal too, and UVM has no way to know that two were intended.
The confirming evidence was the scoreboard's own counter: n_compared == 0 at end
of test, on a run that had driven several thousand transactions.
The scoreboard's analysis connection was deleted during a refactor and never restored. Nothing detected it because nothing could.
The asymmetry is the whole lesson. A uvm_blocking_put_port has min_size = 1:
it is useless unconnected, so UVM checks it and reports a Connection Error at
end of elaboration. A uvm_analysis_port has min_size = 0: broadcasting to zero
subscribers is a legitimate configuration — a monitor in an environment that does
not happen to want its transactions — so there is no shortfall to report.
That makes the analysis connection the one place in TLM where the framework's safety net does not extend, and it is also the connection most likely to be touched by refactoring, because it is the one that crosses component boundaries.
The failure mode compounds it. A missing put connection fails loudly at elaboration. A missing analysis connection produces a silent environment: the monitor broadcasts, the port has zero or too-few subscribers, and every checker downstream of it simply never runs. The regression does not degrade — it reports success while verifying less than it did the day before.
Restore the connection, and then add the check that would have caught its absence, because the same refactor will happen again:
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
wrap.mon.ap.connect(cov.analysis_export);
wrap.mon.ap.connect(scb.analysis_export); // restored
endfunction
// UVM cannot check this for you, so assert the environment's own expectation.
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
if (wrap.mon.ap.size() != 2)
`uvm_fatal("ENV", $sformatf(
"monitor analysis port has %0d subscribers, expected 2 (scoreboard, coverage)",
wrap.mon.ap.size()))
endfunctionThe second, independent guard belongs in the scoreboard, and it catches the whole family rather than this instance:
function void check_phase(uvm_phase phase);
super.check_phase(phase);
if (n_compared == 0)
`uvm_error("SCB", "scoreboard compared nothing - is its analysis port connected?")
endfunctionAn end-of-test check that the checker did any work is the cheapest protection
against every variant of this: a deleted connection, a subscriber connected to the
wrong port, a monitor that never sampled, a run_phase that returned early.
The rule to carry: min_size is the framework's contract, and analysis ports
opt out of it. Anywhere UVM cannot know your intent — how many subscribers you
meant to have, whether a checker was supposed to run — the environment has to
state that intent itself, in end_of_elaboration_phase or check_phase. See
analysis ports for the broadcast semantics and
why scoreboards exist for what is lost
when one silently stops.
§11 — Ready-to-Run Code
A complete, self-contained TLM push model demo. A producer pushes five transactions through a blocking put port to a consumer that implements put(). No UVC, no DUT — just the TLM pattern in isolation so you can study the flow.
// tlm_push_demo.sv — complete runnable TLM push model demo
// Compile: vlog -sv tlm_push_demo.sv
// Run: vsim -c work.tb_tlm_top +UVM_TESTNAME=tlm_push_test -do "run -all; quit"
`include "uvm_macros.svh"
import uvm_pkg::*;
// ── Simple transaction ─────────────────────────────────────────────────
class simple_txn extends uvm_sequence_item;
`uvm_object_utils(simple_txn)
int id;
int data;
function new(string name = "simple_txn"); super.new(name); endfunction
endclass
// ── Producer: pushes transactions through its put_port ─────────────────
class txn_producer extends uvm_component;
`uvm_component_utils(txn_producer)
uvm_blocking_put_port#(simple_txn) put_port;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
put_port = new("put_port", this);
endfunction
task run_phase(uvm_phase phase);
simple_txn txn;
phase.raise_objection(this);
for (int i = 0; i < 5; i++) begin
txn = simple_txn::type_id::create($sformatf("txn%0d",i));
txn.id = i;
txn.data = (i + 1) * 16'hAA;
`uvm_info("PROD", $sformatf("Pushing txn[%0d] data=0x%0h", i, txn.data), UVM_LOW)
put_port.put(txn); // blocks until consumer's put() returns
`uvm_info("PROD", $sformatf("txn[%0d] accepted by consumer", i), UVM_LOW)
end
phase.drop_objection(this);
endtask
endclass
// ── Consumer: implements put() ─────────────────────────────────────────
class txn_consumer extends uvm_component;
`uvm_component_utils(txn_consumer)
uvm_blocking_put_imp#(simple_txn, txn_consumer) put_export;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
put_export = new("put_export", this);
endfunction
task put(simple_txn txn); // called by TLM when producer calls put_port.put()
`uvm_info("CONS", $sformatf("Received txn[%0d] data=0x%0h — processing",
txn.id, txn.data), UVM_LOW)
// Simulate processing time (2ns per transaction)
#2;
endtask
endclass
// ── Test: connects producer to consumer ───────────────────────────────
class tlm_push_test extends uvm_test;
`uvm_component_utils(tlm_push_test)
txn_producer prod;
txn_consumer cons;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
prod = txn_producer::type_id::create("prod", this);
cons = txn_consumer::type_id::create("cons", this);
endfunction
function void connect_phase(uvm_phase phase);
prod.put_port.connect(cons.put_export); // THE connection
endfunction
endclass
module tb_tlm_top;
initial run_test();
endmodule
// Expected output:
// UVM_INFO PROD: Pushing txn[0] data=0xaa — processing
// UVM_INFO CONS: Received txn[0] data=0xaa — processing
// UVM_INFO PROD: txn[0] accepted by consumer ← put() returned after 2ns
// UVM_INFO PROD: Pushing txn[1] data=0x154
// UVM_INFO CONS: Received txn[1] data=0x154 — processing
// UVM_INFO PROD: txn[1] accepted by consumer
// ... (repeats 5 times total)
// --- UVM Report Summary ---
// UVM_INFO: 15 UVM_ERROR: 0 → PASS
//
// Key observation: producer blocks on each put() until consumer's put() returns.
// Blocking TLM is synchronous — the chain waits at each step.§12 — Interview Questions
Port — owned by the initiator, the component that starts the communication. It calls the method: put_port.put(txn) or get_port.get(txn). A port declares what it needs, not what it does.
Imp (implementation) — owned by the responder, the component that actually implements the method body. Its second type parameter names the component class that supplies the implementation, so uvm_blocking_put_imp #(apb_txn, my_scoreboard) means "my_scoreboard has a put() and this imp routes calls to it".
Export — a pass-through wrapper, owned by an intermediate parent. It exists so a component can expose a child's imp to the outside world without the outside world needing to know the hierarchy. It implements nothing itself; it delegates.
The rule that follows: a port connects to an imp, directly or through one or more exports, and the chain must terminate in an imp. An export is never the endpoint — it is a forwarding address, and a chain that ends at one has no implementation to call.
A blocking method is a task and may consume time: put() suspends while the target cannot accept, get() suspends while nothing is available. A non-blocking method is a function, cannot consume time, and returns a status instead: try_put() returns 0 rather than waiting.
The choice is about who owns the flow control. Blocking pushes it into the call, which is why a driver's seq_item_port.get_next_item() blocking until a sequence produces an item is exactly right — the driver has nothing to do until then. See driver architecture for that handshake in full and sequencer architecture for the other end of it. Non-blocking keeps control in the caller, which matters when the caller has other work, or is inside a function and cannot suspend at all.
That last constraint is the one that bites. uvm_analysis_port::write() is a void function, so an analysis subscriber's write() cannot block, cannot wait, and cannot report failure. Any real work it wants to do must be queued for a process that can — which is what uvm_tlm_analysis_fifo exists to provide.
Because the component you are connecting to may not exist yet.
build_phase runs top-down: a parent builds, then its children build. So at the moment a parent's build_phase executes, its grandchildren have not been constructed, and a handle like agent.mon.ap is a null reference. connect_phase runs bottom-up and only after every build_phase in the hierarchy has completed, so the whole tree is guaranteed to exist.
The failure is not subtle when it happens — a null-handle dereference at elaboration — which is why this rule is usually learned once and then followed. The subtler consequence is the ordering guarantee it gives you: because connect runs after all builds, you can connect components built by different parents without reasoning about who was constructed first.
The mirror-image rule is worth remembering alongside it: configuration must be set before the component reads it, which for a uvm_config_db value consumed in a child's build_phase means setting it in the parent's build_phase before super.build_phase(). Build for configuration, connect for wiring.
Because an imp routes calls to a method on the containing component, and two imps of the same type would both route to the same write() — leaving no way to tell which monitor a transaction came from.
The mechanism UVM provides is the `uvm_analysis_imp_decl macro. `uvm_analysis_imp_decl(_req) generates a class uvm_analysis_imp_req whose write() calls write_req() on the parent instead. Declare one per stream:
`uvm_analysis_imp_decl(_req)
`uvm_analysis_imp_decl(_rsp)
class my_scoreboard extends uvm_scoreboard;
uvm_analysis_imp_req #(apb_txn, my_scoreboard) req_imp;
uvm_analysis_imp_rsp #(apb_txn, my_scoreboard) rsp_imp;
function void write_req(apb_txn t); ... endfunction
function void write_rsp(apb_txn t); ... endfunction
endclassThe macro must be invoked at file scope, outside any class, and the suffix becomes part of both the generated type name and the method name — which is the detail people get wrong, declaring _req and then implementing write() instead of write_req().
The alternative for a pull-based scoreboard is a uvm_tlm_analysis_fifo per stream, which also separates them and additionally decouples the monitor from the scoreboard's pace.
UVM checks connection counts against min_size when it resolves bindings during end_of_elaboration. A port declaring min_size = 1 that received zero connections is reported — as a UVM_ERROR with the id Connection Error, not a fatal, and the run continues past it.
Knowing the exact form matters when you go looking: grepping the log for UVM_FATAL after an unconnected port finds nothing.
What UVM does not check is intent. uvm_analysis_port is constructed with min_size = 0, because broadcasting to zero subscribers is a legal configuration — so an analysis port that was supposed to feed a scoreboard and now feeds nobody passes every check the framework performs. The environment goes quiet rather than failing: the monitor broadcasts, no subscriber receives, and every checker downstream simply never runs.
So the division is: min_size is the framework's contract, and analysis ports opt out of it. Anywhere UVM cannot know how many subscribers you meant to have, state it yourself:
if (mon.ap.size() != 2) `uvm_fatal("ENV", "expected 2 subscribers")and back it with a check_phase in the scoreboard asserting that it compared something.
Three possibilities, in the order worth checking.
The chain terminates at an export rather than an imp. An export delegates; it implements nothing. A port connected to an export whose own downstream connection was never made has a path that ends in mid-air — and because the export itself has min_size = 1, this usually is reported, so rule it out with the elaboration log first.
The subscriber count is lower than intended. Print port.size(). This is the analysis-port case above: the connection that exists is real, and a second one you assumed was there is not.
The implementation is on the wrong component. The imp's second type parameter names the class whose method will be called, so uvm_analysis_imp #(apb_txn, my_env) declared inside a scoreboard routes write() to the env, not the scoreboard. It compiles, connects, and calls a method on the wrong object — or fails to compile if that object has no write(), which is the lucky case.
If all three are clean, the remaining possibility is that the caller is not reaching the call. Put a uvm_info immediately before the put() or write() rather than inside the receiving method — that distinguishes "the call is not happening" from "the call is not arriving", which are entirely different investigations.
Where This Is Specified
- IEEE 1800.2-2020 (UVM) — TLM 1 interfaces. The port, export and imp roles; the blocking (
task) and non-blocking (function) interface variants; and the requirement that a connection chain terminate in an implementation. - IEEE 1800.2-2020 —
uvm_port_base.min_size/max_size,size(), andresolve_bindings(), which UVM invokes duringend_of_elaborationand which reports a shortfall as aUVM_ERRORwith the idConnection Error. - IEEE 1800.2-2020 —
uvm_analysis_portanduvm_analysis_imp. The broadcast model, the voidwrite()that can neither block nor report failure, and the construction withmin_size = 0that makes zero subscribers legal. - IEEE 1800.2-2020 —
uvm_analysis_imp_decl. Generating suffixed imp types so one component can receive several independent analysis streams. - IEEE 1800.2-2020 — phasing. The top-down
build_phaseand bottom-upconnect_phaseordering that determines when a handle is safe to connect.
§13 — Best Practices
| Rule | Practice | Why It Matters |
|---|---|---|
| BP-1 | Create ports with new(), not type_id::create() | TLM ports are not UVM objects and don't go through the factory — using create() silently fails |
| BP-2 | Always call connect() in connect_phase(), never in build_phase() | build_phase order is not guaranteed to have all children constructed when connect() is called |
| BP-3 | Always pair get_next_item() with exactly one item_done() | Missing item_done() creates a deadlock on the second transaction — simulation hangs silently |
| BP-4 | Second parameter of uvm_*_imp must be the class that declares it and implements the method | Using the wrong class causes compile errors or null method dispatch at runtime |
| BP-5 | Use uvm_analysis_port for monitors, not uvm_blocking_put_port | Analysis ports support fan-out (multiple subscribers), are non-blocking, and don't require a paired imp — the standard for monitor output |
| BP-6 | Use blocking TLM for driver-sequencer; non-blocking for polled/queued patterns | Blocking TLM matches the natural driver pattern — wait for item, drive, done; non-blocking avoids time-consuming waits in tightly-timed polling loops |
| BP-7 | If a scoreboard needs two input channels, use ``uvm_blocking_put_imp_decl` or switch to analysis ports with a FIFO | Multiple put_imps in one class require different method names — the suffix macro handles this automatically |
| BP-8 | Verify all connections at end_of_elaboration — enable UVM port checking with +UVM_PORT_CHECK | Unconnected required ports cause a clean fatal at start_of_simulation; better to catch before simulation starts than during run |
§14 — Summary
| Aspect | Push Model (put) | Pull Model (get) |
|---|---|---|
| Who initiates | Producer (monitor) calls put(txn) | Consumer (driver) calls get(txn) |
| Data flow | Same direction as control — initiator to responder | Opposite to control — data returns from responder to initiator |
| Blocking behaviour | Producer blocks until consumer's put() returns | Consumer blocks until producer provides data |
| Port type (initiator) | uvm_blocking_put_port #(T) | uvm_blocking_get_port #(T) |
| Imp type (responder) | uvm_blocking_put_imp #(T, comp) | uvm_blocking_get_imp #(T, comp) |
| Method implemented | task put(T txn) | task get(output T txn) |
| Canonical UVM use | Monitor → Scoreboard, Producer → Consumer | Driver → Sequencer (via seq_item_port) |
| Real-world alternative | uvm_analysis_port for monitor output (fan-out, non-blocking) | seq_item_port / get_next_item() is the standard driver pattern |
Continue learning
Related tutorials
- Recommended next
uvm_tlm_fifo
FIFO architecture, producer-consumer decoupling, uvm_tlm_analysis_fifo, sizing, complete patterns.
- Recommended next
Report Catchers
Intercepting and modifying report messages before the report server processes them — the uvm_report_catcher callback on the report path, its actions (catch/suppress, throw/pass, change severity to promote or demote, modify or count), the legitimate uses (documented demotions, escalations, noise suppression), and the cardinal danger that an over-broad demotion or suppression masks real errors.