Verification

TLM in UVM

By CourseTron Team ·  8 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: TLM (transaction-level modeling) in UVM lets components exchange whole transaction objects through typed ports and exports instead of pin wiggles, so a monitor, scoreboard, or reference model can be wired together with a single connect() call and swapped without touching timing logic.

    In a pin-accurate testbench you spend half your life worrying about when a signal changes. UVM's TLM layer throws that away above the driver: components talk in transactions — a uvm_sequence_item carrying address, data, burst length, response — and let the methodology handle the handshake. This is why a well-built UVM environment can route the same stimulus to a DUT and to a C/SystemC reference model in parallel, something that is painful to do at the signal level. If you are preparing for a verification interview, TLM connectivity is one of the three or four topics that comes up every single time.

    Ports, exports, and imps — who implements the method?

    The single idea that unlocks TLM is this: a port is a caller, an export forwards the call, and an imp actually implements the method. A uvm_blocking_put_port #(txn) declares "I will call put() on someone." A uvm_blocking_put_imp #(txn, my_comp) declares "I provide the put() body." You wire them with port.connect(imp). Every TLM connection is just this: find the side that has the method body, and point everyone else at it.

    Point-to-point TLM through a uvm_tlm_fifoProducerput_portuvm_tlm_fifoput_export / get_exportConsumerget_portput()get()Arrows show the direction of the method call. The FIFO holds the method bodies.Both ends connect INTO the FIFO exports.CourseTron · coursetron.com
    Point-to-point TLM: the FIFO owns the method implementation; producer and consumer only hold ports.

    Rule of thumb: the imp lives in whichever component defines the method body. A monitor has an analysis port (it broadcasts); a scoreboard has an analysis imp (it implements write()). If your connect() fails to compile, you have almost always pointed a port at another port instead of at an imp or export.

    Blocking vs non-blocking: the time-consuming question

    TLM interfaces come in two flavors. Blocking methods (put, get, peek) are tasks: they can consume simulation time and will suspend the calling thread until the transaction completes. Non-blocking methods (try_put, can_get, try_peek) are functions: they return immediately with a bit telling you whether the operation succeeded, so the caller must poll or react.

    InterfaceMethod kindConsumes time?Typical use
    put/get/peektask (blocking)Yes — waitsDriver pulling items, FIFO full/empty flow control
    try_put/try_getfunction (non-blocking)No — returns bitZero-delay models, arbiters that must not stall
    get vs peekboth—peek leaves the item in the channel; get removes it

    Choosing wrong is a classic bug: if you call a blocking get() inside a function-context callback, you get an elaboration error because tasks cannot be called from functions. Conversely, using try_get() in a tight loop without a wait can spin the simulator. See how this interacts with sequencer handshaking in our UVM driver and monitor walkthrough.

    The analysis port — one-to-many broadcast

    The put/get family is strictly point-to-point. For monitoring you want the opposite: one monitor observing the bus, feeding a scoreboard, a coverage collector, and maybe a logger simultaneously. That is the uvm_analysis_port. It is non-blocking by nature, calls a write(txn) on every subscriber, and crucially does not care if zero subscribers are connected — so a monitor never stalls waiting for a listener.

    class my_mon extends uvm_monitor;
      uvm_analysis_port #(bus_txn) ap;
      function void build_phase(uvm_phase phase);
        ap = new("ap", this);
      endfunction
      task run_phase(uvm_phase phase);
        forever begin
          bus_txn t = collect();   // sample the pins
          ap.write(t);             // broadcast to all subscribers
        end
      endtask
    endclass

    Subscribers extend uvm_subscriber #(bus_txn) (which carries a built-in analysis_imp and forces you to define write()), or use uvm_analysis_imp directly. One monitor port can fan out to as many as you connect — in Synopsys VCS and Cadence Xcelium UVM libraries this is implemented as an internal list that is simply iterated on each write().

    analysis_port.write() fans out to every subscriberMonitoranalysis_portScoreboardanalysis_imp write()Coverageanalysis_imp write()Loggeranalysis_imp write()One write() call, zero simulation time, same transaction handle to each subscriber.CourseTron · coursetron.com
    Animated: a single ap.write(txn) fans the same transaction out to three independent subscribers in zero time.

    TLM FIFOs and when to reach for them

    When a producer and consumer run at different rates, a uvm_tlm_fifo #(txn) buffers between them. The producer connects its put_port to the FIFO's put_export; the consumer connects its get_port to the FIFO's get_export. Give it a depth of 0 for an unbounded queue, or a finite size to model real backpressure — a blocking put() into a full FIFO suspends until a get() frees a slot, which is exactly how you model a 16-entry hardware queue.

    For analysis streams that must be buffered (e.g. a scoreboard that drains slower than the monitor produces), use uvm_tlm_analysis_fifo — it has a write() imp on the input side and a blocking get on the output, so the monitor stays non-blocking while the scoreboard pulls at its own pace.

    A worked connection in connect_phase

    function void env::connect_phase(uvm_phase phase);
      // monitor broadcasts to scoreboard
      agent.mon.ap.connect(sb.analysis_export);
      // driver gets items from sequencer (seq_item_port <-> seq_item_export)
      drv.seq_item_port.connect(sqr.seq_item_export);
      // rate-matched path through a fifo
      prod.put_port.connect(fifo.put_export);
      cons.get_port.connect(fifo.get_export);
    endfunction

    Everything happens in connect_phase, which runs top-down after build_phase has constructed the hierarchy. The direction is always initiator.port.connect(target.export_or_imp). Teams building reusable VIP — the kind of work done at design-services shops like Avecas — lean on this so the same agent drops into AXI, APB, or custom-protocol environments with only the sequence item changing.

    connect_phase wiring: initiator.port to target imp/exportagent.mon.apsb.analysis_export.connect()drv.seq_item_portsqr.seq_item_export.connect()prod.put_portfifo.put_export.connect()cons.get_portfifo.get_export.connect()initiator (port)target (export / imp)CourseTron · coursetron.com
    The four connections from the worked example: each call goes from an initiator port to an export or imp.

    Common interview traps

    • Port vs export vs imp confusion — the imp is where the method body lives; only one imp per method.
    • Calling a blocking task from a function — write() is a function, so you cannot call get() inside it; buffer through an analysis FIFO instead.
    • Forgetting analysis ports tolerate zero subscribers — a monitor with an unconnected ap still runs; a put_port with no export connected throws a fatal at connect.
    • Depth semantics — uvm_tlm_fifo depth 0 means unbounded, not empty.

    Want to wire a complete AXI agent with monitor, scoreboard, and TLM FIFOs — and debug the connect errors live in VCS/Xcelium? Build it hands-on in CourseTron's UVM Verification track, or sharpen fundamentals first at our training partner ChipXpert.

    FAQ

    What is the difference between a TLM port and an export?

    A port is the caller side that invokes a method like put() or get(); an export forwards that call toward the component that actually implements it (the imp). You connect a port to an export or directly to an imp.

    Is an analysis port blocking or non-blocking?

    Non-blocking. uvm_analysis_port::write() is a function that returns immediately and broadcasts to every connected subscriber. It never suspends and works even with zero subscribers connected.

    When should I use a uvm_tlm_fifo instead of a direct connection?

    Use a FIFO when producer and consumer run at different rates or when you want to model hardware buffering and backpressure. A finite-depth FIFO blocks the producer's put() when full, modeling a real queue.

    Why does my connect_phase give a "cannot find implementation" error?

    You almost certainly connected a port to another port rather than to an export or imp. Trace the chain until you reach the component that defines the method body, and connect toward it.

    Advertisement