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
_declmacro 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()
SFXis a suffix you choose, conventionally starting with an underscore, such as_1,_expor_ingress.- The macro defines a new imp class whose name is the normal imp name with
SFXappended. 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
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
_declmacros 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