The bench's stream sequence library keeps growing: random packets, bursts, back-to-back jobs, the virtual sequence. A reviewer asks for a regression variant in which every packet from every one of them is cut short, so the DMA's error path is hit under all those traffic shapes. Editing each sequence is out of the question; they must keep working for the clean tests. The fault has to be injected below the sequences, where every item passes on its way to the pins. How do you change what a driver does to an item without forking the driver?
A hook is a virtual method the driver calls at a fixed point in its loop. The base class's version does nothing interesting, so clean tests behave exactly as before. A derived driver overrides the hook, and a factory override swaps that derived driver in for one test. Think of a production line with a test socket built into one station: normally a blank plug sits in it, and for a fault campaign you plug in a fixture that bends a part on its way past. The line itself is never rebuilt.
The bench's stream driver has one such socket, pre_drive. It receives the sequence's item and returns the beat to drive: the same item, a modified copy, or null to drop it. Everything else in the driver, including the handshake with the sequencer, stays in the base class, so a fault variant can change what is driven but never forgets to call item_done.
Here is the bench's axis_driver again. Look at two things: pre_drive is declared virtual, and item_done is called whether or not a beat was driven.
// 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
endclassaxis_truncating_driver overrides only the hook. It counts beats per packet, using the original item's TLAST to know where each packet from the sequence ends. Before the cut it returns the item unchanged; at the cut it returns a copy with TLAST forced; after the cut it returns null, so those beats are never driven. The sequence still sees every one of its items completed, so it finishes normally and never knows its packet was shortened.
// 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 clone matters. The item belongs to the sequence. The sequence may read it after finish_item, record it, sample coverage from it, or reuse it for its next beat. Editing it in place would change the sequence's record of what it asked for: its coverage would count a TLAST on beat 3 that it never requested. The rule is the same one monitors follow when they publish a fresh object for each transfer: never modify an object you do not own; copy it first.
// snippet: from the course DMA bench
// Factory override: the stream driver cuts each packet short, so the DMA must
// stop early and report ERR. No sequence or env file changes.
class dma_truncate_test extends dma_base_test;
`uvm_component_utils(dma_truncate_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
axis_driver::type_id::set_type_override(axis_truncating_driver::get_type());
super.build_phase(phase); // the env is created after the override
endfunction
task run_phase(uvm_phase phase);
dma_job_cfg job = dma_job_cfg::type_id::create("job");
bit [31:0] status;
phase.raise_objection(this);
if (!job.randomize() with { len == 8; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
run_job(job, 8, status);
if (status[2] !== 1'b1) `uvm_error("STATUS", $sformatf("ERR not set after a short packet: STATUS=%08h", status))
else `uvm_info("TRUNC", "DMA reported ERR for the short packet, as expected", UVM_LOW)
#100ns;
phase.drop_objection(this);
endtask
endclassThe order inside build_phase is the whole trick. dma_base_test::build_phase creates the env, and the env's build creates the agents, which create the drivers through type_id::create. The override must be registered before that chain starts, so it comes before super.build_phase. A type override is safe here because the bench has exactly one axis_driver: the output agent is passive and builds none. In a bench with two active stream drivers, such as two DMAs side by side, a test that wanted to cut only one stream would use an instance override with a path instead.
The cut position is a plain field set to 3. A fault campaign wants it to vary per test, which means a config_db get in the variant's build_phase. Two traps come with it. The type in the get must match the set exactly (int unsigned is not int, and neither is bit [3:0]), or the get silently finds nothing and the default is used. And a growing list of fault knobs is better grouped in one error-configuration object that the test fills and sets once, so a misspelt knob name is a compile error instead of a silent default.
// snippet: a configurable cut in the fault variant
function void build_phase(uvm_phase phase);
super.build_phase(phase); // still fetches the vif
if (!uvm_config_db#(int unsigned)::get(this, "", "cut", cut))
`uvm_info("TRUNC", $sformatf("no cut set, using the default %0d", cut), UVM_MEDIUM)
endfunction
// In the test, before super.build_phase:
// uvm_config_db#(int unsigned)::set(this, "env.in_agt.drv", "cut", 5);dma_truncate_test, one 8-word job, cut after beat 3 (+UVM_VERBOSITY=UVM_MEDIUM shows the TRUNC line):
UVM_INFO dma_tb.sv(520) @ 275000: uvm_test_top.env.in_agt.drv [TRUNC] TLAST forced on beat 3
UVM_INFO dma_tb.sv(761) @ 385000: uvm_test_top [TRUNC] DMA reported ERR for the short packet, as expected
UVM_INFO dma_tb.sv(411) @ 485000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
UVM_ERROR : 0The TRUNC line comes from uvm_test_top.env.in_agt.drv: the override really built the variant at the input agent. The topology table at the start shows its type, axis_truncating_driver.
The test's own check reads STATUS and finds ERR, the DUT's response to the fault.
4 words matched: beats 0 to 3 went in and came out correctly. Why the data scoreboard is not surprised by a short packet is the subject of Checking expected faults.
While the bench was being written, a factory-overridden scoreboard was built (the topology printed its type) but its extended methods were never called, because the base class had not declared them virtual. The same trap waits for every hook. Delete the word virtual from axis_driver::pre_drive and rerun dma_truncate_test: the topology still shows axis_truncating_driver at env.in_agt.drv, but the driver's loop calls pre_drive through code compiled in the base class, so the base version runs. No TRUNC line appears, all eight beats go in, the DMA ends with DONE, and the test fails with "ERR not set after a short packet".
A factory override replaces the object. Whether the new code runs still depends on virtual dispatch. That is why a VIP's hooks are declared virtual in the base class from the first release, and why a code review of a hook checks the keyword before anything else.
super.build_phase(phase). What happens?Nothing changes: the env, the agent and its plain axis_driver were already created inside super.build_phase, and a later override affects only objects created afterwards. All eight beats are driven, the DMA reports DONE, and the test's ERR check fails.
null for beats 4 to 7. Why does the sequence still finish, and why does it matter?Because the base loop calls item_done() for every item, driven or not, so each finish_item in the sequence returns. If dropping a beat also skipped item_done, the sequence would wait forever for its fifth beat and the test would hang instead of testing anything.
last, not on the beat it returns?The returned beat at the cut has TLAST forced, but the sequence's packet continues for four more items. Resetting on the returned beat would treat beat 4 as the start of a new packet and drive it. The original item's TLAST marks where the sequence's packet really ends, so counting resumes cleanly at the next packet.
Next: the reset test overrides axis_driver with axis_driver_ra, and this test overrides it with axis_truncating_driver. A test that wants both runs into the limit of overrides: Callbacks versus factory overrides.