A chip-level testbench rarely has just one bus. A register interface, a reset controller and a high-speed data link all have their own agents, and a test has to drive all of them in a controlled order. A virtual sequencer gives one test a single place from which to reach every sequencer in the environment.
A UVM virtual sequencer is a uvm_sequencer that is not connected to a driver and does not process sequence items itself. It holds handles to other sequencers so that a virtual sequence running on it can start sequences on all of them.
7 min read | Intermediate Level
What You'll Learn
- What a virtual sequencer is and how it differs from a regular sequencer
- Create a virtual sequencer with handles to real sequencers
- Instantiate it in the environment and connect its handles in
connect_phase - Use it safely from a virtual sequence, including null handle checks
What is a virtual sequencer ?
A regular sequencer sits inside an agent. It arbitrates between sequences and passes sequence items to its driver through the seq_item_export.
A virtual sequencer has no driver. Its only job is to hold references to the "sub-sequencers" it coordinates. These can be regular, driver-connected sequencers or other virtual sequencers, which makes a layered control structure possible.
This is useful whenever one test scenario must control stimulus on several interfaces at once, such as resetting the design over one bus, configuring it over another, and then sending data over a third.
The environment shown above has an APB env, an AXI env and a UART env. Each one has its own sequences and the sequencer on which they run. A virtual sequencer called m_vseqr holds a reference to each of these sequencers, so a virtual sequence running on m_vseqr can reach every sequencer in the testbench.
UVM has no separate uvm_virtual_sequencer class. The standard uvm_sequencer class is used for both regular and virtual sequencers. The difference comes only from how it is used: a virtual sequencer is never connected to a driver.
Where are Virtual Sequencers Used?
Virtual sequencers appear in almost every SoC and subsystem-level UVM environment. A typical example is a boot-flow test where reset, register programming and data traffic each come from a different agent, and a single virtual sequence controls the order through one virtual sequencer.
Syntax
class VSEQR_NAME extends uvm_sequencer; // No item type needed
`uvm_component_utils (VSEQR_NAME) // It is a component, not an object
SEQR_TYPE_A m_handle_a; // Handle to a real sequencer
SEQR_TYPE_B m_handle_b; // Handle to another sequencer
function new (string name, uvm_component parent);
super.new (name, parent);
endfunction
endclass
uvm_sequencer- base class, used with its default parameter (required)`uvm_component_utils- registers it with the factory as a component (required)- Sequencer handles - one per sequencer to be controlled, assigned later by the environment (required)
Steps to create a virtual sequencer
Creating a virtual sequencer is similar to creating a regular sequencer. The key differences are the handles it contains and how they are connected.
1. Derive from uvm_sequencer
Like any other sequencer, a custom virtual sequencer extends the uvm_sequencer base class.
class virtual_sequencer extends uvm_sequencer;
2. Implement Constructor and Factory Registration
A virtual sequencer is a uvm_component, so it is registered with `uvm_component_utils and its constructor takes both a name and a parent.
`uvm_component_utils (virtual_sequencer)
function new (string name = "my_virtual_sequencer", uvm_component parent);
super.new (name, parent);
endfunction
3. Declare Handles for Sub-Sequencers
This is the main difference from a regular sequencer. The class declares one handle for each sequencer it needs to control, using the exact sequencer type from the agent.
// Declare handles to other sequencers here
apb_sequencer m_apb_seqr;
reg_sequencer m_reg_seqr;
wb_sequencer m_wb_seqr;
pcie_sequencer m_pcie_seqr;
endclass
Name each handle after the interface it drives, such as m_apb_seqr for the APB agent. Descriptive names make virtual sequences much easier to read than generic names like seqr1.
4. Instantiate and Connect
Because it is a component, the virtual sequencer is created in the build_phase of a higher-level component, usually the uvm_env. Its handles are assigned in connect_phase.
class my_env extends uvm_env;
`uvm_component_utils (my_env)
apb_env m_apb_env;
reg_env m_reg_env;
wb_env m_wb_env;
virtual_sequencer m_vseqr;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Create the virtual sequencer
m_vseqr = virtual_sequencer::type_id::create("m_vseqr", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// Connect handles of real sequencers to the virtual sequencer
m_vseqr.m_apb_seqr = m_apb_env.m_apb_seqr;
m_vseqr.m_reg_seqr = m_reg_env.m_reg_seqr;
// ...
endfunction
endclass
The handles must be assigned in connect_phase and not in build_phase. The build phase runs top-down, so when the environment's build_phase executes, the agents below it have not created their sequencers yet.
The uvm_config_db can also be used to pass sequencer handles. In large environments, take care that each lookup key is unique.
Quick Check
Q: Does a virtual sequencer need a driver connected to its seq_item_export?
Show Answer
No. The export still exists because it is inherited from uvm_sequencer, but it stays unconnected. Sequence items are always sent through the real sequencers to their own drivers.
Guidelines for Usage
- No driver connection: A virtual sequencer is never connected to a driver. It coordinates stimulus but does not provide it.
- Runs virtual sequences: Virtual sequences run on the virtual sequencer and use its handles to start sub-sequences on the real sequencers.
- Prefer start() over macros: Macros like
`uvm_do_onand`uvm_do_on_withcan start a sequence on a sub-sequencer, but the start() method is recommended because it is explicit and easier to debug. Item macros such as`uvm_dodo not apply, since a virtual sequencer has no items. - Check for null handles: A virtual sequence should check each sub-sequencer handle before using it. A handle stays null if its agent is passive (a passive agent has no sequencer) or if the connection was missed.
The snippet below shows such a null check inside a virtual sequence. It reaches the sub-sequencers through p_sequencer, which is declared with `uvm_declare_p_sequencer.
class my_system_vseq extends uvm_sequence;
`uvm_object_utils(my_system_vseq)
`uvm_declare_p_sequencer(my_virtual_sequencer) // Typed handle to the virtual sequencer
apb_write_read_seq apb_sub_seq;
virtual task body();
...
// Now, check the handles to the subsequencers contained within the virtual sequencer.
// Access these through the 'p_sequencer' handle.
if (p_sequencer.m_apb_seqr == null) begin
// If the APB sequencer handle is null, it indicates a configuration issue
// (e.g., APB agent was passive, or connection in env::connect_phase failed).
`uvm_fatal(get_full_name(), "APB Sequencer handle (p_sequencer.m_apb_seqr) is null! Check agent configuration or env::connect_phase.")
end else begin
`uvm_info(get_full_name(), "APB Sequencer handle is valid.", UVM_LOW)
apb_sub_seq = apb_write_read_seq::type_id::create("apb_sub_seq");
apb_sub_seq.start(p_sequencer.m_apb_seqr, this);
end
...
endtask
endclass
Virtual sequencers can also be layered. A higher-level virtual sequencer can hold handles to lower-level virtual sequencers, which in turn hold handles to driver-connected sequencers. This lets block-level virtual sequences be reused unchanged at the SoC level.
Virtual Sequencer Example
The complete virtual sequencer class combines the steps above into a single definition.
class my_virtual_sequencer extends uvm_sequencer;
`uvm_component_utils (my_virtual_sequencer)
function new (string name = "my_virtual_sequencer", uvm_component parent);
super.new (name, parent);
endfunction
// Declare handles to other sequencers here
apb_sequencer m_apb_seqr;
reg_sequencer m_reg_seqr;
wb_sequencer m_wb_seqr;
pcie_sequencer m_pcie_seqr;
endclass
The top environment creates an instance of the virtual sequencer and connects each real sequencer to its handle.
class top_env extends uvm_env;
...
my_virtual_sequencer m_virt_seqr;
virtual function void build_phase (uvm_phase phase);
...
m_virt_seqr = my_virtual_sequencer::type_id::create ("m_virt_seqr", this);
...
endfunction
// Connect virtual sequencer handles to actual sequencers
virtual function void connect_phase (uvm_phase phase);
...
m_virt_seqr.m_apb_seqr = m_apb_agent.m_apb_seqr;
m_virt_seqr.m_reg_seqr = m_reg_env.m_reg_seqr;
m_virt_seqr.m_pcie_seqr = m_pcie_env.m_pcie_agent.m_pcie_seqr;
...
endfunction
endclass
Notice that each handle can come from a different depth of the hierarchy. The APB sequencer is one level down in an agent, while the PCIe sequencer is two levels down inside its own env. The virtual sequencer hides these paths, so a virtual sequence only needs p_sequencer.m_pcie_seqr.
The output below comes from a version of this testbench with APB, SPI and Wishbone agents.
----------------------------------------------------------------
CDNS-UVM-1.1d
(C) 2007-2013 Mentor Graphics Corporation
(C) 2007-2013 Cadence Design Systems, Inc.
(C) 2006-2013 Synopsys, Inc.
(C) 2011-2013 Cypress Semiconductor Corp.
----------------------------------------------------------------
UVM_INFO @ 0: reporter [RNTST] Running test base_test...
UVM_INFO @ 0: reporter [UVMTOP] UVM testbench topology:
--------------------------------------------------------------
Name Type Size Value
--------------------------------------------------------------
uvm_test_top base_test - @2667
m_top_env top_env - @2733
m_apb_agent apb_agent - @2780
m_apb_drv apb_driver - @4089
rsp_port uvm_analysis_port - @4237
seq_item_port uvm_seq_item_pull_port - @4188
m_apb_mon apb_monitor - @4217
m_apb_seqr uvm_sequencer - @3512
rsp_export uvm_analysis_export - @3569
seq_item_export uvm_seq_item_pull_imp - @4109
arbitration_queue array 0 -
lock_queue array 0 -
num_last_reqs integral 32 'd1
num_last_rsps integral 32 'd1
m_spi_agent spi_agent - @2840
m_spi_drv spi_driver - @4897
rsp_port uvm_analysis_port - @5043
seq_item_port uvm_seq_item_pull_port - @4995
m_spi_mon spi_monitor - @5024
m_spi_seqr uvm_sequencer - @4321
rsp_export uvm_analysis_export - @4377
seq_item_export uvm_seq_item_pull_imp - @4917
arbitration_queue array 0 -
lock_queue array 0 -
num_last_reqs integral 32 'd1
num_last_rsps integral 32 'd1
m_virt_seq virtual_sequencer - @2870
rsp_export uvm_analysis_export - @2928
seq_item_export uvm_seq_item_pull_imp - @3478
arbitration_queue array 0 -
lock_queue array 0 -
num_last_reqs integral 32 'd1
num_last_rsps integral 32 'd1
m_wb_agent wb_agent - @2810
m_wb_drv wb_driver - @5712
rsp_port uvm_analysis_port - @5858
seq_item_port uvm_seq_item_pull_port - @5810
m_wb_mon wb_monitor - @5839
m_wb_seqr uvm_sequencer - @5136
rsp_export uvm_analysis_export - @5192
seq_item_export uvm_seq_item_pull_imp - @5732
arbitration_queue array 0 -
lock_queue array 0 -
num_last_reqs integral 32 'd1
num_last_rsps integral 32 'd1
--------------------------------------------------------------
UVM_INFO ./tb/my_pkg.sv(68) @ 0: uvm_test_top.m_top_env.m_virt_seq@@m_virt_seq [VSEQ] Start of virtual sequence
UVM_INFO ./tb/wb_agent.sv(63) @ 0: uvm_test_top.m_top_env.m_wb_agent.m_wb_seqr@@m_wb_reset_seq [RESET_SEQ] Starting wb_reset_seq
UVM_INFO ./tb/apb_agent.sv(52) @ 20000: uvm_test_top.m_top_env.m_apb_agent.m_apb_seqr@@m_apb_rw_seq [RW_SEQ] Starting apb_rw_seq
UVM_INFO ./tb/spi_agent.sv(74) @ 30000: uvm_test_top.m_top_env.m_spi_agent.m_spi_seqr@@m_spi_tx_seq [tx_SEQ] Starting spi_tx_seq
UVM_INFO ./tb/my_pkg.sv(75) @ 30000: uvm_test_top.m_top_env.m_virt_seq@@m_virt_seq [VSEQ] End of virtual sequence
UVM_INFO ./tb/my_pkg.sv(108) @ 30000: uvm_test_top [SHUT] Shutting down test ...
--- UVM Report catcher Summary ---
Two details in this log confirm how a virtual sequencer works:
- In the topology, the virtual sequencer instance m_virt_seq has a
seq_item_exportlike the agent sequencers, but no driver is connected to it. - The virtual sequence reports from the virtual sequencer's path, while each child sequence reports from its own agent's sequencer path. The children really run on the real sequencers.
Virtual Sequencer vs Regular Sequencer
| Aspect | Regular sequencer | Virtual sequencer |
|---|---|---|
| Base class | uvm_sequencer #(REQ) | uvm_sequencer (default parameter) |
| Location | Inside an agent | Inside an env, above the agents |
| Connected to a driver | Yes | No |
| Runs | Sequences that generate items | Virtual sequences that start other sequences |
| Contains | Arbitration logic and item export | The same, plus handles to other sequencers |
Where is Layering Used?
In large SoC projects, each subsystem team usually delivers its own environment with a virtual sequencer. The SoC environment then adds a top-level virtual sequencer whose handles point to these subsystem virtual sequencers, so subsystem test scenarios can be reused at the chip level.
Common Mistakes
Mistake #1: Assigning handles in build_phase instead of connect_phase
Wrong: m_vseqr.m_apb_seqr = m_apb_env.m_apb_seqr; inside the env's build_phase
Right: the same assignment inside the env's connect_phase
Why? The build phase runs top-down. When the environment's build_phase runs, the agents' sequencers have not been created yet, so the handle copied is null. By connect_phase, the whole hierarchy exists.
Mistake #2: Declaring a handle with the wrong sequencer type
If the agent declares its sequencer as uvm_sequencer #(apb_item) but the virtual sequencer declares the handle as apb_sequencer (a class derived from it), the direct assignment does not compile. A base-class handle cannot be assigned to a derived-class variable without $cast. Declare the handle with exactly the type the agent uses.
Mistake #3: Forgetting that passive agents have no sequencer
When an agent is configured as UVM_PASSIVE, it builds only its monitor. The virtual sequencer's handle for that agent stays null, and any virtual sequence that starts a sequence on it fails. Check the handle for null before calling start(), as shown in the guidelines above.
Practice Exercise
Challenge: Add an SPI Sequencer Handle
Extend my_virtual_sequencer with a handle m_spi_seqr of type spi_sequencer. Then connect it in top_env to the sequencer inside m_spi_agent, but only when that agent is active.
Hint
Every uvm_agent has a get_is_active() method that returns UVM_ACTIVE or UVM_PASSIVE. Use it in connect_phase before assigning the handle.
Solution
class my_virtual_sequencer extends uvm_sequencer;
`uvm_component_utils (my_virtual_sequencer)
apb_sequencer m_apb_seqr;
spi_sequencer m_spi_seqr; // New handle for the SPI agent
function new (string name = "my_virtual_sequencer", uvm_component parent);
super.new (name, parent);
endfunction
endclass
class top_env extends uvm_env;
// Rest of the code (agents, build_phase, etc.)
virtual function void connect_phase (uvm_phase phase);
super.connect_phase (phase);
m_virt_seqr.m_apb_seqr = m_apb_agent.m_apb_seqr;
// A passive agent has no sequencer, so connect only when active
if (m_spi_agent.get_is_active() == UVM_ACTIVE)
m_virt_seqr.m_spi_seqr = m_spi_agent.m_spi_seqr;
endfunction
endclass
A virtual sequence that uses m_spi_seqr should still check it for null, so the same virtual sequence works in both active and passive configurations.
Summary
In this tutorial, you learned:
- A virtual sequencer is a
uvm_sequencerthat has no driver and holds handles to other sequencers - UVM has no separate virtual sequencer class; the difference lies only in how the sequencer is used
- It is registered with
`uvm_component_utilsand created in the environment'sbuild_phase - Its handles must be assigned in
connect_phase, after the agents have built their sequencers - Virtual sequences run on it and start child sequences on the real sequencers, preferably with
start() - Handles should be checked for
null, because passive agents have no sequencer