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.
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
| Class | Description |
|---|---|
uvm_reg_field | Models one field: a group of bits with its own access policy and reset value |
uvm_reg | Models one design register, made up of fields |
uvm_reg_file | Groups related registers inside a block (optional) |
uvm_reg_map | Represents an address map: the base address and the offset of every register and memory |
uvm_mem | Models a memory in the design |
uvm_reg_block | Container 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.
| Policy | Write behavior | Read behavior |
|---|---|---|
"RW" | Value is written | Returns the value |
"RO" | No effect | Returns the value |
"WO" | Value is written | Not readable |
"W1C" | Writing 1 clears the bit, writing 0 has no effect | Returns the value |
"RC" | No effect | Returns 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.
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.
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
);
| Argument | Meaning |
|---|---|
parent | The register that contains this field, usually this |
size / lsb_pos | Width of the field and the bit position of its least significant bit |
access | Access policy string, such as "RW" or "RO" |
volatile | 1 if the hardware can change the field on its own, so the mirror may go stale |
reset / has_reset | Reset value, and whether the field has one |
is_rand | 1 if the field may be randomized |
individually_accessible | 1 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_mapanduvm_reg_blockall derive fromuvm_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 withupdate() - 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()