UVM: How TLM ports work? (The arduous way)

Wondering how uvm_ports work? One calls an API (let’s say put task) in some class and it can invoke the API in some other class and that too without creating object of other component! Recently I was studying UVM source code for the same and created a uvm_port type infrastructure in pure SV. Basically it is just a matter of passing handles through associative array. Thought of sharing this to make the understanding simple.

We will develop a put port in SV and believe me the other ports are just different flavors of this port. Here is the infrastructure that we are going to develop:

Blocking put port to implementation port connection

uvm_tlm_if_base // –> abstract class
uvm_port base extends uvm_tlm_if_base //–> Main connection takes place here
uvm_blocking_put_port extends uvm_port_base

void
object extends void
producer extends object
consumer extends object

env has producer and consumer
ENV: producer.port.connect(consumer.imp_port)

This is going to be somewhat tricky and lengthy, but it is just very simple if one is comfortable with parameterized class and polymorphism. Here is an EDAPlayground Link for working code.

Every port is extended from uvm_tlm_if_base class. It is an abstract class with all the virtual methods defined. If the child classes does not implement the put/get APIs, then this class are called and it shouts an error.

// uvm_tlm_if_base
virtual class tlm_if_base #(type T1=int, type T2=int);
virtual task put(T1 t);
$display("Error.. Should not come here");
endtask
endclass

uvm_void and uvm_object are the base classes of entire UVM library, lets say they are defined as follows:

// uvm_void
virtual class void_type;
endclass

// uvm_object
class object extends void_type;
endclass

Now, lets have some typedefs and macros for a generalized infrastructure. The first typedef tells a port that whether it is a port, implementation port or an export. This is defined as  uvm_port_type_e enum in uvm_object_globals.svh file. This typedef is usually used to check semantics like a port must be connected to an export or an implementation port. It is used to check the connection rules.

// UVM_PUT_PORT, UVM_IMPLEMENTATION etc.
typedef enum{PORT,IMPL} port_t;

A common new function that will be defined in every port class. The uvm_blocking_put_portuvm_blocking_get_port etc. classes invoke this macro and define their new function. Here the min and max size defines that how many connections can be made to this port (blocking put port).

// UVM_PORT_COMMON
`define PORT_NEW(NAME) \
function new(string name,int min_size=1, int max_size=1); \
super.new(name,PORT,min_size,max_size); \
endfunction

While creating an implementation port, we pass two arguments. Hence we need a different constructor for that. The IMP is a type parameter which is the object of implementation class. This object is taken inside the implementation port class.

// UVM_IMP_COMMON
`define IMP_NEW(NAME,IMP) \
local IMP implement; /* actual consumer class handle*/ \
function new(string name,IMP imp); /* usually we pass a 'this' in 2nd argument, i.e. consumers object*/ \
super.new(name,IMPL,1,1); \
implement = imp; /* implement points to actual consumer object*/ \
endfunction

A uvm_blocking_put_port class has a put task which is used to invoke the implementation port’s put task. The implementation port’s put task in-turn calls the ‘implement‘ object put task. Every task takes an argument as the type of packet to be sent from initiator to target.

// UVM_BLOCKING_PUT_IMP -->> different API for different types of ports
`define PUT_TASK(imp, TYPE, arg) \
task put(TYPE arg); \
imp.put(arg); \
endtask

Now that we have all the base classes ready, we will implement a major class: uvm_port_base. This class is parameterized with the transaction type.

  • The “provided_by” stores the information about what class is feeding that port.
  • The “provided_to” stores about which class is going to provide the implementation.
  • The “imp_list” stores the handles to implementation ports connected to this port.
  • The size of this array is controlled by min_size and max_size variables.
  • m_if” is the actual handle of implementation port that is being invoked. This handle iterates through the indexes of “imp_list” and calls put task one-by-one.

// uvm_port_base
class port_base #(type IF=void_type) extends IF;
typedef port_base #(IF) this_type;
local this_type imp_list[string]; // list of imp ports connected to this port
int min_size, max_size;
string name;
local this_type provided_by[string];
local this_type provided_to[string];
port_t port_type;
protected this_type m_if; // actual handle which calls implementation port's put task

function new(string name,port_t port_type,int min_size,int max_size);
this.name = name;
this.port_type = port_type; // PORT/IMPL
this.min_size = min_size; // redundant over here
this.max_size = max_size; // redundant over here
endfunction

function string get_full_name();
return $sformatf("%m");
endfunction

function port_t get_type();
return port_type;
endfunction

When we connect the ports we pass handle of implementation port as input argument. Thereby, “provided_to” and “provided_by” arrays gets filled up. Note that the actual connection has not yet happened.

function void connect(this_type provider);
// Some semantics checking
// ... check port connection relationship ...
provided_by[provider.get_full_name()] = provider; // list of all the providers/ list of all imp ports connected
provider.provided_to[get_full_name()] = this; // redundant over here
endfunction

Now in the end_of_elaboration_phase, the UVM library calls resolve_bindings API for all the ports. Here the main connections happen. Theimp_listof implementation ports gets filled first by the handle of put-port. Thereafter, theimp_listof ports are filled. This happens for each of the connection in “provided_by” array (which was filled in connect API).

The “set_if” API sets the “m_if” (implementation object pointer) to the first entry in “imp_list” array. Hence at a time, only one of the put task will be invoked.

virtual function void resolve_bindings();
if(port_type == IMPL) begin
imp_list[get_full_name()] = this; // add the "port" directly to the imp port
end else begin
foreach (provided_by[nm]) begin // for each implementation port for this "port"
this_type port;
port = provided_by[nm]; // take handle of imp port
port.resolve_bindings(); // resolve bindings of imp port first
m_add_list(port); // add this imp port to the list in ths "port"
end
end

if(size()) begin
set_if(0); // point m_if to 1st element of imp_list
end
endfunction

function int size();
return imp_list.num();
endfunction

function void set_if(int index=0);
m_if = get_if(index); // get handle of first imp port
endfunction

The “m_add_list” API adds entries to “imp_list”.

local function void m_add_list(this_type provider);
this_type imp;
for (int i = 0; i < provider.size(); i++) begin
imp = provider.get_if(i); // get 1st element of m_imp_list from imp port
if (!imp_list.exists(imp.get_full_name()))
imp_list[imp.get_full_name()] = imp; // add this to m_imp_list of this "port"
end
endfunction

get_if” returns the first element of “imp_list“. This will be invoked multiple times from “set_if” API.

function port_base #(IF) get_if(int index=0);
foreach (imp_list[nm]) begin
if (index == 0)
return imp_list[nm]; // return 1st element only
index--;
end
endfunction
endclass

Finally, lets implement the put and implementation classes. These are nothing but the macros that we discussed earlier. Note that the “`PUT_TASK” macro is called with “m_if” handle. This suggests that the “imp.put” (in the macro definition) will invoke the implementation port’s task.

// uvm_blocking_put_port
class put_port #(type T=int) extends port_base #(tlm_if_base #(T,T));
`PORT_NEW("put_port")
`PUT_TASK(this.m_if,T,t)
endclass

Note that the “`PUT_TASK” for implementation port passes “implement” as an argument which is handle of the actual implementation class.

// uvm_blocking_put_imp
class put_imp #(type T=int,type IMP=int) extends port_base #(tlm_if_base #(T,T));
`IMP_NEW("put_imp",IMP)
`PUT_TASK(implement,T,t) // create consumer.put invoking API
endclass

All the infrastructure work is done at this point. Lets develop a producer and consumer class now. Note that the producer calls a put task five times and the consumer has implemented that task.

class producer extends object;
put_port #(int) p;
function new();
p = new("p");
endfunction

task run();
for(int i=0;i<5;i++) begin
$display($time,"\tCalling put for i=%d",i);
p.put(i);
#1;
end
endtask
endclass

// user defined consumer
class consumer extends object;
put_imp #(int,consumer) p_imp;
function new();
p_imp = new("p_imp",this);
endfunction

task put(int i);
$display($time,"\tReceived i=%d",i);
#1; // some delay for processing
endtask
endclass

As a top level env, we will instantiate the producer and consumer and connect the ports. Note that we need to call “resolve_bindings” API which automatically gets invoked in “end_of_elaboration” phase in UVM.

class env;
producer p1;
consumer c1;
function new();
p1 = new();
c1 = new();
endfunction

function void connect();
p1.p.connect(c1.p_imp);
p1.p.resolve_bindings(); // automatically gets called before end of elab phase
endfunction

task run();
fork
p1.run();
c1.run();
join
endtask

endclass

// top
module top();
env e;

initial begin
e = new();
e.connect();
e.run();
end

endmodule

Here this how all of it works. Thanks for bearing me this long. Here is an EDAPlayground Link for working code. It is the same as we discussed over here. Hope this clears out some of the mystery about UVM ports.

Refer to my next post for an easy way to grasp this out.

-Sharvil

profile for sharvil111 at Stack Overflow, Q&A for professional and enthusiast programmers

3 responses

  1. […] from my previous post about “UVM: How the TLM ports work?“, here I am presenting an easy way to understand the mystery. Today we will develop a simple […]

    Like

  2. Via source code I see that resolve_bindings() gets called from uvm_root . But how ( and from where ) is it specifically called for ports directly ( and not exports/imps )

    Like

    1. Hi, Apologies for the late reply. The resolve_bindings API is called for each of children components. Port/Imps are children components of any uvm_component, hence the resolve_bindings of each of the port gets invoked. Refer to uvm_port_base.svh and uvm_component.svh for more details where resolve_bindings gets called in this fashion: uvm_component->uvm_port_component ->uvm_port_base.

      Let me know if this answers your question or not.

      Liked by 1 person

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: