Global training solutions for engineers creating the world's electronics

The Easier UVM Code Generator Tutorial

Part 2: Adding User-Defined Code

In the Getting Started tutorial we used the generated code (almost) out-of-the-box to send a sequence of random transactions to the DUT. The only modification we needed to make to the generated code was to implement the driver to wiggle the pins of the DUT. In this tutorial we will also implement the monitor and coverage collector components so that we can collect functional coverage information during simulation.

You can download the files for this example. The files are in the directory named ./minimal_plus.

Implement the Monitor

In order to do any kind of checking or coverage collection in the verification environment we first need to complete the implementation of the monitor. Similar to the driver in the Getting Started Tutorial, we can implement the monitor by providing the implementation of a task that is already being called by the generated monitor code and then use the template files to instruct the code generator to include our task.

Filename include/clkndata_do_mon.sv

task clkndata_monitor::do_mon();
  forever @(posedge vif.clk)
  begin
    m_trans = data_tx::type_id::create("m_trans");
    m_trans.data = vif.data;
    analysis_port.write(m_trans);
  end
endtask

As for the driver, we have to be careful to use the correct naming conventions within this task. clkndata is the agent name from the interface template file, but the suffix _monitor, the task name do_mon, the virtual interface name vif, the tranasaction variable name m_trans, and the analysis port name analysis_port are all fixed by the code generator, so we have to be sure to use exactly these names. The task should monitor the pin wiggling on the DUT interface, synchronizing with any timing signals and consuming time ( @(posedge vif.clk) ) as necessary, and should then assemble a transaction m_trans which it should send out through the analysis port analysis_port, as shown above.

And as for the driver, you will need to add the name of the specific file to be included in the corresponding interface template file. (See the Reference Guide for a fuller description of extending the generated code.)

Filename clkndata.tpl

...
driver_inc = clkndata_do_drive.sv
monitor_inc = clkndata_do_mon.sv
...
Implement a Covergroup

The code generator will generate a subscriber component, which it connects to the analysis port of the monitor. By default, this subscriber will sample the values of the fields of any incoming analysis transactions using a covergroup. The automatically generated code looks like this:

Filename clkndata_coverage.sv

class clkndata_coverage extends uvm_subscriber #(data_tx);

  `uvm_component_utils(clkndata_coverage)

  bit m_is_covered;

  data_tx m_item;

  covergroup m_cov;
    option.per_instance = 1;
    cp_data: coverpoint m_item.data;
  endgroup

  extern function new(string name, uvm_component_parent);
  extern function void write(input data_tx t);

endclass


function clkndata_coverage::new(string name, uvm_component_parent);
  super.new(name, parent);
  m_is_covered = 0;
  m_cov = new;
endfunction


function void clkndata_coverage::write(input data_tx t);
  m_item = t;
  m_cov.sample();
  if (m_cov.get_inst_coverage() >= 100) m_is_covered = 1;
endfunction

...

In order to complete the coverage model, you can provide a set of user-defined coverpoints and bins in an include file, for example:

Filename include/clkndata_cover_inc.sv

covergroup m_cov;
  option.per_instance = 1;
  cp_data: coverpoint m_item.data {
    bins zero = {0};
    bins one  = {1};
    bins negative = { [-128:-1] };
    bins positive = { [1:127] };
    option.at_least = 16;
  }
endgroup

Once again, you will have to be careful to use the correct naming conventions. The covergroup must be named m_cov, and the name of the variable that refers to the incoming analysis transaction must be m_item.

Once again, you will need to add the name of the specific file to be included in the corresponding interface template file.

Filename clkndata.tpl

...
driver_inc = clkndata_do_drive.sv
monitor_inc = clkndata_do_mon.sv
agent_cover_inc = clkndata_cover_inc.sv
...

The generated subscriber component will now include the covergroup you have just provided:

Filename clkndata_coverage.sv

class clkndata_coverage extends uvm_subscriber #(data_tx);
  ...
  data_tx m_item;

  `include "clkndata_cover_inc.sv"
  ...
  extern function void write(input data_tx t);
  ...
endclass
Further Extending the Generated Code

The above include file clkndata_cover_inc.sv only contains a covergroup. The generated code instantiates that covergroup and contains a write method to sample the covergroup. But what if you need your subscriber to take some other action besides or instead of sampling a covergroup? The answer is that, instead of just including a covergroup, you can pull in all the contents of the class from a set of include files. You also need to suppress the automatic generation of the new and write methods if you are going to supply them yourself. To do so, you would change the interface template file as follows:

Filename clkndata.tpl

...
#agent_cover_inc = clkndata_cover_inc.sv

agent_cover_inc_inside_class = clkndata_cover_inc_inside.sv
agent_cover_inc_after_class  = clkndata_cover_inc_after.sv
agent_cover_generate_methods_inside_class = no
agent_cover_generate_methods_after_class  = no
...

The generated subscriber component would now look like this, leaving you to define the actual content of the class in the include files:

class clkndata_coverage extends uvm_subscriber #(data_tx);

  `uvm_component(clkndata_coverage)

  `include "clkndata_cover_inc_inside.sv"

endclass

`include "clkndata_cover_inc_after.sv"

You might then provide the following two include files:

Filename include/clkndata_cover_inc_inside.sv

covergroup m_cov;
  option.per_instance = 1;
  cp_data: coverpoint m_item.data {
    bins zero = {0};
    bins one  = {1};
    bins negative = { [-128:-1] };
    bins positive = { [1:127] };
    option.at_least = 16;
  }
endgroup

extern function new(string name, uvm_component_parent);
extern function void write(input data_tx t);

Filename include/clkndata_cover_inc_after.sv

function clkndata_coverage::new(string name, uvm_component_parent);
  super.new(name, parent);
  m_cov = new;
endfunction

function void clkndata_coverage::write(input data_tx t);
  m_item = t;
  m_cov.sample();
endfunction: write

You now have the flexibility to provide whatever code you like within these two include files. In this example we have used extern function definitions for new and write, but this is not necessary. We could have defined these function inline in the class definition.

Alternatively, instead of the code generator writing out `include directives, you can have the user-defined code inserted inline in the generated code as follows:

Filename clkndata.tpl

...
agent_cover_inc_inside_class = clkndata_cover_inc_inside.sv  inline
agent_cover_inc_after_class  = clkndata_cover_inc_after.sv   inline
...

Just as we have complete flexibility in defining the contents of the subscriber class using include files, so we can use include files to define the contents of the driver and the monitor component classes and the test harness module. See the Reference Guide for a fuller description of extending the generated code.

It would now be possible to re-generate the code and run simulation using the minimal stimulus from the Getting Started tutorial, but instead, we are going to add some user-defined stimulus in an attempt to improve functional coverage. This will show how to have the generated code execute a user-defined sequence.

Override a Sequence

As we saw in the Getting Started Tutorial, the generated code contains a top-level virtual sequence that starts a default sequence running on each agent, where that default sequence generates a single random transaction:

Filename top_seq_lib.sv

...
task top_default_seq::body();
  super.body();
  ...
  clkndata_default_seq seq;
  seq = clkndata_default_seq::type_id::create("seq");
  seq.randomize();
  seq.start(m_clkndata_agent.m_sequencer, this);
endtask
...

You can add a user-defined sequence by extending any default sequence that is created and started by the generator, e.g.

Filename my_clkndata_seq.sv

class my_clkndata_seq extends clkndata_default_seq;
  ...
  task body();
    ...
    for (int i = 0; i < 16; i++)
    begin
      req = data_tx::type_id::create("req");
      start_item(req);
      if ( !req.randomize() with { data == i; })
        ...
      finish_item(req);
    end
  endtask
endclass

You then need to identify the file containing the sequence in the interface template file, and also add a line that will generate a factory override to start your user-defined sequence in place of the default sequence:

Filename clkndata.tpl

...
agent_cover_inc = clkndata_cover_inc.sv
agent_seq_inc = myclkndata_seq.sv

agent_factory_set = clkndata_default_seq my_clkndata_seq
...

The code generator will then add the following factory override to the top-level test:

Filename top_test.sv

function void top_test::build_phase(uvm_phase phase);
  ...
  clkndata_default_seq::type_id::set_type_override(my_clkndata_seq::get_type());
  ...
endfunction

You are now ready to run the simulation and inspect the functional coverage reports. In order to make it easier to interpret the simulation log, you might want to adjust the verbosity threshold for UVM_INFO messages to suppress all superfluous messages. This can be done by adjusting the simulation script. You might also want to change the simulator command line flags to allow functional coverage to be viewed interactively. Here we show a run script for Questasim, but the same would apply for other simulators:

Filename gui

cd generated_tb/sim;
sed 's/UVM_VERBOSITY=UVM_FULL/UVM_VERBOSITY=UVM_NONE/' <compile_questa.do >result
mv -f result compile_questa.do
vsim -novopt -gui -do "do compile_questa.do; run -all"

Prev    Next