We already have an idea of how registers are laid out in a memory map from Introduction. So we'll simply use existing UVM RAL (Register Abstraction Layer) classes to define individual fields, registers and register-blocks. A register model is an entity that encompasses and describes the hierarchical structure of class objects for each register and its individual fields. We can perform read and write operations on the design using a register model object.
Every register in the model corresponds to an actual hardware register in the design. There are two kinds of variables inside the register within a model.

Desired Value
This is the value we would like the design to have. In other words, the model has an internal variable to store a desired value that can be updated later in the design. For example, if we want the register REG_STAT in the design to have a value of 0x1234_5678, then the desired value of that register has to be set to 0x1234_5678 within the model and an update task should be called for this to be reflected in the design.

Mirrored Value
Every time a read or a write operation occurs on the design, the mirrored values for that particular register will be updated. Hence the mirrored value in the model is the latest known value in the design. This is very useful because we don't have to issue a read operation everytime we want to know the value of a register in the design.

The desired and mirrored values also exist for every register field.
Creating a register class
Consider that we are building the model step by step, and the first logical step would be to identify all the registers in the design and create an object for each register. From Introduction, we'll first create a register class object for REG_CTL since it's fields are already well defined. Every register object should be inherited from uvm_reg
class and every register field should be objects of uvm_reg_field
. Note that you don't have to create a separate class for a register field, but simply create an object for every register field as shown in the example below.

class reg_ctl extends uvm_reg;
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 have been declared as rand
so that they can be randomized when required and are of type uvm_reg_field
. This is only a declaration for all fields, and actual definitions will come later on. Next we have to write the new()
function as we normally do with any UVM class. The super
call will require three arguments with the first one being the name of the register, the second indicates the size of the register which is 32 bits in our case. The last parameter specifies which functional coverage models are present in the extension of the register abstraction class.
function new (string name = "reg_ctl");
super.new (name, 32, UVM_NO_COVERAGE);
endfunction
Until this point, we have not defined anything related to the register fields and hence, let's do that next in a custom user function called build(). We'll keep the function virtual
just in case someone else tries to override this method and call their own version of build().
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 is used to define field attributes to the class object and has 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
);
In a similar way you can define a separate class for each register in the design.
class reg_stat extends uvm_reg;
...
endclass
class reg_dmactl extends uvm_reg;
...
endclass
...
Creating a register block
After all registers have been defined it needs to be made a part of the register block. For this, we'll define a register block that inherits from uvm_reg_block
and create objects of all registers within it. For our purposes let's put only three registers into this block.
class reg_block extends uvm_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
An object of reg_block class is the register model and can be used to access all registers and perform read and write operations on them. We have only covered the first part of how a register model is defined. But the question of how the actual DUV will be written to and read from has not been discussed yet. To perform register operations, you have to send valid bus transactions via a peripheral bus which will be covered in the next section.