Command Pattern in UVM: From Sequence Items to Replayable Stimulus

Chain of Responsibility showed how to route requests along a chain of handlers, each deciding whether to handle or pass along. Now we look at the next Behavioral pattern in this series: Command — how to capture a request as an object so it can be queued, logged, replayed, or dispatched by something other than the code that created it.

The Problem: Stimulus Welded to Protocol Timing

You have a DMA controller in your DUT. A regression test fails after 50 hours of simulation at descriptor #14328. The stimulus is gone — only waveforms remain. There is no way to extract those last 30 descriptors and replay them in 30 seconds.

The Naive Solution — Sequence Body Inlining Protocol Timing

// BAD: sequence body talks to the bus directly -- no transaction object exists
class dma_monolithic_seq extends uvm_sequence;
  `uvm_object_utils(dma_monolithic_seq)
  virtual dma_if vif;  // grabbed from config_db

  task body();
    // Descriptor 1: M2M copy from 0x1000 to 0x2000, 64 bytes
    @(posedge vif.clk);
    vif.desc_src    <= 32'h0000_1000;
    vif.desc_dst    <= 32'h0000_2000;
    vif.desc_len    <= 16'd64;
    vif.desc_mode   <= 2'b00;          // M2M
    vif.desc_valid  <= 1'b1;
    @(posedge vif.clk iff vif.desc_ready);
    vif.desc_valid  <= 1'b0;
    repeat (8) @(posedge vif.clk);     // magic delay -- wait for done

    // Descriptor 2: peripheral-to-memory read, 32 bytes
    @(posedge vif.clk);
    vif.desc_src    <= 32'h4000_0000;
    vif.desc_dst    <= 32'h0000_3000;
    vif.desc_len    <= 16'd32;
    vif.desc_mode   <= 2'b10;          // P2M
    vif.desc_valid  <= 1'b1;
    @(posedge vif.clk iff vif.desc_ready);
    vif.desc_valid  <= 1'b0;
    repeat (8) @(posedge vif.clk);
  endtask
endclass

This compiles and even simulates. The problems surface as soon as you try to do anything beyond run one test in isolation.

  • Cannot queue: the next stimulus source waits for this sequence's timing to complete. Multiple sequences cannot interleave through arbitration — the bus pokes are hard-wired into one task's control flow.
  • Cannot log: there is no descriptor object to record. Post-sim, all you have is waveforms — no transaction-level view, no replayable artifact, no scoreboard input.
  • Cannot replay: when the regression fails at descriptor #14328, you cannot extract a stream of objects and re-issue them. The stimulus only exists as side effects on signals.
  • Cannot reorder: arbitration policies, priority transfers, and reactive sequences require an intermediary that holds requests as data — not inline signal pokes baked into a fixed order.

What you want is each DMA transfer as a self-describing object — a Command — that holds everything needed to execute it later. A separate component (the Invoker) queues those Commands; another component (the Receiver) executes them. Once a transfer is a Command, you can queue it, log it, randomize it, and replay it. And you have already used this pattern every time you wrote a uvm_sequence_item.

Gang of Four: The Command Pattern

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

classDiagram
    class Client {
        +createCommand()
    }
    class Invoker {
        -commands: Command[]
        +setCommand(c: Command)
        +executeCommand()
    }
    class Command {
        <>
        +execute()
    }
    class ConcreteCommand {
        -receiver: Receiver
        -state
        +execute()
    }
    class Receiver {
        +action()
    }
    Client --> ConcreteCommand : creates
    Client --> Receiver : configures
    Invoker o--> Command : holds
    Command <|.. ConcreteCommand
    ConcreteCommand --> Receiver : delegates

Command turns a method call into a data object. The object holds everything the call would have needed — receiver reference, parameters, internal state. Once requests are data, you can queue them, log them, replay them, hand them across thread boundaries, or serialize them to disk. The Invoker triggers execution but does not know what the Command does; the Receiver knows what to do but does not know who asked. Creation and execution are decoupled in time, in place, and in identity.

Command sits beside two earlier patterns in this series, and the distinctions matter. Strategy and Command both wrap behavior in an object, but Strategy is invoked immediately by a Context that holds it — an algorithm-now — while Command is queued for later invocation by an Invoker that does not even know what kind of Command it holds. Strategy decouples the implementation of an algorithm from the code that calls it; Command decouples the moment of invocation and the identity of the caller from the request itself.

Chain of Responsibility and Command both route requests, but in opposite shapes. CoR has many candidate handlers and one request that walks the chain until some handler claims it — the pattern is about who handles. Command has one Receiver and many queued requests that an Invoker dispatches in order — the pattern is about when and how to dispatch. CoR resolves handler identity at runtime; Command resolves dispatch timing at runtime.

Pattern When invoked Number of handlers Decouples what?
Strategy Immediately by Context One (the selected strategy) Algorithm implementation from the code that calls it
Chain of Responsibility Walked through chain until handled Many candidates, one handles Sender from the eventual handler’s identity
Command Later, by an Invoker that holds a queue One Receiver, many queued requests Request creation from request execution (in time and place)

UVM's Command: Sequence Item, Sequencer, Driver

Every time you extended uvm_sequence_item and submitted it through start_item / finish_item, you wrote a Command. The sequencer is the Invoker — it holds the queue, runs arbitration, and dispatches one Command at a time. The driver is the Receiver — it knows the DUT and the virtual interface; it does not know which sequence produced the item. The sequence is the Client — it creates Commands and binds them to a sequencer.

The Canonical Mapping — sequence_item, sequencer, driver

GoF Role UVM Concept
Command (abstract) uvm_sequence_item
ConcreteCommand dma_descriptor_item extends uvm_sequence_item
Receiver dma_driver — knows the DUT and dma_if
Invoker uvm_sequencer#(dma_descriptor_item) — queues, arbitrates, dispatches
Client uvm_sequence — constructs and submits commands
execute() start_item(item) / finish_item(item) handshake

The Second Reveal — uvm_sequence as Composite Command

A uvm_sequence is itself a Command. It has its own lifecycle — start() kicks it off, body() runs, pre_body/post_body bracket the work — and from the caller’s perspective the whole sequence executes as a single unit. Inside body() it issues sub-Commands via `uvm_do or `uvm_do_with, each one a uvm_sequence_item dispatched through the same sequencer. This is GoF Composite applied to Command: a sequence-of-commands looks like a single command to its caller. Composition nests: a parent uvm_sequence can start() another uvm_sequence on the same sequencer inside its own body(), building Command hierarchies arbitrarily deep — a stress test composed of traffic scenarios composed of descriptor bursts, each layer obeying the same Command contract.

GoF Role UVM Concept
Composite Command uvm_sequence — Command made of Commands
Sub-commands uvm_sequence_item instances created via `uvm_do / `uvm_do_with
Nested composition Parent sequence calls child_seq.start(sequencer) inside its body()

The Third Reveal — do_record and Transaction Replay

The original purpose of the Command pattern in GoF was supporting undo and replay — turn requests into data, then write the data to a log you can read back later. UVM ships that capability built into every sequence item via do_record(). Override do_record(uvm_recorder rec) on your transaction so each field is captured into the transaction database. Pass +UVM_TR_RECORD at run time to enable recording. The resulting uvm_tr_database artifact is a serialized Command stream — a sequence of descriptor objects on disk — replayable in a tiny replay sequence (covered in §5). The failing 50-hour regression that stops at descriptor #14328 becomes a 30-second targeted re-run of the last 30 Commands.

GoF Capability UVM Mechanism
Log a command do_record(uvm_recorder rec) override on the item
Capture stream +UVM_TR_RECORD plus uvm_tr_database
Replay stream Custom dma_replay_seq reading a recorded descriptor log

Naming note: uvm_sequence_item does not call itself a ‘Command’ — but its API is the GoF Command interface. Pattern names describe intent, not class names. Once you see start_item / finish_item as the Invoker’s execute() handshake, every UVM testbench you have written is a Command-dispatch engine.

Building the DMA Verification Environment

With the role mapping settled, we now build each Command-pattern role concretely — the ConcreteCommand, the Receiver, the Invoker, the Client, and the recording hook — for a DMA controller testbench.

Part A: dma_descriptor_item as Command

The dma_descriptor_item is the ConcreteCommand. It bundles everything the driver needs to execute one DMA transfer — source address, destination address, length, mode, priority — plus a response field the driver fills in after the transfer completes.

typedef enum bit [1:0] {
  DMA_M2M = 2'b00,  // memory-to-memory
  DMA_M2P = 2'b01,  // memory-to-peripheral
  DMA_P2M = 2'b10   // peripheral-to-memory
} dma_mode_e;

class dma_descriptor_item extends uvm_sequence_item;
  rand bit [31:0]    src_addr;
  rand bit [31:0]    dst_addr;
  rand bit [15:0]    length;       // bytes
  rand dma_mode_e    mode;
  rand bit [2:0]     priority;

  // Response field -- filled by driver
  bit                dma_error;

  `uvm_object_utils_begin(dma_descriptor_item)
    `uvm_field_int (src_addr, UVM_ALL_ON)
    `uvm_field_int (dst_addr, UVM_ALL_ON)
    `uvm_field_int (length,   UVM_ALL_ON)
    `uvm_field_enum(dma_mode_e, mode, UVM_ALL_ON)
    `uvm_field_int (priority, UVM_ALL_ON)
    `uvm_field_int (dma_error, UVM_ALL_ON | UVM_NOCOMPARE)
  `uvm_object_utils_end

  constraint c_length_nonzero { length > 0; length <= 16'd4096; }
  constraint c_addr_aligned   { src_addr[1:0] == 2'b00;
                                dst_addr[1:0] == 2'b00; }

  function new(string name = "dma_descriptor_item");
    super.new(name);
  endfunction

  // do_record: log every field into the transaction database -- replay foundation
  virtual function void do_record(uvm_recorder recorder);
    super.do_record(recorder);
    `uvm_record_field("src_addr", src_addr)
    `uvm_record_field("dst_addr", dst_addr)
    `uvm_record_field("length",   length)
    `uvm_record_field("mode",     mode)
    `uvm_record_field("priority", priority)
    `uvm_record_field("dma_error", dma_error)
  endfunction
endclass

The do_record override is what enables the §5 replay story — every descriptor that flows through the testbench will land in the transaction database, timestamped and field-typed, ready to be read back as a serialized Command stream.

Part B: Sequencer (Invoker) and Driver (Receiver)

The Invoker is mostly free in UVM — one typedef and you have a sequencer that queues commands, arbitrates, and dispatches. The Receiver is where you actually write the protocol-level execution.

// Invoker: queues commands, runs arbitration, dispatches one at a time
typedef uvm_sequencer#(dma_descriptor_item) dma_sequencer;
class dma_driver extends uvm_driver#(dma_descriptor_item);
  `uvm_component_utils(dma_driver)

  virtual dma_if vif;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db#(virtual dma_if)::get(this, "", "vif", vif))
      `uvm_fatal("CFG", "No dma_if found in config_db")
  endfunction

  task run_phase(uvm_phase phase);
    dma_descriptor_item item;
    forever begin
      // Invoker handshake: receive next Command from sequencer
      seq_item_port.get_next_item(item);

      // Receiver action: execute the Command on the DUT
      drive_descriptor(item);

      // Invoker handshake: signal completion, release sequencer
      seq_item_port.item_done();
    end
  endtask

  // Receiver's protocol-level execution -- knows the DUT, not the sequence
  task drive_descriptor(dma_descriptor_item item);
    @(posedge vif.clk);
    vif.desc_src   <= item.src_addr;
    vif.desc_dst   <= item.dst_addr;
    vif.desc_len   <= item.length;
    vif.desc_mode  <= item.mode;
    vif.desc_valid <= 1'b1;
    @(posedge vif.clk iff vif.desc_ready);
    vif.desc_valid <= 1'b0;
    @(posedge vif.clk iff vif.desc_done);
    item.dma_error = vif.desc_err;
  endtask
endclass

The driver receives dma_descriptor_item handles. It never sees the sequence that produced them. It does not know whether the test is a basic write, a chaos test, or a replay of a captured log. That decoupling — the Command pattern’s central property — is what makes the driver test-agnostic.

Part C: Sequences as Client and as Composite Command

Sequences are the Client role — they build Commands and submit them to a sequencer. A sequence is also itself a Command via start(), which means sequences can compose into Command hierarchies arbitrarily deep.

class dma_basic_seq extends uvm_sequence#(dma_descriptor_item);
  `uvm_object_utils(dma_basic_seq)

  function new(string name = "dma_basic_seq");
    super.new(name);
  endfunction

  task body();
    dma_descriptor_item item;

    // Command 1: M2M copy
    `uvm_do_with(item, { mode == DMA_M2M;
                         src_addr == 32'h0000_1000;
                         dst_addr == 32'h0000_2000;
                         length   == 16'd64; })

    // Command 2: P2M read
    `uvm_do_with(item, { mode == DMA_P2M;
                         src_addr == 32'h4000_0000;
                         dst_addr == 32'h0000_3000;
                         length   == 16'd32; })

    // Command 3: M2P write
    `uvm_do_with(item, { mode == DMA_M2P;
                         src_addr == 32'h0000_4000;
                         dst_addr == 32'h4000_0010;
                         length   == 16'd128; })
  endtask
endclass
class dma_burst_seq extends uvm_sequence#(dma_descriptor_item);
  `uvm_object_utils(dma_burst_seq)

  rand int unsigned num_descriptors;
  constraint c_count { num_descriptors inside {[8:32]}; }

  function new(string name = "dma_burst_seq");
    super.new(name);
  endfunction

  task body();
    dma_descriptor_item item;
    repeat (num_descriptors) begin
      `uvm_do(item)  // fully randomized -- fields meet item's own constraints
    end
  endtask
endclass
// Composite Command: a sequence-of-sequences that looks like a single command
class dma_chained_seq extends uvm_sequence#(dma_descriptor_item);
  `uvm_object_utils(dma_chained_seq)

  function new(string name = "dma_chained_seq");
    super.new(name);
  endfunction

  task body();
    dma_basic_seq basic;
    dma_burst_seq burst;

    // Each sub-sequence is itself a Command -- composed inside this parent
    basic = dma_basic_seq::type_id::create("basic_a");
    basic.start(m_sequencer);

    burst = dma_burst_seq::type_id::create("burst");
    burst.start(m_sequencer);

    basic = dma_basic_seq::type_id::create("basic_b");
    basic.start(m_sequencer);
  endtask
endclass

Between start_item and finish_item, the sequence may call randomize() against late-bound state — register values, scoreboard hints, prior responses. start_item requests sequencer arbitration; nothing leaves the sequence until arbitration is granted. finish_item hands the Command to the driver and blocks until item_done() returns. This is the Invoker handshake; many engineers do not realize the sequencer is gating execution at exactly that point.

Part D: Recording as the Replay Foundation

Tying back to the do_record override in Part A: the recorder writes per-descriptor field data into a database that lives past simulation end. The descriptor stream becomes an artifact you can grep, slice, and replay.

# Run with transaction recording enabled
./simv +UVM_TESTNAME=dma_basic_test +UVM_TR_RECORD
// Optional: set the recording detail and database type at the test level
function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  // UVM_FULL captures all fields; UVM_HIGH captures named fields only
  set_report_verbosity_level(UVM_MEDIUM);
  uvm_default_recorder.set_recording_enabled(1);
endfunction

What you get out:

  • A uvm_tr_database artifact (simulator-specific format — VCS, Xcelium, and Questa each have their own).
  • Every dma_descriptor_item shows up as a transaction with timestamped fields.
  • Tools can replay or shrink failing streams — covered in §5.

Common Pitfalls Across the Build

Six failure modes recur often enough across DMA-style Command builds that they belong in the cheat sheet:

  • Forgetting clone() before queuing externally: if you stash a handle to an item after finish_item, the next randomize() on the same handle mutates the queued object. Always clone() if you intend to hold or reuse.
  • Using new() instead of type_id::create(): breaks the Factory override in §6 — the override resolves only through the registered Factory path.
  • do_record never fires: add +UVM_TR_RECORD at run time and confirm recording_detail is set high enough.
  • Misordering start_item / randomize / finish_item: randomizing before start_item skips sequencer arbitration; randomizing after finish_item mutates a queued object.
  • Forgetting item_done() in driver: sequencer stalls indefinitely. Debug hint: get_next_item succeeded once but the next never arrives.
  • Driver storing item handle past item_done(): the sequence may mutate it. Clone before storing.

Scaling Up: Arbitration, Layering, Reactive Commands, Replay

Once stimulus is Commands, the Invoker side — the sequencer — opens up four scaling axes: dispatch policy, exclusive access, two-way communication, and replay.

Sequencer Arbitration — Invoker Dispatch Policy

When multiple sequences run concurrently on the same sequencer, the sequencer decides whose Command goes next. That dispatch policy is the Invoker's arbitration mode. UVM ships several built-in modes; install one in a single line and the policy changes for every Command thereafter.

ModeBehaviorWhen to use
SEQ_ARB_FIFOFirst-come, first-servedDefault; fair, predictable
SEQ_ARB_WEIGHTEDProbability weighted by sequence priorityQoS scenarios — high-priority traffic dominates
SEQ_ARB_RANDOMUniform random across waiting sequencesStress testing — break ordering assumptions
SEQ_ARB_STRICT_FIFOStrict by priority then FIFOPriority traffic always wins ties
SEQ_ARB_STRICT_RANDOMStrict by priority then randomPriority traffic always wins ties
SEQ_ARB_USERUser-defined via user_priority_arbitration()Custom dispatch logic
class dma_qos_test extends base_test;
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);

    // Install the Invoker's dispatch policy -- weighted by sequence priority
    env.dma_agent.sequencer.set_arbitration(SEQ_ARB_WEIGHTED);

    fork
      begin
        dma_burst_seq lo = dma_burst_seq::type_id::create("lo");
        lo.set_priority(100);
        lo.start(env.dma_agent.sequencer);
      end
      begin
        dma_burst_seq hi = dma_burst_seq::type_id::create("hi");
        hi.set_priority(500);  // 5x higher dispatch probability
        hi.start(env.dma_agent.sequencer);
      end
    join

    phase.drop_objection(this);
  endtask
endclass

The arbitration mode is the Invoker's dispatch policy. Different tests can install different policies on the same sequencer without touching the driver, sequences, or items. The Command pattern's separation is what makes this swap free.

Layered Sequences — grab and lock

Two sequencer primitives suspend arbitration so a sequence can emit a contiguous run of Commands:

  • grab(seqr) — exclusive sequencer access; every other sequence waits, regardless of priority. Use for atomic operations that must not be interleaved.
  • lock(seqr) — cooperative variant. Yields between items but still blocks all other sequences from dispatching.
  • Release with ungrab(seqr) / unlock(seqr).
class dma_atomic_reset_seq extends uvm_sequence#(dma_descriptor_item);
  `uvm_object_utils(dma_atomic_reset_seq)

  task body();
    dma_descriptor_item item;

    // Atomic: no other sequence's Command interleaves between these two
    grab(m_sequencer);
      `uvm_do_with(item, { mode == DMA_M2M; length == 16'd0; }) // disarm
      `uvm_do_with(item, { mode == DMA_M2M; length == 16'd64;
                           src_addr == 32'h0; dst_addr == 32'h0; })
    ungrab(m_sequencer);
  endtask
endclass

Nested sequences (child_seq.start(seqr) from inside body()) are the cleaner default — they participate in arbitration like any other Command. Reach for grab/lock specifically when arbitration must be paused.

Reactive Sequences — the Two-Way Command

Strict GoF Command is fire-and-forget — the Invoker calls execute() and forgets. UVM extends Command with a formal return path: the driver may call put_response(rsp) after executing, and the sequence retrieves it via get_response(rsp). This converts Command into a request/response idiom without losing the queuing or replay properties.

task drive_descriptor(dma_descriptor_item item);
  @(posedge vif.clk);
  vif.desc_src   <= item.src_addr;
  vif.desc_dst   <= item.dst_addr;
  vif.desc_len   <= item.length;
  vif.desc_mode  <= item.mode;
  vif.desc_valid <= 1'b1;
  @(posedge vif.clk iff vif.desc_ready);
  vif.desc_valid <= 1'b0;
  @(posedge vif.clk iff vif.desc_done);
  item.dma_error = vif.desc_err;

  // Send response back to originating sequence
  seq_item_port.put_response(item);
endtask
task body();
  dma_descriptor_item item;
  dma_descriptor_item rsp;

  `uvm_do_with(item, { mode == DMA_M2M; length == 16'd64; })
  get_response(rsp);

  if (rsp.dma_error) begin
    `uvm_info("DMA_SEQ", "Previous descriptor errored -- switching to safe mode", UVM_MEDIUM)
    `uvm_do_with(item, { mode == DMA_M2M; length == 16'd4; }) // tiny safe descriptor
  end
endtask

Use put_response / get_response when the next Command depends on the previous Command's result. Fire-and-forget is fine when sequences and the scoreboard are the only consumers of status — and the scoreboard listens on its own analysis port.

Mid-Test Replay — Deterministic Failure Shrinking

A 50-hour regression fails at descriptor #14328. The transaction recording from §4 is the entire Command stream serialized to a database. A dma_replay_seq reads the last N entries — say descriptors 14300 through 14328 — and re-issues them through the same sequencer. The failure reproduces in 30 seconds instead of 50 hours.

typedef struct {
  bit [31:0]  src_addr;
  bit [31:0]  dst_addr;
  bit [15:0]  length;
  dma_mode_e  mode;
  bit [2:0]   priority;
} dma_record_t;

class dma_replay_seq extends uvm_sequence#(dma_descriptor_item);
  `uvm_object_utils(dma_replay_seq)

  // Populated by a helper that parses the recorded log file
  dma_record_t records[$];

  function new(string name = "dma_replay_seq");
    super.new(name);
  endfunction

  // Load records from a text dump produced by post-processing the tr_database
  function void load_from_file(string path);
    int fh = $fopen(path, "r");
    dma_record_t rec;
    if (fh == 0) `uvm_fatal("REPLAY", $sformatf("Cannot open '%s'", path))
    while (!$feof(fh)) begin
      if ($fscanf(fh, "%h %h %d %d %d\n",
                  rec.src_addr, rec.dst_addr, rec.length,
                  rec.mode, rec.priority) == 5)
        records.push_back(rec);
    end
    $fclose(fh);
  endfunction

  task body();
    dma_descriptor_item item;
    foreach (records[i]) begin
      `uvm_do_with(item, { src_addr == records[i].src_addr;
                           dst_addr == records[i].dst_addr;
                           length   == records[i].length;
                           mode     == records[i].mode;
                           priority == records[i].priority; })
    end
  endtask
endclass

Why this works only because of Command: the original stimulus was a stream of data objects, not signal pokes. If §1's monolithic sequence had been the production design, this replay sequence would be impossible — there would be no Commands to record, and no Commands to replay. Replay is the property Command was invented for.

Advanced: Command + Factory

The Factory decides which Command implementation gets created. The Command controls what happens when the Invoker dispatches it. Together: swap descriptor behavior at test time — chaos descriptors that deliberately violate invariants — without touching the driver, sequencer, or any existing sequence. One line in build_phase and every create() in the entire testbench resolves to the chaos variant.

The dma_chaos_descriptor below extends dma_descriptor_item, relaxing the base class's invariants and injecting illegal field combinations in post_randomize:

// Chaos variant -- extends the base Command, deliberately violates invariants
class dma_chaos_descriptor extends dma_descriptor_item;
  `uvm_object_utils(dma_chaos_descriptor)

  // Disable parent constraints that we want to deliberately violate
  constraint c_length_nonzero { length inside {[16'd0:16'd8192]}; }
  constraint c_addr_aligned   { src_addr[1:0] dist { 2'b00 := 50,
                                                     [2'b01:2'b11] := 50 };
                                dst_addr[1:0] dist { 2'b00 := 50,
                                                     [2'b01:2'b11] := 50 }; }

  // Inject chaos in post_randomize -- illegal mode encodings, oversize bursts
  function void post_randomize();
    super.post_randomize();
    if ($urandom_range(0, 9) == 0)  // 10%: oversize length
      length = 16'hFFFF;
    if ($urandom_range(0, 9) == 0)  // 10%: zero length
      length = 16'h0000;
    if ($urandom_range(0, 19) == 0) // 5%: address aliasing
      dst_addr = src_addr;
  endfunction

  function new(string name = "dma_chaos_descriptor");
    super.new(name);
  endfunction
endclass

One line in the test swaps the Command implementation everywhere — the Factory resolves on every create() call in the testbench:

class dma_chaos_test extends base_test;
  `uvm_component_utils(dma_chaos_test)

  function void build_phase(uvm_phase phase);
    // Swap the Command implementation everywhere -- Factory resolves on every create()
    dma_descriptor_item::type_id::set_type_override(
      dma_chaos_descriptor::get_type());
    super.build_phase(phase);
  endfunction
endclass

Every existing sequence calls dma_descriptor_item::type_id::create("desc") — never new(). The Factory resolves the registered type and returns a dma_chaos_descriptor instance instead. Driver, sequencer, sequences, and existing constraints remain untouched. Where the inline constraints in sequences are still satisfiable, they apply; the chaos class violates additional invariants the sequences never asserted.

sequenceDiagram
    participant Test as dma_chaos_test
    participant Factory
    participant Env
    participant Sequence as dma_basic_seq (Client)
    participant Sequencer as dma_sequencer (Invoker)
    participant Driver as dma_driver (Receiver)
    participant DUT

    Test->>Factory: set_type_override(dma_chaos_descriptor)
    Test->>Env: build_phase()
    Env->>Sequence: start(sequencer)
    Sequence->>Factory: create("desc")
    Factory-->>Sequence: dma_chaos_descriptor instance
    Sequence->>Sequence: randomize() — chaos constraints applied
    Sequence->>Sequencer: start_item(item) / finish_item(item)
    Sequencer->>Driver: get_next_item(item)
    Driver->>DUT: drive_descriptor — malformed length / aliased addrs
    DUT-->>Driver: error / unexpected behavior
    Driver->>Sequencer: item_done()

Twelve patterns. Twelve orthogonal concerns. Each one solves a different problem — none of them replaces another.

  • Factorytype_id::create() decides what concrete class to instantiate
  • Singletonuvm_root, uvm_factory, uvm_config_db — one-of-a-kind services
  • Builderuvm_sequence's step-wise construction of multi-item stimulus
  • Prototypeclone() on uvm_object — copy without re-randomizing
  • Adapteruvm_reg_adapter translates register ops to bus items
  • Decorator — analysis subscribers attach to monitors without modifying them
  • Facade — virtual sequences hide multi-agent choreography behind one API
  • Proxyuvm_reg mediates hardware register access; sequencer lock() arbitrates
  • Observeruvm_callback / uvm_event_pool decouple notifications
  • Strategyuvm_reg_adapter / bus_protocol_strategy swap protocol algorithms
  • Chain of Responsibility — address decoders, layered protocol stacks
  • Commanduvm_sequence_item encapsulates requests; sequencer queues; driver executes

Quick Reference

GoF Role → UVM Mapping (Cheatsheet)

GoF RoleUVM Concept
Command (abstract)uvm_sequence_item
ConcreteCommanddma_descriptor_item extends uvm_sequence_item
Receiverdma_driver — knows the DUT
Invokeruvm_sequencer#(T) — queues, arbitrates, dispatches
Clientuvm_sequence#(T) — builds and submits commands
execute()start_item(item) / finish_item(item)
Composite Commanduvm_sequence containing sub-sequences
Command loggingdo_record(uvm_recorder rec) + +UVM_TR_RECORD
Command replayReader sequence consuming the recorded log

Sequence Item / Sequencer / Driver API Summary

APIPurpose
seq.start(sequencer, parent_seq)Begin executing a sequence on a sequencer
start_item(item)Request sequencer arbitration for this item
finish_item(item)Hand item to driver after late-binding randomization
`uvm_do(item)Macro: create + randomize + start_item + finish_item
`uvm_do_with(item, {...})Same, with inline constraint block
get_next_item(item)Driver-side: block until next item arrives
item_done()Driver-side: release sequencer to dispatch next item
put_response(rsp)Driver-side: return response to originating sequence
get_response(rsp)Sequence-side: retrieve driver response
sequencer.set_arbitration(SEQ_ARB_*)Configure Invoker dispatch policy
grab(seqr) / ungrab(seqr)Exclusive sequencer access
lock(seqr) / unlock(seqr)Cooperative exclusive access
do_record(uvm_recorder rec)Override to log fields for transaction recording
type_id::set_type_override(T)Factory swap of Command implementation

Common Mistakes

MistakeFix
Forgetting clone() before queuing externallyAlways clone() if you intend to hold the item after finish_item
Using new() instead of type_id::create()Always create() so Factory overrides resolve
do_record never firesAdd +UVM_TR_RECORD; ensure recording_detail is set high enough
Randomizing before start_itemMove randomization between start_item and finish_item — that’s where late-bound state is visible
Forgetting item_done() in driverSequencer stalls; debug hint: get_next_item succeeded but the next never arrives
Driver storing item handle past item_done()Item may be mutated by the sequence; clone before storing
Expecting fire-and-forget when response is neededUse get_response(rsp) + driver put_response(rsp)
Mixed arbitration mode assumptions across testsInstall the mode in each test’s build_phase; don’t rely on the previous test’s setting

When to Reach for Command

  • “I want to queue / arbitrate transfers between multiple stimulus sources” → standard sequence item + sequencer is already Command; lean on SEQ_ARB_* arbitration.
  • “I want to log every transfer and replay a failing slice” → override do_record, enable +UVM_TR_RECORD, build a replay sequence that reads the log.
  • “I want a test to inject malformed transfers without touching sequence code” → Factory override on the descriptor type; the Command swap is invisible to every layer above.

Previous: Chain of Responsibility — From address decoders to layered protocol stacks

Next: Coming soon

Author
Milan Kubavat
Sharing knowledge about silicon verification, hardware design, and engineering insights.

Comments (0)

Leave a Comment