The subsystem team asks a reasonable question in a design review: "The block env has no virtual sequencer, and dma_vseq_test assigns env.apb.sqr and env.in_agt.sqr by hand. We now have two DMAs, and the chip will have fourteen agents. Do we add a virtual sequencer to every env, one at the top, or keep passing handles?" Each answer is defensible. Which one survives the next two levels of integration?
A virtual sequence needs handles to real sequencers. How it gets them is covered elsewhere: a virtual sequencer holds them and the sequence reaches them through p_sequencer (Virtual Sequencer Wiring), or the test assigns them to the sequence's fields and calls start(null) (Virtual Sequences Without a Virtual Sequencer). This lesson asks the environment-design question behind both: which component knows the map from "the APB sequencer of DMA 1" to a path like env.dma1.apb.sqr?
Keys in an office building make the point. A tenant can keep a key rack for its own rooms (a virtual sequencer per block env). The front desk can keep a master rack whose hooks point at the tenants' racks (a hierarchical virtual sequencer). Or the building manager can hand keys to each contractor personally (handles passed by the test). All three open doors. The difference is who has to change when a tenant moves rooms.
dma_vseq_test writes vseq.apb_sqr = env.apb.sqr; vseq.axis_sqr = env.in_agt.sqr;. It is simple, type-checked at compile time, and fine for one block. Its cost is that the env's internal paths now appear in every test that starts a virtual sequence. Rename in_agt, and every such test changes. At subsystem level, the test writes env.dma0.in_agt.sqr, so it also knows the subsystem env's layout.
You can keep handle passing and still put the map in one place: give the env a function that fills a virtual sequence's handles. The test calls it instead of naming paths.
// snippet
// In a derived dma_env: the one place that knows its own paths.
function void fill_vseq(dma_vseq vseq);
vseq.apb_sqr = apb.sqr;
vseq.axis_sqr = in_agt.sqr; // null if in_agt is passive
endfunction
// In the test's run_phase:
// env.fill_vseq(vseq); vseq.irq_vif = irq_vif; vseq.start(null);This keeps the compile-time checking of option 1 and the encapsulation of a virtual sequencer, with no extra component. What it does not give you is a place in the hierarchy: no full name for reporting and per-path config, and no default_sequence.
Each block env builds a virtual sequencer as a sibling of its agents and assigns its handles in connect_phase. The subsystem env builds one more, whose handles point at the block virtual sequencers:
// snippet
class dma_subsys_vsqr extends uvm_sequencer;
`uvm_component_utils(dma_subsys_vsqr)
dma_vsqr dma0_vsqr, dma1_vsqr; // the block envs' virtual sequencers
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
// In dma_subsys_env (with block envs that build v_sqr):
// build_phase: v_sqr = dma_subsys_vsqr::type_id::create("v_sqr", this);
// connect_phase: v_sqr.dma0_vsqr = dma0.v_sqr; v_sqr.dma1_vsqr = dma1.v_sqr;
// In a subsystem vseq body, with uvm_declare_p_sequencer(dma_subsys_vsqr):
// blk0.start(p_sequencer.dma0_vsqr, this);The block virtual sequences run unchanged on the block virtual sequencers, and the subsystem sequence only orders them. That is the main reason to choose this option: block-level virtual sequences become reusable building blocks one level up, and each level's paths are known only to that level's env.
At subsystem level dma1.in_agt is passive, so dma1.in_agt.sqr is null whichever option you pick. Any block virtual sequence that needs a stream sequencer cannot run on dma1. The bench's dma_vseq checks for exactly that and stops with [VSEQ] set apb_sqr and axis_sqr before starting. The fix is not to make the agent active (it would fight dut0 on the middle bus); it is to split the virtual sequence into a part that programs and services one DMA (APB and interrupt) and a part that supplies data, so the subsystem sequence can program both DMAs and stream only into dma0. Whatever holds handles should also null-check them in connect_phase and say which agent is passive, so the failure names its cause.
Situation | Usual choice | Why |
|---|---|---|
One block, a few virtual sequences, one author | Handles from the test, or an env fill function | Least machinery; compile-time checks. |
Block env meant for reuse; several vseq authors | Env-owned virtual sequencer | Paths stay inside the env; vseqs get a reporting name and per-path config. |
Subsystem built from block envs that have virtual sequencers | Hierarchical virtual sequencer | Block vseqs run unchanged; the subsystem vseq orders them. |
SoC with mostly passive agents | A small top virtual sequencer | Few active agents remain; it mainly holds their handles, events and config. |
The bench's dma_vseq_test, with no virtual sequencer, and a scratch version with an env-owned one from the wiring lesson, print the same job line under different names (+UVM_VERBOSITY=UVM_MEDIUM; times differ between the two builds):
UVM_INFO dma_tb.sv(623) @ 575000: reporter@@vseq [VSEQ] job done: STATUS=00000002 after 4 polls
UVM_INFO dma_tb.sv(623) @ 545000: uvm_test_top.env.v_sqr@@vseq [VSEQ] job done: STATUS=00000002 after 4 pollsreporter@@vseq: the sequence has no sequencer, so it reports through the global reporter, and its config_db full name is just vseq.
uvm_test_top.env.v_sqr@@vseq: the env owns a virtual sequencer, so the sequence has a place in the hierarchy. Per-path verbosity, config_db scoping and default_sequence all work from that name.
Either way, the interrupt handler's line reads uvm_test_top.env.apb.sqr@@vseq.isr: the work happens on the real sequencer.
The subsystem engineer's first attempt reuses dma_vseq twice, filling one from dma0's handles and one from dma1's: vseq1.axis_sqr = env.dma1.in_agt.sqr. The second start stops at once with the vseq's own fatal, set apb_sqr and axis_sqr before starting, because dma1's input agent is passive and built no sequencer. Their first instinct is to set chain_internal = 0 to get a sequencer, which brings back the two-drivers-on-one-bus problem from Vertical Reuse.
The real fault is in the block virtual sequence's design, not in the env: it assumed it owned every interface of its DMA. Splitting it into a programming-and-interrupt sequence and a data sequence made both reusable, and the subsystem virtual sequence became a few lines that order them. Decide this at block level, when you write the first virtual sequence: ask which of its interfaces could be internal one level up.
env.in_agt.sqr to their virtual sequences. The agent is renamed to stream_in. What changes, and what would have limited it?All twelve tests change, because each one knows the env's internal path. An env fill function or an env-owned virtual sequencer would have kept the path in one place, the env, so the rename would touch one file.
So block virtual sequences can run unchanged on their own block virtual sequencer, and so the subsystem env never depends on what is inside a block env. If a block env adds an agent, only that block's virtual sequencer changes. A flat list of real sequencers would make the subsystem level re-implement every block-level coordination.
Next: the environment is designed; the tests that drive it are next. Continue with Tests: Base Classes, Selection and Startup.