A DMA job is a small play in four acts: program KEY and LEN over APB, write GO, stream LEN words on the input, wait for the interrupt and clear STATUS. Two agents, one order. A colleague wants to speed up the random test by starting job 2's register writes while job 1 is still streaming, "since they're on different interfaces". The regression goes red with mismatches that look like data corruption. Nothing in the DUT changed. Who is supposed to own the order across agents?
Each agent's sequencer is a queue for one interface; it knows nothing about the others. A scenario that spans interfaces needs something above them, like a conductor who plays no instrument but tells each section when to come in. In UVM that conductor is a virtual sequence: a sequence that sends no items itself, and instead starts ordinary sequences on the agents' sequencers in the right order.
A virtual sequence needs handles to those sequencers. It can get them from a virtual sequencer, a component that holds the handles and nothing else, or the test can hand them over directly before starting the sequence. Both are common; the virtual sequencer is older and works well with uvm_declare_p_sequencer, while direct handles avoid an extra component. The bench does the simplest version of all: the test itself conducts, in its run_job task.
The base test's run_job task is a virtual sequence in all but name. It starts one-access APB sequences and a packet sequence on two different sequencers, in a fixed order, and waits on the interrupt between jobs.
// snippet: from the course DMA bench
class dma_base_test extends uvm_test;
`uvm_component_utils(dma_base_test)
dma_env env;
virtual irq_if irq_vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = dma_env::type_id::create("env", this);
if (!uvm_config_db#(virtual irq_if)::get(this, "", "irq_vif", irq_vif))
`uvm_fatal("NOVIF", "irq_if not set for the test")
endfunction
function void end_of_elaboration_phase(uvm_phase phase);
uvm_root::get().print_topology();
endfunction
// Register helpers used by every test.
task reg_write(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(env.apb.sqr);
endtask
task reg_read(bit [7:0] addr, output bit [31:0] data);
apb_rw_seq s = apb_rw_seq::type_id::create("s");
s.addr = addr; s.write = 0; s.data = 0;
s.start(env.apb.sqr);
data = s.rdata;
endtask
// One complete job: program the DMA, stream n words, wait for the interrupt,
// return STATUS and clear its DONE/ERR bits.
task run_job(dma_job_cfg job, int unsigned n_words, output bit [31:0] status);
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
reg_write(KEY, job.key);
reg_write(LEN, job.len);
reg_write(CTRL, {30'b0, job.irq_en, 1'b1});
if (!pkt.randomize() with { n == n_words; })
`uvm_fatal("RAND", "axis_pkt_seq randomize failed")
pkt.start(env.in_agt.sqr);
wait (irq_vif.irq === 1'b1);
reg_read(STATUS, status);
reg_write(STATUS, 32'h6); // write 1 to clear DONE and ERR
endtask
endclassThis is a fine starting point, and many real benches stay this way for block-level work. Its limit is reuse: the scenario lives in a test class, so a chip-level test that wants to run "one DMA job" alongside other traffic cannot start it as a sequence.
Moving the task into a sequence makes it something any test, or any other virtual sequence, can start. This version takes sequencer handles directly, so it needs no virtual sequencer component.
// snippet: a DMA job as a virtual sequence (handles set by the starter)
class dma_job_vseq extends uvm_sequence;
`uvm_object_utils(dma_job_vseq)
apb_sequencer apb_sqr; // set by the test before start()
axis_sequencer axis_sqr;
virtual irq_if irq_vif;
dma_job_cfg job;
function new(string name = "dma_job_vseq");
super.new(name);
endfunction
task body();
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
write(KEY, job.key);
write(LEN, job.len);
write(CTRL, {30'b0, job.irq_en, 1'b1}); // GO last
if (!pkt.randomize() with { n == local::job.len; }) `uvm_fatal("RAND", "pkt")
pkt.start(axis_sqr, this); // child of this sequence
wait (irq_vif.irq === 1'b1);
write(STATUS, 32'h6); // clear DONE and ERR
endtask
task write(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);
endtask
endclassThe test sets vseq.apb_sqr = env.apb.sqr, vseq.axis_sqr = env.in_agt.sqr and the interrupt handle, then calls vseq.start(null): a virtual sequence runs on no sequencer of its own. With a virtual sequencer instead, the env copies the same handles into it in connect_phase, and the sequence reaches them through p_sequencer after uvm_declare_p_sequencer. Virtual sequences covers both styles in depth.
dma_rand_test runs three jobs through run_job, one after another. The times on the [JOB] lines show the ordering at work: each job starts only after the previous one's interrupt and STATUS clear (values from the course's run; they differ by simulator and seed):
UVM_INFO dma_tb.sv(731) @ 0: uvm_test_top [JOB] key=90fdd8fd len=10 irq_en=1
UVM_INFO dma_tb.sv(731) @ 645000: uvm_test_top [JOB] key=0e564b57 len=1 irq_en=1
UVM_INFO dma_tb.sv(731) @ 885000: uvm_test_top [JOB] key=5a1793f4 len=3 irq_en=1
UVM_INFO dma_tb.sv(411) @ 1305000: uvm_test_top.env.sb [SB] 14 words matched, 0 mismatchedJob 1 moves 10 words and takes 645 ns; job 2 moves a single word and the next job starts 240 ns later. Every word of every job is compared, because each job's KEY was written while the DMA was idle.
Back to the hook. With job 2's register writes overlapped onto job 1's stream, job 2's KEY write arrives while the DMA is busy. The DUT ignores it, exactly as its spec says (The DMA you will verify lists "KEY and LEN ignored while busy" as a testplan row). The scoreboard does not: its write_apb updates its key on every observed KEY write. So the remaining words of job 1 are predicted with job 2's key, and every one of them mismatches. It looks like data corruption in the DUT; it is a coordination bug in the test, exposed by a gap in the scoreboard's model.
Two fixes, at two layers. The scenario fix is ordering: a job's register writes wait until the previous job has finished, which the job sequence enforces by waiting for the interrupt and clearing STATUS before returning. The model fix is to make the scoreboard as strict as the hardware, ignoring KEY writes while it believes a job is in flight. A strong bench does both, because the second fix is what lets a later test write KEY mid-job on purpose to verify that the DUT ignores it.
Usually not, and the scoreboard is safe too. Until GO is written the DMA is not busy, so s_tready stays low and the stream driver waits with TVALID high; the input monitor publishes a word only when TVALID and TREADY are both high, which cannot happen before GO, and GO is the last APB write. But that safety comes from the DUT's handshake, not from the sequence. Reorder the APB writes, or verify a variant that buffers input before GO, and the scoreboard can predict with a stale key. Programming first, then streaming, is clearer and matches how software drives the block.
dma_job_vseq unchanged. What happens?apb_sqr is null, so the first write fails when it starts apb_rw_seq on it. At chip level the register programming has to come from the real CPU (software or a CPU model), and the virtual sequence must be restructured or replaced. This is why handle copies need guards and why virtual sequences should state which agents they require.
dma_job_vseq start its child sequences with this as the parent (s.start(apb_sqr, this))?So each child runs in the context of the virtual sequence: it inherits its priority, appears under it in sequence traces, and is allowed through if the virtual sequence holds a lock or grab on that sequencer. Started without a parent, the child would compete as an unrelated sequence and could be blocked by its own parent's lock.
Next: that last question, the same env with some agents passive, is the subject of Active-passive mixes and composition debug.