dma_backpressure_test needs a reactive slave on the DMA's output, an agent that drives TREADY. The output agent and the input agent are both axis_agent. A type override, the tool you used for the truncating driver, would turn both into slaves, and the input side would stop sending data. How do you change one instance of a class and leave its twin alone, without editing dma_env?
Every component on the bench is created with type_id::create, which asks the factory, a registry of classes, for an object of the requested type. Before answering, the factory checks its list of overrides: a type override says "whenever anyone asks for type A, build B instead"; an instance override says "build B instead of A only for the component at this path". The test fills in that list in build_phase, and the env, which never changes, simply gets different objects.
Think of a kitchen before service. The chef pins a substitution list to the pass: "all fries become sweet-potato fries tonight" (a type override), and "table 7's burger has no bun" (an instance override). The cooks follow the same recipes as always; the list changes what comes out. The list has to be up before the first order is cooked, which is the timing rule in this lesson.
// snippet: from the course DMA bench
// The output port gets a reactive slave with random backpressure: an instance
// override swaps the agent at env.out_agt only; env.in_agt keeps axis_agent.
class dma_backpressure_test extends dma_base_test;
`uvm_component_utils(dma_backpressure_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
axis_agent::type_id::set_inst_override(axis_sink_agent::get_type(), "env.out_agt", this);
super.build_phase(phase);
endfunction
task run_phase(uvm_phase phase);
axis_sink_agent sink;
axis_ready_seq rsp = axis_ready_seq::type_id::create("rsp");
dma_job_cfg job = dma_job_cfg::type_id::create("job");
bit [31:0] status;
phase.raise_objection(this);
if (!$cast(sink, env.out_agt)) `uvm_fatal("CAST", "env.out_agt is not an axis_sink_agent")
fork
rsp.start(sink.sink_sqr); // the responder runs for the whole test
join_none
if (!job.randomize() with { len == 12; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
run_job(job, 12, status);
if (status[2:1] !== 2'b01) `uvm_error("STATUS", $sformatf("expected DONE only, STATUS=%08h", status))
#200ns;
`uvm_info("BP", $sformatf("the slave stalled the DMA for %0d clocks", sink.sink_drv.stall_clocks), UVM_LOW)
phase.drop_objection(this);
endtask
endclassset_inst_override(axis_sink_agent::get_type(), "env.out_agt", this): the third argument is the context. The path is taken relative to it, so the full path is uvm_test_top.env.out_agt. Leave this out and the string is treated as an absolute path, which matches nothing, silently.
In run_phase the test casts env.out_agt to axis_sink_agent to reach sink_sqr. The env's handle is still of type axis_agent; the object behind it is the derived type. A failed cast here is the cheapest proof that the override missed.
The responder sequence runs in a fork ... join_none for the whole test, answering every beat. Reactive (slave) sequences explains it.
Test | Override | Effect |
|---|---|---|
|
| Every stream driver forces TLAST early. Only |
|
| The scoreboard holds the run phase open until every owed word is out. |
| both drivers → | Three components become reset-aware at once. |
|
| The env itself is replaced by one that adds an interrupt agent. |
The truncate row shows a quiet strength of type overrides: "every axis_driver" turned out to mean one driver, because the passive output agent builds none. If a later change made out_agt active, the override would hit it too. That is the moment to switch to an instance override.
Instance overrides are checked before type overrides. A matching instance override wins for that path; everything else falls through to the type overrides.
Type overrides replace. A second type override for the same requested type replaces the first (the replace argument defaults to 1), so the last one registered wins.
Instance overrides do not replace. They are kept in registration order, and the first one that matches wins. Register specific paths before wildcard paths.
A derived class needs its own registration macro. Without it, the class inherits its parent's type_id, and asking for it builds the parent, with no error.
To see what the factory holds, call uvm_factory::get().print() at end of elaboration. There is no +UVM_FACTORY_TRACE plusarg. For a one-off run, +uvm_set_type_override and +uvm_set_inst_override do the same job from the command line; Command-line overrides has their exact forms.
Every override test on the bench registers its override and then calls super.build_phase. The rule behind that habit is narrower than it looks. An override must be registered before the create call it should affect. The base test's build_phase creates exactly one thing, the env. Everything inside the env is created later, in the env's own build_phase, which runs after the test's build_phase has returned, because build runs top-down.
For dma_irq_agent_test, which replaces the env itself, the order is required. Registered after super.build_phase, the env is already a plain dma_env.
For the driver, scoreboard and agent overrides, the order is a habit: they would still work if registered after super.build_phase, inside the test's build_phase. Keep the habit anyway: it makes every override test look the same, and it stays correct if someone later overrides the env too.
Registering in connect_phase or later is always too late: every component already exists.
The topology of dma_backpressure_test, port rows trimmed, shows exactly one swapped agent:
uvm_test_top dma_backpressure_test - @180
env dma_env - @250
apb apb_agent - @264
in_agt axis_agent - @272
drv axis_driver - @644
mon axis_monitor - @504
sqr uvm_sequencer - @521
out_agt axis_sink_agent - @280
mon axis_monitor - @682
sink_drv axis_sink_driver - @822
sink_sqr uvm_sequencer - @699
sb dma_scoreboard - @288
...
UVM_INFO dma_tb.sv(1246) @ 1005000: uvm_test_top [BP] the slave stalled the DMA for 28 clocks
UVM_INFO dma_tb.sv(411) @ 1005000: uvm_test_top.env.sb [SB] 12 words matched, 0 mismatchedout_agt is an axis_sink_agent with a monitor, a sink driver and a sink sequencer, and no ordinary driver: the env still marks out_agt passive, so the inherited axis_agent build skips the master side, while axis_sink_agent always builds its slave side. in_agt is untouched. The stall count changes with the seed; the 12 matched words must not.
This one happened while building the bench. dma_drain_test overrode the scoreboard with dma_scoreboard_eot, which extends write_apb and write_out to count owed words. The topology printed dma_scoreboard_eot, so the override had clearly hit. Yet the new code never ran: the owed-word count stayed at zero and the test ended early.
The cause was not the factory. The analysis imps that uvm_analysis_imp_decl creates hold a handle of the base scoreboard type and call write_out through it. In the first version of dma_scoreboard, write_out was not virtual, so the call bound to the base version at compile time. The fix was one keyword on each write_* method of the base class. The lesson for interviews: a factory override replaces the object, but whether your new method runs still depends on virtual dispatch.
axis_agent: first "env.*" to agent_a, then "env.out_agt" to agent_b, both with context this. What is out_agt?agent_a. Instance overrides are searched in registration order and the first match wins; env.* matches uvm_test_top.env.out_agt and was registered first. To get agent_b there, register the specific path first.
dma_irq_agent_test to after super.build_phase(phase). What happens at run time?The env is created by the base test as a plain dma_env, so there is no interrupt agent and nothing ever triggers the dma_irq event. The test raises its objection and waits in irq_ev.wait_trigger() forever. With no timeout set, the run burns its slot until the default 9200 s limit; the topology print, showing env as dma_env, is the clue.
dma_drain_test (scoreboard override after super.build_phase) changes nothing. Why the difference?The scoreboard is created inside the env's build_phase, which runs after the test's build_phase returns. The override is registered by then, so the factory still applies it. Only objects created by the test's own build_phase, here the env, are affected by the order of those two lines.
Next: every test on the bench ends itself, in its own run_phase, and six of them end with a fixed delay. Shared run_phase asks whether that policy belongs in one place.