The first chip-level run of your new virtual sequence dies at time 0 with a null object access on the line prog.start(p_sequencer.apb_sqr, this). The virtual sequencer exists, p_sequencer is valid, and the APB agent is in the topology printout with its sequencer. Yet the handle inside the virtual sequencer is null. Where did the assignment go wrong, and how do you make that mistake impossible to miss?
A virtual sequencer is a component whose only job is to hold handles to the real sequencers (and sometimes a virtual interface or a config object) that virtual sequences need. It has no driver connected to it, so nothing it receives ever reaches a pin. Think of the key rack behind a hotel reception desk: the rack opens no doors itself, but anyone who needs a room knows exactly where to find its key.
Virtual sequences start on the virtual sequencer, and then reach through it: p_sequencer.apb_sqr is the APB agent's sequencer. Because the virtual sequencer is a component, it lives in the hierarchy, can be found by path, can be overridden by the factory, and can read config_db like any other component. Those are its advantages over handing handles around by hand, which the bench does instead (see the lesson on that).
The virtual sequencer is a sibling of the agents, not their parent. That placement matters for timing: the env builds both, and only in connect_phase do both exist at once.
The class extends uvm_sequencer with its default parameter (uvm_sequence_item); uvm_virtual_sequencer is a typedef for the same thing. It declares one handle per real sequencer, plus the interrupt interface, which it reads from config_db in its own build_phase.
// snippet
class dma_vsqr2 extends uvm_sequencer;
`uvm_component_utils(dma_vsqr2)
apb_sequencer apb_sqr; // assigned by the env in connect_phase
axis_sequencer axis_sqr;
virtual irq_if irq_vif; // from config_db
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual irq_if)::get(this, "", "irq_vif", irq_vif))
`uvm_fatal("NOVIF", "irq_if not set for the virtual sequencer")
endfunction
endclassRather than edit the bench's dma_env, extend it. The derived env builds the virtual sequencer next to the agents, and assigns its handles in connect_phase, then checks them. A test swaps it in with a factory type override before the env is created, the same move dma_truncate_test makes for the driver (see create-time resolution).
// snippet
class dma_vsqr_env extends dma_env;
`uvm_component_utils(dma_vsqr_env)
dma_vsqr2 v_sqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase); // agents and scoreboard
v_sqr = dma_vsqr2::type_id::create("v_sqr", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase); // monitor -> scoreboard
v_sqr.apb_sqr = apb.sqr;
v_sqr.axis_sqr = in_agt.sqr;
if (v_sqr.apb_sqr == null || v_sqr.axis_sqr == null)
`uvm_fatal("VSQR", "an agent built no sequencer: is it passive?")
endfunction
endclass
// In the test
function void build_phase(uvm_phase phase);
dma_env::type_id::set_type_override(dma_vsqr_env::get_type());
super.build_phase(phase); // creates env: now a dma_vsqr_env
uvm_config_db#(virtual irq_if)::set(this, "env.v_sqr", "irq_vif", irq_vif);
endfunctionWhy connect_phase and not the env's build_phase? Because build is top-down: the env's build_phase creates the agent objects, but each agent creates its own sequencer in its build_phase, which runs only after the env's returns. At the moment the env would copy apb.sqr, that member is still null. The assignment copies a null, and nothing complains until a sequence dereferences it. In connect_phase every component in the tree has been built, so the handles are real.
The VSQR fatal looks paranoid, but it catches a real configuration mistake at the end of elaboration instead of mid-test. If someone sets the stream agent passive (to reuse the env at subsystem level, where another block drives that port), the agent builds no sequencer. The assignment then copies null legally, and without the check the first sign is a null access deep inside a sequence, long after the cause. The check turns it into one line that names the problem.
There are two alternatives to a virtual sequencer worth knowing. A sequence can get handles from config_db (an env does uvm_config_db#(apb_sequencer)::set(...) in connect_phase), or the test can assign them directly to fields of the virtual sequence, which is what the bench does. Both are covered in Virtual Sequences Without a Virtual Sequencer.
This log is from a scratch run: the bench plus dma_vsqr2, dma_vsqr_env and a dma_p_vseq that reads its handles through p_sequencer (the next lesson shows it). The topology printout lists the new component, and the job runs exactly as dma_vseq_test does. Random values differ by seed.
v_sqr dma_vsqr2 - @321
UVM_INFO dma_tb.sv(572) @ 365000: uvm_test_top.env.apb.sqr@@vseq.isr [IRQ] handler has the APB sequencer
UVM_INFO dma_tb.sv(623) @ 545000: uvm_test_top.env.v_sqr@@vseq [VSEQ] job done: STATUS=00000002 after 4 polls
UVM_INFO dma_tb.sv(411) @ 645000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched
UVM_ERROR : 0`uvm_test_top.env.v_sqr@@vseq`: the virtual sequence now has a sequencer, so its messages are reported through it instead of the global reporter.
The interrupt handler still runs on env.apb.sqr. The virtual sequencer only handed out the handle; the work happened on the real sequencer.
That opening crash came from a chip-level env where the virtual sequencer's handles were assigned at the end of the env's build_phase, right after create. On the block bench it had seemed to work, because a different engineer had put the assignment in connect_phase there and the chip team copied an older version. The topology print was perfect, which sent everyone looking at the sequence. The log line to look for is a null access at the first p_sequencer.<handle> use. The fix was the connect_phase assignment plus the null check, which would have stopped the run at elaboration with a sentence naming the cause.
v_sqr.apb_sqr = apb.sqr in end_of_elaboration_phase instead of connect_phase. Does it work?Yes. By end of elaboration every component is built and connected, so the handle is real. connect_phase is the convention because it is where cross-component references are made, and it runs before anything in end_of_elaboration might want to use the handles (for example a sequencer arbitration setting done through the virtual sequencer).
The passive agent builds no sequencer, so in_agt.sqr is null and the assignment copies null. Without the check, the first virtual sequence that starts a packet on p_sequencer.axis_sqr fails with a null access mid-run. With the check, elaboration ends with the VSQR fatal that says an agent built no sequencer. At subsystem level the right fix is a different virtual sequence that does not drive that port.
Next: the virtual sequence reaches the handles through p_sequencer. What that macro really declares, and the fatal you get when the types do not match, is in Declaring p_sequencer.