Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
    • TLM 1: Ports, Exports and Analysis
      • Who Calls, Who Implements: Ports, Exports and Imps
      • connect() Rules: Which Way Each Link Points
      • put, get, peek and transport: Flow-Controlled TLM
      • Analysis Broadcast: One write(), Many Listeners
      • TLM1 Channels and Port Cardinality
      • Analysis Connectivity Patterns: imp_decl vs FIFOs
      • Connection Triage and FIFO Sizing: A Debug Playbook
      • The TLM Lessons' Bench File: Full Source
    • TLM 2.0 Sockets
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹TLM 1: Ports, Exports and Analysis
  1. Topics
  2. UVM Verification
  3. Transaction-Level Modeling (TLM)
  4. TLM 1: Ports, Exports and Analysis
  5. put, get, peek and transport: Flow-Controlled TLM

Part 4 · TLM & Analysis · Intermediate

put, get, peek and transport: Flow-Controlled TLM

Blocking and non-blocking put/get, peek without consuming, transport as one request-response call, the sequencer's pull link, and the zero-time spin that no timeout catches.

On this page

    A colleague adds a producer that puts three APB items into a TLM FIFO, logs each return, and pings you: the second put() returned 20 ns after the first, and the third 20 ns after that. Is the FIFO broken?

    Diagram
    UVM_INFO tlm_tb.sv(83) @ 0: uvm_test_top.req [FIFO] put #0 returned
    UVM_INFO tlm_tb.sv(83) @ 20000: uvm_test_top.req [FIFO] put #1 returned
    UVM_INFO tlm_tb.sv(83) @ 40000: uvm_test_top.req [FIFO] put #2 returned

    Nothing is broken. The consumer takes one item every 20 ns, the FIFO holds one item, and a blocking put() waits until there is room. The receiver is setting the pace, which is exactly what these interfaces are for. When do you want that, and which call gives you which behaviour?

    The idea: the receiver controls the pace

    Analysis write() never waits: the monitor reports what happened and moves on. The flow-controlled families are for links where the receiver must be able to say not yet. put hands a transaction over; get takes one and removes it; peek looks at the next one without removing it; transport sends a request and returns its response in one call. Each comes in a blocking form, a task that may wait, and a non-blocking form, a function that returns at once with a status bit.

    Think of a doorway with a doorman. A blocking put queues at the door until they wave you in. try_put asks once and walks away if the answer is no. can_put only asks whether there is room right now; someone else may take that room before you move. Peeking is looking through the window; getting is walking out with the item.

    Family

    Blocking (task)

    Non-blocking (function)

    Port class examples

    put

    put(t)

    try_put(t), can_put()

    uvm_blocking_put_port, uvm_nonblocking_put_port, uvm_put_port (both)

    get

    get(t)

    try_get(t), can_get()

    uvm_blocking_get_port, uvm_get_port

    peek

    peek(t)

    try_peek(t), can_peek()

    uvm_blocking_peek_port, uvm_get_peek_port (get and peek)

    transport

    transport(req, rsp)

    nb_transport(req, rsp)

    uvm_blocking_transport_port, uvm_transport_port

    The provider must implement every method in the interface its imp declares: a uvm_put_imp needs put, try_put and can_put; a uvm_blocking_put_imp needs only put. A component that implements two imps of the same family uses the matching declaration macro, such as ` uvm_blocking_put_imp_decl(_cfg) `, exactly as the scoreboard uses uvm_analysis_imp_decl ``.

    The picture: a blocking put against a depth-1 FIFO

    Sequence diagram: the producer's first put returns immediately; the second blocks until the consumer's get at 20 ns; the third blocks until the get at 40 ns.
    The run behind the opening log. Each blocking put returns only when the FIFO has room, so the consumer's 20 ns pace becomes the producer's pace.

    Build it: producer, consumer, and the three call styles

    Blocking put and get through a FIFO

    These excerpts are from the test that produced the log: a requester with a put port, a responder with a get port, and a uvm_tlm_fifo between them. The FIFO's put_export and get_export are imps, so both components simply connect across to them.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // snippet: from tlm_tb.sv in the course bench
    // requester: uvm_blocking_put_port #(apb_item) pport;
    for (int i = 0; i < 3; i++) begin
      req = apb_item::type_id::create("req");
      req.addr = 8'h10 + 4 * i;
      pport.put(req);                                   // waits while the fifo is full
      `uvm_info("FIFO", $sformatf("put #%0d returned", i), UVM_LOW)
    end
    
    // responder: uvm_blocking_get_port #(apb_item) gport;
    forever begin
      apb_item t;
      #20ns gport.get(t);                               // one item every 20 ns
    end
    
    // test connect_phase (fifo = new("fifo", this): default depth 1)
    req.pport.connect(fifo.put_export);
    rsp.gport.connect(fifo.get_export);

    The FIFO's default depth of 1 is a classic interview question; the FIFO classes and their other knobs are covered in TLM1 channels and port cardinality.

    Non-blocking: try carries the verdict, can is only a hint

    Non-blocking calls suit a component that must keep doing other work, such as a model that services several queues in one thread. The status bit is the whole contract: a try_put that returns 0 did nothing, and the item is still yours to retry or to count as dropped.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet
    uvm_nonblocking_put_port #(apb_item) nb_port;
    apb_item backlog[$];
    
    task run_phase(uvm_phase phase);
      forever begin
        @(posedge vif.clk);                       // one attempt per clock, never a zero-time loop
        while (backlog.size() > 0 && nb_port.try_put(backlog[0]))
          void'(backlog.pop_front());             // accepted: drop it from the backlog
        // can_put() could be checked first, but only try_put's return is binding
      end
    endtask

    peek observes, get consumes

    peek returns the next item and leaves it where it is; a second peek returns the same item. get returns it and removes it. A checker that peeks to decide whether an item is interesting must still get it eventually, or the queue never advances. And a peeked handle is the same object that is still queued, so changing it changes what the later get returns.

    transport: request in, response out, one call

    transport couples a request with its response, which suits a service such as a register model that answers each access. The caller blocks until the response comes back, so request and response can never be mispaired.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // snippet
    class dma_reg_model extends uvm_component;
      `uvm_component_utils(dma_reg_model)
      uvm_blocking_transport_imp #(apb_item, apb_item, dma_reg_model) tr_imp;
      bit [31:0] regs[bit [7:0]];
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
        tr_imp = new("tr_imp", this);
      endfunction
    
      task transport(input apb_item req, output apb_item rsp);
        $cast(rsp, req.clone());
        if (req.write) regs[req.addr] = req.data;
        else           rsp.rdata = regs.exists(req.addr) ? regs[req.addr] : 32'h0;
      endtask
    endclass

    The sequencer link is a specialised pull

    seq_item_port.get_next_item() behaves like a blocking get with two additions: the sequencer arbitrates between sequences before it answers, and the driver must call item_done() to finish the handshake. The pull port also offers get() (take the item and finish at once) and peek(). The details belong to the driver handshake.

    Watch it run

    The opening log is the whole story. put #0 returns at time 0 because the empty FIFO had room for one item. put #1 waits until the consumer's first get at 20 ns (20000 ps), and put #2 until the second at 40 ns. With a depth of 3, all three puts in this test would return at time 0 and no backpressure would be visible at all; a fourth consecutive put would be the first to wait for a get. Depth decides when backpressure starts, never whether the consumer is the bottleneck.

    What breaks: the zero-time spin no timeout catches

    A reference-model engineer wanted the model to service two input queues in one thread, so they wrote a polling loop with non-blocking gets:

    design.sv
    1
    2
    3
    4
    5
    // bad-example: polls forever without letting time advance
    forever begin
      if (cfg_port.try_get(cfg)) apply(cfg);
      if (data_port.try_get(d))  predict(d);
    end

    Both queues were empty at the start of a test, so neither try_get succeeded and the loop went round again, at the same simulation time, forever. Nothing else could run: the producers that would have filled the queues never got a turn. The simulation sat at time 0 burning CPU. The UVM timeout did not fire, because +UVM_TIMEOUT and set_timeout count simulated time, and simulated time had stopped. The regression job was eventually killed by the farm's wall-clock limit with no UVM log at all.

    The fix is to give every polling loop something that can block: a clock edge, an event, or a blocking call. Here the cleanest version uses two threads with blocking gets, one per queue. If one thread must poll, it waits a clock between attempts.

    Interview

    "When would you use blocking put/get, non-blocking calls, or transport?"

    A weak answer: "blocking is for tasks and non-blocking is for functions." True, but it says nothing about why you would choose one.

    A strong answer: "Blocking calls give natural backpressure: the producer waits until the consumer has room, so nothing is lost. Non-blocking calls suit a thread that must keep servicing other work; the try return is the verdict and can_* is only a hint. Transport pairs a request with its response in one call, so they can never be mispaired. A polling loop of try calls must block on something each pass, or it spins in zero time and even the UVM timeout cannot stop it."

    Expect the follow-ups: What is the default depth of a uvm_tlm_fifo? (1) and How is get_next_item different from get? (arbitration and the item_done handshake).

    Check yourself can_put() returned 1, and the very next try_put() returned 0. How is that possible?

    can_put reports the state at the moment you asked. Another producer, in another thread in the same timestep, filled the last slot before your try_put. Only the try_put return is binding, so code must handle 0 even after a positive can_put.

    Check yourself A checker peeks the next item, decides it is not interesting yet, and loops back to peek. What happens?

    If the loop blocks between peeks it sees the same item forever; if it does not, it spins in zero time. Either way the queue never advances. The checker must get the item to consume it, or skip it explicitly.

    Check yourself Why does the UVM timeout not end a simulation stuck in a zero-time try_get loop?

    The timeout is measured in simulated time. A loop that never blocks never lets simulated time advance, so the timeout's own delay never expires. Only a wall-clock limit outside the simulator ends it.

    Next: the monitor links work the other way: no waiting, many listeners. Analysis broadcast shows how one write() reaches every subscriber, in an order you must not rely on.

    Related topics

    • TLM1 channels and port cardinality

    • Analysis broadcast

    • The driver handshake

    • run_test and timeouts

    Previous

    62 / 280

    Next: Analysis Broadcast: One write(), Many Listeners

    Up next

    Up nextAnalysis Broadcast: One write(), Many Listeners

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Who Calls, Who Implements: Ports, Exports and Imps
    • TLM1 Channels and Port Cardinality
    • Analysis Connectivity Patterns: imp_decl vs FIFOs