UVM register sequences come in two kinds. The built-in register sequences, such as uvm_reg_hw_reset_seq and uvm_reg_bit_bash_seq, test every register in a block automatically using only the register model. Custom register sequences, written by extending uvm_reg_sequence, program the DUT for a specific scenario. Both run on the same register model and the same bus agent.

The built-in sequences are usually the first tests run on a new block or SoC. With no hand-written stimulus at all, they confirm that every register has the right reset value, that every read-write bit really toggles, and that the address map is wired correctly.

8 min read | Intermediate

What You'll Learn

  • Run the built-in register and memory test sequences on a register model
  • Exclude registers that must not be tested automatically
  • Write a custom register sequence with uvm_reg_sequence
  • Decide which checks to automate and which to write by hand

The Built-in Sequences

All the built-in sequences have a model property of type uvm_reg_block. Assign your register model to it, start the sequence, and it walks through every register or memory in that block and its sub-blocks.

SequenceWhat it checksNeeds
uvm_reg_hw_reset_seqReads every register and compares it with its reset value in the modelDUT freshly reset
uvm_reg_bit_bash_seqWrites 1s and 0s to every bit of every register and reads them backKnown access policies, idle DUT
uvm_reg_access_seqWrites through the front door and reads through the back door, then the reverseHDL paths for back-door access
uvm_reg_mem_shared_access_seqWrites through one address map and reads back through every other mapRegisters mapped in more than one map
uvm_mem_walk_seqRuns a walking-ones pattern through every memoryMemories in the model
uvm_mem_access_seqFront-door write and back-door read for every memory location, then the reverseHDL paths for back-door access
uvm_reg_mem_built_in_seqRuns any combination of the block-level sequences aboveWhatever the selected tests need

Most block-level sequences also have a single-register or single-memory version, such as uvm_reg_single_bit_bash_seq or uvm_mem_single_walk_seq, which tests just one element.

Running the Reset and Bit-Bash Tests

The examples use the traffic controller register model from UVM Register Model Example. Its test already resets the DUT in reset_phase, so the built-in sequences can run in main_phase:


class reg_builtin_test extends base_test;
  `uvm_component_utils(reg_builtin_test)
  // Rest of the code (constructor)

  virtual task main_phase(uvm_phase phase);
    uvm_reg_hw_reset_seq rst_seq  = uvm_reg_hw_reset_seq::type_id::create("rst_seq");
    uvm_reg_bit_bash_seq bash_seq = uvm_reg_bit_bash_seq::type_id::create("bash_seq");

    phase.raise_objection(this);
    rst_seq.model  = m_env.m_reg_env.m_ral_model;     // the block to test
    bash_seq.model = m_env.m_reg_env.m_ral_model;

    rst_seq.start(m_env.m_agent.m_seqr);              // reset values first
    bash_seq.start(m_env.m_agent.m_seqr);             // then every RW bit
    phase.drop_objection(this);
  endtask
endclass

The order matters. uvm_reg_hw_reset_seq must run while the DUT still holds its reset values, so it goes first. Its own hook for resetting the DUT, reset_blk(), is empty in UVM 1.2: the DUT has to be reset before the sequence starts, which is exactly what the reset_phase of this test does.

The bit-bash test then writes patterns into every read-write bit. Afterwards the registers hold test values, not reset values, so any test that follows should reprogram the DUT rather than assume it is still in reset.

Each failure is reported as an error naming the register and the expected and actual values. A clean run produces no errors at all, which is why these sequences are so useful as a quick sanity check after every RTL or address map change.

Running several tests at once

uvm_reg_mem_built_in_seq runs a selection of the block-level tests in one go. Its tests property is a bit mask built from UVM_DO_REG_HW_RESET, UVM_DO_REG_BIT_BASH, UVM_DO_REG_ACCESS, UVM_DO_MEM_ACCESS, UVM_DO_SHARED_ACCESS and UVM_DO_MEM_WALK, and defaults to UVM_DO_ALL_REG_MEM_TESTS:


uvm_reg_mem_built_in_seq all_seq = uvm_reg_mem_built_in_seq::type_id::create("all_seq");
all_seq.model = m_env.m_reg_env.m_ral_model;
all_seq.tests = UVM_DO_REG_HW_RESET | UVM_DO_REG_BIT_BASH;   // only these two
all_seq.start(m_env.m_agent.m_seqr);

Selecting tests explicitly is safer than running all of them. The access tests need back-door HDL paths, and the shared-access test needs registers mapped in several maps. A model without those features would make the extra tests skip or fail.

Excluding Registers

Not every register survives automatic testing. Writing random patterns to a clock-control or soft-reset register can stop the DUT, and a status register that changes by itself will fail a bit-bash check. Such registers are excluded by putting a bit named after the test into the resource database, scoped with "REG::" followed by the register's full name:


// Don't bit-bash ctrl: setting mod_en starts the traffic controller
uvm_resource_db #(bit)::set({"REG::", m_ral_model.cfg.ctrl.get_full_name()},
                            "NO_REG_BIT_BASH_TEST", 1, this);

// Skip every built-in test for the whole cfg block
uvm_resource_db #(bit)::set({"REG::", m_ral_model.cfg.get_full_name(), ".*"},
                            "NO_REG_TESTS", 1, this);

NO_REG_TESTS disables every register test, while names such as NO_REG_HW_RESET_TEST, NO_REG_BIT_BASH_TEST, NO_REG_ACCESS_TEST, NO_MEM_TESTS and NO_MEM_WALK_TEST disable just one kind. How the scope string is matched is explained in Understanding the resource database.

The best place for these settings is the register model's own build(), next to the register definition, so that every test using the model inherits them. A resource cannot be removed once set, so exclusions made inside one test sequence also affect every later sequence in the same simulation.

Quick Check

Q: uvm_reg_hw_reset_seq reports a mismatch on every register, all showing the value your previous test wrote. What went wrong?

Show Answer

The DUT was not reset before the sequence ran. The sequence resets only the model, not the hardware, so it compared reset values against registers that still held old data. Run it straight after the DUT reset, before any other register traffic.

Writing a Custom Register Sequence

Built-in sequences check that registers exist and behave. They don't know what the DUT is for. To program a real scenario, write your own sequence. Extending uvm_reg_sequence gives you the same model handle plus convenience tasks such as write_reg(), read_reg(), update_reg() and mirror_reg(), which take the register as their first argument and pass the sequence as the parent automatically:


class peak_mode_seq extends uvm_reg_sequence;
  `uvm_object_utils(peak_mode_seq)
  // Rest of the code (constructor)

  virtual task body();
    ral_sys_traffic m_ral;
    uvm_status_e    status;

    if (!$cast(m_ral, model)) `uvm_fatal("SEQ", "model is not a ral_sys_traffic block")

    // Program both timers, then switch on the module in peak mode
    write_reg(m_ral.cfg.timer[0], status, 32'h0000_0400);
    write_reg(m_ral.cfg.timer[1], status, 32'h0000_0100);

    m_ral.cfg.ctrl.mod_en.set(1);
    m_ral.cfg.ctrl.profile.set(1);
    update_reg(m_ral.cfg.ctrl, status);                // one write with both fields

    mirror_reg(m_ral.cfg.ctrl, status, UVM_CHECK);     // confirm the DUT took it
    if (status != UVM_IS_OK) `uvm_error("SEQ", "ctrl access failed")
  endtask
endclass

model is declared as uvm_reg_block, so the sequence casts it to the generated block type to reach registers by name. The test starts it exactly like the built-in sequences: assign model, then call start() on the bus sequencer.

Extending uvm_reg_sequence is a convenience, not a requirement. A plain uvm_sequence can call write() and read() on the registers directly, as described in UVM Register Access Methods.

Built-in or Custom?

  • Built-in for structural checks that apply to every register: reset values, read-write bits, address decoding, back-door paths and memory integrity. They scale to thousands of registers with no extra code.
  • Custom for anything that depends on what the register means: configuration flows, interrupt enable and clear, side effects such as a write starting an operation, and registers excluded from the automatic tests.

A typical register test plan uses both: the built-in sequences as a smoke test on every build, and a set of custom sequences for each feature of the block.

Common Mistakes

Mistake #1: Forgetting to assign model

Every built-in sequence, and every uvm_reg_sequence, needs its model handle set before start(). Without it the sequence has nothing to test and fails on a null handle.

Mistake #2: Running the reset test after other traffic

uvm_reg_hw_reset_seq does not reset the DUT. Run it straight after a hardware reset, before anything writes to the registers.

Mistake #3: Bit-bashing registers with side effects

Toggling bits of a clock, reset or enable register can change how the DUT behaves for the rest of the test. Exclude such registers with NO_REG_BIT_BASH_TEST and cover them with a custom sequence.

Mistake #4: Running tests the model cannot support

The access tests need back-door HDL paths, and shared-access tests need several address maps. Select only the tests your model supports with the tests mask of uvm_reg_mem_built_in_seq.

Summary

In this article, you learned:

  • Built-in sequences test reset values, read-write bits, back-door paths, shared maps and memories from the register model alone
  • Each needs its model set, and the reset test needs a freshly reset DUT
  • NO_REG_TESTS and the per-test attributes in the resource database exclude registers that must not be tested automatically
  • uvm_reg_sequence adds write_reg(), read_reg(), update_reg() and mirror_reg() for custom sequences
  • Use built-in sequences for structural checks and custom sequences for functional register behaviour
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5