How to Startup uvm_reg SystemVerilog Library

How to Startup uvm_reg SystemVerilog Library

Posted by

In this post I will describe all the steps I take to startup the uvm_reg register model. If you do things differently please share your view in the comments section below.


If you want to see what is the equivalent of this in e language, with vr_ad library, have a look at this article: How to Startup vr_ad e Library.


Step #1 – Declare Each Register Model

In uvm_reg each register is modeled by a predefined class called uvm_reg. All the fields of the register are modeled with a class called uvm_reg_field. See an example below:

class cfs_dut_reg_ctrl extends uvm_reg; 

   //reserved
   rand uvm_reg_field reserved;

   //control for enabling the DUT;
   rand uvm_reg_field enable;

   `uvm_object_utils(cfs_dut_reg_config)

   function new(string name = "");
      //specify the name of the register, its width in bits and if it has coverage
      super.new(name, 32, 1);
   endfunction

   virtual function void build();
      reserved = uvm_reg_field::type_id::create("reserved");

      //specify parent, width, lsb position, rights, volatility,
      //reset value, has reset, is_rand, individually_accessible
      reserved.configure(this, 31, 1, "RO", 0, 0, 1, 1, 1);

      enable = uvm_reg_field::type_id::create("enable");
      enable.configure(this, 1, 0, "RW", 0, 0, 1, 1, 1);

   endfunction
endclass

You can control a lot of parameters of the field but most important are:

  • field width in bits
  • position of its least significant bit
  • field access rights
  • field reset value

Step #2 – Instantiate the Registers in a Register Block

In uvm_reg the registers can be contained in two entities: a register file or a register block.

A register file (uvm_reg_file) can contain registers and other register files.

A register block (uvm_reg_block) can contain registers, register files, memories and other register blocks.

You can take advantage of organizing your registers with register files but, most of the times, you can use just a register block:

class cfs_dut_reg_block extends uvm_reg_block;

   //Control register
   rand cfs_dut_reg_ctrl ctrl;

   //Status register
   rand cfs_dut_reg_status status;

   `uvm_object_utils(cfs_dut_reg_block)

   function new(string name = "");
      super.new(name, UVM_CVR_ALL);

      ctrl = cfs_dut_reg_ctrl::type_id::create("ctrl");
      status = cfs_dut_reg_status::type_id::create("status");
   endfunction

   virtual function void build();
      ctrl.configure(this, null, "");
      ctrl.build();

      status.configure(this, null, "");
      status.build();
      ...
   endfunction
endclass

Step #3 – Add Registers in a Physical Address Space

In uvm_reg each register is added in an address map with some offset relative to that address map (register offset). After that, the address map is configured with some offset relative to the entire address space accessible through that address map (address map offset).

So when you access a register, its absolute address will be:

address map offset + register offset

Step #3.1 – Create an Address Map

Creating an address map is quite straight forward: you simply take advantage of the method create_map() with which you configure that address map offset:

class cfs_dut_reg_block extends uvm_reg_block;
   ...
   virtual function void build();
      ...
      default_map = create_map(
         .name(       "apb_map"), 

         //Configure the "address map offset"
         .base_addr(  'hF000), 

         .n_bytes(    4), 
         .endian(     UVM_BIG_ENDIAN));

      default_map.set_check_on_read(1);

   endfunction
endclass

Step #3.2 – Add the Register to the Address Map(s)

In uvm_reg library an address map (class uvm_reg_map) models the register access via a physical interface like APB or UART.

Because of this, all registers which can be accessed via a particular interface must be added to the corresponding address map. This can be done using add_reg() with which you can configure that register offset.
If one register can be accessed via two physical interfaces then it must be added to two address maps.

class cfs_dut_reg_block extends uvm_reg_block;
   ...
   virtual function void build();
      ...

      default_map.add_reg(ctrl,   'h00, "RW");
      default_map.add_reg(status, 'h04, "RO");
   endfunction
endclass

Step #4 – Connect the Register Model to an Agent

When it comes to the register model, a minimum requirement is to connect it to the monitoring logic of an agent so that the register model is updated during write operations and checked/updated during read operations.

Connecting the register model to the driving logic of an agent gives us the possibility to separate the test logic from the physical protocol. This means that in the test code you can write actions like “read register” or “write register” without knowing/caring how this translate on a physical interface.

Let’s see how these two connections can be implemented.

Step #4.1 – Connect the Register Model to Monitoring Logic

In order to connect the register model to the monitoring logic you first need to tell uvm_reg how to transform an APB transfer (which can have many implementations) to something that uvm_reg can understand, which is an uvm_reg_bus_op class.

So for this you need to create an adapter, which extends from uvm_reg_adapter, and in this adapter you have to implement this transformation in bus2reg() method.

One important thing here: you must be careful that when you develop the physical monitor, the collected information must stay in a class which extends from uvm_sequence_item. Otherwise you will not be able to connect the monitor to the register model.

class cfs_dut_reg_adapter extends uvm_reg_adapter;

   `uvm_object_utils(cfs_dut_reg_adapter)

   function new(string name = "cfs_dut_reg_adapter");
      super.new(name);
   endfunction
      
   virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
      acme_apb_mon_transfer transfer;
           
      if($cast(transfer, bus_item)) begin
         if(transfer.direction == APB_WRITE) begin
            rw.kind = UVM_WRITE;
         end
         else begin
            rw.kind = UVM_READ;
         end

         rw.addr = transfer.address;
         rw.data = transfer.data;
         rw.status = UVM_IS_OK;
      end
      else begin
         `uvm_fatal(get_name(), $sformatf("Could not cast to acme_apb_mon_transfer: %s",
            bus_item.get_type_name()))
      end
   endfunction
endclass

Next, you have to create an instance of a predictor class (uvm_reg_predictor) which needs to know several important things:

  • what information was collected by the APB monitor from the bus
  • what adapter can use to transform the APB transfer to an uvm_reg_bus_op
  • what address map is accessible via that APB interface

These can be done in the following manner:

class cfs_dut_env extends uvm_env;
   ...

   //register block
   cfs_dut_reg_block reg_block;

   //APB register adapter
   cfs_dut_reg_adapter adapter;

   //APB register predictor
   uvm_reg_predictor#(acme_apb_mon_transfer) predictor;

   `uvm_component_utils(cfs_dut_env)

   function new(string name = "cfs_dut_env", uvm_component parent);
      super.new(name, parent);
   endfunction
       
   virtual function void build_phase(uvm_phase phase);
      super.build_phase(phase);
     
      //create all the elements of the environment
      reg_block = cfs_dut_reg_block::type_id::create("reg_block");
      adapter = cfs_dut_reg_adapter::type_id::create("adapter");
      predictor = uvm_reg_predictor#(acme_apb_mon_transfer)::type_id::create("predictor", this);
      ...
   endfunction

   virtual function void connect_phase(uvm_phase phase);
      super.connect_phase(phase);
     
      //configure the predictor with an address map and an adapter 
      predictor.map = reg_block.default_map;
      predictor.adapter = adapter;

      //connect the APB monitor with the predictor
      apb_agent.monitor.output_port.connect(predictor.bus_in);

      ...
   endfunction
endclass

If you need to do some filtering on the information coming from the monitor, you can create your own custom predictor, by extending a class from uvm_reg_predictor and overriding the write() method of the bus_in port, but in most of the cases the default implementation would do just fine.

Step #4.2 – Connect the Register Model to Driving Logic

In order to connect the register model to the driving logic you first need to tell uvm_reg how to transform an uvm_reg_bus_op class to an APB sequence item which the APB sequencer can understand.

So for this you need to implement a different method in the adapter called reg2bus().

class cfs_dut_reg_adapter extends uvm_reg_adapter;
   ...
   virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
      acme_apb_drv_transfer transfer = acme_apb_drv_transfer::type_id::create("transfer");

      if(rw.kind == UVM_WRITE) begin
         transfer.direction = APB_WRITE;
      end
      else begin
         transfer.direction = APB_READ;
      end
           
      transfer.data = rw.data;
      transfer.address = rw.addr;
     
      return transfer;
   endfunction
endclass

Next, you have to tell the predictor what sequencer it can use in order to send register accesses on the APB bus:

class cfs_dut_env extends uvm_env;
   ...
   virtual function void connect_phase(uvm_phase phase);
      ...
     
      //required to start physical register accesses using the registers
      reg_block.default_map.set_sequencer(apb_agent.sequencer, adapter);
     
      ...
   endfunction
endclass

Step #5 – Miscellaneous Configurations/Actions

Some other small configurations/actions that you need to do are the following:

  • build the register model
  • lock the register model
  • trigger an initial reset to start from a known state

Here is how these actions can be done:

class cfs_dut_env extends uvm_env;
   
   virtual function void build_phase(uvm_phase phase);
      ...
      //build the register model
      reg_block.build();

      //lock the register model
      reg_block.lock_model();

      //trigger an initial reset to start from a known state
      reg_block.reset("HARD");
   endfunction
   
   ...
endclass

At this point your register model is up and running.

Based on your DUT functionality there are more things that you might use from the uvm_reg. Here are some uvm_reg features that I used so far. I will try to explain them all in next posts:

Cristian Slav

4 Comments

  • idimitrijevic@elfak.rs' Ilija says:

    Thanks for well structured guide for reg model implementation. I have one small remark/doubt: Shouldn’t we do register model lock on level where top reg block is instantiated ? As I understood, it work recursively from top to bottom, so if you do lock within reg block build function, and then reuse that block as part of bigger block, it may not work.

  • love.theme@gmail.com' Guru says:

    I’m confused that the UVM LRM said that “RO” have to be “un-modelled”. but some educational site of UVM used RAL model with “RO” modelled. What usage does the correct? Do I have to model or not about “RO” in RAL model.?

  • Read-only register fields can be modeled in your environment. For example, status registers must be implemented as RO but you still have to model their value.

Leave a Reply

Your email address will not be published.