How to Build UVM Environment Part – 3

AGENTS

As mentioned, an agent is a container that instantiates the driver, monitor, and sequencer. Agents can be either active or passive. In active mode, all three components are created, while in passive mode only the monitor is created. Since we have already built the necessary components, creating the agent is simply a matter of instantiating them.

Let’s review the code.

class pipe_agent extends uvm_agent;
  protected uvm_active_passive_enum is_active = UVM_ACTIVE;
  
  pipe_sequencer sequencer;
  pipe_driver    driver;
  pipe_monitor   monitor;
  
  `uvm_component_utils_begin(pipe_agent)
    `uvm_field_enum(uvm_active_passive_enum, is_active, UVM_ALL_ON)
  `uvm_component_utils_end
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction: new
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if(is_active == UVM_ACTIVE) begin
      sequencer = pipe_sequencer::type_id::create("sequencer", this);
      driver    = pipe_driver::type_id::create("driver", this);
    end
    
    monitor     = pipe_monitor::type_id::create("monitor", this);
    
    `uvm_info(get_full_name(), "Build Stage Complete", UVM_LOW)
  endfunction: build_phase
  
  function void connect_phase(uvm_phase phase);
    if(is_active == UVM_ACTIVE) begin
      driver.seq_item_port.connect(sequencer.seq_item_export);
    end
    
    `uvm_info(get_full_name(), "Connect Stage Complete", UVM_LOW)
  endfunction: connect_phase
  
endclass: pipe_agent

The first declaration in the agent is the variable “is_active” which is of the type “uvm_active_passive_enum”. This enumerated type can be found in the UVM library and has two possible values: UVM_PASSIVE and UVM_ACTIVE.

I have assigned it UVM_ACTIVE, but this can be changed to UVM_PASSIVE when needed via the configuration database. Notice that I have included “is_active” in the field automation list with the `uvm_field_enum list. By doing this,
when I instantiate this agent using the factory “is_active” will be given the value in the configuration database and that will override the default. This concept will be illustrated momentarily when we discuss the environment.
The build_phase function implementation is quite simple. For an agent configured to be active, it creates the sequencer, driver, and then the monitor. If the agent is configured to be passive, then it only creates the monitor. In the connect_phase, we once again see the use of the connect function. Here, the driver’s communication port is connected to the sequencer’s communication export.

Once again, this is why UVM is so powerful. In one line of code, it appears that I have connected my driver and sequencer so that sequence items can be passed and executed. I used the word appears because the library does the hard part for you. There is underlying transaction-level modeling, TLM, a communication machine that handles the handshake between the driver and the sequencer. Now, it is good to read the code in the UVM library to understand how this works but to get started all you need to know is that you use the connect function in one line of code.

Now, the agent that we just created needs to be instantiated. This instantiation is typically done in an environment.

ENVIRONMENTS

An environment is another container. This particular container holds agents and other environments. It also contains other objects that are needed for the simulation such as a scoreboard, register model, memory models, coverage objects, etc. It’s called an environment because it contains the components that are necessary to create an effective verification environment.

The code for this environment is as simple as the architecture diagram looks in the earlier post.

class pipe_env extends uvm_env;
  pipe_agent agent;
  
  `uvm_component_utils(pipe_env)
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction: new
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    
    agent = pipe_agent::type_id::create("agent", this);
    
    `uvm_info(get_full_name(), "Build Stage Complete", UVM_LOW)
  endfunction: build_phase
  
endclass: pipe_env

I have created an environment that instantiations my pipe environment twice. One instantiation is for the input and
the other is for the output. It will also instantiate my scoreboard. However, before they are created in the build_phase, I use the configuration database to set some values.

class dut_env extends uvm_env;
  
  pipe_env        penv_in;
  pipe_env        penv_out;
  pipe_scoreboard sb;
  
  `uvm_component_utils(dut_env)
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction: new
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    
    uvm_config_db#(int)::set(this, "penv_in.agent", "is_active", UVM_ACTIVE);
    uvm_config_db#(int)::set(this, "penv_out.agent", "is_active", UVM_PASSIVE);
    
    uvm_config_db#(string)::set(this, "penv_in.agent.monitor", "monitor_intf", "in_intf");
    uvm_config_db#(string)::set(this, "penv_out.agent.monitor", "monitor_intf", "out_intf");
    
    penv_in  = pipe_env::type_id::create("penv_in", this);
    penv_out = pipe_env::type_id::create("penv_out", this);
    sb       = pipe_scoreboard::type_id::create("sb", this);
    
    `uvm_info(get_full_name(), "Build Stage Complete", UVM_LOW)
  endfunction: build_phase
  
  function void connect_phase(uvm_phase phase);
    penv_in.agent.monitor.item_collected_port.connect(sb.input_packets_collected.analysis_export);
    penv_out.agent.monitor.item_collected_port.connect(sb.output_packets_collected.analysis_export);
    `uvm_info(get_full_name(), "Connect Phase Complete", UVM_LOW)
  endfunction: connect_phase
  
endclass: dut_env

I am setting the input agent to be active and the output agent to be passive. It is important that I set these values in the database before I actually create the objects. Now, when the agent objects are created, the items in the field automation list, such as “is_active” will get the values stored in the database. What if you do this in reverse order? If that’s the case, then you must use the static get function of the configuration database to retrieve the values.

In the connect_phase, as we have seen before, I’m connecting the input and output monitors to the scoreboard for checking. One key element missing from this environment is a coverage object.

In the next blog posts, we will explore how to develop a functional coverage object in UVM, but you now have the tools to instantiate and connect that coverage object in the environment.