While the course bench was being built, a test swapped in an end-of-test scoreboard with a factory override. The topology table printed at the start of the run showed dma_scoreboard_eot in the env, exactly as intended. Yet its extended write_apb and write_out never ran: no words were ever counted as owed, and the end-of-test hold never happened. The override had worked, and the new code was dead.
This lesson is about code that runs at the wrong moment or not at all, although everything compiles and the schedule did exactly what it promised. There are three families: things that happen too late for the phase that needed them, methods that never get called, and races inside a single time step.
Think of a relay race. The rules are fixed: runners go in order and hand over the baton in the exchange zone. When a relay goes wrong it is almost never the rules; it is a runner who started too early, a baton handed to someone not on the team, or two runners who each thought the other had it. UVM's phase schedule is the rulebook: build runs top-down, connect bottom-up, run in parallel, then the post-run phases. Ordering bugs come from code that assumes something the rules never promised.
Action | Must happen before | Too late if done in | Symptom |
|---|---|---|---|
Factory override for a component | The | After the parent created it (for the env: after | Original type built; override silently unused |
config_db set for a component | That component's build_phase calls | connect_phase or later |
|
| Runtime phases start | run_phase | No effect on the current run |
Raising an objection | The first blocking statement in the phase | After any wait | Phase ends at time 0 |
The bench's dma_truncate_test shows the correct order. The override is registered first, then super.build_phase() runs the base test's build, which creates the env, whose build creates the driver. Because build is top-down, everything below the test is created after the override exists.
// 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 precise rule is that an override must exist before the create call for that component runs. Here the driver is created two levels down, in the agent's build_phase, which runs after the test's build_phase returns, so even swapping the two lines would still work. It would not work for the env itself, which super.build_phase() creates. Override the env type after that call and the original env runs, nothing is truncated, and the test fails with ERR not set after a short packet; a test with a weaker check would simply pass. Registering first is the habit that is right for every level.
Do not skip super.build_phase() either. The base uvm_component::build_phase applies field-macro configuration (apply_config_settings), and uvm_agent::build_phase reads is_active that way. Leaving it out does not stop children from being built by your own code, but it silently loses that configuration.
function void build_phsae(uvm_phase phase) compiles: it is simply a new method that nobody calls. Only the name typo is silent. A wrong argument type, or a function where the base declares a task (function void run_phase is a compile error, because run_phase is a task), fails at compile time because a virtual override must match the base declaration.
This was the opening bug. The analysis imp that uvm_analysis_imp_decl(_out) creates holds a handle of the base scoreboard type and calls write_out through it. In the first version of the bench, the base class's write_out was not virtual, so the call always went to the base version, whatever object the factory had built. The fix, now in the bench, is one keyword:
// snippet
// In dma_scoreboard: virtual, so a derived scoreboard's version is the one called.
virtual function void write_out(axis_item t);A factory override replaces the object. Whether your new code runs still depends on virtual dispatch. Any method meant to be extended by an override (analysis writes, driver hooks such as the bench's pre_drive) must be virtual in the base class.
Phases order whole stages. Inside a stage, threads that wake on the same clock edge run in an order SystemVerilog does not define. The bench met this too. The APB driver finished the GO write on a clock edge; in the same time step the stream driver took its first item and waited on the stream interface's own clocking block. Two clocking blocks on the same edge fire in an undefined order, so the stream driver sometimes caught the current edge, "saw" TREADY on a beat the DUT never took, and each packet arrived one word short. The DMA reported ERR on 2 of 3 random jobs.
The fix is the rule the bench now follows everywhere: drivers wait on @(posedge clk), one shared event, and drive with nonblocking assignments; monitors sample through a clocking block with #1step input skew. Know which event each thread waits on, and keep every thread that hands work to another on the same event.
Switch or call | Shows | Use it for |
|---|---|---|
| Every phase start and end, per domain | Which phase never ended; jumps; domain drift |
| Every raise and drop at every level | Who holds a phase; late or missing raises |
| Every config_db set and get, with paths | A get that fails or reads the wrong value |
| The built tree with each component's type | Whether an override built the right type |
| Registered overrides | Whether an override was registered at all |
There is no +UVM_FACTORY_TRACE; use the factory's print() and debug_create_by_type(). The bench's base test calls print_topology() once, in its end_of_elaboration_phase. Print the tree in one place, the test, so a bench with ten agents does not print it ten times.
A team's agent configuration came from an env-level helper that ran in connect_phase, "once everything exists". Every agent's config_db::get in build_phase failed and fell back to defaults, so all agents were active, including the one that should have been passive and now fought the DUT for a bus. +UVM_CONFIG_DB_TRACE showed every get logged before every set. Build is top-down and finishes for the whole tree before connect starts, so a set in connect is always too late for a get in build. Moving the sets into the env's build_phase, before creating the agents, fixed it.
build_phsae compiles cleanly but its log line never prints. What happened, and how would you have caught it sooner?The misspelling defines a new method; the scheduler calls build_phase, which is the inherited base version. An entry log at UVM_HIGH in each phase override catches it on the first run, as does a lint rule for methods ending in _phase that override nothing.
set_type_override in connect_phase. Which components get the new type?None that already exist. All components are created during build, which has finished for the whole tree before connect starts. Only objects created later by the factory, such as sequence items, would pick it up.
Different clocking blocks on the same edge trigger in an undefined order within the time step, so a thread that starts waiting in that step may catch the current edge instead of the next. Have drivers wait on one shared event, @(posedge clk), drive with nonblocking assignments, and keep clocking blocks for monitor sampling.
Next: the most common debugging job of all is the test that never ends. Objection triage and hang forensics is the playbook.