Virtual Sequencers and Virtual Sequences in UVM

A virtual sequencer is a UVM component that does not directly control any drivers or interfaces. Instead, it coordinates the execution of multiple sequences running on different sequencers. Virtual sequencers are crucial in verifying systems with multiple agents, where the interactions between agents need to be synchronized and controlled. In other words, if there are multiple driving agents and simulation coordination is required between them, we will need a virtual sequencer. A virtual sequencer is not connected to a driver.

Key Points:

  • Abstraction Layer: Virtual sequencers act as a higher-level control mechanism, abstracting away the details of individual agent sequencers.
  • Synchronization: They facilitate the coordination of multiple sequencers to ensure proper stimulus delivery.
  • Centralized Control: Virtual sequencers manage the execution of virtual sequences, which define the overall behavior of a test.

What is a Virtual Sequence in UVM?

A virtual sequence is a specialized sequence that runs on a virtual sequencer. It serves as a container for controlling other sequences running on various agent sequencers. Virtual sequences are pivotal in creating scenarios that require interactions between different components of the DUT (Device Under Test).

Key Points:

  • Multi-Agent Scenarios: Virtual sequences are essential for defining scenarios involving multiple agents.
  • Reusability: By decoupling the testbench logic from individual sequencers, virtual sequences promote reusability.
  • Hierarchy: Virtual sequences can call other sequences or virtual sequences, enabling hierarchical test structures.

Why Use Virtual Sequencers and Virtual Sequences?

In complex SoCs, where multiple blocks (e.g., CPU, memory, and peripherals) interact, testing requires synchronized stimulus. Without virtual sequencers and sequences, managing such interactions would be cumbersome and error-prone.

For instance, consider a DUT comprising a processor, a memory controller, and a communication interface. Testing scenarios like DMA transfers, cache coherency, or interrupt handling require seamless interaction among these components. Virtual sequencers and sequences make it easier to organize these interactions.

Example:

We will create a testbench with two sequencers (seqr_a and seqr_b), each driving a separate driver. A virtual sequencer and a virtual sequence will coordinate the execution of sequences on these sequencers. There are two agents (agent_a and agent_b).

Virtual Sequence and virtual sequencer:

// Sequencer
class example_sequencer extends uvm_sequencer #(example_transaction);
  `uvm_component_utils(example_sequencer)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass

class virtual_sequencer extends uvm_sequencer;
  `uvm_component_utils(virtual_sequencer)

  example_sequencer seqr_a;
  example_sequencer seqr_b;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass

    // Sequence for seqr_a
class seq_a extends uvm_sequence #(example_transaction);
  `uvm_object_utils(seq_a)

  function new(string name = "seq_a");
    super.new(name);
  endfunction

  task body();
    for (int i = 0; i < 3; i++) begin
      example_transaction tx = example_transaction::type_id::create("tx");
      tx.data = 100 + i;
      start_item(tx);
      finish_item(tx);
    end
  endtask
endclass

// Sequence for seqr_b
class seq_b extends uvm_sequence #(example_transaction);
  `uvm_object_utils(seq_b)

  function new(string name = "seq_b");
    super.new(name);
  endfunction

  task body();
    for (int i = 0; i < 3; i++) begin
      example_transaction tx = example_transaction::type_id::create("tx");
      tx.data = 200 + i;
      start_item(tx);
      finish_item(tx);
    end
  endtask
endclass
    
class virtual_sequence extends uvm_sequence;
  `uvm_object_utils(virtual_sequence)

  virtual_sequencer vseqr;
  example_env env;

  function new(string name = "virtual_sequence");
    super.new(name);
  endfunction

  task body();
    seq_a seq_a_inst = seq_a::type_id::create("seq_a_inst");
    seq_b seq_b_inst = seq_b::type_id::create("seq_b_inst");
    
    //calling the env handle
    if(!$cast(env, uvm_top.find("uvm_test_top.env"))) `uvm_error(get_name(), "env is not found");

    // Start sequences on the respective sequencers
    fork
      seq_a_inst.start(env.vseqr.seqr_a);
      seq_b_inst.start(env.vseqr.seqr_b);
    join
  endtask
endclass

Output:

UVM_INFO @ 0: reporter [RNTST] Running test example_test...
Driver [uvm_test_top.env.agent_a.drv_b] received transaction: data = 200
Driver [uvm_test_top.env.agent_a.drv_a] received transaction: data = 100
Driver [uvm_test_top.env.agent_a.drv_a] received transaction: data = 101
Driver [uvm_test_top.env.agent_a.drv_b] received transaction: data = 201
Driver [uvm_test_top.env.agent_a.drv_b] received transaction: data = 202
Driver [uvm_test_top.env.agent_a.drv_a] received transaction: data = 102
UVM_INFO /apps/vcsmx/vcs/U-2023.03-SP2//etc/uvm-1.2/src/base/uvm_objection.svh(1276) @ 50: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase
UVM_INFO /apps/vcsmx/vcs/U-2023.03-SP2//etc/uvm-1.2/src/base/uvm_report_server.svh(904) @ 50: reporter [UVM/REPORT/SERVER]