The DMA spec says: if a packet ends early (TLAST arrives before LEN words), stop, set STATUS.ERR and raise the interrupt. You need a test for it. The stream driver in the bench always sends complete packets, and it is shared by every other test. You could add an if (truncate) knob to the driver, copy the agent, or fork the env. Each option touches code that forty passing tests depend on.
The DMA bench does it with one line in one test, dma_truncate_test, and no other file changes. This lesson explains the machinery behind that line: what the factory is, what it stores, and what happens every time a component is created.
Think of a staffing agency. The env never hires a specific driver; it phones the agency and asks for "a stream driver for in_agt". Normally the agency sends the standard one. But a test can leave a note at the agency beforehand: "this week, whenever anyone asks for a stream driver, send the truncating one". The env makes exactly the same call, and gets a different worker who can do the same job plus one extra trick.
In UVM terms, the factory is a single, global registry of class proxies (small objects that know how to construct one class). Every registered class puts its proxy in the registry when the simulation starts. A test records overrides: substitution rules of the form "when someone asks for type A, build type B instead". And every type_id::create call goes through the registry, which applies the overrides at that moment. Three things meet at each create: registration, policy and the request.
The substitute must be able to do the job: it has to be the requested class or a class derived from it, because the caller stores the result in a handle of the requested type. A truncating driver is an axis_driver, so it fits wherever an axis_driver handle is waiting.
Notice what is not in the picture: the agent never names axis_truncating_driver. The env and the agent were written months before the truncating driver existed, and they did not need to change.
Every class in the bench carries a registration macro. ` uvm_component_utils(axis_truncating_driver) `` creates the proxy and registers it under both its type and its name string. What the macro generates is the subject of Registration macros; here, all that matters is that the class is now known to the factory.
The agent builds its driver through the factory. It asks for the base type and stores the result in a base-type handle:
// snippet: from axis_agent::build_phase in the bench
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); // the factory decides the class
endNow the test. dma_truncate_test adds one rule in its build_phase, then lets the base test build the env as usual. The run phase programs an 8-word job and expects the DMA to report ERR.
// 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
endclassset_type_override is a type override: it applies to every axis_driver created anywhere in the testbench. In this bench that means exactly one object, because only in_agt is active; out_agt is passive and builds no driver at all, and the APB driver is a different type. When a substitution must hit one instance and not another, you use an instance override instead; the rules for which one wins live in Override resolution.
The derived driver itself is small. It reuses everything from axis_driver and changes only one virtual hook, pre_drive, which the base driver calls for every beat. The full listing and why it clones before changing an item are in Override timing, safety and debug.
Run the bench with +UVM_TESTNAME=dma_truncate_test. The relevant lines (Verilator; the random key differs by simulator and seed, times are in ps):
+UVM_TESTNAME=dma_truncate_test
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 first line comes from code that exists only in axis_truncating_driver, reported from the path uvm_test_top.env.in_agt.drv. The agent's drv handle really holds the derived object.
4 words matched: beats 0 to 3 went through, and the scoreboard checked each against word ^ key. The override changed the stimulus; the checking stayed honest.
The topology printed at the start of the run (from print_topology in the base test) shows drv with type axis_truncating_driver. That table is the quickest proof an override took effect.
A team adds a truncation test exactly like this one, and it fails with UVM_ERROR ... [STATUS] ERR not set after a short packet. The TRUNC message never appears. Someone prints the factory and the override is right there in the table. The test author is sure the factory is broken.
The cause is in an agent written by someone else, long ago: drv = new("drv", this);. Calling new on a component is perfectly legal SystemVerilog, so nothing complained. But new builds exactly the class named in the handle declaration. It never asks the factory, so no override can ever apply to that object. The fix is one line, drv = axis_driver::type_id::create("drv", this);, and a review rule: every component and every object that a test might want to replace is created through type_id::create. The type_id::create pattern shows the full set of create calls.
dma_truncate_test sets its override, how many axis_truncating_driver objects exist in the bench, and why not more?One, at uvm_test_top.env.in_agt.drv. The type override applies to every axis_driver created through the factory, but out_agt is passive (the env sets is_active to UVM_PASSIVE for it), so it builds no driver. The APB agent's driver is an apb_driver, an unrelated type, so the override does not touch it.
axis_driver::type_id::set_type_override(apb_driver::get_type()) by mistake. The code compiles. What happens at run time?The factory looks up the override and constructs an apb_driver, but axis_driver::type_id::create must return an axis_driver handle, and the cast inside the registry fails. UVM reports a fatal error from the factory saying it could not return an object of the requested type. The rule behind it: an override must be the requested class or derived from it.
Read the topology table printed at end of elaboration. The drv row under in_agt shows type axis_truncating_driver instead of axis_driver. You can also call uvm_factory::get().print() in the test to list the registered overrides, but the topology shows what was actually built, which is the stronger evidence.
Next: the macro did a lot of quiet work here. Registration macros opens it up, including the one-word mistake that turns into a baffling compile error.