A regression of 400 tests has one that, on a rarely used configuration, builds two scoreboards instead of one. Both see every transaction, both match, the test passes, and the coverage numbers are double-counted for a month. Nothing was wrong at run time. The tree was wrong at time 0, and nobody looked at it there.
Between the last connect and the first clock edge there is a moment when the whole testbench exists and nothing has happened yet. It is the cheapest moment to find a structural mistake: no waveform, no transaction, just the tree. UVM gives that moment two phases. end_of_elaboration_phase is the building inspection: is everything that should be here, here, and nothing else? start_of_simulation_phase is the announcement as the doors open: this is the test, this is the seed, this is the configuration. Both are functions, both run at time 0, and both walk the tree bottom-up.
The diamond is the rule worth remembering: any `UVM_ERROR` reported before end_of_elaboration ends the test at time 0 with BUILDERR. A missing config, a binding error, a raise in build: none of them gets a chance to run traffic and confuse the picture.
// snippet: from the course DMA bench
class dma_base_test extends uvm_test;
`uvm_component_utils(dma_base_test)
dma_env env;
virtual irq_if irq_vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = dma_env::type_id::create("env", this);
if (!uvm_config_db#(virtual irq_if)::get(this, "", "irq_vif", irq_vif))
`uvm_fatal("NOVIF", "irq_if not set for the test")
endfunction
function void end_of_elaboration_phase(uvm_phase phase);
uvm_root::get().print_topology();
endfunction
// Register helpers used by every test.
task reg_write(bit [7:0] addr, bit [31:0] data);
apb_rw_seq s = apb_rw_seq::type_id::create("s");
s.addr = addr; s.write = 1; s.data = data;
s.start(env.apb.sqr);
endtask
task reg_read(bit [7:0] addr, output bit [31:0] data);
apb_rw_seq s = apb_rw_seq::type_id::create("s");
s.addr = addr; s.write = 0; s.data = 0;
s.start(env.apb.sqr);
data = s.rdata;
endtask
// One complete job: program the DMA, stream n words, wait for the interrupt,
// return STATUS and clear its DONE/ERR bits.
task run_job(dma_job_cfg job, int unsigned n_words, output bit [31:0] status);
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
reg_write(KEY, job.key);
reg_write(LEN, job.len);
reg_write(CTRL, {30'b0, job.irq_en, 1'b1});
if (!pkt.randomize() with { n == n_words; })
`uvm_fatal("RAND", "axis_pkt_seq randomize failed")
pkt.start(env.in_agt.sqr);
wait (irq_vif.irq === 1'b1);
reg_read(STATUS, status);
reg_write(STATUS, 32'h6); // write 1 to clear DONE and ERR
endtask
endclassThe bench's base test prints the topology in its end_of_elaboration_phase. Two details are worth knowing. The bench calls uvm_root::get().print_topology(); older code writes uvm_top, the global handle for the root, which IEEE 1800.2 deprecates, so uvm_root::get() is the form to copy. And instead of overriding the phase just to print, a test can set uvm_root::get().enable_print_topology = 1 in build, and the library prints it at the step shown in the diagram.
A real audit checks what the configuration promised against what was built. Errors here are cheap and precise, because no simulated time has passed:
// snippet
function void end_of_elaboration_phase(uvm_phase phase);
dma_scoreboard sbs[$];
uvm_component found[$];
super.end_of_elaboration_phase(phase);
uvm_root::get().find_all("uvm_test_top.env.*", found);
foreach (found[i]) begin
dma_scoreboard s;
if ($cast(s, found[i])) sbs.push_back(s);
end
if (sbs.size() != 1)
`uvm_error("ELAB", $sformatf("expected 1 scoreboard, built %0d", sbs.size()))
if (env.out_agt.get_is_active() != UVM_PASSIVE)
`uvm_error("ELAB", "out_agt must be passive: the DUT drives that port")
check_config_usage(); // report config_db settings nobody read
endfunctionErrors reported from end_of_elaboration do not trigger BUILDERR (that check has already run), so the test continues and the error is counted in the final summary. Use uvm_fatal when continuing makes no sense, such as a null interface handle.
Choose the severity by what continuing would cost:
Finding | Severity | Why |
|---|---|---|
Topology printed, configuration echoed |
| Documentation for triage, not a problem. |
Passive agent with a driver, unused config setting |
| Runnable but wrong; an error makes regressions notice. |
Scoreboard count or agent mode differs from the configuration |
| The test would run and its results would mean something else. |
Null virtual interface, missing mandatory component |
| The first access would crash anyway; stop with a clear message. |
Because end_of_elaboration is bottom-up, each agent can audit itself before the env aggregates: the agent checks its own mode and handles, the env checks counts across agents, and the test checks the configuration as a whole. A finding is then reported by the component that owns it, which makes the message point at the right file.
start_of_simulation is the last moment before time moves, and the right place for a banner a triage engineer will read first: the test name, the seed, and any configuration chosen randomly in build. Keep it to printing and arming. Anything that waits, drives a pin or starts a sequence is a task and belongs in run_phase.
// snippet
function void start_of_simulation_phase(uvm_phase phase);
string seed;
super.start_of_simulation_phase(phase);
// The regression script passes the seed to the simulator and also as +SEED=...
if (!$value$plusargs("SEED=%s", seed)) seed = "see simulator log";
`uvm_info("BANNER", $sformatf("TEST=%s SEED=%s", get_type_name(), seed), UVM_NONE)
endfunctionstart_of_simulation is also bottom-up, so a test-level banner prints after any banners its children print, and it is the last line before traffic. That makes it the natural place for the one line a triage engineer copies into a bug report: test, seed, and the handful of randomized choices that make this run different from the last.
Seed reporting is simulator-specific: each tool has its own seed switch and prints the seed it used in its own way. $get_initial_random_seed is a vendor extension, not standard SystemVerilog, so a portable bench echoes a plusarg its regression script sets.
The smoke test's topology appears at time 0, before any traffic. Reading it is a habit worth building: look for the type you expect for every component you configured or overrode, and for components that should not be there.
UVM_INFO uvm_root.svh(531) @ 0: reporter [UVMTOP] UVM testbench topology:
...
out_agt axis_agent - @272
mon axis_monitor - @674
sb dma_scoreboard - @280A test hangs with the simulator's time stuck at 0. A colleague says "something in end_of_elaboration must be waiting". It cannot be: a function cannot contain a wait, and the compiler would have refused it. A hang at time 0 is almost always a zero-delay loop in a task phase: a forever in a run_phase with no blocking statement on some path, a while (!flag) that never yields, or two threads waking each other through events in the same time step.
In this case it was a monitor's forever begin if (vif.valid) ... end with the clock wait inside the if. Before reset, valid was X, the if never entered, and the loop spun forever at time 0. Moving @(vif.mon_cb) to the top of the loop, as both bench monitors do, fixed it. When time does not advance, look for the loop that does not block, not for a phase that waits.
check_config_usage() deserves a place in every base test's audit. It lists every config_db setting that no component ever read. Most entries are harmless leftovers, but a misspelled field name or a path that matches nothing shows up there at time 0, long before anyone wonders why a knob had no effect.
Back to the opening story: the double scoreboard was found by adding the scoreboard count check above, and the cause was a configuration flag that created a second scoreboard "for the checker team". The audit now fails that configuration at time 0.
config_db get fails in an agent's build and reports uvm_error, not uvm_fatal. What happens to the test?It stops at time 0 with BUILDERR as end_of_elaboration starts, because an error was reported before that phase. Using uvm_error is enough to stop the test; uvm_fatal stops it immediately and names the line.
uvm_error("ELAB", ...). Does the test stop?No: the BUILDERR check runs before your end_of_elaboration methods. The error is counted, so the test fails in the summary, but traffic still runs. Use uvm_fatal if running makes no sense.
No. start() is a task and start_of_simulation_phase is a function, so it will not compile. Sequences start in run_phase or a runtime phase.
Next: time is about to move. The Run Phase: Parallel Execution and Duration shows who decides how long it moves for.