The UVM _decl macros, such as `uvm_blocking_put_imp_decl and `uvm_analysis_imp_decl, let one component offer several implementation ports of the same kind, each calling its own method. A component normally has only one put() or write(), so without these macros every connection into it would end up in the same method. With them, transactions from different sources arrive in different methods and can be handled separately.

The most common place you will meet this is a scoreboard. It receives expected transactions from one monitor and actual transactions from another, both through analysis connections, and it must know which is which.

7 min read | Intermediate Level

What You'll Learn

  • Explain why a component cannot simply declare two plain imps of the same kind
  • Use an imp _decl macro to create imps with their own method names
  • Build a two-input scoreboard with `uvm_analysis_imp_decl
  • Avoid the naming rules that trip people up

The Problem

An imp forwards calls to a method with a fixed name. A uvm_blocking_put_imp always calls put(), and a uvm_analysis_imp always calls write(). A SystemVerilog class can only have one method with a given name, so if componentA and componentC both send to componentB through plain put imps, both streams land in the same put() and componentB cannot tell them apart.

Syntax


`uvm_blocking_put_imp_decl(SFX)   // defines class uvm_blocking_put_impSFX, calling put()
`uvm_analysis_imp_decl(SFX)       // defines class uvm_analysis_impSFX, calling write()
  • SFX is a suffix you choose, conventionally starting with an underscore, such as _1, _exp or _ingress.
  • The macro defines a new imp class whose name is the normal imp name with SFX appended. The method it calls also gets the suffix: put_1(), write_exp(), and so on.
  • The macro defines a class, so it must appear outside any class, usually at the top of the file or package that uses it.

A macro exists for every TLM-1 interface: put, get, peek and get_peek in blocking, nonblocking and combined forms, plus transport, master, slave and analysis. For the combined forms, every method gets the suffix, so `uvm_put_imp_decl(_1) expects put_1(), try_put_1() and can_put_1().

Example: Two Senders, Two Put Methods

componentA and componentC each send two packets through an ordinary blocking put port.


class componentA extends uvm_component;
  `uvm_component_utils(componentA)
  uvm_blocking_put_port #(simple_packet) put_port;

  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);
    put_port = new("put_port", this);
  endfunction

  virtual task run_phase(uvm_phase phase);
    repeat (2) begin                              // send two packets
      simple_packet pkt = simple_packet::type_id::create("pkt");
      assert(pkt.randomize());
      `uvm_info("COMPA", "Packet sent to CompB", UVM_LOW)
      pkt.print(uvm_default_line_printer);
      put_port.put(pkt);
    end
  endtask
endclass

// componentC is identical, except that it reports with the ID "COMPC"

componentB uses two _decl macros, declares one imp of each new class, and implements a separate task for each.


`uvm_blocking_put_imp_decl(_1)
`uvm_blocking_put_imp_decl(_2)

class componentB extends uvm_component;
  `uvm_component_utils(componentB)

  uvm_blocking_put_imp_1 #(simple_packet, componentB) put_imp1;   // calls put_1()
  uvm_blocking_put_imp_2 #(simple_packet, componentB) put_imp2;   // calls put_2()

  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);
    put_imp1 = new("put_imp1", this);
    put_imp2 = new("put_imp2", this);
  endfunction

  task put_1(simple_packet pkt);
    `uvm_info("COMPB", "Packet received from put_1", UVM_LOW)
    pkt.print(uvm_default_line_printer);
  endtask

  task put_2(simple_packet pkt);
    `uvm_info("COMPB", "Packet received from put_2", UVM_LOW)
    pkt.print(uvm_default_line_printer);
  endtask
endclass

The environment connects each sender to a different imp.


class my_env extends uvm_env;
  `uvm_component_utils(my_env)
  componentA compA;
  componentC compC;
  componentB compB;

  function new(string name = "my_env", 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);
    compC = componentC::type_id::create("compC", this);
    compB = componentB::type_id::create("compB", this);
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    compA.put_port.connect(compB.put_imp1);
    compC.put_port.connect(compB.put_imp2);
  endfunction
endclass

// Rest of the code: the test creates my_env as m_top_env and keeps the run phase alive
Output
UVM_INFO @ 0: reporter [RNTST] Running test base_test...
UVM_INFO @ 0: reporter [UVMTOP] UVM testbench topology:
---------------------------------------------------
Name            Type                    Size  Value
---------------------------------------------------
uvm_test_top    base_test               -     @2602
  m_top_env     my_env                  -     @2668
    compA       componentA              -     @2699
      put_port  uvm_blocking_put_port   -     @2808
    compB       componentB              -     @2759
      put_imp1  uvm_blocking_put_imp_1  -     @2861
      put_imp2  uvm_blocking_put_imp_2  -     @2910
    compC       componentC              -     @2729
      put_port  uvm_blocking_put_port   -     @2962
---------------------------------------------------

UVM_INFO ./tb/tlm.sv(95) @ 0: uvm_test_top.m_top_env.compC [COMPC] Packet sent to CompB
pkt: (simple_packet@3032) { addr: 'h53  data: 'hc4  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(144) @ 0: uvm_test_top.m_top_env.compB [COMPB] Packet received from put_2
pkt: (simple_packet@3032) { addr: 'h53  data: 'hc4  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(95) @ 0: uvm_test_top.m_top_env.compC [COMPC] Packet sent to CompB
pkt: (simple_packet@3051) { addr: 'h40  data: 'h3f  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(144) @ 0: uvm_test_top.m_top_env.compB [COMPB] Packet received from put_2
pkt: (simple_packet@3051) { addr: 'h40  data: 'h3f  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(61) @ 0: uvm_test_top.m_top_env.compA [COMPA] Packet sent to CompB
pkt: (simple_packet@3014) { addr: 'h2f  data: 'h64  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(139) @ 0: uvm_test_top.m_top_env.compB [COMPB] Packet received from put_1
pkt: (simple_packet@3014) { addr: 'h2f  data: 'h64  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(61) @ 0: uvm_test_top.m_top_env.compA [COMPA] Packet sent to CompB
pkt: (simple_packet@3061) { addr: 'hb8  data: 'h27  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(139) @ 0: uvm_test_top.m_top_env.compB [COMPB] Packet received from put_1
pkt: (simple_packet@3061) { addr: 'hb8  data: 'h27  rwb: 'h0  }

--- UVM Report catcher Summary ---

The topology shows the two generated classes, uvm_blocking_put_imp_1 and uvm_blocking_put_imp_2. Every packet from compC lands in put_2() and every packet from compA in put_1(). Both senders run in parallel at time 0, so which one goes first is decided by the simulator's process scheduling; in this run compC happened to start first.

Quick Check

Q: If you write `uvm_blocking_put_imp_decl(_lo), what class do you declare and what method must the component implement?

Show Answer

The imp class is uvm_blocking_put_imp_lo #(T, IMP) and the method is task put_lo(T t). The suffix is appended to both names exactly as written.

The Common Case: A Two-Input Scoreboard

In practice the analysis version is used far more than the put version. A scoreboard receives expected transactions from one analysis port and actual ones from another, and implements a separate write method for each.


`uvm_analysis_imp_decl(_ingress)
`uvm_analysis_imp_decl(_egress)

class myscoreboard extends uvm_component;
  `uvm_component_utils(myscoreboard)

  uvm_analysis_imp_ingress #(mydata, myscoreboard) ingress;   // calls write_ingress()
  uvm_analysis_imp_egress  #(mydata, myscoreboard) egress;    // calls write_egress()
  mydata ingress_list[$];

  function new(string name, uvm_component parent);
    super.new(name, parent);
    ingress = new("ingress", this);
    egress  = new("egress", this);
  endfunction

  // Transactions going into the DUT become expected results
  function void write_ingress(mydata t);
    ingress_list.push_back(t);
  endfunction

  // Transactions coming out of the DUT are checked against them
  function void write_egress(mydata t);
    mydata exp;
    if (ingress_list.size() == 0)
      `uvm_error("SB", "Output with no matching input")
    else begin
      exp = ingress_list.pop_front();
      if (!t.compare(exp))
        `uvm_error("SB", "Output does not match the expected transaction")
    end
  endfunction
endclass

// In the environment:
//   m_in_agent.m_monitor.ap.connect(m_sb.ingress);
//   m_out_agent.m_monitor.ap.connect(m_sb.egress);

Each method is a function, just like an ordinary write(), so it must not consume time. This simple version assumes the DUT keeps transactions in order; a reordering DUT needs a lookup by ID instead of pop_front().

Notice what the scoreboard does not need to know: which monitors are connected, or where they sit in the hierarchy. The environment decides that with two connect() calls. The same scoreboard can therefore be reused unchanged in a block-level and a chip-level environment, simply by connecting different monitors to its two imps.

An Alternative: One Analysis FIFO per Input

The _decl approach processes every transaction inside a function at the moment it arrives. When the comparison needs time, for example waiting for a delayed response or modeling latency, a different structure works better. Give the scoreboard one uvm_tlm_analysis_fifo per input, connect each monitor to its FIFO's analysis_export, and let the scoreboard's run_phase pull from both.


uvm_tlm_analysis_fifo #(mydata) m_exp_fifo;    // fed by the input monitor
uvm_tlm_analysis_fifo #(mydata) m_act_fifo;    // fed by the output monitor

virtual task run_phase(uvm_phase phase);
  mydata exp, act;
  forever begin
    m_act_fifo.get(act);                        // wait for the next actual
    m_exp_fifo.get(exp);                        // and its expected partner
    if (!act.compare(exp)) `uvm_error("SB", "Mismatch")
  end
endtask

No macro is needed, because the two FIFOs are separate components. Both designs are common; choose _decl for simple, zero-time checking and FIFOs when the checker needs its own thread. The FIFO is described in UVM TLM Fifo.

Name suffixes after what the input means, such as _exp and _act or _ingress and _egress, not after numbers. The suffix appears in method names throughout the scoreboard, so a meaningful name makes the code much easier to follow.

Common Mistakes

Mistake #1: Putting the macro inside the class

The _decl macros expand into a class definition, and SystemVerilog does not allow a class to be defined inside another class in this way. Place the macro calls before the class that uses them, outside it.

Mistake #2: Forgetting the suffix on the method

Wrong: task put(simple_packet pkt); with a uvm_blocking_put_imp_1
Right: task put_1(simple_packet pkt);

Why? The generated imp calls the suffixed name. Without a matching method the component fails to compile.

Mistake #3: Declaring the same suffix twice

Each _decl call defines a class. Calling `uvm_analysis_imp_decl(_exp) in two files that end up in the same package defines the same class twice and causes a compile error. Keep the macro calls in one place, for example at the top of the package.

Mistake #4: Implementing a blocking put method as a function

For put, get and peek imps the blocking methods must be tasks, exactly as for the plain imps. Only analysis write methods and the nonblocking try_/can_ methods are functions.

Summary

In this article, you learned:

  • A plain imp always calls one fixed method, so several imps of the same kind would share it
  • _decl macros define a new imp class whose name and method both carry a suffix you choose
  • The macros must be called outside any class, and each suffix only once per scope
  • The most common use is a scoreboard with separate write_<suffix>() methods for expected and actual transactions
  • Method kinds are unchanged: blocking put and get methods are tasks, analysis writes are functions
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5