UVM memories and special registers cover the parts of a register map that don't behave like a plain read-write register. A memory is modelled with uvm_mem and accessed by offset, without a mirror. Special registers, such as write-1-to-clear status bits, clear-on-read counters, aliased registers and read-only and write-only registers that share one address, are modelled with field access policies, address map options and register callbacks.
Real designs are full of these. An interrupt status register that clears when you write a 1 to it, a packet counter that resets when read, or a buffer RAM behind a register interface all need the model to predict their behaviour correctly. Otherwise the mirror drifts from the hardware and every mirror() check fails.
8 min read | Intermediate
What You'll Learn
- Add a memory to a register block and access it with
write(),read()and burst calls - Use field access policies to model status, counter and write-only fields
- Model registers that share an address or appear at several addresses
- Use register callbacks to add custom side effects to the model
Memories with uvm_mem
A memory has many locations of the same width, accessed by an offset. Creating a register class per location would be impossible, so UVM provides uvm_mem:
function new(string name, longint unsigned size, int unsigned n_bits,
string access = "RW", int has_coverage = UVM_NO_COVERAGE);
function void configure(uvm_reg_block parent, string hdl_path = "");
size is the number of locations and n_bits the width of each. access is "RW" for a RAM or "RO" for a ROM. The optional hdl_path names the memory array in the RTL, for back-door access.
Adding a memory to a block
Here a DMA block has a control register, a status register and a 1K-word buffer RAM at offset 'h1000:
class dma_blk extends uvm_reg_block;
`uvm_object_utils(dma_blk)
rand dma_ctrl_reg ctrl;
dma_status_reg status;
uvm_mem buf_mem;
// Rest of the code (constructor)
virtual function void build();
default_map = create_map("default_map", 'h0, 4, UVM_LITTLE_ENDIAN);
ctrl = dma_ctrl_reg::type_id::create("ctrl", , get_full_name());
ctrl.configure(this);
ctrl.build();
status = dma_status_reg::type_id::create("status", , get_full_name());
status.configure(this);
status.build();
buf_mem = new("buf_mem", 1024, 32, "RW"); // 1024 locations of 32 bits
buf_mem.configure(this, "u_buf.mem"); // HDL path for back-door access
default_map.add_reg(ctrl, 'h00);
default_map.add_reg(status, 'h04);
default_map.add_mem(buf_mem, 'h1000);
lock_model();
endfunction
endclass
Accessing a memory
Memory access looks like register access with an extra offset argument. The offset counts locations, not bytes; the address map works out the bus address.
uvm_status_e status;
uvm_reg_data_t value;
uvm_reg_data_t block[] = '{32'h1, 32'h2, 32'h3, 32'h4};
m_ral.buf_mem.write(status, 'h10, 32'hDEAD_BEEF, .parent(this)); // one location
m_ral.buf_mem.read (status, 'h10, value, .parent(this));
m_ral.buf_mem.burst_write(status, 'h20, block, .parent(this)); // four consecutive locations
Memories are not mirrored. A uvm_mem has no desired or mirrored value, and no mirror(), update() or predict(): keeping a copy of every location would cost too much memory in a big design. To check memory contents, read them back and compare, or use the built-in memory tests described in UVM Register Sequences: Built-in and Custom.
Special Fields: Access Policies
Most special behaviour is per field, and the field's access policy, given in configure(), tells the model how a read or write changes it. The mirror then predicts the right value automatically. A few policies cover most real registers:
| Policy | Write | Read | Typical use |
|---|---|---|---|
RW | Stores the value | No effect | Configuration bits |
RO | No effect | No effect | Hardware status, version IDs |
WO | Stores the value | Not readable | Command or trigger registers |
W1C | A 1 clears the bit, a 0 leaves it | No effect | Interrupt status flags |
W1S | A 1 sets the bit, a 0 leaves it | No effect | Set registers |
RC | No effect | Clears the field | Clear-on-read counters |
A status register with one interrupt flag per source, set by hardware and cleared by writing 1, looks like this:
class dma_status_reg extends uvm_reg;
`uvm_object_utils(dma_status_reg)
uvm_reg_field done_irq;
uvm_reg_field err_irq;
function new(string name = "dma_status_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
done_irq = uvm_reg_field::type_id::create("done_irq");
err_irq = uvm_reg_field::type_id::create("err_irq");
// configure(parent, size, lsb_pos, access, volatile, reset, has_reset, is_rand, individually_accessible)
done_irq.configure(this, 1, 0, "W1C", 1, 1'b0, 1, 0, 0);
err_irq.configure (this, 1, 1, "W1C", 1, 1'b0, 1, 0, 0);
endfunction
endclass
The fields are marked volatile, because the hardware sets them by itself. When a test writes 'b01 to the register, the mirror correctly predicts that done_irq becomes 0 and err_irq keeps its value. When the DUT sets a bit on its own, the testbench either predicts it, or reads the register so the predictor picks up the new value, as described in UVM Register Access Methods.
Quick Check
Q: err_irq is set in the DUT and the mirror knows it. A sequence writes 'b00 to the status register. What does the mirror predict for err_irq?
Show Answer
It stays 1. With W1C, writing 0 has no effect; only a 1 clears the bit. Writing all zeros to a W1C register changes nothing, which is a common source of "the interrupt never clears" bugs in tests.
Registers Sharing or Repeating an Address
Read-only and write-only at the same address
Some designs put a read-only register and a write-only register at the same address, for example a receive data register and a transmit data register. Model them as two registers and add both to the map at the same offset, with access rights that say which direction each one serves:
default_map.add_reg(rx_data, 'h08, "RO"); // reads at 'h08 go to rx_data
default_map.add_reg(tx_data, 'h08, "WO"); // writes at 'h08 go to tx_data
The rights argument of add_reg() can be "RW", "RO" or "WO", and limits what can be done to the register through that map, on top of its fields' own policies.
Aliased registers
An aliased register is one hardware register visible at two addresses, often with different access at each: read-write at one address and read-only at the other. A model needs one register instance per address, because each instance can be mapped only once. The two instances then have to be kept in step, so a write through one updates the mirror of the other.
That synchronisation is done with a register callback. The field callback's post_predict() runs after every prediction, and can copy a written value to the matching field of the alias:
class copy_to_alias extends uvm_reg_cbs;
local uvm_reg_field m_alias;
function new(uvm_reg_field alias_fld);
super.new("copy_to_alias");
m_alias = alias_fld;
endfunction
virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
inout uvm_reg_data_t value, input uvm_predict_e kind,
input uvm_path_e path, input uvm_reg_map map);
if (kind == UVM_PREDICT_WRITE)
void'(m_alias.predict(value, -1, UVM_PREDICT_WRITE, path, map));
endfunction
endclass
// In the block's build(), after both registers exist
begin
copy_to_alias cb = new(mode_ro.mode);
uvm_reg_field_cb::add(mode_rw.mode, cb); // writes via mode_rw also update mode_ro
end
uvm_reg_field_cb is a convenience typedef for registering callbacks on fields. The same mechanism, with pre_write() and post_write(), can model other side effects, such as a write to one register clearing another.
Registers that have no address of their own, such as indirect registers reached through an index and a data register, are added to the map as unmapped and given a user-defined front door, a uvm_reg_frontdoor sequence that performs the index-then-data accesses. This is the most involved of the special cases and usually comes from a register generator.
Common Mistakes
Mistake #1: Expecting a memory to have a mirror
uvm_mem keeps no copy of its contents. Calling mirror() or expecting predict() on a memory doesn't work. Read back and compare, or use a scoreboard with its own memory model.
Mistake #2: Modelling W1C fields as RW
With RW, the model predicts that writing 1 sets the bit, while the hardware clears it. Every later check of that register then fails. Use the policy that matches the specification.
Mistake #3: Forgetting that hardware-set fields are volatile
Status flags and counters change without any bus access. Mark them volatile and predict their changes, or exclude them from checks, so mirror(UVM_CHECK) doesn't report false errors.
Mistake #4: Mapping one register instance at two addresses
Each register instance can appear only once in a map. An aliased register needs one instance per address, linked by a callback.
Summary
In this article, you learned:
uvm_memmodels memories, added withadd_mem()and accessed by offset withwrite(),read()and burst calls- Memories are not mirrored, so their contents are checked by reading back
- Field access policies such as
W1C,RCandWOlet the mirror predict special fields correctly add_reg()rights let a read-only and a write-only register share one address- Aliased registers use one instance per address, kept in step by a
uvm_reg_cbscallback