UVM
Sequence Design Patterns
The reusable patterns of sequence design — a base sequence for boilerplate, layered sequences for abstraction, the sequence library for a random mix, and reactive sequences that respond to DUT-initiated activity.
Sequences · Module 9 · Page 9.7
The Engineering Problem
The module so far built the mechanics of sequences: the body(), the handshake, composition, control, and random generation (Modules 9.1–9.6). Mechanics let you write a sequence; they don't tell you how to organize sequences so a large testbench's stimulus stays maintainable. As scenarios multiply — dozens of tests, shared setup, abstraction layers, responder behavior — ad-hoc sequences become a tangle of duplicated boilerplate and one-off shapes. What's needed is a small set of recurring patterns — the "design patterns" of stimulus.
This chapter is that catalog. A base sequence holds the boilerplate every sequence needs (objection handling, typed sequencer access, common knobs), and concrete scenarios extend it — so you write the scaffolding once. Layered sequences stack high-level scenario sequences over low-level transaction sequences, giving stimulus abstraction layers (Module 9.5's composition, formalized). The sequence library (uvm_sequence_library) runs a random mix of registered sequences, a built-in mechanism for varied regression (Module 9.6, productized). Reactive sequences respond to DUT-initiated activity — the responder/slave pattern — rather than driving pre-planned stimulus. Each is a reusable shape for a recurring need, and one recurring mechanism — p_sequencer for typed sequencer access — underlies several. This chapter is the patterns: what each is, when to reach for it, and the p_sequencer mistake that bites across them.
What are the reusable sequence design patterns — base sequences, layered sequences, the sequence library, and reactive sequences — when do you use each, and how does
p_sequencergive a sequence the typed sequencer access these patterns rely on?
Motivation — why patterns, not ad-hoc sequences
Sequence patterns exist because stimulus at scale has recurring structural needs that ad-hoc sequences solve badly:
- Boilerplate recurs, so factor it into a base. Every sequence needs the same scaffolding — raise/drop the objection, access the sequencer's context, expose knobs. Duplicating it across dozens of sequences is error-prone (forget the objection and the sequence is killed, Module 9.1). A base sequence holds it once; scenarios inherit it.
- Stimulus has abstraction levels, so layer it. "Run a memory test" is a scenario built from "configure," "burst write," "burst read" — which are built from items. Layered sequences make those levels explicit, so high-level tests read as scenarios and low-level sequences stay reusable (Module 9.5).
- Regression needs a varied mix, so make selection a first-class mechanism. Running a random selection of many sequences is exactly what coverage-driven regression wants (Module 9.6). The sequence library productizes this — register sequences, pick a mode — instead of hand-coding the random
case. - Some agents respond rather than drive, so support reactive stimulus. A slave/responder (a memory model) doesn't generate planned traffic; it reacts to DUT-initiated requests. The reactive sequence pattern handles this DUT-driven flow, which proactive sequences can't express.
- Patterns need sequencer context, so
p_sequenceris the shared mechanism. Many patterns — a base reaching a config, a reactive sequence reaching the agent's state — need typed access to their sequencer.p_sequencer(declared) provides it; it's the connective tissue under the patterns.
The motivation, in one line: stimulus at scale recurringly needs shared boilerplate, abstraction layers, a varied mix, and reactive response — so the sequence patterns give a reusable shape for each (base, layered, library, reactive), with p_sequencer as the typed-context mechanism several depend on.
Mental Model
Hold the sequence patterns as a toolkit of proven stimulus blueprints:
The patterns are blueprints you reach for, each shaped to a recurring stimulus need — the scaffold, the stack, the grab-bag, and the responder. The scaffold (base sequence) is the pre-built frame every scenario stands on: it already has the objection wiring and sequencer access, so each new scenario is just the part that differs, bolted onto a frame you built once. The stack (layered sequences) is abstraction in tiers: a top tier of scenarios resting on a middle tier of transaction sequences resting on items — you work at whatever tier you need, and lower tiers are reused by many upper ones. The grab-bag (sequence library) is a bag of registered scenarios the testbench draws from at random, so a regression run is a different handful each time. The responder (reactive sequence) is the blueprint that waits and reacts: instead of pushing planned work, it answers whatever the DUT asks. And running through several of them is the same wiring —
p_sequencer, the typed line back to the sequencer's context, which you must connect (declare) or it dangles (null). You don't invent a shape per scenario; you pick the blueprint that fits and fill in the specifics.
So designing stimulus is choosing blueprints: a base for the shared frame, layers for abstraction, the library for a random mix, reactive for DUT-driven response — and wiring p_sequencer where a pattern needs sequencer context. The blueprints are what keep a large testbench's stimulus coherent instead of a pile of one-offs.
Visual Explanation — the base sequence pattern
The foundational pattern is the base sequence: shared boilerplate in a base class, concrete scenarios as derived sequences. It's the scaffold the others build on.
The base sequence pattern is the template-method idea applied to stimulus. The base sequence holds everything every scenario needs but shouldn't re-write: the objection handling (raising and dropping it around the run, often via the starting-phase mechanism so the sequence manages its own lifetime — Module 9.1), the typed sequencer access (p_sequencer, so the sequence can reach the agent's config and context), and common knobs (shared rand fields or config the scenarios tune). Each concrete scenario — smoke_seq, stress_seq, error_seq — simply extends the base and overrides body() with the part that differs: smoke sends a few transactions, stress floods, error injects faults. The win is the same as the component/sequence reuse seen throughout the module: the scaffolding is written once, and a new scenario is a small derived class containing only its distinctive behavior, standing on a frame that already handles the boilerplate correctly. This matters most for the easy-to-forget boilerplate — the objection in particular, whose omission silently kills a sequence (Module 9.1) — because putting it in the base means every derived scenario inherits the correct handling automatically. So the base sequence isn't just tidiness; it's a correctness pattern: it makes the right scaffolding the default, so individual scenarios can't get it wrong.
RTL / Simulation Perspective — the patterns in code
Each pattern is a recognizable code shape. Seeing the base sequence, the library, and the p_sequencer declaration together shows how they're built.
// ── p_sequencer: typed access to the sequencer (declare it, or p_sequencer is null) ──
class base_seq extends uvm_sequence #(bus_item);
`uvm_object_utils(base_seq)
`uvm_declare_p_sequencer(my_sequencer) // ← generates p_sequencer (typed cast of m_sequencer)
function new(string name="base_seq"); super.new(name); endfunction
// common boilerplate: scenarios inherit objection handling + p_sequencer access
task pre_body();
if (starting_phase != null) starting_phase.raise_objection(this); // manage own lifetime
endtask
task post_body();
if (starting_phase != null) starting_phase.drop_objection(this);
endtask
endclass
// ── DERIVED scenario: only the behavior that differs; reaches config via p_sequencer ──
class stress_seq extends base_seq;
`uvm_object_utils(stress_seq)
function new(string name="stress_seq"); super.new(name); endfunction
task body();
bus_item req;
repeat (p_sequencer.cfg.stress_count) // ← p_sequencer gives typed sequencer context
`uvm_do(req)
endtask
endclass
// ── SEQUENCE LIBRARY: run a random mix of registered sequences ──
class my_seq_lib extends uvm_sequence_library #(bus_item);
`uvm_object_utils(my_seq_lib)
`uvm_sequence_library_utils(my_seq_lib)
function new(string name="my_seq_lib"); super.new(name); init_sequence_library(); endfunction
endclass
// register sequences into the library, set the mode (e.g. UVM_SEQ_LIB_RANDOM) and count
// `uvm_add_to_seq_lib(stress_seq, my_seq_lib)The code shows the patterns concretely. p_sequencer is declared with `uvm_declare_p_sequencer(my_sequencer), which generates a typed handle (p_sequencer) that is the sequence's m_sequencer cast to my_sequencer — so the sequence can reach sequencer-specific members like p_sequencer.cfg (the DebugLab is what happens if you use it without declaring it). The base sequence (base_seq) puts the objection handling in pre_body/post_body (called automatically around body()), using the starting phase so the sequence raises/drops its own objection — so every derived scenario inherits correct lifetime management without re-writing it. The derived scenario (stress_seq) extends the base and overrides only body(), reaching the config through p_sequencer.cfg.stress_count — a small class with just its distinctive behavior. The sequence library (my_seq_lib) extends uvm_sequence_library, uses the library utils macro, and (via `uvm_add_to_seq_lib and a mode like UVM_SEQ_LIB_RANDOM) runs a random selection of registered sequences — the built-in version of Module 9.6's random case. (Reactive sequences, the fourth pattern, are shown in the Runtime section.) Each pattern is a small, recognizable shape, and p_sequencer is the recurring mechanism that gives them sequencer context.
Verification Perspective — layered sequences
The pattern that most shapes a testbench's stimulus architecture is layering: high-level scenario sequences built from low-level transaction sequences, in explicit tiers. It's Module 9.5's composition, formalized into reusable abstraction levels.
Layered sequences turn composition into architecture. The top tier is scenario sequences — mem_test, error_test — which express what a test means at the highest level; a scenario sequence's body() runs transaction sequences, not items. The middle tier is transaction sequences — configure, burst_write, burst_read — reusable operations that are meaningful units of protocol activity; each is built from items. The bottom tier is items, generated via the handshake (Modules 9.3–9.4). The power of making these tiers explicit is twofold. First, readability: a scenario reads as a sequence of named operations ("configure, then burst-write, then burst-read"), not a wall of item generation — the test's intent is visible at the top tier. Second, reuse: the middle-tier transaction sequences are written once and reused by many scenarios — burst_write serves mem_test, error_test, and a stress scenario alike — so the bulk of stimulus code is shared. This is the same layering principle that keeps software maintainable (high-level logic over reusable mid-level functions over primitives), applied to stimulus, and it composes with the base sequence pattern (every tier's sequences can extend a common base) and random generation (a scenario can randomly select among transaction sequences, Module 9.6). Layering is what lets stimulus scale from a handful of sequences to a large, navigable library without becoming a tangle — which is why it's the central architectural pattern of sequence design.
Runtime / Execution Flow — reactive sequences
The pattern that inverts the usual flow is the reactive sequence: instead of driving planned stimulus, it responds to DUT-initiated activity. It's how responder/slave agents work, and its control flow is the mirror of a proactive sequence.
The reactive sequence inverts the proactive flow this module has assumed. A proactive sequence (everything up to now) decides what stimulus to generate and drives it — the sequence initiates. A reactive sequence waits for the DUT to initiate, then responds. Step 1, the DUT initiates a request — it drives a transaction onto the interface (a read request to a memory, a transfer to a peripheral) that the responder agent observes. Step 2, that request reaches the reactive sequence — the responder's driver/sequencer passes the observed request to the sequence, rather than the sequence pulling a pre-planned item. Step 3, the sequence generates the appropriate response by reacting to the request's content — computing the read data for the requested address, forming an ack, modeling the slave's behavior. Step 4, the response is driven back to the DUT. This DUT-initiated, sequence-responds flow is essential for responder/slave components — a memory model that answers reads, a peripheral that acknowledges transfers — which can't be expressed as proactive stimulus because their behavior is defined by the DUT's requests, not chosen in advance. Reactive sequences typically lean on p_sequencer and the response path (Module 8.2's request/response identity) to reach the request and route the reply. So the pattern catalog covers both directions of stimulus: proactive sequences (base, layered, library) that drive planned traffic, and reactive sequences that respond to the DUT — a testbench's masters are proactive, its slaves reactive.
Waveform Perspective — proactive vs reactive
The proactive/reactive distinction is a difference in who initiates, and it's visible on a timeline: proactive stimulus is sequence-initiated, reactive stimulus follows a DUT request.
Proactive (sequence-initiated) vs reactive (DUT-initiated) stimulus
12 cyclesThe waveform captures the direction of initiation that distinguishes the patterns. In the PROACTIVE region, the sequence initiates: seq_drive goes high first — the sequence decides to send — and then the bus carries the sequence's transaction (TX). This is the master-side flow every prior chapter assumed: the sequence is the source, and stimulus originates from its body(). In the REACTIVE region, the DUT initiates: dut_req goes high first — the DUT makes a request (RQ on the bus) — and then the reactive sequence responds (seq_resp), driving the reply (RS). The order is reversed: proactive is sequence-then-bus, reactive is DUT-request-then-response. This visual difference is the whole concept: a proactive sequence is a source of stimulus (it leads), while a reactive sequence is a responder (it follows the DUT). A testbench typically has both — masters whose agents run proactive sequences driving planned traffic, and slaves whose agents run reactive sequences answering the DUT — and recognizing which an agent needs is the design decision the reactive pattern addresses. The timeline makes it concrete: watch who goes first, and you know whether you're looking at proactive or reactive stimulus.
DebugLab — the null p_sequencer
A sequence that crashed because p_sequencer was used without being declared
A sequence accessed the agent's config through p_sequencer.cfg and crashed with a null-handle dereference the moment it ran — p_sequencer was null. The sequence was started correctly on the right sequencer (its items would have driven fine), and the sequencer did have a valid cfg. But p_sequencer itself was null, so reaching through it to .cfg faulted immediately.
The sequence used p_sequencer but never declared it with `uvm_declare_p_sequencer, so the typed p_sequencer handle was never created — only the untyped m_sequencer was set:
class my_seq extends uvm_sequence #(bus_item);
// ✗ MISSING: `uvm_declare_p_sequencer(my_sequencer)
task body();
repeat (p_sequencer.cfg.count) ... // ✗ p_sequencer is null → null-handle crash
endtask
endclass
what UVM sets: m_sequencer → the sequencer this sequence runs on (UNTYPED uvm_sequencer_base)
what you used: p_sequencer → a TYPED handle (my_sequencer) — only exists if you DECLARE it
without the declaration: p_sequencer is never created → null → dereferencing it crashes
fix — declare p_sequencer so it's generated as a typed cast of m_sequencer:
class my_seq extends uvm_sequence #(bus_item);
`uvm_declare_p_sequencer(my_sequencer) // ✓ generates p_sequencer = typed(m_sequencer)
task body();
repeat (p_sequencer.cfg.count) ... // ✓ p_sequencer now valid, typed access works
endtask
endclassThis is the recurring p_sequencer mistake. When a sequence runs, UVM sets m_sequencer — the untyped handle (uvm_sequencer_base) to the sequencer it's running on. That handle is enough to send items, but it's untyped, so you can't reach sequencer-specific members (like cfg) through it. To get typed access, you declare `uvm_declare_p_sequencer(my_sequencer), which generates a p_sequencer handle that is m_sequencer cast to my_sequencer — and only then does p_sequencer exist and point at the real sequencer. The sequence in the bug used p_sequencer (assuming it was available) but never declared it, so p_sequencer was never created and stayed null, and dereferencing it (p_sequencer.cfg) crashed immediately. The fix is to add the `uvm_declare_p_sequencer(my_sequencer) macro (typically in a base sequence so all scenarios inherit it). The general rule: m_sequencer is the untyped handle UVM always sets (good for sending items); p_sequencer is the typed handle you must declare to reach sequencer-specific context — and using p_sequencer without declaring it is a guaranteed null crash. This is why several patterns (base sequences, reactive sequences) put the declaration in a shared base: so the typed access the patterns rely on is always present.
The tell is a null crash on p_sequencer while the sequence otherwise runs. Diagnose p_sequencer bugs:
- Confirm the null is
p_sequencer, not its target. A crash atp_sequencer.Xcould be a nullp_sequenceror a nullX. Checkp_sequenceritself first — if it's null, the declaration is missing. - Look for
`uvm_declare_p_sequencer. Confirm the sequence (or its base) declaresp_sequencerwith the correct sequencer type. Its absence is the cause. - Check the sequencer type matches. The declared type must be the actual sequencer the sequence runs on; a mismatched type can fail the cast (leaving
p_sequencernull) even with the macro present. - Confirm
m_sequenceris set. If evenm_sequenceris null, the sequence wasn't started on a sequencer at all (Module 9.1) — a different problem from a missingp_sequencerdeclaration.
Declare p_sequencer where typed access is needed:
- Add
`uvm_declare_p_sequencer(type)to declare the typed handle. Whenever a sequence reaches sequencer-specific context (config, sibling handles), declarep_sequencerwith the correct sequencer type. - Put the declaration in a base sequence. So all derived scenarios inherit typed sequencer access and none can forget it — the base-sequence pattern applied to
p_sequencer. - Use
m_sequencerfor type-agnostic needs. If you only need to send items (not reach sequencer members),m_sequencersuffices and no declaration is needed; reach forp_sequenceronly for typed context. - Match the declared type to the actual sequencer. Ensure the type in the macro is the sequencer the sequence runs on, so the cast succeeds and
p_sequenceris valid.
The one-sentence lesson: m_sequencer is the untyped sequencer handle UVM always sets (enough to send items), while p_sequencer is the typed handle you must declare with `uvm_declare_p_sequencer(type) to reach sequencer-specific context — so using p_sequencer without declaring it leaves it null and crashes, and the fix (best placed in a base sequence) is to declare it with the correct sequencer type.
Common Mistakes
- Using
p_sequencerwithout`uvm_declare_p_sequencer. The typed handle is never created and stays null; declare it (in a base sequence) before reaching sequencer-specific context. - Duplicating boilerplate instead of a base sequence. Re-writing objection handling and
p_sequencerin every sequence invites errors (a forgotten objection kills the sequence); factor it into a base. - Flattening abstraction instead of layering. Hand-coding items in a top-level scenario loses reuse; layer scenario sequences over reusable transaction sequences.
- Hand-rolling a random
caseinstead of the sequence library. For a random mix of registered sequences,uvm_sequence_libraryis the built-in mechanism — register and set a mode. - Trying to express a responder as a proactive sequence. A slave's behavior is DUT-initiated; use a reactive sequence, not pre-planned stimulus.
- Mismatching the declared
p_sequencertype. The type must be the actual sequencer; a mismatch fails the cast and leavesp_sequencernull even with the macro.
Senior Design Review Notes
Interview Insights
Four patterns recur. The base sequence pattern puts the boilerplate every sequence needs — objection handling, typed sequencer access via p_sequencer, common knobs — in a base class, and concrete scenarios extend it and override only body(); this writes the scaffolding once and, crucially, makes easy-to-forget boilerplate like the objection correct by inheritance. Layered sequences stack abstraction tiers: high-level scenario sequences built from mid-level transaction sequences built from items, so tests read as named operations and the mid-level sequences are reused across many scenarios — the same layering that keeps software maintainable, applied to stimulus. The sequence library, uvm_sequence_library, runs a random selection of registered sequences in a chosen mode, which is the built-in, productized version of hand-coding a random case for a varied regression mix. And reactive sequences invert the usual flow: instead of driving pre-planned stimulus, they respond to DUT-initiated activity, which is how responder and slave agents work — a memory model that answers reads, a peripheral that acks — because their behavior is defined by the DUT's requests rather than chosen in advance. Several of these rely on p_sequencer, the typed sequencer handle a sequence declares to reach sequencer-specific context. The unifying idea is that stimulus at scale has recurring structural needs — shared boilerplate, abstraction layers, a varied mix, reactive response — and each pattern is a reusable shape for one of them, so a large testbench's stimulus stays coherent instead of a pile of one-offs.
Both are handles to the sequencer a sequence runs on, but m_sequencer is untyped and p_sequencer is typed. When a sequence runs, UVM always sets m_sequencer, which is of the base type uvm_sequencer_base — it's enough to send items through the handshake, but because it's untyped, you can't reach sequencer-specific members through it, like a config object or sibling handles the sequencer holds. p_sequencer is a typed handle to the same sequencer, of your specific sequencer class, that lets you reach those members. The difference is that p_sequencer only exists if you declare it with the `uvm_declare_p_sequencer macro, passing your sequencer type; the macro generates p_sequencer as m_sequencer cast to that type. If you use p_sequencer without declaring it, the handle is never created and stays null, so dereferencing it crashes — which is a very common bug. So the rule is: use m_sequencer when you only need to send items and don't need sequencer-specific context, since it's always set and needs no declaration; declare and use p_sequencer when you need typed access to the sequencer's members. The declared type must match the actual sequencer the sequence runs on, or the cast fails and p_sequencer is null even with the macro present. In practice the declaration is often placed in a base sequence so all derived scenarios inherit the typed access, which is the base-sequence pattern applied to p_sequencer — making the typed context the patterns rely on always available.
Exercises
- Design a base sequence. Sketch a base sequence that handles the objection and declares
p_sequencer, and a derived scenario that extends it and overrides onlybody(). - Fix the null. A sequence crashes on
p_sequencer.cfg. Name the missing macro, where to put it, and howp_sequencerdiffers fromm_sequencer. - Layer a test. For "configure, then run mixed read/write traffic," describe the scenario, transaction, and item tiers and which sequences live at each.
- Proactive or reactive? For (a) a bus master driving traffic and (b) a memory slave answering reads, say which pattern each agent's sequence uses and why.
Summary
- Sequence design patterns turn mechanics into maintainable stimulus architecture: a base sequence (boilerplate — objection,
p_sequencer, knobs — once, scenarios as derived classes), layered sequences (scenario tier over transaction tier over items), the sequence library (uvm_sequence_libraryruns a random mix of registered sequences), and reactive sequences (respond to DUT-initiated activity). - The base sequence is a correctness pattern, not just tidiness: it makes the easy-to-forget boilerplate (especially the objection, Module 9.1) correct by inheritance, so derived scenarios can't get it wrong.
- Layering is the central architectural pattern: explicit scenario/transaction/item tiers make tests readable and mid-level sequences reusable across many scenarios — the software-layering principle applied to stimulus.
p_sequenceris the shared mechanism:m_sequenceris the untyped handle UVM always sets (enough to send items),p_sequenceris the typed handle you must declare with`uvm_declare_p_sequencer(type)to reach sequencer-specific context — using it undeclared is a null crash (best fixed by declaring in a base).- The durable rule of thumb: factor boilerplate into a base sequence, layer scenarios over reusable transaction sequences, use the sequence library for a random mix and reactive sequences for responders — and declare
p_sequencer(with the right type, in the base) wherever typed sequencer access is needed, because these patterns are the reusable shapes that keep a large testbench's stimulus coherent.
Next — Virtual Sequences: the patterns so far run on one sequencer; coordinating stimulus across multiple agents needs a level above. The next module opens with the virtual sequence — a top-level orchestrator that runs sub-sequences on many sequencers at once, synchronizing whole-system scenarios.