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()andcan_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_portanduvm_put_impoffer the blockingput()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.
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);
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);
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_portanduvm_nonblocking_put_imp try_put()returns 1 if the transaction was accepted and 0 if not, andcan_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