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
- Gang of Four: The Command Pattern
- UVM's Command: Sequence Item, Sequencer, Driver
- Building the DMA Verification Environment
- Scaling Up: Arbitration, Layering, Reactive Commands, Replay
- Advanced: Command + Factory
- Quick Reference
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_databaseartifact (simulator-specific format — VCS, Xcelium, and Questa each have their own). - Every
dma_descriptor_itemshows 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 afterfinish_item, the nextrandomize()on the same handle mutates the queued object. Alwaysclone()if you intend to hold or reuse. - Using
new()instead oftype_id::create(): breaks the Factory override in §6 — the override resolves only through the registered Factory path. do_recordnever fires: add+UVM_TR_RECORDat run time and confirmrecording_detailis set high enough.- Misordering
start_item/ randomize /finish_item: randomizing beforestart_itemskips sequencer arbitration; randomizing afterfinish_itemmutates a queued object. - Forgetting
item_done()in driver: sequencer stalls indefinitely. Debug hint:get_next_itemsucceeded 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.
| Mode | Behavior | When to use |
|---|---|---|
SEQ_ARB_FIFO | First-come, first-served | Default; fair, predictable |
SEQ_ARB_WEIGHTED | Probability weighted by sequence priority | QoS scenarios — high-priority traffic dominates |
SEQ_ARB_RANDOM | Uniform random across waiting sequences | Stress testing — break ordering assumptions |
SEQ_ARB_STRICT_FIFO | Strict by priority then FIFO | Priority traffic always wins ties |
SEQ_ARB_STRICT_RANDOM | Strict by priority then random | Priority traffic always wins ties |
SEQ_ARB_USER | User-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.
- Factory —
type_id::create()decides what concrete class to instantiate - Singleton —
uvm_root,uvm_factory,uvm_config_db— one-of-a-kind services - Builder —
uvm_sequence's step-wise construction of multi-item stimulus - Prototype —
clone()onuvm_object— copy without re-randomizing - Adapter —
uvm_reg_adaptertranslates register ops to bus items - Decorator — analysis subscribers attach to monitors without modifying them
- Facade — virtual sequences hide multi-agent choreography behind one API
- Proxy —
uvm_regmediates hardware register access; sequencerlock()arbitrates - Observer —
uvm_callback/uvm_event_pooldecouple notifications - Strategy —
uvm_reg_adapter/bus_protocol_strategyswap protocol algorithms - Chain of Responsibility — address decoders, layered protocol stacks
- Command —
uvm_sequence_itemencapsulates requests; sequencer queues; driver executes
Quick Reference
GoF Role → UVM Mapping (Cheatsheet)
| GoF Role | UVM Concept |
|---|---|
| Command (abstract) | uvm_sequence_item |
| ConcreteCommand | dma_descriptor_item extends uvm_sequence_item |
| Receiver | dma_driver — knows the DUT |
| Invoker | uvm_sequencer#(T) — queues, arbitrates, dispatches |
| Client | uvm_sequence#(T) — builds and submits commands |
execute() | start_item(item) / finish_item(item) |
| Composite Command | uvm_sequence containing sub-sequences |
| Command logging | do_record(uvm_recorder rec) + +UVM_TR_RECORD |
| Command replay | Reader sequence consuming the recorded log |
Sequence Item / Sequencer / Driver API Summary
| API | Purpose |
|---|---|
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
| Mistake | Fix |
|---|---|
Forgetting clone() before queuing externally | Always 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 fires | Add +UVM_TR_RECORD; ensure recording_detail is set high enough |
Randomizing before start_item | Move randomization between start_item and finish_item — that’s where late-bound state is visible |
Forgetting item_done() in driver | Sequencer 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 needed | Use get_response(rsp) + driver put_response(rsp) |
| Mixed arbitration mode assumptions across tests | Install 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
Comments (0)
Leave a Comment