Skip to content

UVM RAL · Chapter 7 · RAL Sequences

User-Defined Register Sequences

Built-in sequences test the register map, but user-defined register sequences encode functional intent, such as putting a block in loopback mode, arming a DMA and waiting until it is ready, or clearing an error state. These become the reusable building blocks that your tests and virtual sequences compose. This lesson is about designing them the RAL way so they actually pay off. Drive the model through register and field handles rather than hardcoded bus addresses, parameterize with a config object instead of magic numbers, and poll status bits with a timeout instead of an unbounded wait that can hang the simulation. It ends with the anti-pattern that quietly defeats RAL: a reusable config sequence that hardcodes a raw address, so it keeps passing while writing the wrong register the moment the address map is re-laid-out.

Foundation12 min readUVM RALRegister Sequencesreuseconfig objectpoll timeout

Chapter 7 · Section 7.5 · RAL Sequences

1. Why Should I Learn This?

User-defined register sequences are where your verification actually uses the register model — they turn 'configure the block' into reusable, portable building blocks that every test and virtual sequence composes, so writing them well multiplies across the whole environment. And writing them well means writing them the RAL way: through model handles, parameterized, robust — because a register sequence that hardcodes addresses or hangs on a poll silently gives back everything RAL bought you.

Learning to design register sequences for reuse — model handles not offsets, config not magic numbers, poll-with-timeout not unbounded waits — is what makes the register model pay off in practice. It builds on the register-sequence mechanics of 7.1 and feeds the sequence composition of 7.6 and virtual sequences (Chapter 11).

2. Industry Story — the config sequence that wrote the wrong register

A team writes a configure_datapath register sequence early in the project — it programs a mode register and enables the block — and it is reused everywhere: dozens of tests call it. To 'keep it fast,' whoever wrote it skipped the model and wrote the mode register by its raw address, a literal 0x40 baked into the sequence. It worked, it was reused, everyone trusted it.

Mid-project the address map was re-laid-out (a common event — a block was added, offsets shifted, 3.1), and the mode register moved from 0x40 to 0x60. The register model was updated to match, so every sequence that went through the model kept working automatically — that is exactly what RAL is for. But configure_datapath did not go through the model; it wrote the literal 0x40, which now belonged to a different register. So dozens of tests silently began configuring the wrong register: configure_datapath still 'passed' (the write succeeded), the mode register was never actually programmed, and the datapath ran in its default mode across the whole regression. The bug surfaced weeks later as a pile of confusing functional failures, all tracing back to one hardcoded address in a 'reusable' sequence. The post-mortem lesson: a register sequence must drive the model through register handles, never hardcoded addresses — the whole point of RAL is that the model absorbs address-map changes, and a sequence that hardcodes an offset opts out of that protection and silently writes the wrong register when the map moves.

3. Concept — encode intent against the model, parameterized and robust

A user-defined register sequence is a uvm_reg_sequence (7.1) whose body performs a functional series of register operations — the design-specific intent the built-ins do not cover. Three design principles make it reusable and safe:

  • Drive the model, not addresses. Every operation goes through a register or field handle — mode.write(...), ctrl.enable.set(1); ctrl.update(...), status.ready.read(...) — never a raw address or literal offset. This is what makes the sequence portable (it works through any map and adapter the register is reachable by, 5.x) and durable (a map re-lay-out updates the model, and the sequence follows automatically, 3.1). Bypassing the model with a hardcoded address throws away exactly the protection RAL provides.
  • Parameterize, do not hardcode. Drive the sequence from a config object or arguments (mode, thresholds, enables), not magic numbers baked into the body — so one 'configure the block' sequence serves many tests with different settings, instead of a copy per configuration.
  • Be robust — poll with a timeout. When the sequence waits on a status bit (a 'ready,' a 'done'), bound the wait: read the bit in a loop with a timeout, and fail cleanly if it never sets — never an unbounded wait that hangs the whole simulation if the DUT never asserts it (7.6).

Here is a reusable 'configure and start' sequence expressed as intent against the model:

Reusable configure-and-start register sequence: write MODE from config, enable CTRL, poll STATUS.ready with timeoutconfig reg seqregister modelDUT registersmode.write(cfg.mode) — from config, not a literalmode.write(cfg.mode)— from config, not …frontdoor write MODEctrl.enable.set(1);ctrl.update()frontdoor write CTRLloop: status.ready.read() until set OR timeoutloop:status.ready.read()…ready = 0 ... 0 ...1ready set -> done (timeout -> fail cleanly, never hang)ready set -> done(timeout -> fail…
Figure 1 — a reusable configure-and-start register sequence, all against the MODEL (not addresses). Write the MODE register from a config field, set CTRL.enable=1 and update, then POLL STATUS.ready in a bounded loop until it sets or a timeout fires — failing cleanly on timeout rather than hanging. Because every step names a register/field handle, the sequence is portable across maps and survives address-map re-lay-outs; because it reads MODE from config, one sequence serves many tests.

4. Mental Model — a register sequence is a named intent, spoken to the model

5. Working Example — a reusable, parameterized config sequence

A uvm_reg_sequence that configures the block from a config object, drives the model by handle, and polls with a timeout:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A reusable configuration sequence: parameterized by a config object, driven entirely through
// the MODEL, robust against a ready bit that never sets.
class configure_datapath_seq extends uvm_reg_sequence#(uvm_sequence#(uvm_reg_item));
  `uvm_object_utils(configure_datapath_seq)
  my_reg_block   regs;      // the model — set by the caller
  datapath_cfg   cfg;       // config object — values, NOT magic numbers
  function new(string name = "configure_datapath_seq"); super.new(name); endfunction
 
  virtual task body();
    uvm_status_e s;
    uvm_reg_data_t rd;
    // 1. Program MODE from CONFIG, through the model handle (portable, survives map changes).
    regs.mode.write(s, cfg.mode);                      // NOT: bus.write(0x40, ...)
    // 2. Enable the block via a FIELD handle (desired -> update writes it, 4.2).
    regs.ctrl.enable.set(1'b1);
    regs.ctrl.update(s);
    // 3. Poll STATUS.ready with a TIMEOUT — bounded wait, fail cleanly, never hang (7.6).
    for (int i = 0; i < cfg.ready_timeout; i++) begin
      regs.status.ready.read(s, rd);
      if (rd == 1) break;
    end
    if (rd != 1) `uvm_error("CFG", "datapath ready never asserted within timeout")
  endtask
endclass
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Reuse: one sequence, many tests, different settings — no copy-paste, no magic numbers.
configure_datapath_seq cfg_seq = configure_datapath_seq::type_id::create("cfg_seq");
cfg_seq.regs = reg_model;                 // the model handle
cfg_seq.cfg  = loopback_cfg;              // this test's settings (mode, timeout)
cfg_seq.start(bus_sqr);
// Change the address map tomorrow (3.1): update the model, and this sequence still works untouched,
// because it names registers (mode, ctrl.enable, status.ready) rather than addresses.

Every access names a register or field handle and every value comes from config — so the sequence is portable across maps, survives a re-lay-out, and serves many tests. The next section is what happens when one access hardcodes an address instead.

6. Debugging Session — the hardcoded address that survived a map change

1

A 'reusable' config sequence hardcodes a raw address instead of driving the model, so it silently writes the wrong register after the map is re-laid-out

DRIVE THE MODEL, NOT ADDRESSES
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A 'reusable' config sequence that skipped the model to write MODE by its raw address:
class configure_datapath_seq extends uvm_reg_sequence#(uvm_sequence#(uvm_reg_item));
  // ...
  virtual task body();
    uvm_status_e s;
    // BUG: hardcoded bus address 0x40 instead of regs.mode.write(...). Bypasses RAL entirely.
    bus_write(0x40, cfg.mode);            // writes whatever register lives at 0x40 TODAY
    regs.ctrl.enable.set(1'b1);
    regs.ctrl.update(s);
    // ...
  endtask
endclass
// After the map is re-laid-out and MODE moves to 0x60, this still writes 0x40 -> the WRONG register.
Symptom

Everything looks fine for months: the sequence 'passes,' dozens of tests use it, MODE is configured. Then the address map is re-laid-out (a block added, offsets shifted, 3.1) and MODE moves from 0x40 to 0x60. The model is updated to match, so every other sequence keeps working. But this sequence keeps writing the literal 0x40 — which now belongs to a different register — so the mode register is silently never programmed, the datapath runs in its default mode, and a wave of confusing functional failures appears across the regression. Nothing reports a register error: the write to 0x40 succeeds, and the sequence completes 'successfully' while configuring the wrong register.

Root Cause

The sequence bypassed the register model for one access, writing the mode register by a hardcoded bus address (0x40) instead of through the model handle (regs.mode.write(...)). RAL's core value is that the model is the single place the register-to-address mapping lives, so when the address map changes you update the model once and every sequence that drives the model follows automatically (3.1). A hardcoded address opts out of that: it pins the access to a physical offset that the model no longer controls, so when the map moves, the model updates but the literal does not, and the sequence now writes whatever register happens to live at the old offset. The failure is silent because writing an arbitrary valid address still succeeds at the bus level — there is no error, just the wrong register written — which is precisely the class of bug (a valid-but-wrong address) that RAL exists to eliminate, reintroduced by stepping outside RAL.

Fix

Route the access through the model like every other one: replace bus_write(0x40, cfg.mode) with regs.mode.write(s, cfg.mode), so the sequence names the register and the model supplies its current address. Re-run after the map re-lay-out and it targets 0x60 automatically, untouched. Then audit the codebase for other hardcoded addresses and magic offsets in sequences — each is the same latent bug. The rule the bug teaches: a register sequence must drive the model through register and field handles, never hardcoded addresses; the model absorbs address-map changes, and any access that hardcodes an offset opts out of that protection and will silently write the wrong register when the map moves. The moment a literal address or magic number appears in a sequence, RAL's guarantee is gone for that access.

7. Common Mistakes

  • Hardcoding a bus address or literal offset in a sequence. It bypasses the model and silently writes the wrong register after a map change (3.1). Always drive the model by handle.
  • Baking magic numbers into the body. Parameterize with a config object so one sequence serves many tests, instead of a copy per configuration.
  • Unbounded polling on a status bit. A wait that never returns hangs the whole sim if the bit never sets — poll with a timeout and fail cleanly (7.6).
  • Duplicating config logic per test. Write one reusable register sequence and compose it, rather than re-implementing 'configure the block' repeatedly.
  • Writing fields with raw write when set+update is meant. Use field handles and update for read-modify-write intent (4.2).

8. Industry Best Practices

  • Drive the model, always. Every register access through a register/field handle — never a hardcoded address or offset (3.1). This is the non-negotiable RAL rule.
  • Parameterize with config objects. Values from config, not magic numbers — one reusable sequence per intent, many tests.
  • Poll with a timeout. Bounded waits on status bits that fail cleanly; never an unbounded wait that can hang the simulation (7.6).
  • Build a small library of intent sequences. 'configure,' 'arm,' 'clear-error' — the reusable blocks tests and virtual sequences compose (Chapter 11).
  • Audit sequences for literal addresses and magic numbers. Each is a latent 'wrong register after a map change' bug — treat them as review red flags.

9. Interview / Review Questions

10. Key Takeaways

  • User-defined register sequences encode your design's functional intent — 'configure,' 'arm,' 'clear-error' — as reusable uvm_reg_sequence building blocks that tests and virtual sequences compose; the built-ins (7.2–7.4) test the map, these use it.
  • Drive the model, not addresses. Every access through a register/field handle — never a hardcoded offset — so the sequence is portable across maps and survives address-map re-lay-outs (3.1).
  • Parameterize with a config object. Values from config, not magic numbers, so one sequence serves many tests.
  • Poll with a timeout. Bounded waits on status bits that fail cleanly — never an unbounded wait that hangs the whole regression (7.6).
  • The signature anti-pattern is a hardcoded address in a 'reusable' sequence: it works until the map moves, then silently writes the wrong register — exactly the valid-but-wrong-address bug RAL exists to prevent (1.2), reintroduced by bypassing RAL.