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
| Mistake | Problem | Fix |
|---|---|---|
Starting sequence on null | Crashes simulation | Validate handles in pre_body() |
Missing join after fork | Race conditions | Always pair fork with explicit join variant |
join_any without disable fork | Zombie threads | Clean up losing threads |
Forgetting super.body() | Handles not initialized | Always 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
Comments (0)
Leave a Comment