This UVM TLM example puts the pieces of the TLM chapter together in one small testbench: put ports, get ports, TLM FIFOs, and a port-to-export connection across two levels of hierarchy. Transactions flow through three stages that each run at a different speed, and FIFOs absorb the rate differences between them.
Real testbenches look like this more often than the single-connection examples suggest. Stimulus is generated inside one component, passes through a model or a filter, and is consumed somewhere else entirely, with buffering wherever two parts run at different rates.
8 min read | Intermediate Level
What You'll Learn
- Combine put ports, get ports and TLM FIFOs in one hierarchy
- Forward transactions out of one component and into another
- Trace back-pressure through two FIFOs using the simulation log
- Decide where FIFOs belong in a multi-stage flow
The Testbench Structure
- componentA holds subComp1, which produces a packet every 50 ns, and subComp2, which takes one every 100 ns and forwards it out of componentA. A depth-2 FIFO sits between them.
- componentB holds subComp3, which consumes a packet every 200 ns, behind another depth-2 FIFO.
- The test connects componentA's put port to componentB's put export, the only connection between the two.
All examples use the familiar packet class.
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
Inside componentA
subComp1 is the producer from UVM TLM Fifo: it creates a packet every 50 ns and puts it through a blocking put port.
class subComp1 extends uvm_component;
`uvm_component_utils(subComp1)
uvm_blocking_put_port #(Packet) m_put_port;
int m_num_tx;
function new(string name = "subComp1", 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);
repeat (m_num_tx) begin
Packet pkt = Packet::type_id::create("pkt");
assert(pkt.randomize());
#50;
`uvm_info("SUBCOMP1", "Packet sent to compA:tlm_fifo", UVM_LOW)
pkt.print(uvm_default_line_printer);
m_put_port.put(pkt);
end
endtask
endclass
subComp2 has two ports. It pulls packets from the FIFO through a get port every 100 ns, and pushes each one onward through a put port.
class subComp2 extends uvm_component;
`uvm_component_utils(subComp2)
uvm_blocking_get_port #(Packet) m_get_port; // from componentA's FIFO
uvm_blocking_put_port #(Packet) m_put_port; // out of componentA
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_get_port = new("m_get_port", this);
m_put_port = new("m_put_port", this);
endfunction
virtual task run_phase(uvm_phase phase);
Packet pkt;
forever begin
#100;
m_get_port.get(pkt);
`uvm_info("SUBCOMP2", "Packet received from compA:tlm_fifo, forward it", UVM_LOW)
pkt.print(uvm_default_line_printer);
m_put_port.put(pkt);
end
endtask
endclass
componentA creates both subcomponents and the FIFO, wires them together, and forwards subComp2's output through its own port. It also reports every 10 ns while its FIFO is full.
class componentA extends uvm_component;
`uvm_component_utils(componentA)
subComp1 m_subcomp_1;
subComp2 m_subcomp_2;
uvm_tlm_fifo #(Packet) m_tlm_fifo;
uvm_blocking_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_subcomp_1 = subComp1::type_id::create("m_subcomp_1", this);
m_subcomp_2 = subComp2::type_id::create("m_subcomp_2", this);
m_tlm_fifo = new("uvm_tlm_fifo", this, 2); // depth 2
m_put_port = new("m_put_port", this);
m_subcomp_1.m_num_tx = m_num_tx; // set by the test before this runs
endfunction
virtual function void connect_phase(uvm_phase phase);
m_subcomp_1.m_put_port.connect(m_tlm_fifo.put_export);
m_subcomp_2.m_get_port.connect(m_tlm_fifo.get_peek_export);
m_subcomp_2.m_put_port.connect(this.m_put_port); // port to port: out of componentA
endfunction
virtual task run_phase(uvm_phase phase);
forever begin
#10 if (m_tlm_fifo.is_full())
`uvm_info("COMPA", "componentA:TLM_Fifo is now FULL !", UVM_MEDIUM)
end
endtask
endclass
Passing m_num_tx on in build_phase works because build_phase runs top-down: the test sets componentA's value right after creating it, before componentA's own build_phase runs.
Inside componentB
subComp3 is the slowest stage, taking a packet every 200 ns. componentB puts a FIFO in front of it and exposes the FIFO's input through an export.
class subComp3 extends uvm_component;
`uvm_component_utils(subComp3)
uvm_blocking_get_port #(Packet) m_get_port;
int m_num_tx;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_get_port = new("m_get_port", this);
endfunction
virtual task run_phase(uvm_phase phase);
Packet pkt;
repeat (m_num_tx) begin
#200;
m_get_port.get(pkt);
`uvm_info("SUBCOMP3", "Packet received from componentA", UVM_LOW)
pkt.print(uvm_default_line_printer);
end
endtask
endclass
class componentB extends uvm_component;
`uvm_component_utils(componentB)
subComp3 m_subcomp_3;
uvm_tlm_fifo #(Packet) m_tlm_fifo;
uvm_blocking_put_export #(Packet) m_put_export;
int m_num_tx;
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_subcomp_3 = subComp3::type_id::create("m_subcomp_3", this);
m_tlm_fifo = new("tlm_fifo", this, 2); // depth 2
m_put_export = new("m_put_export", this);
m_subcomp_3.m_num_tx = m_num_tx;
endfunction
virtual function void connect_phase(uvm_phase phase);
m_put_export.connect(m_tlm_fifo.put_export); // export to export
m_subcomp_3.m_get_port.connect(m_tlm_fifo.get_peek_export);
endfunction
virtual task run_phase(uvm_phase phase);
forever begin
#10 if (m_tlm_fifo.is_full())
`uvm_info("COMPB", "componentB:TLM_Fifo is now FULL !", UVM_MEDIUM)
end
endtask
endclass
The FIFO's put_export is itself an export backed by an imp, so componentB's export simply forwards into it. The run that produced the log below connected the consumers to the FIFO's get_export, an alias of get_peek_export that the library also provides.
The Test
The test picks a random packet count between 4 and 10, passes it to both components, and makes the one connection between them. It holds an objection for 1000 ns so every stage has time to finish.
class my_test extends uvm_test;
`uvm_component_utils(my_test)
componentA compA;
componentB compB;
int m_num_tx;
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("componentA", this);
compB = componentB::type_id::create("componentB", this);
std::randomize(m_num_tx) with { m_num_tx inside {[4:10]}; };
`uvm_info("TEST", $sformatf("Create %0d packets in total", m_num_tx), UVM_LOW)
compA.m_num_tx = m_num_tx;
compB.m_num_tx = m_num_tx;
endfunction
virtual function void connect_phase(uvm_phase phase);
compA.m_put_port.connect(compB.m_put_export); // port to export: across
endfunction
virtual task run_phase(uvm_phase phase);
phase.raise_objection(this);
#1000; // let every stage finish
phase.drop_objection(this);
endtask
endclass
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO testbench.sv(220) @ 0: uvm_test_top [TEST] Create 4 packets in total
UVM_INFO testbench.sv(48) @ 50: uvm_test_top.componentA.m_subcomp_1 [SUBCOMP1] Packet sent to compA:tlm_fifo
pkt: (Packet@2663) { addr: 'h1f data: 'h31 }
UVM_INFO testbench.sv(79) @ 100: uvm_test_top.componentA.m_subcomp_2 [SUBCOMP2] Packet received from compA:tlm_fifo, forward it
pkt: (Packet@2663) { addr: 'h1f data: 'h31 }
UVM_INFO testbench.sv(48) @ 100: uvm_test_top.componentA.m_subcomp_1 [SUBCOMP1] Packet sent to compA:tlm_fifo
pkt: (Packet@2685) { addr: 'hf4 data: 'h91 }
UVM_INFO testbench.sv(48) @ 150: uvm_test_top.componentA.m_subcomp_1 [SUBCOMP1] Packet sent to compA:tlm_fifo
pkt: (Packet@2699) { addr: 'hab data: 'ha8 }
UVM_INFO testbench.sv(126) @ 150: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 160: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 170: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 180: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 190: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(154) @ 200: uvm_test_top.componentB.m_subcomp_3 [SUBCOMP3] Packet received from componentA
pkt: (Packet@2663) { addr: 'h1f data: 'h31 }
UVM_INFO testbench.sv(79) @ 200: uvm_test_top.componentA.m_subcomp_2 [SUBCOMP2] Packet received from compA:tlm_fifo, forward it
pkt: (Packet@2685) { addr: 'hf4 data: 'h91 }
UVM_INFO testbench.sv(48) @ 200: uvm_test_top.componentA.m_subcomp_1 [SUBCOMP1] Packet sent to compA:tlm_fifo
pkt: (Packet@2681) { addr: 'h77 data: 'he7 }
UVM_INFO testbench.sv(126) @ 200: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 210: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 220: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 230: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 240: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 250: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 260: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 270: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 280: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(126) @ 290: uvm_test_top.componentA [COMPA] componentA:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(79) @ 300: uvm_test_top.componentA.m_subcomp_2 [SUBCOMP2] Packet received from compA:tlm_fifo, forward it
pkt: (Packet@2699) { addr: 'hab data: 'ha8 }
UVM_INFO testbench.sv(198) @ 300: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 310: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 320: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 330: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 340: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 350: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 360: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 370: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 380: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 390: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(154) @ 400: uvm_test_top.componentB.m_subcomp_3 [SUBCOMP3] Packet received from componentA
pkt: (Packet@2685) { addr: 'hf4 data: 'h91 }
UVM_INFO testbench.sv(79) @ 400: uvm_test_top.componentA.m_subcomp_2 [SUBCOMP2] Packet received from compA:tlm_fifo, forward it
pkt: (Packet@2681) { addr: 'h77 data: 'he7 }
UVM_INFO testbench.sv(198) @ 400: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 410: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 420: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 430: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 440: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 450: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 460: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 470: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 480: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 490: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 500: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 510: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 520: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 530: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 540: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 550: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 560: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 570: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 580: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(198) @ 590: uvm_test_top.componentB [COMPB] componentB:TLM_Fifo is now FULL !
UVM_INFO testbench.sv(154) @ 600: uvm_test_top.componentB.m_subcomp_3 [SUBCOMP3] Packet received from componentA
pkt: (Packet@2699) { addr: 'hab data: 'ha8 }
UVM_INFO testbench.sv(154) @ 800: uvm_test_top.componentB.m_subcomp_3 [SUBCOMP3] Packet received from componentA
pkt: (Packet@2681) { addr: 'h77 data: 'he7 }
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 1000: 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) @ 1000: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---
Reading the Log
This run created 4 packets. Follow packet @2663 first: subComp1 sends it at 50 ns, subComp2 picks it up and forwards it at 100 ns, and subComp3 finally receives it at 200 ns. The same object travels the whole way, through two FIFOs and four connections.
- componentA's FIFO fills at 150 ns, because subComp1 produces twice as fast as subComp2 consumes. It stays full, apart from the instant at 200 ns when one packet leaves and the last one enters, until subComp2's next get at 300 ns.
- componentB's FIFO fills at 300 ns, when subComp2 forwards the third packet while subComp3 still holds its turn for 200 ns. It drains at 600 ns.
- The last packet reaches subComp3 at 800 ns. The test's objection then keeps the phase open until 1000 ns.
Quick Check
Q: What would happen to subComp2 if componentB's FIFO were removed and subComp3 were connected directly with a blocking put/get?
Show Answer
subComp2's put() would block until subComp3 was ready, so subComp2 would slow down to 200 ns per packet. componentA's FIFO would then stay full for longer and subComp1 would block too. The slowest stage would set the pace for the whole chain.
Where to Put FIFOs
Place a FIFO wherever a fast producer meets a slow consumer and the producer should not be held back. Its depth decides how large a burst can be absorbed before back-pressure reaches the producer. If the end-to-end throughput is limited by the slowest stage anyway, as here, FIFOs only change when earlier stages wait, not how long the whole transfer takes.
Extending the Example
Because each stage only knows its own ports, the structure is easy to change. A monitor-style tap can be added by connecting a subscriber to one of the FIFOs' get_ap analysis ports, which broadcasts every packet as it leaves the FIFO, without touching any of the three subcomponents. A test can also replace subComp3 with a slower or faster derived version through a factory override, and the FIFO behavior changes accordingly, again without editing componentB.
This is the practical payoff of TLM: the components are written once against interfaces, and the testbench around them decides how they are wired and buffered.
Common Mistakes
Mistake #1: Extending uvm_env for a test
The original version of this example declared class my_test extends uvm_env. It still runs, because run_test() creates the class by name, but a test should extend uvm_test so that tools, reports and other engineers recognize it for what it is.
Mistake #2: Ending the test before the slowest stage finishes
Here only the test holds an objection, with a fixed 1000 ns delay. With more packets, subComp3 would still be working when the phase ends. A robust testbench has the final consumer hold an objection until it has received every expected packet.
Mistake #3: Connecting consumers to the wrong side of a FIFO
Producers connect to put_export and consumers to get_peek_export. Swapping them is a type-compatibility error, because a get port cannot use a put interface.
Summary
In this article, you learned:
- Put ports, get ports, FIFOs and port-to-export connections combine naturally in one hierarchy
- A component can pull from one connection and push into another, forwarding transactions between stages
- FIFOs absorb rate differences, and the log shows exactly when each one fills and drains
- The slowest stage limits overall throughput; FIFOs only change where the waiting happens
- The test should extend
uvm_test, and the final consumer is the best place for the end-of-test objection