Skip to content

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_sequencer give 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_sequencer is 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 wiringp_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.

A base sequence holds boilerplate; derived scenario sequences extend it and override body()extendsextendsextendssmoke_seqoverrides body() onlystress_seqoverrides body() onlyerror_seqoverrides body() onlybase_seq (the scaffold)objection handling · p_sequencer ·common knobs12
Figure 1 — the base sequence pattern. A base sequence holds the boilerplate every scenario needs: objection handling (raise/drop, often via the starting phase), typed sequencer access (p_sequencer), and common knobs. Concrete scenarios (smoke, stress, error) extend the base and override body() with just the behavior that differs. So the scaffolding is written once in the base, and each scenario is a small derived class — the same reuse as components, applied to stimulus.

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 scenariosmoke_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.

base sequence, p_sequencer, and the sequence library
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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: scenario tier on transaction tier on itemsscenario sequences → transaction sequences → itemsscenario sequences → transaction sequences → itemsScenario sequences (top)mem_test, error_test — what a test means; built from transaction sequences. The reusable test-level tier.mem_test, error_test — what a test means; built from transaction sequences. The reusable test-level tier.Transaction sequences (middle)configure, burst_write, burst_read — reusable operations; built from items. Shared across scenarios.configure, burst_write, burst_read — reusable operations; built from items. Shared across scenarios.Items (bottom)individual transactions via start_item/finish_item — the handshake (Modules 9.3–9.4).individual transactions via start_item/finish_item — the handshake (Modules 9.3–9.4).
Figure 2 — layered sequences give stimulus abstraction tiers. The top tier is scenario sequences (mem_test, error_test) — what a test means. The middle tier is transaction sequences (configure, burst_write, burst_read) — reusable operations. The bottom tier is items (the handshake). Each tier is built from the one below: scenarios call transaction sequences, which emit items. You work at the tier you need, and lower tiers are reused by many upper ones — the same layering that keeps software maintainable, applied to stimulus.

Layered sequences turn composition into architecture. The top tier is scenario sequencesmem_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 sequencesconfigure, burst_write, burst_readreusable 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.

A reactive sequence waits for a DUT-initiated request, generates a response, and sends it backDUT initiates → sequence reacts → response driven backDUT initiates → sequence reacts → response driven back1DUT initiates a requestthe DUT drives a transaction (a read request, a transfer) theresponder agent observes.2Request reaches the reactive sequencethe responder driver/sequencer passes the request to the sequence(not a pre-planned item).3Sequence generates the responseit computes the appropriate reply (read data, an ack) — reacting tothe request's content.4Response driven back to the DUTthe responder drives the reply; the flow is DUT-initiated,sequence-responds.
Figure 3 — a reactive (responder) sequence responds to DUT-initiated requests. A proactive sequence decides what to send and drives it. A reactive sequence instead waits for the DUT to initiate (a request observed on the interface), then generates the appropriate response and sends it back — driven by the DUT's activity, not a pre-planned script. The responder agent's driver passes the request to the sequence, which reacts. Used for slaves and models: a memory that answers reads, a peripheral that acks. The flow is DUT-initiated, sequence-responds.

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 cycles
Proactive (sequence-initiated) vs reactive (DUT-initiated) stimulusproactive: sequence initiates (seq_drive) → drives its transaction (TX)proactive: sequence in…reactive: DUT initiates (dut_req) → request appears (RQ)reactive: DUT initiate…reactive: sequence responds (seq_resp) → drives the reply (RS)reactive: sequence res…order reversed: proactive=seq-first, reactive=dut-firstorder reversed: proact…clkmodePROPROPROPRO--REAREAREAREAREAREA--seq_drivedut_reqseq_respbus--TXTX------RQ--RSRS----t0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — proactive vs reactive timing. In the PROACTIVE region, the sequence initiates: seq_drive goes high first (the sequence decides to send), then the bus carries the sequence's transaction. In the REACTIVE region, the DUT initiates: dut_req goes high first (the DUT makes a request), then the reactive sequence responds (seq_resp) and drives the reply. The order of initiation is reversed: proactive is seq-then-bus, reactive is dut_req-then-response. Masters drive proactively; slaves respond reactively.

The 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

Symptom

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.

Root cause

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:

why p_sequencer was null
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
  endclass

This 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.

Diagnosis

The tell is a null crash on p_sequencer while the sequence otherwise runs. Diagnose p_sequencer bugs:

  1. Confirm the null is p_sequencer, not its target. A crash at p_sequencer.X could be a null p_sequencer or a null X. Check p_sequencer itself first — if it's null, the declaration is missing.
  2. Look for `uvm_declare_p_sequencer. Confirm the sequence (or its base) declares p_sequencer with the correct sequencer type. Its absence is the cause.
  3. 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_sequencer null) even with the macro present.
  4. Confirm m_sequencer is set. If even m_sequencer is null, the sequence wasn't started on a sequencer at all (Module 9.1) — a different problem from a missing p_sequencer declaration.
Prevention

Declare p_sequencer where typed access is needed:

  1. Add `uvm_declare_p_sequencer(type) to declare the typed handle. Whenever a sequence reaches sequencer-specific context (config, sibling handles), declare p_sequencer with the correct sequencer type.
  2. 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.
  3. Use m_sequencer for type-agnostic needs. If you only need to send items (not reach sequencer members), m_sequencer suffices and no declaration is needed; reach for p_sequencer only for typed context.
  4. 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_sequencer is 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_sequencer without `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_sequencer in 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 case instead of the sequence library. For a random mix of registered sequences, uvm_sequence_library is 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_sequencer type. The type must be the actual sequencer; a mismatch fails the cast and leaves p_sequencer null 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

  1. 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 only body().
  2. Fix the null. A sequence crashes on p_sequencer.cfg. Name the missing macro, where to put it, and how p_sequencer differs from m_sequencer.
  3. Layer a test. For "configure, then run mixed read/write traffic," describe the scenario, transaction, and item tiers and which sequences live at each.
  4. 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_library runs 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_sequencer is the shared mechanism: m_sequencer is the untyped handle UVM always sets (enough to send items), p_sequencer is 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.