In a design review, a senior engineer looks at the DMA bench and asks: "Where's the virtual sequencer?" There isn't one. The test hands dma_vseq two sequencer handles and an interface, and calls vseq.start(null). Is that a shortcut that will hurt at chip level, or a legitimate design? And if it is legitimate, what does the virtual sequencer buy you that this does not?
A virtual sequence never sends an item of its own. Everything it does goes through the start() calls of its children, and each of those names a real sequencer. So what the virtual sequence needs is handles to real sequencers. A virtual sequencer is one way to deliver them: a component that holds the handles, which the virtual sequence reaches through p_sequencer. Assigning them straight to fields of the virtual sequence is another.
Think of a contractor working on a house. The owner can hand over the keys directly (handles assigned by the test), or leave them on a rack at reception for the contractor to collect (a virtual sequencer). Either way the contractor gets in. The rack is worth having when many contractors come and go, or when the keys change; for one job, handing them over is simpler and leaves less to get wrong.
// snippet: from the course DMA bench
// One whole DMA job across two agents. It has no sequencer of its own: the
// test hands it the two agent sequencers and the interrupt interface, and
// starts it with start(null).
class dma_vseq extends uvm_sequence;
`uvm_object_utils(dma_vseq)
apb_sequencer apb_sqr;
axis_sequencer axis_sqr;
virtual irq_if irq_vif;
rand bit [31:0] key;
rand int unsigned len;
constraint len_c { len inside {[2:16]}; }
bit [31:0] status; // STATUS as read by the interrupt handler
function new(string name = "dma_vseq");
super.new(name);
endfunction
task write_reg(bit [7:0] addr, bit [31:0] data);
apb_rw_seq s = apb_rw_seq::type_id::create("s");
s.addr = addr; s.write = 1; s.data = data;
s.start(apb_sqr, this); // this vseq is the parent
endtask
task body();
apb_poll_seq poll = apb_poll_seq::type_id::create("poll");
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
dma_irq_seq isr = dma_irq_seq::type_id::create("isr");
if (apb_sqr == null || axis_sqr == null)
`uvm_fatal("VSEQ", "set apb_sqr and axis_sqr before starting")
write_reg(KEY, key);
write_reg(LEN, len);
write_reg(CTRL, 32'h3); // IRQ_EN | GO
if (!pkt.randomize() with { n == len; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
fork
poll.start(apb_sqr, this); // background polling
pkt.start(axis_sqr, this); // the data, in parallel
begin // the interrupt handler
wait (irq_vif.irq === 1'b1);
isr.start(apb_sqr, this);
status = isr.status;
poll.stop = 1;
end
join
`uvm_info("VSEQ", $sformatf("job done: STATUS=%08h after %0d polls", status, poll.polls), UVM_LOW)
endtask
endclassWalk through it in the order it runs:
`extends uvm_sequence` with no parameter: the item type defaults to uvm_sequence_item, a reminder that this class sends no items of its own.
The three handle fields are typed: apb_sequencer, axis_sequencer, virtual irq_if. Assigning the wrong kind of sequencer is a compile error in the test, not a run-time fatal.
The null check at the top of body turns a forgotten assignment into one clear fatal: VSEQ with "set apb_sqr and axis_sqr before starting". It checks only the sequencer handles. Comparing a virtual interface handle with null is legal SystemVerilog, but it miscompiles in Verilator 5.048, which the bench is verified on; on a commercial simulator you would check irq_vif too.
`write_reg` starts one apb_rw_seq per register with s.start(apb_sqr, this). Passing this as the parent is not decoration: it names the child vseq.s, and it makes the child part of the vseq's family for lock and priority purposes.
The fork runs the poller, the packet and the interrupt branch; the interrupt branch starts dma_irq_seq, copies the STATUS it read, and stops the poller (Scenario Patterns covers the shape).
The test side is four lines, shown in The Coordination Problem: assign apb_sqr, axis_sqr and irq_vif, randomize, then vseq.start(null). With a null sequencer, start skips everything that needs one (there is no arbitration for the vseq itself) and just runs the hooks and body.
With more than one virtual sequence, copying three fields and a check into each is how a bench drifts. Put them in a base class with one helper the test calls, and derive every scenario from it:
// snippet
virtual class dma_base_vseq extends uvm_sequence;
apb_sequencer apb_sqr;
axis_sequencer axis_sqr;
virtual irq_if irq_vif;
function new(string name = "dma_base_vseq");
super.new(name);
endfunction
// The test calls this once; child vseqs copy from their parent the same way.
function void set_handles(apb_sequencer a, axis_sequencer x, virtual irq_if i);
apb_sqr = a; axis_sqr = x; irq_vif = i;
endfunction
task pre_body();
if (apb_sqr == null || axis_sqr == null)
`uvm_fatal("VSEQ", {get_full_name(), ": call set_handles() before start()"})
endtask
endclass
// usage in a test: vseq.set_handles(env.apb.sqr, env.in_agt.sqr, irq_vif); vseq.start(null);The class is virtual (abstract) and has no registration macro; each concrete scenario registers itself with ` uvm_object_utils `. pre_body runs only for a sequence started with call_pre_post set (the default for start`), so the check covers top-level vseqs and children started normally.
If you would rather the test not know agent paths, the env can publish the sequencer handles in connect_phase and the vseq can fetch them. Mind the path: a sequence started with start(null) and no parent has a full name that is just its own name (vseq), with no component path in front. A set scoped to "uvm_test_top.env.*" never matches it.
// snippet
// dma_env::connect_phase
uvm_config_db#(apb_sequencer)::set(null, "*", "apb_sqr", apb.sqr);
uvm_config_db#(axis_sequencer)::set(null, "*", "axis_sqr", in_agt.sqr);
// dma_base_vseq::pre_body, before the null check
if (apb_sqr == null)
void'(uvm_config_db#(apb_sequencer)::get(null, get_full_name(), "apb_sqr", apb_sqr));
if (axis_sqr == null)
void'(uvm_config_db#(axis_sequencer)::get(null, get_full_name(), "axis_sqr", axis_sqr));The "*" scope is deliberately broad: these are handles, not knobs, and there is one env. Configuring Sequences explains how a sequence's full name is formed and how to scope per-sequence knobs.
Handles in the vseq (the bench) | Virtual sequencer + p_sequencer | |
|---|---|---|
Wrong handle type | Compile error in the test |
|
Missing handle | Your own null check in the vseq | Your own null check in the env's connect_phase |
Extra component | None | One per env, plus a derived env or factory override |
Reporting path |
|
|
Config_db for the vseq | Full name is just | Full name starts with the virtual sequencer's path |
Default sequence per phase | Not possible: needs a sequencer | Possible through config_db on the virtual sequencer |
Moving to chip level | The chip test or chip vseq passes handles down | The chip env builds a bigger virtual sequencer |
Neither is wrong. Handles in the vseq are simpler and fail at compile time; a virtual sequencer gives you a place in the hierarchy for reporting, config and default sequences, which matters most in large benches with many virtual sequences and several engineers.
The real log of dma_vseq_test with +UVM_VERBOSITY=UVM_MEDIUM (random values differ by seed):
UVM_INFO dma_tb.sv(572) @ 405000: uvm_test_top.env.apb.sqr@@vseq.isr [IRQ] handler has the APB sequencer
UVM_INFO dma_tb.sv(623) @ 575000: reporter@@vseq [VSEQ] job done: STATUS=00000002 after 4 polls
UVM_INFO dma_tb.sv(411) @ 675000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatchedThe two name forms tell the whole story of this lesson. reporter@@vseq is a sequence with no sequencer; uvm_test_top.env.apb.sqr@@vseq.isr is its child, named under its parent and reported through the real sequencer it ran on. Compare the virtual-sequencer version from Virtual Sequencer Wiring, where the same line reads uvm_test_top.env.v_sqr@@vseq.
A team moved their DMA vseq handles into config_db, set from the env with set(this, "*", "apb_sqr", apb.sqr). Every run died at time 0 with their own "handle not set" fatal. The env's context made the scope uvm_test_top.env.*, while the vseq, started with start(null), looked itself up as plain vseq. Nothing was wrong with the set or the get on their own; the two names simply never met. Printing get_full_name() from the vseq made it obvious in one line. They fixed it with a null context and "*" scope for the handles, which is the pattern above. With a virtual sequencer the same get would have worked, because the vseq's full name would then start with uvm_test_top.env.v_sqr.
dma_vseq is started with start(null). Inside it, write_reg starts apb_rw_seq with s.start(apb_sqr), without this. What changes in a normal run, and what changes if the vseq ever grabs the APB sequencer?In a normal run, only the names: the child is reported as uvm_test_top.env.apb.sqr@@s instead of ...@@vseq.s, and config_db lookups for it change. If the vseq holds a lock or grab on the APB sequencer, the child is no longer its descendant, so it is blocked by its own parent's lock and the vseq deadlocks.
apb_sqr and axis_sqr for null but not irq_vif?The comparison would be legal SystemVerilog, but it miscompiles in Verilator 5.048, which the bench is verified with. A missing irq_vif would instead show up as a null access at the wait on the interrupt. On a commercial simulator, check it too.
Next: inside dma_vseq, the interrupt handler calls grab() while the poller keeps queuing reads. Why grab and not lock, and what each really does to the queue, is lock vs grab: Who Gets the Sequencer.