Virtual Sequences - The Mediator Pattern for Multi-Agent Coordination

When verifying SoCs with multiple interfaces - PCIe, DDR, Ethernet, GPIO - you need coordinated stimulus across all of them simultaneously. Virtual sequences implement the Mediator Pattern, centralizing complex coordination logic that would otherwise scatter across your testbench.

The Problem: Distributed Coordination is Hard

Imagine testing a DMA controller that reads from memory via AXI and writes to a peripheral via APB. Without central coordination:

// BAD: Tight coupling between independent agents
class axi_sequence extends uvm_sequence;
  apb_sequencer apb_sqr;  // Why does AXI know about APB?
  
  task body();
    // AXI sequence directly controlling APB - violation of SRP
    fork
      drive_axi_read();
      apb_seq.start(apb_sqr);  // Coupling!
    join
  endtask
endclass

This creates a dependency nightmare - AXI sequences shouldn't know APB exists.

The Mediator Solution

The Mediator Pattern introduces a central coordinator that manages interactions between components that shouldn't know about each other:

flowchart TB
    subgraph "Without Mediator"
        A1[AXI Seq] <-->|tight coupling| B1[APB Seq]
        A1 <-->|tight coupling| C1[DMA Seq]
        B1 <-->|tight coupling| C1
    end
    
    subgraph "With Mediator (Virtual Sequence)"
        VS[Virtual Sequence] --> A2[AXI Seq]
        VS --> B2[APB Seq]
        VS --> C2[DMA Seq]
    end

Virtual Sequencer: The Facade Pattern

The virtual sequencer implements the Facade Pattern - providing a simplified interface to a complex subsystem of sequencers:

// Facade: Single access point to multiple sequencers
class soc_virtual_sequencer extends uvm_sequencer;
  `uvm_component_utils(soc_virtual_sequencer)
  
  // Handles to actual sequencers (set during connect_phase)
  axi_sequencer   axi_sqr;
  apb_sequencer   apb_sqr;
  intr_sequencer  intr_sqr;
  
  // Optional: Shared state across sequences
  soc_config      cfg;
  memory_model    mem;
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
endclass

Connecting the Facade

class soc_env extends uvm_env;
  `uvm_component_utils(soc_env)
  
  // Agents
  axi_agent  axi_agt;
  apb_agent  apb_agt;
  intr_agent intr_agt;
  
  // Virtual sequencer (Facade)
  soc_virtual_sequencer v_sqr;
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    axi_agt  = axi_agent::type_id::create("axi_agt", this);
    apb_agt  = apb_agent::type_id::create("apb_agt", this);
    intr_agt = intr_agent::type_id::create("intr_agt", this);
    v_sqr    = soc_virtual_sequencer::type_id::create("v_sqr", this);
  endfunction
  
  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    // Wire up the facade
    v_sqr.axi_sqr  = axi_agt.sequencer;
    v_sqr.apb_sqr  = apb_agt.sequencer;
    v_sqr.intr_sqr = intr_agt.sequencer;
  endfunction
  
endclass

Virtual Sequence: The Mediator

Virtual sequences coordinate without generating items directly - they orchestrate other sequences:

class soc_base_vseq extends uvm_sequence #(uvm_sequence_item);
  `uvm_object_utils(soc_base_vseq)
  `uvm_declare_p_sequencer(soc_virtual_sequencer)
  
  // Convenience handles (reduces p_sequencer.xxx verbosity)
  axi_sequencer  axi_sqr;
  apb_sequencer  apb_sqr;
  intr_sequencer intr_sqr;
  
  function new(string name = "soc_base_vseq");
    super.new(name);
  endfunction
  
  // Template Method: Setup sequencer handles
  virtual task pre_body();
    // Cast and cache handles once
    axi_sqr  = p_sequencer.axi_sqr;
    apb_sqr  = p_sequencer.apb_sqr;
    intr_sqr = p_sequencer.intr_sqr;
    
    // Validate connections (Fail-Fast principle)
    if (axi_sqr == null) `uvm_fatal("VSEQ", "AXI sequencer not connected")
    if (apb_sqr == null) `uvm_fatal("VSEQ", "APB sequencer not connected")
  endtask
  
  virtual task body();
    // Subclasses implement coordination logic
  endtask
  
endclass

Coordination Patterns

Pattern 1: Sequential Execution

Use when operations must complete in order - like initialization sequences:

class init_vseq extends soc_base_vseq;
  `uvm_object_utils(init_vseq)
  
  task body();
    super.body();
    
    // Step 1: Reset
    reset_seq.start(p_sequencer);  // Virtual sequencer itself
    
    // Step 2: Configure clocks via APB
    apb_config_seq.start(apb_sqr);
    
    // Step 3: Initialize memory via AXI
    axi_init_seq.start(axi_sqr);
    
    // Step 4: Enable interrupts
    intr_enable_seq.start(intr_sqr);
    
    `uvm_info("VSEQ", "Initialization complete", UVM_LOW)
  endtask
  
endclass

Pattern 2: Parallel Execution

Use when operations are independent and can overlap - like stress testing:

class stress_vseq extends soc_base_vseq;
  `uvm_object_utils(stress_vseq)
  
  task body();
    super.body();
    
    fork
      // Parallel traffic on all interfaces
      axi_traffic_seq.start(axi_sqr);
      apb_traffic_seq.start(apb_sqr);
      interrupt_storm_seq.start(intr_sqr);
    join  // Wait for ALL to complete
    
    `uvm_info("VSEQ", "Stress test complete", UVM_LOW)
  endtask
  
endclass

Pattern 3: Fork-Join Variants

class advanced_coordination_vseq extends soc_base_vseq;
  `uvm_object_utils(advanced_coordination_vseq)
  
  task body();
    super.body();
    
    // Pattern A: Wait for any one to complete (race)
    fork
      timeout_seq.start(null);     // Watchdog
      dma_transfer_seq.start(axi_sqr);
    join_any
    disable fork;  // Kill the loser
    
    // Pattern B: Fire and forget (background)
    fork
      monitor_seq.start(apb_sqr);  // Runs in background
    join_none  // Don't wait
    
    // Pattern C: Synchronized phases
    fork
      begin
        write_phase_seq.start(axi_sqr);
        -> write_done;  // Signal completion
      end
      begin
        @write_done;  // Wait for signal
        read_verify_seq.start(axi_sqr);
      end
    join
  endtask
  
endclass

Pattern 4: Producer-Consumer with Events

Implement the Observer Pattern for reactive coordination:

class reactive_vseq extends soc_base_vseq;
  `uvm_object_utils(reactive_vseq)
  
  // Shared events for coordination
  uvm_event dma_request_event;
  uvm_event dma_complete_event;
  
  function new(string name = "reactive_vseq");
    super.new(name);
    dma_request_event = new("dma_request_event");
    dma_complete_event = new("dma_complete_event");
  endfunction
  
  task body();
    super.body();
    
    fork
      producer_thread();
      consumer_thread();
    join
  endtask
  
  // Producer: APB initiates DMA requests
  task producer_thread();
    repeat(10) begin
      apb_dma_setup_seq.start(apb_sqr);
      dma_request_event.trigger();  // Notify consumer
      dma_complete_event.wait_trigger();  // Wait for completion
    end
  endtask
  
  // Consumer: AXI performs DMA transfers
  task consumer_thread();
    forever begin
      dma_request_event.wait_trigger();
      axi_dma_seq.start(axi_sqr);
      dma_complete_event.trigger();
    end
  endtask
  
endclass

Real-World Example: DMA Controller Verification

class dma_transfer_vseq extends soc_base_vseq;
  `uvm_object_utils(dma_transfer_vseq)
  
  // Configuration
  rand bit [31:0] src_addr;
  rand bit [31:0] dst_addr;
  rand int unsigned transfer_size;
  
  constraint valid_c {
    transfer_size inside {[64:4096]};
    src_addr[1:0] == 0;  // Word aligned
    dst_addr[1:0] == 0;
  }
  
  task body();
    super.body();
    
    // Phase 1: Setup memory source data
    `uvm_info("VSEQ", $sformatf("Setting up %0d bytes at 0x%08x", 
                                transfer_size, src_addr), UVM_MEDIUM)
    begin
      axi_write_seq wr_seq = axi_write_seq::type_id::create("wr_seq");
      wr_seq.addr = src_addr;
      wr_seq.size = transfer_size;
      wr_seq.start(axi_sqr);
    end
    
    // Phase 2: Configure DMA via APB
    fork
      begin
        apb_dma_config_seq cfg_seq = apb_dma_config_seq::type_id::create("cfg_seq");
        cfg_seq.src = src_addr;
        cfg_seq.dst = dst_addr;
        cfg_seq.len = transfer_size;
        cfg_seq.start(apb_sqr);
      end
      
      // Phase 3: Respond to AXI reads (slave response)
      begin
        axi_slave_resp_seq resp_seq = axi_slave_resp_seq::type_id::create("resp_seq");
        resp_seq.start(axi_sqr);
      end
    join_any
    
    // Phase 4: Wait for interrupt
    begin
      intr_wait_seq wait_seq = intr_wait_seq::type_id::create("wait_seq");
      wait_seq.expected_irq = DMA_COMPLETE_IRQ;
      wait_seq.start(intr_sqr);
    end
    
    // Phase 5: Verify destination data
    begin
      axi_read_verify_seq verify_seq = axi_read_verify_seq::type_id::create("verify_seq");
      verify_seq.addr = dst_addr;
      verify_seq.size = transfer_size;
      verify_seq.expected = p_sequencer.mem.read_block(src_addr, transfer_size);
      verify_seq.start(axi_sqr);
    end
    
    `uvm_info("VSEQ", "DMA transfer verified successfully", UVM_LOW)
  endtask
  
endclass

Design Guidelines

1. Single Responsibility for Virtual Sequences

// BAD: One vseq doing everything
class kitchen_sink_vseq extends soc_base_vseq;
  task body();
    // 200 lines mixing init, traffic, errors, shutdown
  endtask
endclass

// GOOD: Focused vseqs composed in test
class test_dma_stress extends uvm_test;
  task run_phase(uvm_phase phase);
    init_vseq.start(v_sqr);
    repeat(100) dma_transfer_vseq.start(v_sqr);
    shutdown_vseq.start(v_sqr);
  endtask
endclass

2. Avoid Circular Dependencies

// BAD: Sequences knowing too much about each other
class axi_seq extends uvm_sequence;
  apb_seq apb_ref;  // Why?
endclass

// GOOD: Coordinate via virtual sequence only
class coordinator_vseq extends soc_base_vseq;
  axi_seq axi_s;
  apb_seq apb_s;
  
  task body();
    // Coordination logic lives HERE, not in individual sequences
    axi_s.start(axi_sqr);
    apb_s.context_from_axi = axi_s.result;  // Pass data explicitly
    apb_s.start(apb_sqr);
  endtask
endclass

3. Use Factory for Flexibility

class configurable_vseq extends soc_base_vseq;
  task body();
    // Factory enables test-specific overrides
    axi_seq axi_s = axi_seq::type_id::create("axi_s");
    
    // Test can override: set_type_override_by_type(axi_seq::get_type(),
    //                                              axi_error_seq::get_type())
    axi_s.start(axi_sqr);
  endtask
endclass

Common Mistakes and Fixes

MistakeProblemFix
Starting sequence on nullCrashes simulationValidate handles in pre_body()
Missing join after forkRace conditionsAlways pair fork with explicit join variant
join_any without disable forkZombie threadsClean up losing threads
Forgetting super.body()Handles not initializedAlways call parent

Key Takeaways

  • Virtual Sequencer = Facade: Single access point to multiple agent sequencers
  • Virtual Sequence = Mediator: Centralizes coordination logic
  • Fork-join mastery: Know when to use join, join_any, join_none
  • Events for reactive coordination: Observer pattern for inter-sequence communication
  • Composition in tests: Combine focused vseqs rather than building monoliths
  • Factory everything: Enable test-specific customization

Previous: Part 1 - Sequences Fundamentals | Next: Part 3 - Advanced Sequence Architectures

Sources consulted: ChipVerify, EmtechSA, Verification Academy

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

Comments (0)

Leave a Comment