A UVM TLM nonblocking put port lets a component offer a transaction to another component without ever waiting. Instead of the blocking put() task, the sender calls try_put(), a function that returns 1 if the receiver accepted the transaction and 0 if it did not, or can_put(), which only asks whether the receiver is ready. The sender then decides what to do with the answer.

Nonblocking connections are for senders that must keep running regardless of what the receiver is doing, for example a component that has to react to an interface every clock cycle and cannot afford to be stuck inside a put() call.

7 min read | Intermediate Level

What You'll Learn

  • Declare a nonblocking put port and its imp
  • Implement try_put() and can_put() in a receiver
  • Handle a refused transfer in the sender by retrying
  • Avoid polling loops that never let simulation time advance

Syntax


uvm_nonblocking_put_port #(T)       port_name;   // in the sender
uvm_nonblocking_put_imp  #(T, IMP)  imp_name;    // in the receiver

virtual function bit try_put(T t);   // accept t and return 1, or refuse and return 0
virtual function bit can_put();      // return 1 if a try_put() now would be accepted
  • Both methods are functions, so they cannot consume simulation time. The receiver must answer immediately.
  • A nonblocking imp must implement both functions, even if the sender only ever calls one of them.
  • The plain uvm_put_port and uvm_put_imp offer the blocking put() and both nonblocking functions together.

The Sender

componentA creates packets as before, but offers each one with try_put() and checks the return value.


class componentA extends uvm_component;
  `uvm_component_utils(componentA)

  uvm_nonblocking_put_port #(Packet) m_put_port;
  int m_num_tx;

  function new(string name = "componentA", uvm_component parent = null);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    m_put_port = new("m_put_port", this);
  endfunction

  virtual task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    repeat (m_num_tx) begin
      bit success;
      Packet pkt = Packet::type_id::create("pkt");
      assert(pkt.randomize());

      `uvm_info("COMPA", "Packet sent to CompB", UVM_LOW)
      pkt.print(uvm_default_line_printer);

      // Offer the packet; this call returns immediately
      success = m_put_port.try_put(pkt);
      if (success)
        `uvm_info("COMPA", $sformatf("COMPB was ready to accept and transfer is successful"), UVM_MEDIUM)
      else
        `uvm_info("COMPA", $sformatf("COMPB was NOT ready to accept and transfer failed"), UVM_MEDIUM)
    end
    phase.drop_objection(this);
  endtask
endclass

The Receiver

componentB implements both functions. Here it is always ready, so try_put() accepts every packet.


class componentB extends uvm_component;
  `uvm_component_utils(componentB)

  uvm_nonblocking_put_imp #(Packet, componentB) m_put_imp;

  function new(string name = "componentB", uvm_component parent = null);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    m_put_imp = new("m_put_imp", this);
  endfunction

  // Accept the packet and report success to the sender
  virtual function bit try_put(Packet pkt);
    `uvm_info("COMPB", "Packet received", UVM_LOW)
    pkt.print(uvm_default_line_printer);
    return 1;
  endfunction

  virtual function bit can_put();
    return 1;                        // always ready in this example
  endfunction
endclass

The test connects the two exactly as for a blocking put, with compA.m_put_port.connect(compB.m_put_imp) in its connect_phase, and prints the topology at the end of elaboration.

Output
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(579) @ 0: reporter [UVMTOP] UVM testbench topology:
-----------------------------------------------------
Name            Type                      Size  Value
-----------------------------------------------------
uvm_test_top    my_test                   -     @1836
  compA         componentA                -     @1905
    m_put_port  uvm_nonblocking_put_port  -     @1971
  compB         componentB                -     @1936
    m_put_imp   uvm_nonblocking_put_imp   -     @2010
-----------------------------------------------------

UVM_INFO testbench.sv(60) @ 0: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2048) { addr: 'ha1  data: 'h64  }
UVM_INFO testbench.sv(96) @ 0: uvm_test_top.compB [COMPB] Packet received
pkt: (Packet@2048) { addr: 'ha1  data: 'h64  }
UVM_INFO testbench.sv(66) @ 0: uvm_test_top.compA [COMPA] COMPB was ready to accept and transfer is successful
UVM_INFO testbench.sv(60) @ 0: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2091) { addr: 'hc1  data: 'hb9  }
UVM_INFO testbench.sv(96) @ 0: uvm_test_top.compB [COMPB] Packet received
pkt: (Packet@2091) { addr: 'hc1  data: 'hb9  }
UVM_INFO testbench.sv(66) @ 0: uvm_test_top.compA [COMPA] COMPB was ready to accept and transfer is successful
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 0: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---

Everything happens at time 0, because neither side consumes time. In this example the "Packet sent" message is printed before the packet is offered, so it means "about to send"; the success message after try_put() is what confirms the transfer.

Retrying a Refused Transfer

The real value of try_put() shows when the receiver is sometimes busy. Here componentB decides at random whether it is ready, and componentA retries the same packet every 1 ns until it is accepted.


// componentB: refuse at random to model a busy receiver
virtual function bit try_put(Packet pkt);
  bit ready;
  std::randomize(ready);
  if (ready) begin
    `uvm_info("COMPB", "Packet received", UVM_LOW)
    pkt.print(uvm_default_line_printer);
    return 1;
  end else begin
    return 0;
  end
endfunction

// componentA: rest of the code remains the same, but the send becomes a retry loop
do begin
  success = m_put_port.try_put(pkt);
  if (success)
    `uvm_info("COMPA", $sformatf("COMPB was ready to accept and transfer is successful"), UVM_MEDIUM)
  else
    `uvm_info("COMPA", $sformatf("COMPB was NOT ready to accept and transfer failed, try after 1ns"), UVM_MEDIUM)
  #1;
end while (!success);
Output
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(579) @ 0: reporter [UVMTOP] UVM testbench topology:
-----------------------------------------------------
Name            Type                      Size  Value
-----------------------------------------------------
uvm_test_top    my_test                   -     @1836
  compA         componentA                -     @1905
    m_put_port  uvm_nonblocking_put_port  -     @1971
  compB         componentB                -     @1936
    m_put_imp   uvm_nonblocking_put_imp   -     @2010
-----------------------------------------------------

UVM_INFO testbench.sv(60) @ 0: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2048) { addr: 'ha1  data: 'h64  }
UVM_INFO testbench.sv(74) @ 0: uvm_test_top.compA [COMPA] COMPB was NOT ready to accept and transfer failed, try after 1ns
UVM_INFO testbench.sv(125) @ 1: uvm_test_top.compB [COMPB] Packet received
pkt: (Packet@2048) { addr: 'ha1  data: 'h64  }
UVM_INFO testbench.sv(72) @ 1: uvm_test_top.compA [COMPA] COMPB was ready to accept and transfer is successful
UVM_INFO testbench.sv(60) @ 2: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2104) { addr: 'h68  data: 'h7d  }
UVM_INFO testbench.sv(125) @ 2: uvm_test_top.compB [COMPB] Packet received
pkt: (Packet@2104) { addr: 'h68  data: 'h7d  }
UVM_INFO testbench.sv(72) @ 2: uvm_test_top.compA [COMPA] COMPB was ready to accept and transfer is successful
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 3: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 3: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---

The first offer at 0 ns is refused. After a 1 ns wait the same packet is offered again and accepted. The #1 also runs after a successful attempt, so the second packet is offered at 2 ns and the loop finishes at 3 ns.

A retry loop like this makes the sender behave much like a blocking put(). The difference is that the sender stays in control between attempts and could do other work, give up after a number of tries, or count how often the receiver was busy.

Quick Check

Q: Why must the receiver's try_put() never contain a delay such as #5?

Show Answer

try_put() is a function, and SystemVerilog functions cannot contain timing controls, so the code would not compile. More fundamentally, a nonblocking call promises the sender an immediate answer. A receiver that needs time should refuse the transfer and accept it on a later attempt.

Asking First with can_put()

can_put() lets the sender check readiness before offering anything. In this version the receiver's can_put() returns a random value, and the sender loops until it gets a 1.


// componentB: report readiness at random
virtual function bit can_put();
  return $urandom_range(0,1);
endfunction

// componentA: wait until the receiver says it is ready, then send
`uvm_info("COMPA", $sformatf("Waiting for receiver to be ready ..."), UVM_MEDIUM)
do begin
  success = m_put_port.can_put();
end while (!success);
`uvm_info("COMPA", $sformatf("Receiver is now ready to accept transfers"), UVM_MEDIUM)
m_put_port.try_put(pkt);
Output
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(579) @ 0: reporter [UVMTOP] UVM testbench topology:
-----------------------------------------------------
Name            Type                      Size  Value
-----------------------------------------------------
uvm_test_top    my_test                   -     @1837
  compA         componentA                -     @1906
    m_put_port  uvm_nonblocking_put_port  -     @1972
  compB         componentB                -     @1937
    m_put_imp   uvm_nonblocking_put_imp   -     @2011
-----------------------------------------------------

UVM_INFO testbench.sv(60) @ 0: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2049) { addr: 'ha1  data: 'h64  }
UVM_INFO testbench.sv(81) @ 0: uvm_test_top.compA [COMPA] Waiting for receiver to be ready ...
UVM_INFO testbench.sv(85) @ 0: uvm_test_top.compA [COMPA] Receiver is now ready to accept transfers
UVM_INFO testbench.sv(125) @ 0: uvm_test_top.compB [COMPB] Packet received
pkt: (Packet@2049) { addr: 'ha1  data: 'h64  }
UVM_INFO testbench.sv(60) @ 0: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2059) { addr: 'h24  data: 'hba  }
UVM_INFO testbench.sv(81) @ 0: uvm_test_top.compA [COMPA] Waiting for receiver to be ready ...
UVM_INFO testbench.sv(85) @ 0: uvm_test_top.compA [COMPA] Receiver is now ready to accept transfers
UVM_INFO testbench.sv(125) @ 0: uvm_test_top.compB [COMPB] Packet received
pkt: (Packet@2059) { addr: 'h24  data: 'hba  }
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 0: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---

This run only worked because a random can_put() eventually returns 1 even without time passing. The loop above has no delay, so if the receiver's readiness depended on anything that changes over time, such as a buffer being drained, the loop would spin forever at the same timestamp and the simulation would hang. Always wait inside a polling loop:


while (!m_put_port.can_put())
  @(posedge vif.clk);          // or #1 - anything that lets time advance
void'(m_put_port.try_put(pkt));

Choosing Between Blocking and Nonblocking Put

Ask who should own the waiting. With a blocking put(), the waiting happens inside the receiver's task, and the sender's code stays a simple loop. With try_put(), the receiver answers at once and all of the waiting logic moves into the sender, which makes the sender more complex but also more flexible.

Use a blocking put when the sender has nothing else to do while the receiver is busy. Use a nonblocking put when the sender must keep servicing something else, when it should drop or count transactions the receiver cannot take, or when it needs a timeout on how long it is willing to wait.

Offering Both with uvm_put_port

A receiver that can support both styles uses the combined imp, and each sender then picks the method that suits it.


// Receiver: one imp, all three methods
uvm_put_imp #(Packet, componentB) m_put_imp;

virtual task      put    (Packet pkt);  /* may wait until there is room */  endtask
virtual function bit try_put(Packet pkt);  /* accept now or return 0     */  endfunction
virtual function bit can_put();            /* report readiness           */  endfunction

// Senders can use either kind of port with it
uvm_blocking_put_port    #(Packet) m_blocking_port;    // calls put()
uvm_nonblocking_put_port #(Packet) m_nonblocking_port; // calls try_put() / can_put()

Both ports can connect to the same uvm_put_imp, because the combined imp provides every method either port needs. The reverse does not work: a uvm_put_port cannot connect to a nonblocking-only imp, because the imp has no put() task to offer.

A common real-world use of nonblocking put is a component that models a hardware buffer with limited space. The receiver's try_put() refuses transactions while the buffer is full, and the sender counts the refusals as back-pressure events for coverage.

Common Mistakes

Mistake #1: Polling without letting time advance

A do ... while (!can_put()) loop with no delay or event inside never lets the receiver change state, so it either succeeds by luck or hangs the simulation. Put a clock edge or a delay in every polling loop.

Mistake #2: Implementing only one of the two functions

A uvm_nonblocking_put_imp requires both try_put() and can_put(). Leaving one out fails to compile, and leaving can_put() empty silently returns 0, which tells every caller the receiver is never ready.

Mistake #3: Ignoring the return value of try_put()

If the sender discards the result, a refused packet is silently lost. Either retry, count the drop, or report it, so a busy receiver never makes transactions disappear unnoticed. If ignoring it really is intended, write void'(m_put_port.try_put(pkt)); so the intent is visible in the code.

Summary

In this article, you learned:

  • A nonblocking put uses uvm_nonblocking_put_port and uvm_nonblocking_put_imp
  • try_put() returns 1 if the transaction was accepted and 0 if not, and can_put() only checks readiness
  • Both are functions, so the receiver must answer immediately and must implement both
  • The sender decides what to do with a refusal, for example retrying after a delay
  • Polling loops must always let simulation time advance
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5