How to Startup vr_ad e Library

How to Startup vr_ad e Library

Posted by

In this post I will describe all the steps I take to startup the vr_ad register model, implemented in e language. 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 SystemVerilog, with UVM library, have a look at this article: How to Startup uvm_reg SystemVerilog Library.


Step #1 – Declare Each Register Model

In vr_ad each register is modeled by a predefined class called vr_ad_reg. Usually, all the fields of the register are modeled with scalar types, as physicals fields (to take advantage of the “built in” packing and unpacking feature of the e language). See an example below:

reg_def CFS_DUT_REG_CTRL {
  //reserved
  reg_fld reserved : uint(bits:31) : R  : 0 : cov;

  //control for enabling the DUT
  reg_fld enable   : uint(bits:1)  : RW : 0 : cov;
};

The format of the reg_fld macro is this one:

reg_fld <NAME> : <TYPE> : <MASK> : <RESET_VALUE> [: <COVERAGE>]

The parameters that you can control are:

  • <NAME> – the name of the field
  • <TYPE> – the type of the field
  • <MASK> – the access attribute like RW, R, WC etc
  • <RESET_VALUE> – reset value
  • <COVERAGE> – switch to enable coverage, and what kind of coverage

You can also use the reg_def macro to add directly the register to some register file, at some given offset, but I usually do this by hand in some next step.

Step #2 – Instantiate the Registers in a Register File

In vr_ad the registers can be contained in a vr_ad_reg_file.

We take advantage of the AOP part of the e language and declare all the instances of our registers under an extension of vr_ad_reg_file based on the kind field:

//Create a new kind for the register file
extend vr_ad_reg_file_kind : [CFS_REG_FILE];

//Create the instances to the registers
extend CFS_REG_FILE vr_ad_reg_file {
   
   //Control register
   %ctrl : CFS_DUT_REG_CTRL vr_ad_reg;

   //Status register
   %status : CFS_DUT_REG_STATUS vr_ad_reg;
};

Step #3 – Add Registers in a Physical Address Space

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

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

register file offset + register offset

Step #3.1 – Create an Address Map

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

Because of this, all the register files which can be accessed via a particular interface must be added to the corresponding address map. This can be done using add_with_offset() method with which we can configure that register file offset.

extend cfs_dut_env {
   
   //Instance of the address map
   addr_map : vr_ad_map;

   //Instance of the register file
   reg_file : CFS_REG_FILE vr_ad_reg_file;

   connect_pointers() is also {
      //Add the register file to the address map and configure 
      //the "register file offset"
      addr_map.add_with_offset(0xF000, reg_file);
   };
};

Step #3.2 – Add the Registers to a Register File

At this point, we need to specify the offset at which each register can be accessed, relative to the register file – this is the register offset mentioned earlier.

Keep in mind that the same thing can be done automatically via reg_def macro.

extend CFS_REG_FILE vr_ad_reg_file {
   
   add_registers() is also {
      add_with_offset(0x00, ctrl);
      add_with_offset(0x04, status);
   };
};

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 we can write actions like “read register” or “write register” without knowing/caring how this translate on a physical interface.

Let’s see how we can accomplish these two connections.

Step #4.1 – Connect the Register Model to Monitoring Logic

Connecting the register model to the monitoring logic of an agent is very easy. When the monitor collects information about a read or a write access you need to call two methods:

  • update() – for a write access
  • compare_and_update() – for a read access()

Let’s assume that we have an APB agent and its monitor sends a notification that it collected an APB transaction via some analysis port. Our connectivity logic would look like this:

extend cfs_dut_env {
   
   //Analysis port to get information from the APB monitor
   apb_transfer_end_port : in interface_port of tlm_analysis of 
      acme_apb_mon_transfer using suffix=_apb is instance;
   keep bind(apb_transfer_end_port, empty);

   //Implementation of the write() method associated with 
   //apb_transfer_end_port analysis port.
   write_apb(transfer : acme_apb_mon_transfer) is {
      case(transfer.dir) {
         WRITE : {
            addr_map.update(transfer.addr, pack(packing.low, transfer.data), {});
         };
         READ : {
            compute addr_map.compare_and_update(
               transfer.addr, 
               pack(packing.low, transfer.data));
         };
         default : {
            error(appendf("Unsupported direction: %s", transfer.dir));
         };
      };
   };

   connect_ports() is also {
      //connect the monitor port with the port from environment
      apb_agent.monitor.apb_transfer_end_port.connect(apb_transfer_end_port);
   };
};

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 vr_ad how to transform an vr_ad_operation class to an APB sequence item which the APB driver can understand.

So for this you need to implement a method in the driver called vr_ad_execute_op().

extend acme_apb_master_driver {

   //sequence through which to execute the register operations
   private reg_sequence : CFS_REG acme_apb_master_sequence;
   keep reg_sequence.driver == me;
   
   vr_ad_execute_op(operation : vr_ad_operation) : list of byte @clock is {
      case(operation.direction) {
         WRITE : {
            reg_sequence.write(operation.address, operation.get_data());
         };
         READ : {
            unpack(packing.low, reg_sequence.read(operation.address), result);
         };
         WRITE : {
            error(appendf("Unsupported direction: %s", operation.direction));
         };
      };
   };
};

One possible implementation for the sequence used in the example above can be this one:

extend acme_apb_master_sequence_kind : [CFS_REG];

extend CFS_REG acme_apb_master_sequence {
   
   private !transfer : acme_apb_master_sequence_item;

   //method for performing a write operation
   write(addr : acme_apb_addr_t, data : acme_apb_data_t)@driver.clock is {
      do transfer keeping {
         .addr == addr;
         .dir  == WRITE;
         .data == data;
      };
   };

   //method for performing a read operation
   read(addr : acme_apb_addr_t) : acme_apb_data_t @driver.clock is {
      do transfer keeping {
         .addr == addr;
         .dir  == READ;
      };

      assert transfer != NULL else
         error("transfer is NULL");

      return transfer.data;
   };  
};

Next, create a register sequence driver, as a subtype of vr_ad_sequence_driver, to easily specify its clock:

unit cfs_dut_register_sequence_driver like vr_ad_sequence_driver {
   
   //Pointer to Signal Map Unit
   !smp : acme_apb_smp;
   
   event clock is only @smp.clock;

};

After that we need to instantiate this driver and and tell it what address map and what driver to use in order to access the registers:

extend cfs_dut_env {

   //register sequence driver   
   rsd : cfs_dut_register_sequence_driver is instance;
   keep rsd.addr_map == value(addr_map);
   keep rsd.default_bfm_sd == apb_agent.driver;

   connect_pointers() is also {
      rsd.smp = apb_agent.smp;
   };
};

Step #5 – Miscellaneous Configurations/Actions

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

  • configure the size of the register file
  • trigger an initial reset to start from a known state

Here is how these actions can be done:

extend CFS_REG_FILE vr_ad_reg_file {
   
   //constrain the size of the register file, in bytes
   keep size == 1024;

   post_generate() is also {
      reset();
   };
};

At this point your register model is up and running.

Because we connect the driving logic to the register model, the tests can be written independent of the interface protocol:

extend MAIN vr_ad_sequence {

   !reg_ctrl   : CFS_DUT_REG_CTRL vr_ad_reg;

   !reg_status : CFS_DUT_REG_STATUS vr_ad_reg;

   body()@driver.clock is only {
      driver.raise_objection(TEST_DONE);
   
      wait[100];
      
      write_reg reg_ctrl {
         .enable == 1;
      };
      
      read_reg reg_ctrl;
      
      read_reg  reg_status;
      
      wait[100];
      
      driver.drop_objection(TEST_DONE);
   };
};

You can play around with this in a small project I did on EDA Playground.

Hope you found this useful 🙂

Cristian Slav

Leave a Reply

Your email address will not be published.