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
Output
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
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5
Cron Job Starts