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. SIMPLE CONNECTION — Port directly to ImpProducer (Initiator)e.g. apb_monitorput_portuvm_blocking_put_port #(T)connect_phase:port.connect(imp)Consumer (Target)e.g. my_scoreboardput_impuvm_blocking_put_imp #(T, scb)Consumer implements task put(T txn); directlyHIERARCHICAL CONNECTION — Port to Export to Imp (wrap through parent)Producerput_portport.connect(outer.export)Parent Component (env or agent)put_exportouter interfaceSub-componentput_imp (implements put())export.connect(imp)resultUse Export when:• Sub-component has the IMP• Parent needs to expose it• connect_phase: parent export → child imp (in parent) 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.
PUSH MODEL — put()MONITORput_portSCOREBOARDput_impControl: put(txn)Data: txn →Initiator decides WHEN to push.Control and data flow in same direction.Initiator: monitor, driver (response)PULL MODEL — get()DRIVERget_portSEQUENCERget_impControl: get(txn)← Data: txn returnedInitiator decides WHEN to pull.Control and data flow in opposite directions.Initiator: driver requesting next txn
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 ────────────────────────────────────────
// At start_of_simulation_phase, UVM automatically checks that all ports
// with min_size > 0 are connected. If a required port is not connected:
// UVM_FATAL: Port "mon.put_port" [INVRST] has 0 connections — 1 requiredBlocking 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 & Debugging
Bug 1 — TLM Connection in build_phase Instead of connect_phase
⚠️ Production Bug — Fatal at start_of_simulation: Target Component Not Yet Built
An engineer calls mon.put_port.connect(scb.put_export) inside build_phase. In build_phase, both components may not be fully constructed yet — their port objects may be null. UVM catches this and fires a null-handle fatal. Even if it doesn't crash immediately, the connection state is undefined because build_phase is not guaranteed to run all components before the first connect call.
// ❌ 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
⚠️ Compile-Time Error — Second Parameter Is the Method Owner
The second template parameter of uvm_blocking_put_imp tells UVM which class implements the put() task. Engineers commonly omit it or use the wrong class. If you use a parent class that doesn't implement put(), the TLM framework can't find the method — compile error or runtime null method dispatch.
// ❌ 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§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
Beginner Level
Intermediate Level
Senior / Architect Level
§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 |