A UVM TLM blocking put port lets one component send a transaction to another by calling put(), and makes the sender wait until the receiver has accepted it. The sender declares a uvm_blocking_put_port, the receiver declares a uvm_blocking_put_imp and implements a put() task, and a parent component connects the two.
Blocking put is the simplest TLM connection and a good model for how all the others work. It also gives you flow control for free: a slow receiver automatically slows the sender down, without any extra handshake code.
7 min read | Intermediate Level
What You'll Learn
- Declare and create a blocking put port in a sender
- Implement the
put()task in a receiver with a put imp - Connect the port to the imp in the parent's
connect_phase - Explain how a blocking
put()stalls the sender
Syntax
uvm_blocking_put_port #(T) port_name; // in the sender
uvm_blocking_put_imp #(T, IMP) imp_name; // in the receiver; IMP is the receiver's class
virtual task put(T t); // the receiver must define this task
Tis the transaction type, and it must be the same on both sides.IMPis the class that implementsput(), almost always the class the imp is declared in. The imp forwards every call to that object'sput()task.- Ports and imps are objects, so they must be created with
new(name, parent), usually inbuild_phase.
The Transaction
A small class, Packet, carries two random fields from sender to receiver.
class Packet extends uvm_object;
rand bit[7:0] addr;
rand bit[7:0] data;
`uvm_object_utils_begin(Packet)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "Packet");
super.new(name);
endfunction
endclass
Step 1: The Sender
componentA owns a uvm_blocking_put_port for Packet objects. In run_phase it creates, randomizes and sends a configurable number of packets.
class componentA extends uvm_component;
`uvm_component_utils(componentA)
uvm_blocking_put_port #(Packet) m_put_port; // sends Packet objects
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); // ports are objects: create them
endfunction
virtual task run_phase(uvm_phase phase);
phase.raise_objection(this);
repeat (m_num_tx) begin
Packet pkt = Packet::type_id::create("pkt");
assert(pkt.randomize());
m_put_port.put(pkt); // returns once componentB accepts pkt
`uvm_info("COMPA", "Packet sent to CompB", UVM_LOW)
pkt.print(uvm_default_line_printer);
end
phase.drop_objection(this);
endtask
endclass
put() is not implemented in componentA. Calling it on the port routes the call to whatever implementation the port is connected to.
Step 2: The Receiver
componentB owns a uvm_blocking_put_imp and defines the put() task that the imp calls. Because the interface is blocking, put() is a task and is allowed to consume time.
class componentB extends uvm_component;
`uvm_component_utils(componentB)
uvm_blocking_put_imp #(Packet, componentB) m_put_imp; // calls this->put()
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
// Implementation of put(): here it simply prints the packet
virtual task put(Packet pkt);
`uvm_info("COMPB", "Packet received from CompA", UVM_LOW)
pkt.print(uvm_default_line_printer);
endtask
endclass
Step 3: Connect in the Parent
Neither component knows about the other. The connection is made one level up, in the component that owns both - here the test.
class my_test extends uvm_test;
`uvm_component_utils(my_test)
componentA compA;
componentB compB;
function new(string name = "my_test", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
compA = componentA::type_id::create("compA", this);
compB = componentB::type_id::create("compB", this);
compA.m_num_tx = 2;
endfunction
// The port is connected to its implementation
virtual function void connect_phase(uvm_phase phase);
compA.m_put_port.connect(compB.m_put_imp);
endfunction
endclass
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO testbench.sv(81) @ 0: uvm_test_top.compB [COMPB] Packet received from CompA
pkt: (Packet@1903) { addr: 'h2d data: 'hd1 }
UVM_INFO testbench.sv(57) @ 0: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@1903) { addr: 'h2d data: 'hd1 }
UVM_INFO testbench.sv(81) @ 0: uvm_test_top.compB [COMPB] Packet received from CompA
pkt: (Packet@2061) { addr: 'h42 data: 'h62 }
UVM_INFO testbench.sv(57) @ 0: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2061) { addr: 'h42 data: 'h62 }
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---
For each packet, the "received" message from componentB appears before the "sent" message from componentA. That is the blocking behavior in action: componentA prints only after put() returns, and put() returns only after componentB's task has run. Both messages refer to the same object, Packet@1903, because TLM passes a handle rather than a copy.
Quick Check
Q: After calling put(pkt), componentA changes pkt.addr and sends the same handle again. What risk does this create?
Show Answer
TLM passes the handle, so if componentB stored the first packet, the change in componentA silently modifies the object componentB is holding. Create a new packet for each transfer, or have the receiver keep a clone.
Seeing the Sender Block
To make the blocking visible, give the receiver's put() some work to do. Here it takes 20 ns to process each packet. In this version, componentA prints its message before calling put(), and the test also prints the topology at the end of elaboration.
virtual task put(Packet pkt);
// The receiver takes some time to process the packet. The sender's
// put() call does not return until this task returns.
`uvm_info("COMPB", $sformatf("Processing packet"), UVM_LOW)
#20;
`uvm_info("COMPB", $sformatf("Processing packet finished ..."), UVM_LOW)
`uvm_info("COMPB", "Packet received from CompA", UVM_LOW)
pkt.print(uvm_default_line_printer);
endtask
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_blocking_put_port - @1972
compB componentB - @1937
m_put_imp uvm_blocking_put_imp - @2011
--------------------------------------------------
UVM_INFO testbench.sv(63) @ 0: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2049) { addr: 'ha1 data: 'h64 }
UVM_INFO testbench.sv(97) @ 0: uvm_test_top.compB [COMPB] Processing packet
UVM_INFO testbench.sv(99) @ 20: uvm_test_top.compB [COMPB] Processing packet finished ...
UVM_INFO testbench.sv(102) @ 20: uvm_test_top.compB [COMPB] Packet received from CompA
pkt: (Packet@2049) { addr: 'ha1 data: 'h64 }
UVM_INFO testbench.sv(63) @ 20: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2059) { addr: 'hc1 data: 'hb9 }
UVM_INFO testbench.sv(97) @ 20: uvm_test_top.compB [COMPB] Processing packet
UVM_INFO testbench.sv(99) @ 40: uvm_test_top.compB [COMPB] Processing packet finished ...
UVM_INFO testbench.sv(102) @ 40: uvm_test_top.compB [COMPB] Packet received from CompA
pkt: (Packet@2059) { addr: 'hc1 data: 'hb9 }
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 40: 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) @ 40: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---
The second packet is announced at 20 ns, not at 0 ns, because componentA spent the first 20 ns inside put() waiting for componentB. The run phase ends at 40 ns, after the second put() returns and componentA drops its objection.
This is the main property of a blocking interface: the receiver controls the pace. If you need the sender to keep running while the receiver is busy, use a nonblocking port, covered in UVM TLM Nonblocking Put Port, or decouple the two with a UVM TLM Fifo.
When to Use a Blocking Put
A blocking put suits any producer that should not run ahead of its consumer. Typical examples are a stimulus component feeding a reference model, or a model forwarding expected transactions to a scoreboard that checks them one at a time. The sender's code stays simple, a plain loop of create, randomize and put, and the receiver decides how fast the loop runs.
It is less suitable when the sender must react to something else while it waits, such as a monitor that has to keep sampling an interface every clock. A monitor should never block on a downstream component, which is why monitors use analysis ports instead, as described in UVM TLM Analysis Port.
Where the Objection Belongs
In the example, componentA raises and drops the objection around its loop. Because every put() blocks until componentB has finished with the packet, dropping the objection after the last put() returns also guarantees that componentB has processed every packet. With a nonblocking connection or a FIFO in between, that is no longer true, and the receiver may need to hold an objection of its own until it has drained its work.
The same port and imp work across levels of hierarchy. If the sender and receiver sit inside other components, add ports and exports on the way up and down, as shown in UVM TLM Port to Export to Imp.
Common Mistakes
Mistake #1: Forgetting to create the port or imp
Declaring uvm_blocking_put_port #(Packet) m_put_port; only declares a handle. Without m_put_port = new("m_put_port", this); in build_phase, the connect() call in the parent dereferences a null handle.
Mistake #2: Implementing put() as a function
Wrong: virtual function void put(Packet pkt);
Right: virtual task put(Packet pkt);
Why? The blocking put interface calls a task. A function with that name does not match, and the imp fails to compile.
Mistake #3: Giving the imp the wrong IMP class
The second parameter of uvm_blocking_put_imp must be the class that defines put(), and the imp must be constructed with that object as its parent. Copying an imp declaration from another class without updating IMP sends the calls to the wrong type, or fails to compile.
Mistake #4: Leaving the port unconnected
A put port requires exactly one connection. If the parent never calls connect(), UVM reports a connection error when it resolves the bindings just before end_of_elaboration_phase, and the run does not start. The message names the port, which makes it quick to find the missing connect().
Summary
In this article, you learned:
- A sender declares
uvm_blocking_put_port #(T)and callsput()on it - A receiver declares
uvm_blocking_put_imp #(T, IMP)and implementsput()as a task - The parent connects them in
connect_phasewithport.connect(imp) - A blocking
put()returns only when the receiver's task finishes, so the receiver sets the pace - TLM passes object handles, not copies