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_fifo with 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_fifo is 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.

Output
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_fifo buffers transactions between a producer and a consumer running at different rates
  • Producers connect to put_export and consumers to get_peek_export
  • The depth is set at construction, defaults to 1, and 0 means unbounded
  • A full FIFO blocks put() and an empty FIFO blocks get(), giving natural back-pressure
  • uvm_tlm_analysis_fifo is an unbounded FIFO fed through an analysis export
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5