The subsystem team takes your DMA block env on a Monday. By Tuesday they are back with questions. Why does the base test's reg_write helper only work on a sequencer called env.apb.sqr? Why is there no switch to turn the block scoreboard off? Why does the register model lock itself? Their subsystem test ended up rewriting the job-programming code from scratch, and nobody is sure the two copies agree.
Every one of those questions comes from the real course bench. This lesson treats a block bench as a product with two customers, the block regression and the next level up, and checks the DMA bench against the contract that makes upstream reuse work.
A block bench that only serves the block regression is a script: it works, and it works only where it was written. A block bench that the subsystem and chip teams can instantiate without editing is a component, and it needs an interface the way any component does. That interface has a few parts: agents that can stop driving, monitors that publish everything a checker needs, checkers fed only by monitors, a configuration object instead of hard-coded decisions, stimulus written as sequences that run on any sequencer, and a register model that can become a sub-block.
A Lego brick is the picture. It is useful in a thousand models because of its studs and tubes, the standard interface on its outside; nobody cares how it is moulded inside. A bench whose decisions are moulded inside, such as a hard-coded path or a fixed active agent, is a brick with no studs: it only works as itself.
Here is the contract with an honest verdict for each item. The good news first: the core of the bench was built for reuse, which is why dma_subsys_env could instantiate dma_env twice unchanged.
Contract item | The DMA bench today | Verdict |
|---|---|---|
Agents build only a monitor when passive |
| Met |
Monitors publish a fresh object per transfer, with everything checkers need | All three monitors create a new item per transfer | Met |
Scoreboard fed only by monitors |
| Met: this is why |
No hard-coded interface paths | Every driver and monitor gets | Met: the subsystem top sets the same names one level deeper |
A config object with the env's knobs |
| Not met |
Stimulus as sequences that take a sequencer |
| Not met: |
Register model usable as a sub-block |
| Partly: works, but generated blocks leave locking to the top |
Coverage instances named distinctly |
| Met |
Protocol assertions that travel with the RTL | None in the block bench | Not met: see assertion VIP and bind |
No global side effects from the env | The verdict server and timeout are installed by tests, not the env | Met |
The env decides structure and nothing else. It makes the DUT-driven port passive, creates three agents and a scoreboard, and wires monitors to the scoreboard. Note what it does not do: it never names an interface, a test or a sequence.
// snippet: from the course DMA bench
class dma_env extends uvm_env;
`uvm_component_utils(dma_env)
apb_agent apb;
axis_agent in_agt, out_agt;
dma_scoreboard sb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// The output side is observed only: the DUT drives it.
uvm_config_db#(uvm_active_passive_enum)::set(this, "out_agt", "is_active", UVM_PASSIVE);
apb = apb_agent::type_id::create("apb", this);
in_agt = axis_agent::type_id::create("in_agt", this);
out_agt = axis_agent::type_id::create("out_agt", this);
sb = dma_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
apb.ap.connect(sb.apb_export);
in_agt.ap.connect(sb.in_export);
out_agt.ap.connect(sb.out_export);
endfunction
endclassThe agent builds its monitor unconditionally and its driver only when active. That single if is what lets the same class watch the middle bus of the subsystem, where dut0 drives and nothing in the testbench may.
// snippet: from the course DMA bench
class axis_agent extends uvm_agent;
`uvm_component_utils(axis_agent)
axis_sequencer sqr;
axis_driver drv;
axis_monitor mon;
uvm_analysis_port #(axis_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = axis_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = axis_sequencer::type_id::create("sqr", this);
drv = axis_driver::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
ap = mon.ap;
if (get_is_active() == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclassThe base test's helpers are convenient for block tests and useless upstream, because they reach for env.apb.sqr. Moving the same steps into a sequence makes them portable: the caller picks the sequencer, so the same code programs dma0 or dma1 in the subsystem, or runs on an interconnect agent's sequencer at chip level through a layering sequence. This is a proposal, not bench code:
// snippet: a portable job-programming sequence (a proposal, not in the bench)
class dma_program_seq extends uvm_sequence #(apb_item);
`uvm_object_utils(dma_program_seq)
rand bit [31:0] key;
rand bit [15:0] len;
rand bit irq_en;
function new(string name = "dma_program_seq");
super.new(name);
endfunction
task body();
write_reg(KEY, key);
write_reg(LEN, len);
write_reg(CTRL, {30'b0, irq_en, 1'b1}); // IRQ_EN and GO
endtask
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(m_sequencer, this); // child of this sequence, same sequencer
if (s.slverr) `uvm_error("PROG", $sformatf("SLVERR writing %02h", addr))
endtask
endclass
// Block test: seq.start(env.apb.sqr);
// Subsystem test: seq.start(env.dma1.apb.sqr);The other two gaps have the same shape. A dma_env_cfg with has_scoreboard, has_coverage and per-agent modes replaces decisions buried in build_phase (the configuration lesson builds it). And the register block should leave lock_model() to whoever owns the root model, so the chip can compose it before anything is locked.
The subsystem test runs two unmodified copies of dma_env, each with its own block scoreboard, plus the end-to-end checker:
+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 : 0Same line number, two paths. Both block scoreboards report from dma_tb.sv(411): one class, two instances, no fork of the code.
`dma1.sb` matched 10 words using a passive input agent on a bus the testbench never drives. That works only because the scoreboard listens to monitors.
What the log hides is the duplicated program_dma in the subsystem test, the cost of the one contract item the bench did not meet.
Reuse means other people extend your classes, usually through the factory. While the bench was being built, a factory-overridden scoreboard redefined its write_* methods, and none of them ever ran (bench war story 2). The base class had declared them without virtual, so calls through a base-class handle (which is how the analysis imps call them) went to the base versions. There was no error and no warning; the override simply had no effect, and the test passed while checking with the old logic.
For a block bench that will be reused, every method a consumer might reasonably change is a contract: write_*, predict(), driver hooks such as pre_drive, and report hooks. Declare them virtual, document which ones are meant to be overridden, and keep one test in the block regression that overrides each and proves the override runs. A reuse contract nobody exercises is a reuse contract that breaks silently.
The scoreboard is not receiving monitor traffic at chip level, most often because an agent's monitor is not connected or its virtual interface points at the wrong bus. Less often, the scoreboard depended on something only active mode provided (a sequence callback or driver state). Check the topology print for the passive agents' monitors, check the vif paths, and check that every scoreboard input comes from a monitor's analysis port.
dma_program_seq started with s.start(m_sequencer, this) instead of s.start(env.apb.sqr)?m_sequencer is whatever sequencer the parent sequence was started on, so the child follows the caller's choice. Passing this as the parent makes it a child sequence, which also keeps it inside any lock or grab the parent holds. Naming env.apb.sqr would bind the sequence to one path and defeat the point of moving the code out of the test.
Next: with a reusable block in hand, From Block to Subsystem to SoC composes it upwards and asks what each level must add.