A UVM TLM FIFO, uvm_tlm_fifo, is a ready-made component that stores transactions between two components running at different rates. The producer puts transactions into the FIFO through its put_export, the consumer takes them out in arrival order through its get_peek_export, and the FIFO's depth decides how far the producer can run ahead before it has to wait.
Without a FIFO, a blocking put ties the producer to the consumer's pace on every transaction. With one in between, the producer can send a burst and move on, and the consumer can work through the backlog at its own speed.
7 min read | Intermediate Level
What You'll Learn
- Create a
uvm_tlm_fifowith a chosen depth - Connect a put port and a get port to the FIFO's exports
- Predict when the producer blocks because the FIFO is full
- Use the FIFO's status methods and the unbounded analysis FIFO
Syntax
uvm_tlm_fifo #(T) fifo;
fifo = new("fifo", this, size); // size defaults to 1; 0 means no upper bound
// Exports to connect to
fifo.put_export // put(), try_put(), can_put()
fifo.get_peek_export // get(), try_get(), can_get(), peek(), try_peek(), can_peek()
// Status methods
fifo.size() fifo.used() fifo.is_empty() fifo.is_full() fifo.flush()
uvm_tlm_fifois a component, so it is created with a name and a parent like any other component, plus the depth.- The default depth is 1. A depth of 0 makes the FIFO unbounded, so
put()never blocks. - Because both exports offer blocking and nonblocking methods, any put or get port variant can connect, as long as the transaction type matches.
The Producer and the Consumer
componentA creates a packet every 50 ns and puts it through a blocking put port. componentB takes one every 100 ns through a blocking get port. The consumer is half as fast as the producer.
class componentA extends uvm_component;
`uvm_component_utils(componentA)
uvm_blocking_put_port #(Packet) m_put_port;
int m_num_tx = 2;
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
Packet pkt = Packet::type_id::create("pkt");
assert(pkt.randomize());
#50;
`uvm_info("COMPA", "Packet sent to CompB", UVM_LOW)
pkt.print(uvm_default_line_printer);
m_put_port.put(pkt); // blocks only when the FIFO is full
end
phase.drop_objection(this);
endtask
endclass
class componentB extends uvm_component;
`uvm_component_utils(componentB)
uvm_blocking_get_port #(Packet) m_get_port;
int m_num_tx = 2;
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;
phase.raise_objection(this);
repeat (m_num_tx) begin
#100;
m_get_port.get(pkt); // blocks only when the FIFO is empty
`uvm_info("COMPB", "ComponentA just gave me the packet", UVM_LOW)
pkt.print(uvm_default_line_printer);
end
phase.drop_objection(this);
endtask
endclass
Neither component knows the FIFO exists. componentA calls put() and componentB calls get(), exactly as if they were connected to each other directly.
Connecting Through the FIFO
The test creates a FIFO with depth 2 and connects both ports to it. It also picks a random number of packets between 4 and 10, and checks every 10 ns whether the FIFO is full.
class my_test extends uvm_test;
`uvm_component_utils(my_test)
componentA compA;
componentB compB;
int m_num_tx;
uvm_tlm_fifo #(Packet) m_tlm_fifo;
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);
std::randomize(m_num_tx) with { m_num_tx inside {[4:10]}; };
compA.m_num_tx = m_num_tx;
compB.m_num_tx = m_num_tx;
m_tlm_fifo = new("uvm_tlm_fifo", this, 2); // depth 2
endfunction
virtual function void connect_phase(uvm_phase phase);
compA.m_put_port.connect(m_tlm_fifo.put_export); // producer side
compB.m_get_port.connect(m_tlm_fifo.get_peek_export); // consumer side
endfunction
// Report whenever the FIFO is full
virtual task run_phase(uvm_phase phase);
forever begin
#10;
if (m_tlm_fifo.is_full())
`uvm_info("UVM_TLM_FIFO", "Fifo is now FULL !", UVM_MEDIUM)
end
endtask
endclass
Some older examples, including the run that produced the log below, connect the consumer to get_export instead. The library provides that name as well, and it behaves the same for a get port.
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO testbench.sv(49) @ 50: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2234) { addr: 'ha1 data: 'h64 }
UVM_INFO testbench.sv(91) @ 100: uvm_test_top.compB [COMPB] ComponentA just gave me the packet
pkt: (Packet@2234) { addr: 'ha1 data: 'h64 }
UVM_INFO testbench.sv(49) @ 100: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2256) { addr: 'hc1 data: 'hb9 }
UVM_INFO testbench.sv(49) @ 150: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@2270) { addr: 'hf0 data: 'hae }
UVM_INFO testbench.sv(137) @ 150: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 160: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 170: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 180: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 190: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(91) @ 200: uvm_test_top.compB [COMPB] ComponentA just gave me the packet
pkt: (Packet@2256) { addr: 'hc1 data: 'hb9 }
UVM_INFO testbench.sv(49) @ 200: uvm_test_top.compA [COMPA] Packet sent to CompB
pkt: (Packet@1879) { addr: 'h68 data: 'h7d }
UVM_INFO testbench.sv(137) @ 200: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 210: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 220: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 230: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 240: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 250: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 260: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 270: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 280: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(137) @ 290: uvm_test_top [UVM_TLM_FIFO] Fifo is now FULL !
UVM_INFO testbench.sv(91) @ 300: uvm_test_top.compB [COMPB] ComponentA just gave me the packet
pkt: (Packet@2270) { addr: 'hf0 data: 'hae }
UVM_INFO testbench.sv(91) @ 400: uvm_test_top.compB [COMPB] ComponentA just gave me the packet
pkt: (Packet@1879) { addr: 'h68 data: 'h7d }
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 400: 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) @ 400: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---
Reading the Log
This run chose 4 packets. The chart below tracks how many entries the FIFO held at each moment.
- 50 ns: the first packet goes in. The FIFO holds 1.
- 100 ns: componentB takes packet 1 and componentA puts packet 2. Still 1.
- 150 ns: packet 3 goes in and the FIFO reaches its depth of 2. The monitor loop starts reporting FULL.
- 200 ns: componentB takes packet 2, which makes room, and packet 4 goes straight in. Full again.
- 300 and 400 ns: componentB drains the last two packets, and the run phase ends once it drops its objection.
If componentA had more packets to send, its fifth put() at 250 ns would block until 300 ns, when the consumer next frees a slot. That is the FIFO's back-pressure at work: a full FIFO turns put() back into a blocking wait.
Quick Check
Q: What would change in this example if the FIFO were created with depth 0?
Show Answer
A depth of 0 means no upper bound, so put() would never block and the FULL message would never appear. componentA would finish at 200 ns as before, and componentB would still need until 400 ns to drain the queue, because its own pace does not change.
Choosing a Depth
The depth should cover the longest burst the producer can generate faster than the consumer can absorb. Too small, and the producer stalls more than the real hardware would; unbounded, and a consumer that has stopped working can let the FIFO grow without any warning. A common compromise in scoreboards is an unbounded FIFO plus an end-of-test check that it is empty, so leftover transactions are reported as errors.
Watching Traffic with put_ap and get_ap
Every uvm_tlm_fifo also has two analysis ports. put_ap broadcasts each transaction as it is put into the FIFO, and get_ap broadcasts each transaction as it is taken out or peeked. Connecting a subscriber to them lets you log, count or check the traffic through the FIFO without touching the producer or the consumer.
// In the test's connect_phase: watch everything that leaves the FIFO
m_tlm_fifo.get_ap.connect(m_traffic_logger.analysis_export);
Because they are analysis ports, connecting nothing to them costs nothing, and connecting several subscribers is fine.
The Analysis FIFO
uvm_tlm_analysis_fifo is an unbounded TLM FIFO with an analysis_export on its input. An analysis port can write into it without ever blocking, and a component pulls the stored transactions out with get(). This is the usual way to connect a monitor's analysis port to a scoreboard that processes transactions in its own run_phase, as described in UVM TLM Analysis Port.
uvm_tlm_analysis_fifo #(Packet) m_expected_fifo; // in the scoreboard
// connect_phase in the environment:
// m_agent.m_monitor.ap.connect(m_scoreboard.m_expected_fifo.analysis_export);
Common Mistakes
Mistake #1: Forgetting the FIFO is a component
A uvm_tlm_fifo must be created with a name and a parent, normally in build_phase, so it becomes part of the hierarchy. Creating it with only a name, or after build_phase, leaves it outside the topology and its exports cannot be connected properly.
Mistake #2: Relying on the default depth
Leaving out the size argument creates a FIFO of depth 1, which makes the producer wait after almost every transaction. Pass the depth you actually need, or 0 for an unbounded FIFO.
Mistake #3: Leaving transactions in the FIFO at the end of the test
If the consumer stops early, entries can remain in the FIFO when the simulation ends, and nothing reports them. Check is_empty() or used() in the check_phase and flag leftover transactions as an error.
Mistake #4: Expecting the FIFO to copy transactions
The FIFO stores handles, not copies. If the producer reuses or modifies a packet object after putting it, the entry waiting in the FIFO changes too. Create a fresh object for every put().
Summary
In this article, you learned:
uvm_tlm_fifobuffers transactions between a producer and a consumer running at different rates- Producers connect to
put_exportand consumers toget_peek_export - The depth is set at construction, defaults to 1, and 0 means unbounded
- A full FIFO blocks
put()and an empty FIFO blocksget(), giving natural back-pressure uvm_tlm_analysis_fifois an unbounded FIFO fed through an analysis export