UVM Sequences Fundamentals - A Software Engineer's Perspective
If you're coming from a software engineering background, UVM sequences might seem familiar yet different. They're essentially generators that produce stimulus using patterns similar to the Iterator and Command design patterns. This post explores sequences through the lens of software engineering principles.
The Mental Model: Sequences as Generators
Think of UVM sequences like Python generators or C# IEnumerable - they yield items on demand rather than producing everything upfront. This lazy evaluation is crucial for verification where stimulus can be infinite or data-dependent.
flowchart LR
subgraph "Producer (Sequence)"
S[Sequence] -->|yields| SI[Sequence Item]
end
subgraph "Consumer (Driver)"
SI -->|consumed by| D[Driver]
end
D -->|requests next| S
Sequence vs Sequence Item: Separation of Concerns
Following the Single Responsibility Principle (SRP), UVM separates:
| Component | Responsibility | SW Analogy |
|---|---|---|
uvm_sequence_item | Data container with constraints | DTO (Data Transfer Object) |
uvm_sequence | Generation logic and ordering | Generator/Iterator |
uvm_sequencer | Routing and arbitration | Message Broker/Dispatcher |
uvm_driver | Protocol-level signaling | Adapter/Gateway |
This separation enables independent evolution - you can change generation logic without touching the data structure, or swap drivers without modifying sequences.
Sequence Anatomy: The Template Method Pattern
UVM sequences implement the Template Method design pattern. The base class defines the skeleton algorithm, and subclasses override specific steps:
class uvm_sequence #(type REQ=uvm_sequence_item) extends uvm_sequence_base;
// Template method - defines the algorithm skeleton
// Called internally when sequence starts
virtual task start(uvm_sequencer_base sequencer, ...);
// 1. Setup phase
pre_start();
pre_body(); // Hook for subclass
// 2. Core execution
body(); // Abstract - subclass MUST implement
// 3. Cleanup phase
post_body(); // Hook for subclass
post_start();
endtask
// The "abstract" method subclasses implement
virtual task body();
// Default: empty - subclass provides implementation
endtask
endclass
Hook Methods for Extension
The pre/post hooks follow the Open-Closed Principle - open for extension, closed for modification:
class protocol_base_seq extends uvm_sequence #(packet_item);
`uvm_object_utils(protocol_base_seq)
// Template: Setup protocol state before any transaction
virtual task pre_body();
`uvm_info("SEQ", "Initializing protocol state", UVM_MEDIUM)
endtask
// Template: Cleanup after all transactions
virtual task post_body();
`uvm_info("SEQ", "Protocol cleanup complete", UVM_MEDIUM)
endtask
// Concrete sequences only implement body()
virtual task body();
// Subclass implements specific stimulus
endtask
endclass
Execution Flow: The Handshake Protocol
The sequence-driver communication follows a request-response pattern with clear synchronization points:
sequenceDiagram
participant S as Sequence
participant SQR as Sequencer
participant D as Driver
S->>SQR: start_item(req)
Note over SQR: Waits for driver ready
D->>SQR: get_next_item(req)
SQR-->>S: Grant access
S->>S: Randomize req
S->>SQR: finish_item(req)
SQR-->>D: Deliver item
D->>D: Drive on interface
D->>SQR: item_done()
Note over S: Can proceed to next item
The Canonical Pattern
class write_sequence extends uvm_sequence #(bus_transaction);
`uvm_object_utils(write_sequence)
function new(string name = "write_sequence");
super.new(name);
endfunction
task body();
// Step 1: Create item (Factory Pattern)
req = bus_transaction::type_id::create("req");
// Step 2: Request arbitration (blocks until granted)
start_item(req);
// Step 3: Randomize with constraints (late randomization)
if (!req.randomize() with {
direction == WRITE;
addr inside {[32'h1000:32'h1FFF]};
}) `uvm_error("SEQ", "Randomization failed")
// Step 4: Send to driver (blocks until driven)
finish_item(req);
endtask
endclass
Why Late Randomization Matters
Randomization happens after start_item() returns - this is intentional:
- Reactive stimulus: Base constraints on current DUT state
- Protocol compliance: Previous transaction results can influence next
- Resource awareness: Check availability before committing
task body();
req = bus_transaction::type_id::create("req");
start_item(req);
// Can access DUT state here via p_sequencer or config_db
if (!req.randomize() with {
// Constraint based on current state
addr != p_sequencer.last_written_addr; // Avoid WAW hazard
}) `uvm_error("SEQ", "Randomization failed")
finish_item(req);
endtask
Sequence Macros: Convenience vs Control
UVM provides macros that combine multiple steps. Understanding their expansion helps you choose wisely:
| Macro | Expands To | Use When |
|---|---|---|
`uvm_do(item) | create + start_item + randomize + finish_item | Simple, unconstrained stimulus |
`uvm_do_with(item, constraints) | Above + inline constraints | Quick constrained randomization |
`uvm_create(item) | Factory create only | Manual control over timing |
`uvm_send(item) | start_item + finish_item (no randomize) | Pre-configured items |
Macro Expansion Example
// This macro call:
`uvm_do_with(req, { addr == 32'h100; data == 32'hDEAD; })
// Expands to approximately:
req = bus_transaction::type_id::create("req");
start_item(req);
if (!req.randomize() with { addr == 32'h100; data == 32'hDEAD; })
`uvm_warning("RNDFLD", "Randomization failed")
finish_item(req);
When to Avoid Macros
Prefer explicit start_item/finish_item when you need:
- Custom error handling for randomization failures
- State inspection between arbitration and randomization
- Response processing before next item
- Complex multi-phase transactions
Accessing the Sequencer: m_sequencer vs p_sequencer
This is where UVM's typing gets interesting - it's essentially solving the Liskov Substitution challenge:
| Handle | Type | Access | Use Case |
|---|---|---|---|
m_sequencer | uvm_sequencer_base | Always available | Generic operations (grab, lock) |
p_sequencer | Your sequencer type | After `uvm_declare_p_sequencer | Access custom fields/methods |
The p_sequencer Pattern
// Custom sequencer with protocol-specific state
class axi_sequencer extends uvm_sequencer #(axi_transaction);
`uvm_component_utils(axi_sequencer)
// Protocol state accessible to sequences
bit [3:0] outstanding_ids[$];
int max_outstanding = 8;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function bit can_issue_new_txn();
return outstanding_ids.size() < max_outstanding;
endfunction
endclass
// Sequence that needs sequencer state
class axi_write_seq extends uvm_sequence #(axi_transaction);
`uvm_object_utils(axi_write_seq)
`uvm_declare_p_sequencer(axi_sequencer) // Creates typed p_sequencer
task body();
// Wait until we can issue (backpressure handling)
wait(p_sequencer.can_issue_new_txn());
req = axi_transaction::type_id::create("req");
start_item(req);
// Avoid ID collision using sequencer state
if (!req.randomize() with {
!(id inside {p_sequencer.outstanding_ids});
}) `uvm_error("SEQ", "Randomization failed")
finish_item(req);
// Track issued ID
p_sequencer.outstanding_ids.push_back(req.id);
endtask
endclass
Design Pattern: Base Sequence with Common Utilities
Apply DRY (Don't Repeat Yourself) by creating a base sequence with shared functionality:
class protocol_base_seq #(type T=uvm_sequence_item) extends uvm_sequence #(T);
// Common configuration
rand int unsigned num_transactions;
rand int unsigned inter_txn_delay;
constraint default_c {
soft num_transactions inside {[1:100]};
soft inter_txn_delay inside {[0:10]};
}
function new(string name = "protocol_base_seq");
super.new(name);
endfunction
// Utility: Send item with standard error handling
protected task send_item(T item, input string context_msg = "");
start_item(item);
if (!item.randomize())
`uvm_fatal("SEQ", {"Randomization failed: ", context_msg})
finish_item(item);
// Optional delay between transactions
if (inter_txn_delay > 0)
#(inter_txn_delay * 1ns);
endtask
// Utility: Wait for response with timeout
protected task wait_response(output T rsp, input int timeout_ns = 1000);
fork
get_response(rsp);
begin
#(timeout_ns * 1ns);
`uvm_error("SEQ", "Response timeout")
end
join_any
disable fork;
endtask
endclass
Composing Sequences: Favor Composition Over Inheritance
Rather than deep inheritance hierarchies, compose complex scenarios from simple sequences:
class memory_test_seq extends uvm_sequence #(mem_transaction);
`uvm_object_utils(memory_test_seq)
// Composition: Use other sequences as building blocks
write_seq wr_seq;
read_seq rd_seq;
verify_seq verify_seq;
task body();
// Initialize sequences (Factory Pattern)
wr_seq = write_seq::type_id::create("wr_seq");
rd_seq = read_seq::type_id::create("rd_seq");
verify_seq = verify_seq::type_id::create("verify_seq");
// Compose test scenario
repeat(10) begin
// Write phase
wr_seq.start(m_sequencer, this);
// Read back
rd_seq.addr = wr_seq.addr; // Pass context
rd_seq.start(m_sequencer, this);
// Verify
verify_seq.expected = wr_seq.data;
verify_seq.actual = rd_seq.data;
verify_seq.start(m_sequencer, this);
end
endtask
endclass
Anti-Patterns to Avoid
1. God Sequence
// BAD: One sequence doing everything
class test_all_seq extends uvm_sequence;
task body();
// 500 lines of mixed concerns
// Reset, config, normal ops, errors, corner cases...
endtask
endclass
// GOOD: Compose focused sequences
class system_test_seq extends uvm_sequence;
task body();
reset_seq.start(m_sequencer, this);
config_seq.start(m_sequencer, this);
traffic_seq.start(m_sequencer, this);
error_inject_seq.start(m_sequencer, this);
endtask
endclass
2. Hardcoded Values
// BAD: Magic numbers everywhere
`uvm_do_with(req, { addr == 32'h1000; size == 4; })
// GOOD: Parameterized and configurable
class configurable_seq extends uvm_sequence;
rand bit [31:0] base_addr;
rand int burst_size;
// Constraints provide defaults, tests override
constraint defaults_c {
soft base_addr == 32'h1000;
soft burst_size inside {[1:16]};
}
endclass
3. Ignoring Randomization Failures
// BAD: Silent failure
req.randomize(); // Returns 0 on failure, ignored
// GOOD: Explicit handling
if (!req.randomize() with { constraints; })
`uvm_fatal("SEQ", $sformatf("Cannot generate valid stimulus: %s",
req.sprint()))
Key Takeaways
- Sequences are generators - They yield items on demand, enabling reactive stimulus
- Template Method pattern - Override
body(), use hooks for setup/cleanup - Late randomization - Randomize after
start_item()for state-dependent constraints - Macros vs explicit - Use macros for simple cases, explicit calls for control
- p_sequencer - Access protocol state when needed, but minimize coupling
- Composition over inheritance - Build complex scenarios from simple sequences
- Apply SOLID principles - SRP, OCP, and DRY make sequences maintainable
Next: Part 2 - Virtual Sequences & Multi-Agent Coordination
Sources consulted: ChipVerify, Verification Academy, Doulos Easier UVM
Comments (0)
Leave a Comment