While the DMA bench was being built, dma_drain_test overrode the scoreboard with dma_scoreboard_eot, a subclass that counts the words each job owes and holds the run phase open until they arrive. The topology printed sb dma_scoreboard_eot. The override had worked. And yet the new write_apb and write_out never ran: the owed count stayed at zero, and the run phase ended with a word still inside the DMA. The factory did its job perfectly. So what went wrong?
Swapping a worker is only half of the job; the people around them must also talk to the new worker. A factory override changes which class is constructed. It does not change how the rest of the testbench calls that object. Every caller holds a handle of the base type (the agent's axis_driver drv, the env's dma_scoreboard sb, an analysis imp's internal handle). A call through a base handle reaches the derived code only if the method is virtual. If it is not, the base method runs, every time, no matter what was built.
So a safe override needs three things: it is registered before the create runs; the substitute derives from the requested type; and every method it changes is reached through virtual dispatch (a virtual method, or a virtual hook the base class calls).
Build is top-down. The test's build_phase creates the env object; the env's build_phase runs afterwards and creates the agents; each agent's build_phase runs after that and creates its driver. An override must be in place before the create it should affect. The bench puts every override first, before super.build_phase:
// snippet: from the course DMA bench
// The test drops its objection as soon as the last input word is accepted, with
// no settling delay. The end-of-test scoreboard keeps the run phase open until
// the last output word arrives.
class dma_drain_test extends dma_base_test;
`uvm_component_utils(dma_drain_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
dma_scoreboard::type_id::set_type_override(dma_scoreboard_eot::get_type());
super.build_phase(phase);
endfunction
task run_phase(uvm_phase phase);
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
phase.raise_objection(this);
reg_write(KEY, 32'h1234_5678);
reg_write(LEN, 6);
reg_write(CTRL, 32'h1); // GO, interrupt not needed
if (!pkt.randomize() with { n == 6; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
pkt.start(env.in_agt.sqr);
phase.drop_objection(this); // the last word is still inside the DMA
endtask
endclassStrictly, an override for the scoreboard or the driver would still work one line later, after super.build_phase, because those objects are created later in the env's and agent's own build_phase. It would not work for anything the base test creates directly, such as the env itself. Putting overrides first is the habit that is right in every case, including the day someone overrides dma_env.
The stream driver was designed to be extended. Its run loop calls a virtual hook for every beat, and the base version returns the beat unchanged:
// snippet: from the course DMA bench
class axis_driver extends uvm_driver #(axis_item);
`uvm_component_utils(axis_driver)
virtual axis_if vif;
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#(virtual axis_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "axis_if not set for this driver")
endfunction
task run_phase(uvm_phase phase);
vif.tvalid <= 1'b0;
@(posedge vif.rst_n);
forever begin
axis_item beat;
seq_item_port.get_next_item(req);
beat = pre_drive(req);
if (beat != null) begin
repeat (beat.idle) @(posedge vif.clk);
@(posedge vif.clk);
vif.tdata <= beat.data;
vif.tlast <= beat.last;
vif.tvalid <= 1'b1;
do @(posedge vif.clk); while (!vif.tready); // the beat moves on TVALID && TREADY
vif.tvalid <= 1'b0;
end
seq_item_port.item_done();
end
endtask
// Hook for derived drivers: return the beat to drive (the sequence's item, or a
// modified copy), or null to drop it. The base driver drives every item as is.
virtual function axis_item pre_drive(axis_item it);
return it;
endfunction
endclassThe truncating driver overrides only the hook. Note the clone: the item belongs to the sequence, which may still read it or reuse it, so the driver changes a copy and returns that.
// snippet: from the course DMA bench
// Fault injection without touching the sequence: cuts every packet after beat
// `cut` by setting TLAST there and dropping the rest of the packet.
class axis_truncating_driver extends axis_driver;
`uvm_component_utils(axis_truncating_driver)
int unsigned cut = 3; // index of the beat that gets TLAST
int unsigned beat_no;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function axis_item pre_drive(axis_item it);
axis_item b;
int unsigned i = beat_no;
beat_no = it.last ? 0 : beat_no + 1; // count beats per packet
if (i > cut) return null; // after the cut: drop the beat
if (i < cut) return it;
$cast(b, it.clone()); // never edit the sequence's own item
b.last = 1'b1;
`uvm_info("TRUNC", $sformatf("TLAST forced on beat %0d", i), UVM_MEDIUM)
return b;
endfunction
endclassThe scoreboard's write_* methods are declared virtual for the same reason, with a comment in the bench that points at this story. The derived scoreboard calls super.write_out first, so the base checking still happens, then adds its owed-word count.
// snippet: from the course DMA bench
// A scoreboard that owns end of test. It learns how many words each job owes
// from the observed LEN and GO register writes, and when every objection drops
// while words are still owed, it raises its own objection until they arrive.
// (Counting on the expected queue alone is not enough: the last input word may
// not have been published yet when the test drops its objection.)
class dma_scoreboard_eot extends dma_scoreboard;
`uvm_component_utils(dma_scoreboard_eot)
int unsigned len_reg, owed;
event drained;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void write_apb(apb_item t);
super.write_apb(t);
if (!t.write || t.slverr) return;
if (t.addr == LEN) len_reg = t.data[15:0];
if (t.addr == CTRL && t.data[0]) owed += len_reg; // GO: this job owes len_reg words
endfunction
virtual function void write_out(axis_item t);
super.write_out(t);
if (owed > 0) owed--;
if (owed == 0) -> drained;
endfunction
function void phase_ready_to_end(uvm_phase phase);
if (phase.get_imp() != uvm_run_phase::get() || owed == 0) return;
phase.raise_objection(this, $sformatf("%0d word(s) still owed by the DMA", owed));
fork begin
@drained;
phase.drop_objection(this, "all words out");
end join_none
endfunction
endclassTool | What it tells you |
|---|---|
Topology ( | What was actually built, with each component's type name. The first thing to read. |
| Registered instance and type overrides, plus user-defined types. |
| Runs the lookup for one path and shows every matching override and the winner. Cross-check it with |
| The same, starting from a type name. |
| Config_db sets and gets. It does not trace the factory. |
With virtual write_* methods, dma_drain_test drops its objection as soon as the last input word is accepted, the derived scoreboard raises its own objection because words are still owed, and the run phase stays open until all six come out:
+UVM_TESTNAME=dma_drain_test
UVM_INFO dma_tb.sv(411) @ 375000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched
UVM_ERROR : 0Six of six. The line is printed by the base class's report_phase, but the count is complete only because the derived class's write_out ran and kept the phase alive. How phase_ready_to_end does that is covered in phase_ready_to_end.
Here is the bug from the opening, as it happened. The first version of dma_scoreboard declared plain function void write_out(axis_item t). The macro ` uvm_analysis_imp_decl(_out) ` creates an imp class that stores a handle of the scoreboard's declared type, dma_scoreboard, and calls write_out on it. With a non-virtual method, SystemVerilog binds that call at compile time to dma_scoreboard::write_out`. The derived object was there, the topology said so, and its methods were simply unreachable.
The symptom was quiet: no error, a smaller match count, an end of test that came one word early. The fix was one keyword, virtual, on every write_* method of a class meant to be extended. The broader lesson for interviews: a factory override replaces the object; whether your new code runs still depends on virtual dispatch. When a derived class "does nothing", read the topology first (was it built?), then check that each overridden method is virtual in the base class.
dma_env::type_id::set_type_override(dma_env_dbg::get_type()) after super.build_phase(phase). What is built, and why?The original dma_env. The base test's build_phase (called through super.build_phase) already ran dma_env::type_id::create("env", this), so the override arrives after the only create it could have affected. Overrides for the driver or scoreboard would still work after that line, because those are created later, in the env's and agent's own build_phase.
axis_truncating_driver sets last on the item it was given instead of on a clone. What could go wrong?The item belongs to the sequence. A sequence that inspects or reuses its item after finish_item (for coverage, logging or the next beat) sees a modified object, and any scoreboard or coverage that stored the same handle sees it too. Cloning keeps the change local to what the driver drives.
super.write_out in its write_out. What breaks?The base class's compare. dma_scoreboard::write_out pops the prediction and checks the word; skipping it means no word is ever compared, and the base check_phase reports leftover predictions or an empty compare. Extending behaviour means calling super and adding to it.
Next: the basics are complete. Factory advanced covers the cases senior interviews like: parameterized classes, abstract classes, by-name overrides and the inherited type_id trap.