The DMA is going into a subsystem: two copies in a chain, dma0's output stream feeding dma1's input. The block env is reused twice, unchanged, and each copy's scoreboard still passes. Then the architect asks the question block scoreboards cannot answer: does a word that enters dma0 leave dma1 correctly? You add a checker, wire it up, and get ten errors on the first run, while both block scoreboards report 10 out of 10. Which one do you believe, and where is the bug?
A scoreboard does not check "the DUT". It checks a contract between two observation points: what goes in at one set of monitors must come out, transformed by the spec, at another. dma_scoreboard checks one DMA, from its input stream and APB bus to its output stream. An end-to-end checker picks observation points further apart, the input of dma0 and the output of dma1, and checks the whole chain's contract without looking at the middle.
Wiring is how you choose those points. It happens in connect_phase, after every component exists, and it should go through each agent's own analysis port (agt.ap), never through its internals (agt.mon.ap). An agent's ap is its public face: the agent may later add filtering, a second monitor or a protocol checker behind it, and every env that wired to ap keeps working. Think of it as plugging into a building's front-door intercom rather than running a wire through a window to one particular flat.
// 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
endclassconnect_phase has one line per stream, from each agent's ap to the matching scoreboard imp. The agent sets ap = mon.ap in its own connect_phase, a handle copy. That works only because connect_phase runs bottom-up: the agents' connect_phase finishes before the env's starts, so apb.ap is a real port by the time the env uses it. Doing the same wiring in the env's build_phase would dereference a null handle.
// snippet: from the course DMA bench
// Checks the chain as one: every word out of dma1 equals the word into dma0
// XOR both keys. It needs no knowledge of the word in the middle.
class dma_e2e_scoreboard extends uvm_scoreboard;
`uvm_component_utils(dma_e2e_scoreboard)
uvm_analysis_imp_key0 #(apb_item, dma_e2e_scoreboard) key0_export;
uvm_analysis_imp_key1 #(apb_item, dma_e2e_scoreboard) key1_export;
uvm_analysis_imp_first #(axis_item, dma_e2e_scoreboard) first_export;
uvm_analysis_imp_last #(axis_item, dma_e2e_scoreboard) last_export;
bit [31:0] key0, key1;
bit [31:0] expected[$];
int unsigned n_match, n_mismatch;
function new(string name, uvm_component parent);
super.new(name, parent);
key0_export = new("key0_export", this);
key1_export = new("key1_export", this);
first_export = new("first_export", this);
last_export = new("last_export", this);
endfunction
virtual function void write_key0(apb_item t);
if (t.write && !t.slverr && t.addr == KEY) key0 = t.data;
endfunction
virtual function void write_key1(apb_item t);
if (t.write && !t.slverr && t.addr == KEY) key1 = t.data;
endfunction
virtual function void write_first(axis_item t);
expected.push_back(t.data ^ key0 ^ key1);
endfunction
virtual function void write_last(axis_item t);
bit [31:0] exp;
if (expected.size() == 0) begin
`uvm_error("E2E_UNEXP", $sformatf("word %08h out of dma1 with nothing expected", t.data))
n_mismatch++; return;
end
exp = expected.pop_front();
if (t.data !== exp) begin
`uvm_error("E2E_MISMATCH", $sformatf("expected %08h, got %08h", exp, t.data))
n_mismatch++;
end else n_match++;
endfunction
function void check_phase(uvm_phase phase);
if (expected.size() != 0) `uvm_error("E2E_LEFTOVER", $sformatf("%0d word(s) never left dma1", expected.size()))
if (n_match == 0) `uvm_error("E2E_EMPTY", "nothing was compared end to end")
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("E2E", $sformatf("%0d words matched end to end, %0d mismatched", n_match, n_mismatch), UVM_LOW)
endfunction
endclassThe model is t.data ^ key0 ^ key1: a word entering dma0 should leave dma1 XORed with both keys. It needs no knowledge of the word in the middle, and that is deliberate. The block scoreboards check each block; the end-to-end checker checks that the blocks were composed correctly: right order, right keys, nothing lost between them. Its structure mirrors the block scoreboard: in-order queue, !== compare, a leftover check and an empty check.
// snippet: from the course DMA bench
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
endclassTwo details matter. dma1.in_agt is made passive before the block envs are built, because dma0 drives that bus; its monitor still runs, which is why dma1.sb keeps working. And the checker is optional (has_e2e_checker), created and wired only when the config asks for it, so the same env serves runs that want only block checks.
+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 scoreboards, three independent verdicts on the same ten words. When they agree, you have checked each block and the composition. When they disagree, the pattern tells you where to look, as the next section shows.
This is the opening scene, run for real on a scratch copy of the bench. One line of the subsystem env's connect_phase was copy-pasted and not edited: dma0.apb.ap.connect(e2e.key1_export), where it should say dma1. The connection is legal, so nothing complains at elaboration.
(scratch run: subsystem env with one miswired connection)
UVM_ERROR dma_tb.sv(1171) @ 325000: uvm_test_top.env.e2e [E2E_MISMATCH] expected 46ee4a23, got 57ff6801
UVM_ERROR dma_tb.sv(1171) @ 375000: uvm_test_top.env.e2e [E2E_MISMATCH] expected 5921544c, got 4830766e
... eight more ...
UVM_ERROR dma_tb.sv(1178) @ 875000: uvm_test_top.env.e2e [E2E_EMPTY] nothing was compared end to end
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] 0 words matched end to end, 10 mismatched
UVM_ERROR : 11Read the three lines at the end together. Both block scoreboards pass, so each DMA did its job and the data between them was right. Only the checker that spans them fails, on every word. That combination points at the checker, not the hardware. Then XOR one pair: 0x46ee4a23 ^ 0x57ff6801 = 0x11112222, which is dma0's key (0x00002222) XOR dma1's key (0x11110000). The checker's model used dma0's key twice, so it was missing dma1's key exactly. One debug_connected_to() on dma1.apb.ap would have shown it had no connection to the checker at all.
This is the main reason to keep the block scoreboards when you add an integration checker: they localise a failure. Remove them to save run time and the same log reads the chain is broken, with no hint whether the bug is in dma0, dma1, the composition, or the checker. If run time matters, turn them off per run with a configuration knob, not by deleting them.
dma1.sb fails on every word while dma0.sb and the end-to-end checker both pass. What is the most likely cause?If the chain end to end is right and dma0 is right, the words leaving dma1 are right too. A failing dma1.sb then points at dma1's own scoreboard inputs: most likely its passive input agent is watching the wrong bus or its APB stream is miswired, so its model is predicting from the wrong data or the wrong key. Check its connections before touching dma1's RTL.
connect calls into the env's build_phase, right after creating the agents. What happens, and why?The run dies with a null-handle error. In build_phase the agents exist but have not run their own build_phase yet (build is top-down), so neither their monitors nor their ap handles exist. Connections belong in connect_phase, which runs bottom-up after every component is built.
program_dma calls?For the same reason the block scoreboard does: the monitors show what each DMA actually received. If a programming write went to the wrong DMA or was rejected, the test's values and the DUTs' registers differ, and only the observed writes predict what the chain really does.
Next: with the streams connected, the scoreboard must pair each output with its expected partner. In-Order Matching looks closely at the simplest pairing rule and at the field nobody compared.