Skip to content

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.

TLM port export imp diagram
TLM port export imp diagram

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.

RoleDeclared ByClassKey Characteristic
PortThe initiator — the component that INITIATES communicationuvm_blocking_put_port #(T)Must be connected before simulation starts. Calling methods on an unconnected port causes a fatal error at start_of_simulation.
ExportA pass-through wrapper — used when the IMP is inside a subcomponentuvm_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 methoduvm_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.

SystemVerilog — complete push model: producer → consumer
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endclass

The 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 vs Pull TLM model diagram
Push vs Pull TLM model diagram

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.

SystemVerilog — pull model: get() port and implementation
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endclass

Connection Rules — What Can Connect to What

FromToValid?Notes
uvm_*_portuvm_*_impYESMost common. Direct connection from initiator to responder.
uvm_*_portuvm_*_exportYESUsed when the imp is inside a subcomponent exposed via export.
uvm_*_portuvm_*_portNOPorts cannot connect to each other. One side must be an imp or export.
uvm_*_exportuvm_*_impYESUsed inside a component to chain export → imp. Called in the parent's connect_phase.
uvm_*_exportuvm_*_exportYESFor multi-level hierarchical passing through intermediate components.
uvm_analysis_portMultiple impsYESAnalysis ports support fanout — one port, many subscribers. Covered in Module 13.
SystemVerilog — connection in connect_phase
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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.

InterfaceMethodsReturnsUse 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-blockingProvides both task and function interfaces.When the target needs to support both blocking and non-blocking callers.
SystemVerilog — non-blocking interface: try_put and can_put
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endfunction

The 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.

SystemVerilog — complete sequencer-driver TLM pattern
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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 sequence

Quick Reference — TLM Class Names and Patterns

NeedPort (initiator)Imp (target)Method implemented
Push a transactionuvm_blocking_put_port #(T)uvm_blocking_put_imp #(T, comp)task put(T txn)
Pull a transactionuvm_blocking_get_port #(T)uvm_blocking_get_imp #(T, comp)task get(output T txn)
Peek without consuminguvm_blocking_peek_port #(T)uvm_blocking_peek_imp #(T, comp)task peek(output T txn)
Non-blocking pushuvm_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-driverseq_item_port (in uvm_driver)seq_item_export (in uvm_sequencer)Built-in TLM FIFO internally
SystemVerilog — TLM cheat sheet: rules in comments
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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.

SystemVerilog — push model: monitor to scoreboard, complete
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endclass

Example 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.

SystemVerilog — hierarchical TLM: monitor port exposed via agent
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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);
endfunction

Example 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.

SystemVerilog — dual-port scoreboard: write channel + read channel
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endfunction

Example 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.

SystemVerilog — missing item_done() deadlock and fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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

SystemVerilog — wrong phase for connect() and the fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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
endclass

Bug 2 — Forgetting the Second Parameter of uvm_blocking_put_imp

SystemVerilog — wrong second parameter in put_imp
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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)

SystemVerilog — port-to-port: what goes wrong and why
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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.

1

The scoreboard compared nothing for three weeks and every regression passed

ANALYSIS-PORT-UNCONNECTED
Symptom

A 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.

Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endclass
Diagnostic Evidence

print_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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`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 missing

One 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.

Root Cause

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.

Fix

Restore the connection, and then add the check that would have caught its absence, because the same refactor will happen again:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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()))
endfunction

The second, independent guard belongs in the scoreboard, and it catches the whole family rather than this instance:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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?")
endfunction

An 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 — compile and run
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`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
endclass

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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(), and resolve_bindings(), which UVM invokes during end_of_elaboration and which reports a shortfall as a UVM_ERROR with the id Connection Error.
  • IEEE 1800.2-2020 — uvm_analysis_port and uvm_analysis_imp. The broadcast model, the void write() that can neither block nor report failure, and the construction with min_size = 0 that 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_phase and bottom-up connect_phase ordering that determines when a handle is safe to connect.

§13 — Best Practices

RulePracticeWhy It Matters
BP-1Create 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-2Always 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-3Always pair get_next_item() with exactly one item_done()Missing item_done() creates a deadlock on the second transaction — simulation hangs silently
BP-4Second parameter of uvm_*_imp must be the class that declares it and implements the methodUsing the wrong class causes compile errors or null method dispatch at runtime
BP-5Use uvm_analysis_port for monitors, not uvm_blocking_put_portAnalysis ports support fan-out (multiple subscribers), are non-blocking, and don't require a paired imp — the standard for monitor output
BP-6Use blocking TLM for driver-sequencer; non-blocking for polled/queued patternsBlocking TLM matches the natural driver pattern — wait for item, drive, done; non-blocking avoids time-consuming waits in tightly-timed polling loops
BP-7If a scoreboard needs two input channels, use ``uvm_blocking_put_imp_decl` or switch to analysis ports with a FIFOMultiple put_imps in one class require different method names — the suffix macro handles this automatically
BP-8Verify all connections at end_of_elaboration — enable UVM port checking with +UVM_PORT_CHECKUnconnected required ports cause a clean fatal at start_of_simulation; better to catch before simulation starts than during run

§14 — Summary

AspectPush Model (put)Pull Model (get)
Who initiatesProducer (monitor) calls put(txn)Consumer (driver) calls get(txn)
Data flowSame direction as control — initiator to responderOpposite to control — data returns from responder to initiator
Blocking behaviourProducer blocks until consumer's put() returnsConsumer 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 implementedtask put(T txn)task get(output T txn)
Canonical UVM useMonitor → Scoreboard, Producer → ConsumerDriver → Sequencer (via seq_item_port)
Real-world alternativeuvm_analysis_port for monitor output (fan-out, non-blocking)seq_item_port / get_next_item() is the standard driver pattern

Continue learning