The two-DMA subsystem bench passes: both block scoreboards and the end-to-end checker agree on every word. Then the chip integration lead asks a question that bench cannot answer: if somebody swapped the two DMAs' interrupt lines in the subsystem netlist, which of your tests would fail? You look at dma_subsys_test. It waits until both interrupts are high. Swapped, they would still both go high. None of your tests would fail.
This lesson builds upwards from the block bench to the subsystem and to a chip around it, and asks the question that organises every level: what can this level see that the level below cannot?
A higher level is not a bigger block bench. Block benches have already checked each block's behaviour with full control and visibility. A subsystem or chip bench repeats none of that; it adds integration checks: connections between blocks, flows that cross them, shared resources, and the software view. Everything else it inherits by reusing the block envs, with their scoreboards still running where they can see their inputs.
An orchestra rehearses the same way. Each section practises its part alone, where a wrong note is easy to hear. The full rehearsal does not re-check whether the violins can play scales; it checks entries, balance and whether everyone follows the same conductor. A full rehearsal that spends its time on scales finds integration problems last.
dma_subsys_env creates two copies of dma_env, makes dma1's input agent passive because dut0 drives that bus, and adds one checker that sees the whole chain. Its config object carries the two decisions that differ between subsystem configurations.
// snippet: from the course DMA bench
// Two DMAs in a chain (dma0's output feeds dma1's input), verified by reusing the
// block env twice, unchanged, plus an end-to-end checker.
class dma_subsys_cfg extends uvm_object;
bit has_e2e_checker = 1; // the end-to-end scoreboard
bit chain_internal = 1; // dma1's input is driven by dma0, so its agent is passive
`uvm_object_utils(dma_subsys_cfg)
function new(string name = "dma_subsys_cfg");
super.new(name);
endfunction
endclass
class dma_subsys_env extends uvm_env;
`uvm_component_utils(dma_subsys_env)
dma_subsys_cfg cfg;
dma_env dma0, dma1; // the block env, reused as is
dma_e2e_scoreboard e2e;
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#(dma_subsys_cfg)::get(this, "", "cfg", cfg))
cfg = dma_subsys_cfg::type_id::create("cfg"); // defaults if the test set none
if (cfg.chain_internal)
uvm_config_db#(uvm_active_passive_enum)::set(this, "dma1.in_agt", "is_active", UVM_PASSIVE);
dma0 = dma_env::type_id::create("dma0", this);
dma1 = dma_env::type_id::create("dma1", this);
if (cfg.has_e2e_checker) e2e = dma_e2e_scoreboard::type_id::create("e2e", this);
endfunction
function void connect_phase(uvm_phase phase);
if (cfg.has_e2e_checker) begin
dma0.apb.ap.connect(e2e.key0_export);
dma1.apb.ap.connect(e2e.key1_export);
dma0.in_agt.ap.connect(e2e.first_export);
dma1.out_agt.ap.connect(e2e.last_export);
end
endfunction
endclassNotice where the end-to-end checker gets its inputs: the APB monitors for both keys, dma0's input monitor and dma1's output monitor. All four already exist inside the block envs. The integration checker adds a model of the chain (in ^ key0 ^ key1), not new observation points. Vertical reuse walks the top and the test in detail.
The subsystem plan lists only what no block bench can check. For the two-DMA chain:
Integration item | Why the block benches miss it | Check |
|---|---|---|
Data through the chain | Each block sees half the path |
|
Backpressure from | The block bench's sink is a testbench agent, not another DMA | Both block scoreboards plus E2E, with |
Each interrupt reaches its own controller input | One DMA per bench; no wiring to get wrong | Run one DMA alone and check only its interrupt; or formal connectivity |
Each instance answers at its own base address | The block sits at offset 0 | Chip register model with submaps; block sequences run per instance |
Both DMAs active at once on shared resources | No sharing at block level | Concurrent jobs; bandwidth or arbitration checks where a fabric exists |
Reset of one domain while the other runs | One reset per block bench | Reset-aware scoreboards per instance |
The software flow: driver programs, waits, clears | Block tests are UVM sequences, not driver code | C tests on the CPU at chip level |
The APB agents go passive. The CPU, through the interconnect, now owns the register bus. The block's APB monitor keeps publishing, so dma0.sb still learns each KEY write and keeps checking.
Stimulus moves to software and virtual sequences. Directed C tests on the CPU exercise the real driver flow; UVM virtual sequences coordinate the CPU, external traffic and checking. Block sequences can still run on an interconnect agent through layering in a CPU-less configuration.
The register model composes. The chip register model places each block's map at its base address, and block register sequences run on soc.dma1 without edits.
Some questions leave simulation. Pin and interrupt connectivity under every configuration is a structural question, cheaper as formal connectivity checking; an OS boot needs emulation; power-domain behaviour needs power-aware simulation.
+UVM_TESTNAME=dma_subsys_test (run.sh dma_subsys_tb.sv)
UVM_INFO dma_tb.sv(411) @ 875000: uvm_test_top.env.dma0.sb [SB] 10 words matched, 0 mismatched
UVM_INFO dma_tb.sv(411) @ 875000: uvm_test_top.env.dma1.sb [SB] 10 words matched, 0 mismatched
UVM_INFO dma_tb.sv(1182) @ 875000: uvm_test_top.env.e2e [E2E] 10 words matched end to end, 0 mismatched
UVM_ERROR : 0Three checkers, three claims. dma0.sb says block 0 is right, dma1.sb says block 1 is right, e2e says the chain is right. If only e2e failed, the bug would be in the connection between them.
What it does not claim. Nothing in this log says which interrupt belongs to which DMA, which address decodes to which instance, or what happens when one DMA is reset alone. Those rows of the integration plan need their own tests.
Read dma_subsys_test again: it programs both DMAs, sends ten words and then waits for irq0_vif.irq === 1'b1 && irq1_vif.irq === 1'b1. If the two interrupt wires were swapped in the subsystem, both would still rise, both waits would still finish, and every checker would still pass. The test was written to make the chain work, not to tell the two DMAs apart, and that is the most common shape of an integration hole: a test whose stimulus is symmetric cannot detect a symmetric wiring error.
Teams find this class of bug in the lab more often than they would like, typically as a driver that services the wrong channel's interrupt. The fixes are cheap once you see the pattern: an asymmetric test (start only dma1, assert irq1 rises and irq0 stays low), a check in the interrupt monitor that each interrupt follows its own DMA's STATUS, or a formal connectivity check that proves each interrupt output reaches the right controller input. Put the row in the integration plan first; the test follows.
Between the blocks or in the E2E model itself: a wiring or width problem on the middle bus, or a chain model that gets the key order wrong. Each block was correct on what its own monitors saw. That localisation is the reason to keep the block scoreboards running at higher levels: without them, every E2E failure starts with "which block?"
dma0.sb still know the KEY for each job?Its APB monitor still watches dma0's APB interface and publishes every transfer, whoever drives it. The scoreboard's write_apb sees the CPU's KEY write exactly as it saw the block driver's. That is why the scoreboard was built to learn KEY from the monitor rather than from the stimulus.
Interrupt and address-decode connectivity: a formal connectivity check proves every interrupt output reaches the intended controller input, and every base address decodes to the right instance, for every configuration, in minutes. A simulation test can only show one configuration at a time.
Next: three levels of env means three levels of settings. Configuration Strategy Across Levels keeps them in one tree.