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:

PolicyWriteReadTypical use
RWStores the valueNo effectConfiguration bits
RONo effectNo effectHardware status, version IDs
WOStores the valueNot readableCommand or trigger registers
W1CA 1 clears the bit, a 0 leaves itNo effectInterrupt status flags
W1SA 1 sets the bit, a 0 leaves itNo effectSet registers
RCNo effectClears the fieldClear-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_mem models memories, added with add_mem() and accessed by offset with write(), read() and burst calls
  • Memories are not mirrored, so their contents are checked by reading back
  • Field access policies such as W1C, RC and WO let 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_cbs callback
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5