A UVM register model is a hierarchy of SystemVerilog class objects that mirrors the registers, fields and memories of a design. It is built from a small set of UVM base classes, mainly uvm_reg_field, uvm_reg and uvm_reg_block. This article explains each class, the desired and mirrored values that every register keeps, and how to write the model for a real register.

10 min read | Intermediate Level

What You'll Learn

  • What a register model is and how it is usually generated
  • The role of each UVM RAL class and its most-used methods
  • The difference between desired, mirrored and actual DUT values
  • Write a register class, configure its fields and add it to a register block

What is a register model ?

A register model provides a structured, standard way to model and verify the registers and memory-mapped structures of a design. It is a hierarchy of UVM class objects organized the same way as the registers and memories in the design: blocks contain registers, and registers contain fields.

uvm_ral_overview

Typical designs contain hundreds of registers, so register models are usually generated by scripts or commercial tools rather than written by hand. These tools take a register specification, such as an IP-XACT XML file, as input and generate a SystemVerilog file that describes every field and register using the standard UVM register classes.


// Sample IPXACT register specification

<spirit:usage>register</spirit:usage>
	<spirit:register>
    	<spirit:name>REG_CTL</spirit:name>
      	<spirit:addressOffset>0x0</spirit:addressOffset>
      	<spirit:size>32</spirit:size>
      	<spirit:reset>
         	<spirit:value>0xE014</spirit:value>
      	</spirit:reset>
      	<spirit:access>read-write</spirit:access>
      	<spirit:field>
          <spirit:name>EN</spirit:name>
          <spirit:description>Enable the module</spirit:description>
          <spirit:bitOffset>0</spirit:bitOffset>
          <spirit:bitWidth>1</spirit:bitWidth>
          <spirit:access>read-write</spirit:access>
      </spirit:field>

      ...      

The reset value 0xE014 in this specification matches the field reset values used in the model below: Mode = 2, Halt = 1 and Speed = 'h1c.

Where are Register Models Generated?

On most chip projects, the design team owns a single register description, often IP-XACT or a spreadsheet. The same source generates the RTL register logic, the UVM register model, C header files for firmware, and the register documentation. A change to a register is made once and appears consistently everywhere.

UVM RAL Classes

uml_uvm_ral_hier
ClassDescription
uvm_reg_fieldModels one field: a group of bits with its own access policy and reset value
uvm_regModels one design register, made up of fields
uvm_reg_fileGroups related registers inside a block (optional)
uvm_reg_mapRepresents an address map: the base address and the offset of every register and memory
uvm_memModels a memory in the design
uvm_reg_blockContainer for registers, register files, memories, maps and sub-blocks

uvm_reg_field

uvm_reg_field models an individual field within a register. A field is a specific bit or group of bits with its own function, access policy, reset value and other attributes.

These are some of the most commonly used methods of uvm_reg_field. Refer to the UVM reference manual for the full set.


	// Configure the register field class
	extern function void configure(uvm_reg        parent, 		// Parent register handle
                                   int unsigned   size, 		// Bit-width of the field
                                   int unsigned   lsb_pos, 		// LSB index of the field in register
                                   string         access, 		// Read/read-write/etc access policy
                                   bit            volatile, 		
                                   uvm_reg_data_t reset,
                                   bit            has_reset, 	
                                   bit            is_rand,
                                   bit            individually_accessible); 


	// Get width of the field, in number of bits
	extern virtual function int unsigned get_n_bits();

	// Returns index of LSB of the field in the register that instantiates it
	extern virtual function int unsigned get_lsb_pos();

	// Set desired value in the regmodel for this field
	extern virtual function void set(uvm_reg_data_t  value,
                                    string          fname = "",
                                    int             lineno = 0);

	// Get desired value in regmodel for this field
	extern virtual function uvm_reg_data_t get(string fname = "",
                                              int    lineno = 0);

	// Get mirrored value in regmodel for this field
	extern virtual function uvm_reg_data_t get_mirrored_value(string fname = "",
                                               				 int    lineno = 0);

Here is how a register field of type uvm_reg_field is created and configured inside a register.


	// Declare register field handle
	uvm_reg_field 	m_enable;

	// Create an instance and configure the register field handle
	m_enable = uvm_reg_field::type_id::create("m_enable");
	m_enable.configure(this, 1, 0, "RW", 0, 1'h0, 1, 1, 1);

The access argument is a string that sets the field's access policy. The most common values are listed below.

PolicyWrite behaviorRead behavior
"RW"Value is writtenReturns the value
"RO"No effectReturns the value
"WO"Value is writtenNot readable
"W1C"Writing 1 clears the bit, writing 0 has no effectReturns the value
"RC"No effectReturns the value, then clears it

uvm_reg

uvm_reg is the UVM base class for modeling registers. User-defined register classes extend it.

These are some of the most commonly used methods of uvm_reg.


   // Get fields of this register as a queue
   extern virtual function void get_fields (ref uvm_reg_field fields[$]);

   // Get a particular field by name
   extern virtual function uvm_reg_field get_field_by_name(string name);

   // Get address of this register
   extern virtual function uvm_reg_addr_t get_address (uvm_reg_map map = null);

   // Set desired value in regmodel
   extern virtual function void set (uvm_reg_data_t  value,
                                     string          fname = "",
                                     int             lineno = 0);

   // Get desired value in regmodel
   extern virtual function uvm_reg_data_t  get(string  fname = "",
                                               int     lineno = 0);

   // Get last known design value 
   extern virtual function uvm_reg_data_t  get_mirrored_value(string  fname = "",
                                               int     lineno = 0);

   // Issue a register write with the given value
   extern virtual task write(output uvm_status_e      status,
                             input  uvm_reg_data_t    value,
                             input  uvm_door_e        path = UVM_DEFAULT_DOOR,
                             input  uvm_reg_map       map = null,
                             input  uvm_sequence_base parent = null,
                             input  int               prior = -1,
                             input  uvm_object        extension = null,
                             input  string            fname = "",
                             input  int               lineno = 0);

   // Issue a register read and get value into the given variable
   extern virtual task read(output uvm_status_e      status,
                            output uvm_reg_data_t    value,
                            input  uvm_door_e        path = UVM_DEFAULT_DOOR,
                            input  uvm_reg_map       map = null,
                            input  uvm_sequence_base parent = null,
                            input  int               prior = -1,
                            input  uvm_object        extension = null,
                            input  string            fname = "",
                            input  int               lineno = 0);

   // Update design if desired value is not same as mirrored value
   extern virtual task update(output uvm_status_e      status,
                              input  uvm_door_e        path = UVM_DEFAULT_DOOR,
                              input  uvm_reg_map       map = null,
                              input  uvm_sequence_base parent = null,
                              input  int               prior = -1,
                              input  uvm_object        extension = null,
                              input  string            fname = "",
                              input  int               lineno = 0);

   // Update regmodel with mirrored value
   extern virtual task mirror(output uvm_status_e      status,
                              input uvm_check_e        check  = UVM_NO_CHECK,
                              input uvm_door_e         path = UVM_DEFAULT_DOOR,
                              input uvm_reg_map        map = null,
                              input uvm_sequence_base  parent = null,
                              input int                prior = -1,
                              input  uvm_object        extension = null,
                              input string             fname = "",
                              input int                lineno = 0);

These prototypes follow the IEEE 1800.2 version of UVM, which uses uvm_door_e and UVM_DEFAULT_DOOR. In UVM 1.1 and 1.2, the same argument is of type uvm_path_e with the default UVM_DEFAULT_PATH.

Here is an example of a user register extended from the uvm_reg base class.


class reg_ctl extends uvm_reg;
	`uvm_object_utils(reg_ctl)

	rand uvm_reg_field  En;

	function new (string name = "reg_ctl");
		super.new (name, 32, UVM_NO_COVERAGE);
	endfunction

	virtual function void build ();	
		// Create object instance for each field
		this.En     = uvm_reg_field::type_id::create ("En");
		
		// Configure each field
		this.En.configure (this, 1, 0, "RW", 0, 1'h0, 1, 1, 1);
	endfunction
endclass

uvm_reg_block

A uvm_reg_block can contain registers, register files, memories and sub-blocks. These are some of its most commonly used methods.


	// Get queue of registers
   extern virtual function void get_registers (ref uvm_reg regs[$],
                                               input uvm_hier_e hier=UVM_HIER);

	// Get register block by name
  extern virtual function uvm_reg_block get_block_by_name (string name);  

	// Get register by name
   extern virtual function uvm_reg get_reg_by_name (string name);

	// Get field by name
   extern virtual function uvm_reg_field get_field_by_name (string name);

	// Update registers in this reg block
   extern virtual task update(output uvm_status_e       status,
                              input  uvm_door_e         path = UVM_DEFAULT_DOOR,
                              input  uvm_sequence_base  parent = null,
                              input  int                prior = -1,
                              input  uvm_object         extension = null,
                              input  string             fname = "",
                              input  int                lineno = 0);

Quick Check

Q: A field is configured with configure(this, 3, 1, "RW", 0, 3'h2, 1, 1, 1). Which bits does it occupy, and what is its reset contribution to the register value?

Show Answer

It is 3 bits wide starting at bit 1, so it occupies bits 3:1. Its reset value of 2 shifted left by 1 gives 'h4 in the register's reset value.

What are desired and mirrored values ?

Every register in the model corresponds to an actual hardware register in the design. Inside the model, each register and each field keeps two values in addition to the real value held by the DUT.

design-register-model-desired-mirrored-values

The desired value is what we want the design to contain. It is changed with set() and only becomes real when the design is updated. For example, to give the register REG_STAT a value of 0x1234_5678, you set its desired value to 0x1234_5678 in the model and then call update(), which writes the value to the design.

The mirrored value is updated every time a read or write is performed on the design through the model, or observed on the bus by a predictor. The mirrored value is therefore the latest value known to be in the design, and you do not have to issue a bus read every time you want to know a register's value.

Both the desired and the mirrored values also exist for every register field.

The mirror is only as current as the last observed access. For volatile fields that the hardware changes on its own, such as status bits or counters, read the register again before relying on its value.

Where are Desired Values Used?

Configuration sequences often call set() on many fields of a block, then a single update() on the whole block. Only registers whose desired value differs from the mirror are written, which keeps configuration fast even in blocks with hundreds of registers.

Example

This example creates the register class for REG_CTL. Every register class is derived from uvm_reg, and every register field is an object of uvm_reg_field. You do not need a separate class for each field. Instead, create one uvm_reg_field object per field, as shown below.

register uvm example

class reg_ctl extends uvm_reg;
	`uvm_object_utils(reg_ctl)

	rand uvm_reg_field  En;
	rand uvm_reg_field  Mode;
	rand uvm_reg_field  Halt;
	rand uvm_reg_field  Auto;
	rand uvm_reg_field  Speed;

The fields are declared as rand so that they can be randomized when required, and they are of type uvm_reg_field. This is only the declaration of the fields; they are created and configured further down. The `uvm_object_utils macro registers the class with the factory, which is needed because the register block creates it with type_id::create().

Next, write the new() function, as with any UVM class. The super.new() call takes three arguments: the name of the register, its size in bits (32 here), and which functional coverage models are present in this register class.


	function new (string name = "reg_ctl");
		super.new (name, 32, UVM_NO_COVERAGE);
	endfunction

The fields themselves are created and configured in a user function called build(). The function is declared virtual so that a derived class can override it with its own version.


	virtual function void build ();
	
		// Create object instance for each field
		this.En     = uvm_reg_field::type_id::create ("En");
		this.Mode   = uvm_reg_field::type_id::create ("Mode");
		this.Halt   = uvm_reg_field::type_id::create ("Halt");
		this.Auto   = uvm_reg_field::type_id::create ("Auto");
		this.Speed  = uvm_reg_field::type_id::create ("Speed");
		
		// Configure each field
		this.En.configure (this, 1, 0, "RW", 0, 1'h0, 1, 1, 1);
		this.Mode.configure (this, 3, 1, "RW", 0, 3'h2, 1, 1, 1);
		this.Halt.configure (this, 1, 4, "RW", 0, 1'h1, 1, 1, 1);
		this.Auto.configure (this, 1, 5, "RW", 0, 1'h0, 1, 1, 1);
		this.Speed.configure (this, 5, 11, "RW", 0, 5'h1c, 1, 1, 1);
	endfunction
endclass

The configure() method sets the attributes of each field and takes the following arguments.


function void configure(
                            uvm_reg        parent,
                            int unsigned   size,
                            int unsigned   lsb_pos,
                            string         access,
                            bit            volatile,
                            uvm_reg_data_t reset,
                            bit            has_reset,
                            bit            is_rand,
                            bit            individually_accessible
                        );
ArgumentMeaning
parentThe register that contains this field, usually this
size / lsb_posWidth of the field and the bit position of its least significant bit
accessAccess policy string, such as "RW" or "RO"
volatile1 if the hardware can change the field on its own, so the mirror may go stale
reset / has_resetReset value, and whether the field has one
is_rand1 if the field may be randomized
individually_accessible1 if the field can be accessed alone, without the rest of the register

In the same way, define a separate class for each register in the design.


class reg_stat extends uvm_reg;
	...
endclass

class reg_dmactl extends uvm_reg;
	...
endclass

...

Once all registers are defined, they must be made part of a register block. The block class inherits from uvm_reg_block and creates an object for each register. For this example, the block contains only three registers.


class reg_block extends uvm_reg_block;
	`uvm_object_utils(reg_block)

	rand 	reg_ctl 	m_reg_ctl;
	rand 	reg_stat 	m_reg_stat;
	rand 	reg_inten 	m_reg_inten;
	
	function new (string name = "reg_block");
		super.new (name, UVM_NO_COVERAGE);
	endfunction
	
	virtual function void build ();
	
		// Create an instance for every register
		this.default_map = create_map ("", 0, 4, UVM_LITTLE_ENDIAN, 0);
		this.m_reg_ctl = reg_ctl::type_id::create ("m_reg_ctl", , get_full_name);
		this.m_reg_stat = reg_stat::type_id::create ("m_reg_stat", , get_full_name);
		this.m_reg_inten = reg_inten::type_id::create ("m_reg_inten", , get_full_name);
		
		// Configure every register instance
		this.m_reg_ctl.configure (this, null, "");
		this.m_reg_stat.configure (this, null, "");
		this.m_reg_inten.configure (this, null, "");
		
		// Call the build() function to build all register fields within each register
		this.m_reg_ctl.build();
		this.m_reg_stat.build();
		this.m_reg_inten.build();
		
		// Add these registers to the default map
		this.default_map.add_reg (this.m_reg_ctl, `UVM_REG_ADDR_WIDTH'h0, "RW", 0);
		this.default_map.add_reg (this.m_reg_stat, `UVM_REG_ADDR_WIDTH'h4, "RO", 0);
		this.default_map.add_reg (this.m_reg_inten, `UVM_REG_ADDR_WIDTH'h8, "RW", 0);
	endfunction
endclass

The block's build() creates the default address map, creates and configures each register, builds its fields, and finally places each register at its offset with add_reg(). The arguments of create_map() are the map name, base address, bus width in bytes (4 here), endianness, and whether addresses are byte-addressed.

An object of the reg_block class is the register model, and it can be used to access all registers and perform read and write operations on them. After creating and building it, call its lock_model() method so that no more registers can be added and the address map is finalized.

The model alone does not reach the DUT. Register operations must be turned into valid transactions on a peripheral bus, which is set up in the UVM Register Environment and Connecting Register Env articles.

Common Mistakes

Mistake #1: Forgetting the factory registration macro

Wrong: a register class without `uvm_object_utils(reg_ctl), created with reg_ctl::type_id::create()
Right: add `uvm_object_utils to every register and block class

Why? type_id is defined by the macro. Without it, the code does not compile, and factory overrides of the register cannot be used.

Mistake #2: Overlapping or misplaced fields

Every configure() call places a field by size and lsb_pos. If two fields overlap, or a field extends past bit 31 of a 32-bit register, UVM reports an error when the field is configured. A field at the wrong position without overlapping compiles cleanly, but reads and writes the wrong bits. Check positions against the specification, or generate the model from it.

Mistake #3: Expecting set() to change the DUT

Wrong: m_reg_ctl.Mode.set(3); and then waiting for the DUT to change mode
Right: m_reg_ctl.Mode.set(3); m_reg_ctl.update(status);

Why? set() only changes the desired value in the model. update(), or a direct write(), is what sends the value to the hardware.

Practice Exercise

Challenge: Model REG_STAT

REG_STAT is a 32-bit register at offset 0x4 with two fields: busy (bit 0, read-only, reset 0, changed by hardware) and err_cnt (bits 7:4, cleared when 1s are written to it, reset 0). Write the reg_stat class.

Hint

Use "RO" and "W1C" as the access policies. Both fields are changed by the hardware, so set volatile to 1. Neither field is a value you would randomize, so set is_rand to 0.

Solution

class reg_stat extends uvm_reg;
  `uvm_object_utils (reg_stat)

  uvm_reg_field busy;                        // Status fields: not randomized
  uvm_reg_field err_cnt;

  function new (string name = "reg_stat");
    super.new (name, 32, UVM_NO_COVERAGE);
  endfunction

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

    //                 parent size lsb access volatile reset has_reset is_rand indiv
    busy.configure    (this, 1,   0,  "RO",  1,       1'h0, 1,        0,      0);
    err_cnt.configure (this, 4,   4,  "W1C", 1,       4'h0, 1,        0,      0);
  endfunction
endclass

In reg_block, it is added at offset 0x4 with default_map.add_reg(m_reg_stat, 'h4, "RW", 0). The register is mapped "RW" because err_cnt can be written to clear it, while the field policies decide what each bit does.

Summary

In this tutorial, you learned:

  • A register model is a class hierarchy that mirrors the design's blocks, registers and fields, and is usually generated from a specification
  • uvm_reg_field, uvm_reg, uvm_reg_file, uvm_mem, uvm_reg_map and uvm_reg_block all derive from uvm_object
  • Fields are created and placed with configure(), which sets size, position, access policy and reset value
  • The desired value is changed with set() and sent to the DUT with update()
  • The mirrored value tracks the last known DUT value and is refreshed by every access or by a predictor
  • A register block creates its map, builds every register, adds each at its offset, and is locked with lock_model()
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5