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:

ComponentResponsibilitySW Analogy
uvm_sequence_itemData container with constraintsDTO (Data Transfer Object)
uvm_sequenceGeneration logic and orderingGenerator/Iterator
uvm_sequencerRouting and arbitrationMessage Broker/Dispatcher
uvm_driverProtocol-level signalingAdapter/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:

MacroExpands ToUse When
`uvm_do(item)create + start_item + randomize + finish_itemSimple, unconstrained stimulus
`uvm_do_with(item, constraints)Above + inline constraintsQuick constrained randomization
`uvm_create(item)Factory create onlyManual 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:

HandleTypeAccessUse Case
m_sequenceruvm_sequencer_baseAlways availableGeneric operations (grab, lock)
p_sequencerYour sequencer typeAfter `uvm_declare_p_sequencerAccess 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

Author
Mayur Kubavat
VLSI Design and Verification Engineer sharing knowledge about SystemVerilog, UVM, and hardware verification methodologies.

Comments (0)

Leave a Comment