UVM-26: RAL Stimulus & Built-in Sequences — VLSI Trainers
VLSI Trainers UVM Series · UVM-26
UVM Series · UVM-26

RAL Stimulus & Built-in Sequences

Using the integrated register model — the desired/mirrored value model, all access methods (write, read, set, get, update, mirror, peek, poke, reset), complete sequence patterns, and the full suite of UVM built-in register and memory test sequences.

📋 Desired and Mirrored Values

Every register in the model maintains two internal values that track the relationship between the software model and the actual DUT hardware:

Both start equal to the register’s reset value. They diverge when a set() call changes the desired without writing to hardware, or when hardware modifies a volatile field without a software write.

Desired and Mirrored Values — Lifecycle After reset() desired = 0x00 mirror = 0x00 (equal) After set(0xFF) desired = 0xFF mirror = 0x00 (diverged — no bus cycle) After update() desired = 0xFF mirror = 0xFF (equal — bus write done) After HW event (volatile) desired = 0xFF mirror = 0xAA (mirror updated by predictor) mirror() reads hardware and updates mirrored value. write/read cause predictor to update both. vlsitrainers.com
Figure 1 — Desired and mirrored value lifecycle. After reset() both are equal to the reset value. set() changes only the desired value — no bus cycle. update() writes the desired to hardware and the predictor updates the mirror. For volatile fields, hardware events change the mirror (via predictor) without changing the desired.

📋 write() and read()

The most commonly used methods. Both trigger actual bus transfers (front door) or zero-time database access (back door). Both update the mirror via the predictor on completion.

// ── write() — performs a bus write to the register ───────
// task write(output uvm_status_e status,
//            input  uvm_reg_data_t value,
//            input  uvm_path_e path = UVM_DEFAULT_PATH,
//            input  uvm_reg_map map = null,
//            input  uvm_sequence_base parent = null, ...)

gpio_reg_block  rm;
uvm_status_e    status;
uvm_reg_data_t  data;

// Write the direction register — all 32 GPIOs as outputs
rm.dir_reg.write(status, 32'hFFFF_FFFF, .parent(this));
if (status != UVM_IS_OK)
  `uvm_error("SEQ", "dir_reg write failed")

// ── read() — performs a bus read from the register ───────
// task read(output uvm_status_e status,
//           output uvm_reg_data_t value, ...)

rm.data_reg.read(status, data, .parent(this));
`uvm_info("SEQ", $sformatf("GPIO data = 0x%08h", data), UVM_MEDIUM)

// ── Field-level access (use with caution) ────────────────
// Field-level write only works reliably when the field occupies
// exactly one byte lane. For portability, prefer register-level access.
rm.ien_reg.ien.write(status, 32'h0000_000F, .parent(this));

// ── Back door access ──────────────────────────────────────
rm.data_reg.write(status, 32'hDEAD_BEEF,
  .path(UVM_BACKDOOR));          // zero simulation time
rm.data_reg.read(status, data,
  .path(UVM_BACKDOOR));          // reads RTL directly
Always use register-level write/read, not field-level, for portable stimulus. Field-level write uses byte enables to target individual fields. If the target bus does not support byte enables (APB has none), field-level writes may corrupt adjacent fields or produce unexpected results. Write to the entire register with appropriate field values combined.

📋 set() and get()

set() and get() are purely internal register model operations — they modify or read the desired value without touching the hardware. No bus cycle occurs. They are used to build up a new register value field by field before committing it with update().

// ── set() assigns the desired value (no bus cycle) ───────
// function void set(uvm_reg_data_t value, ...)

// Register-level set: set the desired value for the whole register
rm.ctrl_reg.set(32'h0000_0000);

// Field-level set: build up value field by field
rm.ctrl_reg.go_bsy.set(1);         // set go bit
rm.ctrl_reg.ie.set(0);             // disable interrupt
rm.ctrl_reg.char_len.set(7'd8);   // 8-bit character
// Now write the assembled value to hardware:
rm.ctrl_reg.update(status, .parent(this));

// ── get() returns the desired value (no bus cycle) ───────
// function uvm_reg_data_t get(...)

data = rm.ctrl_reg.get();                     // register level
logic [6:0] len = rm.ctrl_reg.char_len.get(); // field level

// ── get_mirrored_value() — returns the mirror, not desired
data = rm.ctrl_reg.get_mirrored_value();

Key behaviour of set() depends on the field access type. For RW fields, set() stores the value as-is. For W1C fields, set() computes the bitwise result — writing 1 clears bits in the mirror. For RO fields, set() has no effect. For cumulative sets before an update(), each set uses the previous desired as the new mirror input.

📋 update()

update() compares the desired value against the mirrored value. If they differ, it performs a bus write to bring the hardware in line with the desired. If they are already equal, update() produces no bus activity.

update() can be called at register level (one write) or block level (iterates through all registers that need updating).

// ── Register-level update ─────────────────────────────────
// task update(output uvm_status_e status,
//             input uvm_path_e path = UVM_DEFAULT_PATH,
//             input uvm_sequence_base parent = null, ...)

// Configure control register fields, then write once
rm.ctrl_reg.char_len.set(7'd8);
rm.ctrl_reg.ie.set(0);
rm.ctrl_reg.go_bsy.set(0);
rm.ctrl_reg.update(status, .parent(this));  // one bus write

// ── Block-level update: write ALL changed registers ──────
// Iterates registers in block order — fixed order only
rm.update(status, .parent(this));

// ── Randomise then update: auto-configure the DUT ────────
if (!rm.ctrl_reg.randomize() with {
      char_len.value inside {[31:33]};   // 32-bit characters
      go_bsy.value  == 0;               // don't start yet
    })
  `uvm_fatal("SEQ", "ctrl_reg randomize failed")
rm.ctrl_reg.update(status, .parent(this));
The set/update pattern is the right way to build register configurations. It separates intent (what values you want in each field) from execution (the single bus write). It avoids read-modify-write pitfalls — you never need to read the register first to preserve adjacent fields. Set each relevant field, then update() writes the assembled value in one transfer.

📋 mirror()

mirror() reads the hardware register and updates the model’s mirrored value — it is the inverse of update(). Unlike read(), mirror() does not return the data value to the caller. Instead it optionally checks the read-back value against the existing mirrored value and reports an error if they differ.

// ── mirror() — read hardware, update mirror ───────────────
// task mirror(output uvm_status_e status,
//             input uvm_check_e check = UVM_NO_CHECK,
//             input uvm_path_e path = UVM_DEFAULT_PATH, ...)

// Mirror without checking — just update the local mirror
rm.data_reg.mirror(status, .parent(this));

// Mirror WITH checking — reports error if HW != expected mirror
rm.dir_reg.mirror(status, UVM_CHECK, .parent(this));
if (status != UVM_IS_OK)
  `uvm_error("SEQ", "dir_reg mirror check failed!")

// Block-level mirror — reads ALL registers in the block
// Generates one read per register — can be slow for large blocks
rm.mirror(status, UVM_CHECK, .parent(this));

// Backdoor mirror — zero simulation time
rm.mirror(status, UVM_CHECK, .path(UVM_BACKDOOR));

📋 peek() and poke()

peek() and poke() are the backdoor shorthand equivalents of read() and write(). They bypass the bus entirely and directly read or force the RTL register flip-flops using the simulator database. Both update the model automatically and complete in zero simulation time.

Backdoor access requires HDL paths to be defined on the registers during model construction (the third argument of reg.configure()).

// ── poke() — force RTL bits, zero time ───────────────────
rm.data_reg.poke(status, 32'hCAFE_BABE, .parent(this));

// ── peek() — read RTL bits, zero time ────────────────────
rm.data_reg.peek(status, data, .parent(this));

// Field-level peek/poke are also supported:
rm.ctrl_reg.char_len.peek(status, data, .parent(this));
rm.ctrl_reg.char_len.poke(status, 7'd16, .parent(this));

// ── Typical use: initialise registers before test starts ─
function void start_of_simulation_phase(uvm_phase phase);
  uvm_status_e s;
  // Set known state without driving bus cycles
  m_rm.data_reg.poke(s, 32'h0000_0000);
  m_rm.dir_reg.poke(s,  32'hFFFF_0000);
  m_rm.ien_reg.poke(s,  32'h0000_000F);
endfunction

📋 reset() and get_reset()

reset() sets the register model’s desired and mirrored values back to the configured reset value. It produces no bus cycles — it only resets the software model. Call it whenever the DUT receives a hardware reset to keep the model in sync.

get_reset() returns the configured reset value without modifying anything. Use it in reset-value verification sequences.

// ── reset() — reset the software model to reset values ───
// function void reset(string kind = "HARD")

rm.reset();               // block-level — reset all registers
rm.ctrl_reg.reset();      // register-level
rm.ctrl_reg.char_len.reset(); // field-level

// ── Typical usage: align model to DUT hardware reset ─────
task run_phase(uvm_phase phase);
  // At start, or after PRESETn deasserts
  @(posedge vif.PRESETn);
  m_rm.reset();              // sync software model to hardware reset
endtask

// ── get_reset() — read expected reset value ───────────────
// function uvm_reg_data_t get_reset(string kind = "HARD")

uvm_reg_data_t exp_val = rm.ctrl_reg.get_reset();
rm.ctrl_reg.read(status, data, .parent(this));
if (data != exp_val)
  `uvm_error("SEQ", $sformatf(
    "ctrl_reg reset mismatch: exp=0x%0h act=0x%0h", exp_val, data))

📋 Access Method Summary

MethodBus cycle?TimeUpdates desiredUpdates mirrorTypical use
write()Yes (FD) / No (BD)Real / ZeroYesVia predictor (FD) / Auto (BD)Write a known value to hardware
read()Yes (FD) / No (BD)Real / ZeroNoVia predictor (FD) / Auto (BD)Read and return current hardware value
set()NoZeroYesNoStage field values before update()
get()NoZeroNoNoRead current desired value
get_mirrored_value()NoZeroNoNoRead last known hardware state
update()Yes if desired≠mirrorReal (if write needed)NoVia predictorCommit staged field values to hardware
mirror()Yes (FD) / No (BD)Real / ZeroNoYesRefresh mirror; optionally verify hardware matches model
poke()No (backdoor only)ZeroYesYesForce register bits directly (initialisation, debug)
peek()No (backdoor only)ZeroNoYesRead register bits directly (fast check without bus)
reset()NoZeroReset valueReset valueSync model after hardware reset
get_reset()NoZeroNoNoGet expected reset value for comparison

📋 Register Sequence Patterns

A base sequence class retrieves the register model handle once and makes it available to derived sequences:

class gpio_base_reg_seq extends uvm_sequence #(uvm_sequence_item);
  `uvm_object_utils(gpio_base_reg_seq)

  gpio_reg_block rm;
  uvm_status_e   status;
  uvm_reg_data_t data;

  function new(string name = "gpio_base_reg_seq");
    super.new(name);
  endfunction

  virtual task body();
    if (!uvm_config_db #(gpio_reg_block)::get(
          m_sequencer, "", "gpio_rm", rm))
      `uvm_fatal("SEQ", "No register model handle!")
  endtask
endclass

// ── Pattern 1: write specific register values ─────────────
class gpio_init_seq extends gpio_base_reg_seq;
  `uvm_object_utils(gpio_init_seq)
  function new(string n="gpio_init_seq"); super.new(n); endfunction

  task body();
    super.body();
    rm.dir_reg.write(status,  32'hFFFF_0000, .parent(this));
    rm.ien_reg.write(status,  32'h0000_000F, .parent(this));
    rm.data_reg.write(status, 32'h0000_0000, .parent(this));
  endtask
endclass

// ── Pattern 2: set fields then update ─────────────────────
class gpio_cfg_seq extends gpio_base_reg_seq;
  `uvm_object_utils(gpio_cfg_seq)
  function new(string n="gpio_cfg_seq"); super.new(n); endfunction

  rand logic [31:0] output_mask;

  task body();
    super.body();
    // Stage field values (no bus cycles)
    rm.dir_reg.gpio_dir.set(output_mask);
    rm.ien_reg.ien.set(32'h0);
    // Write them all at once
    rm.dir_reg.update(status, .parent(this));
    rm.ien_reg.update(status, .parent(this));
  endtask
endclass

// ── Pattern 3: read-back and verify ───────────────────────
class gpio_verify_seq extends gpio_base_reg_seq;
  `uvm_object_utils(gpio_verify_seq)
  function new(string n="gpio_verify_seq"); super.new(n); endfunction

  task body();
    super.body();
    // Mirror all registers (read HW, update model)
    rm.mirror(status, UVM_CHECK, .parent(this));
    if (status != UVM_IS_OK)
      `uvm_error("SEQ", "Register mirror check FAILED")
  endtask
endclass

📋 Built-in Register Sequences

UVM provides a library of register test sequences that can be applied to any register model without writing a single line of test code. They cover the most important sanity checks for register data paths.

SequenceLevelWhat it testsDisable attribute
uvm_reg_hw_reset_seqBlock or RegReads every register and checks that the hardware reset value matches the value specified in the modelNO_REG_HW_RESET_TEST
uvm_reg_single_bit_bash_seqReg onlyWrites then reads walking 1s and 0s to all RW bits in the register, verifying data path integrityNO_REG_BIT_BASH_TEST
uvm_reg_bit_bash_seqBlock onlyExecutes uvm_reg_single_bit_bash_seq on every register in the block and sub-blocksNO_REG_BIT_BASH_TEST
uvm_reg_single_access_seqReg onlyWrites via front door, verifies via back door; writes via back door, reads back via front door. Tests both paths. Requires HDL path.NO_REG_ACCESS_TEST
uvm_reg_access_seqBlock onlyRuns uvm_reg_single_access_seq on every register in the blockNO_REG_ACCESS_TEST
uvm_reg_shared_access_seqReg onlyFor registers in multiple maps: writes in one map, reads back from all other maps. Tests address aliasing.NO_SHARED_ACCESS_TEST

📋 Built-in Memory Sequences

SequenceLevelWhat it testsDisable attribute
uvm_mem_single_walk_seqMem onlyWrites a walking-ones pattern to every location in the memory range, then reads back to verifyNO_MEM_WALK_TEST
uvm_mem_walk_seqBlock onlyRuns uvm_mem_single_walk_seq on all memories in the blockNO_MEM_WALK_TEST
uvm_mem_single_access_seqMem onlyFor each memory location: writes via front door, checks via back door; writes via back door, reads via front door. Requires HDL path.NO_MEM_ACCESS_TEST
uvm_mem_access_seqBlock onlyRuns uvm_mem_single_access_seq on all memories in the blockNO_MEM_ACCESS_TEST
uvm_reg_mem_built_in_seqBlock onlyRuns ALL block-level built-in register and memory sequences — a complete basic sanity suiteAll of the above

📋 Running Built-in Sequences

// ── Run the hardware reset check on the whole block ───────
task run_phase(uvm_phase phase);
  uvm_reg_hw_reset_seq reset_seq;
  phase.raise_objection(this);

  reset_seq = uvm_reg_hw_reset_seq::type_id::create("reset_seq");
  reset_seq.model = m_env_cfg.gpio_rm;   // point at the register block
  reset_seq.start(m_env.m_agent.m_seqr);

  phase.drop_objection(this);
endtask

// ── Run all built-in sequences at once ────────────────────
task run_phase(uvm_phase phase);
  uvm_reg_mem_built_in_seq all_seq;
  phase.raise_objection(this);

  all_seq = uvm_reg_mem_built_in_seq::type_id::create("all_seq");
  all_seq.model = m_env_cfg.gpio_rm;
  all_seq.start(m_env.m_agent.m_seqr);

  phase.drop_objection(this);
endtask

// ── Exclude specific registers from a built-in test ───────
// Done in the register block build() or from a sequence
// using UVM resource_db:
uvm_resource_db #(bit)::set(
  {"REG::", gpio_rm.isr_reg.get_full_name()},
  "NO_REG_HW_RESET_TEST", 1);
// The isr_reg is a W1C register — reset test not applicable
Exclude registers where the built-in test is not applicable. Clock control registers should never have random data written to them (disabling the clock halts the simulation). W1C status registers will clear bits on read, making the reset test fail even when the DUT is correct. Use NO_REG_HW_RESET_TEST and similar attributes to exclude these registers — set in the block build() for permanent exclusion, or from a sequence for test-specific exclusion.

📋 Quick Reference

MethodKey fact
write(status, value, .parent(this))Front-door write — bus cycle, updates mirror via predictor
read(status, value, .parent(this))Front-door read — bus cycle, returns data in value, updates mirror
set(value)No bus cycle. Sets desired value only. Respects access type semantics.
get()No bus cycle. Returns desired value.
get_mirrored_value()No bus cycle. Returns last known hardware value (mirror).
update(status, .parent(this))Bus write only if desired≠mirror. Commits staged values to hardware.
mirror(status, check, .parent(this))Bus read, updates mirror. UVM_CHECK reports error if value differs.
poke(status, value)Backdoor write — zero time, forces RTL. Updates desired and mirror.
peek(status, value)Backdoor read — zero time. Updates mirror.
reset()No bus cycle. Sets desired and mirror to configured reset value. Call on hardware reset.
get_reset()Returns configured reset value for comparison. No bus cycle.
set/update patternset() fields individually → update() writes once. Preferred for multi-field configuration.
uvm_reg_hw_reset_seqChecks all registers read as their reset value
uvm_reg_bit_bash_seqWalking 1s/0s on all RW bits — verifies data path
uvm_reg_mem_built_in_seqRuns all block-level built-in sequences in one call
Disable attributeSet via uvm_resource_db with “REG::” + reg.get_full_name() and attribute string
Scroll to Top