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.
| Sequence | What it checks | Needs |
|---|---|---|
uvm_reg_hw_reset_seq | Reads every register and compares it with its reset value in the model | DUT freshly reset |
uvm_reg_bit_bash_seq | Writes 1s and 0s to every bit of every register and reads them back | Known access policies, idle DUT |
uvm_reg_access_seq | Writes through the front door and reads through the back door, then the reverse | HDL paths for back-door access |
uvm_reg_mem_shared_access_seq | Writes through one address map and reads back through every other map | Registers mapped in more than one map |
uvm_mem_walk_seq | Runs a walking-ones pattern through every memory | Memories in the model |
uvm_mem_access_seq | Front-door write and back-door read for every memory location, then the reverse | HDL paths for back-door access |
uvm_reg_mem_built_in_seq | Runs any combination of the block-level sequences above | Whatever 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
modelset, and the reset test needs a freshly reset DUT NO_REG_TESTSand the per-test attributes in the resource database exclude registers that must not be tested automaticallyuvm_reg_sequenceaddswrite_reg(),read_reg(),update_reg()andmirror_reg()for custom sequences- Use built-in sequences for structural checks and custom sequences for functional register behaviour