A regression fails because an AXI agent never issues back-to-back bursts. In the triage meeting someone says "UVM doesn't support that", and the room nods. It is wrong, and an interviewer will catch it in one follow-up: UVM knows nothing about AXI bursts at all. Burst behaviour lives in the agent, which is project code. Where exactly is the line between what UVM gives you and what you are responsible for?
UVM supplies mechanism: machinery that works the same way in every testbench, such as when components are built, how a class gets swapped, or how a transaction reaches three listeners. Your project supplies policy: what your protocol looks like, what correct behaviour is, and which scenarios matter. A third slice belongs to the tool: compiling, running seeds, dumping waves and merging coverage.
A building is a good analogy. The building code (UVM) says how wiring, plumbing and fire exits must work, so any electrician can work in any building. It does not say what rooms you need or what you put in them; that is the architect's job (yours). And the construction crew's machines (the simulator) are not in the building code at all. Knowing the boundary keeps blame in the right place, which is exactly what the triage meeting got wrong.
UVM gives you (mechanism) | You supply (policy) | The tool owns |
|---|---|---|
Phases and objections: one lifecycle, one way to end a test | Items and sequences for your protocol | Compilation and elaboration |
The factory: register a type, swap it at create time | Drivers and monitors that know your pins and timing | Choosing seeds, running regressions |
config_db and resources: settings and handles by path | Scoreboards, predictors and reference models | Wave dumping and debug views |
TLM ports, exports, analysis ports and FIFOs | Covergroups and the coverage plan | Coverage collection and merging |
The sequence, sequencer and driver handshake | Tests that compose scenarios and overrides | Licences, compute farms and runtime |
Reporting: severity, verbosity, IDs, actions | Which messages matter and how loud they are | |
The register layer base classes | The register model of your design | |
Callbacks, events, barriers, pools; printer, comparer, packer policies | When to use them |
Read the middle column as the test of any "UVM can't do X" claim. If X is about a protocol, a check or a scenario, it is yours. If X is about lifecycle, substitution, connection or messaging, UVM has a mechanism for it, and the question becomes whether your code uses it correctly.
The cleanest place to see the split is the stream driver. The handshake with the sequencer (get_next_item, item_done) is pure mechanism: every driver in every UVM bench uses the same two calls. Everything between them is policy: TVALID, TREADY, idle cycles, the clock edge to drive on.
// snippet: the axis_driver run loop, annotated
task run_phase(uvm_phase phase);
vif.tvalid <= 1'b0; // policy: reset value of this protocol
@(posedge vif.rst_n);
forever begin
axis_item beat;
seq_item_port.get_next_item(req); // MECHANISM: the standard handshake
beat = pre_drive(req); // policy hook (a project choice)
if (beat != null) begin
repeat (beat.idle) @(posedge vif.clk); // policy: idle cycles between beats
@(posedge vif.clk);
vif.tdata <= beat.data; // policy: AXI-Stream signalling
vif.tlast <= beat.last;
vif.tvalid <= 1'b1;
do @(posedge vif.clk); while (!vif.tready);
vif.tvalid <= 1'b0;
end
seq_item_port.item_done(); // MECHANISM: tell the sequence we are done
end
endtaskIf the DMA ever needed back-to-back beats with no bubble between them, nothing in UVM would stand in the way. The change would be in the policy lines: keep TVALID high and present the next beat in the same cycle the current one is accepted. That is the triage meeting's bug, located.
One piece of UVM machinery is invisible until it saves you. Every component reseeds its random generator in its constructor from the simulation seed, its type name and its full path, and every sequence and item is reseeded the same way when it is started on a sequencer (its full path includes the sequencer's path). So a random stream depends on who owns it, not on how many other objects were created before it. Add a new agent to the env, and the stream items' random choices stay the same; rename the agent the stream sequencer lives in, and they change. That is why a failing seed usually still fails after an unrelated edit, and why renaming components in a "harmless" refactor can make a hard-won failing seed stop reproducing.
At the start of every run, the base test prints the component tree with print_topology(). This is the DMA bench's tree from the course's run, trimmed to the components:
Name Type Size Value
----------------------------------------------------------------
uvm_test_top dma_smoke_test - @172
env dma_env - @242
apb apb_agent - @256
drv apb_driver - @458
mon apb_monitor - @318
sqr uvm_sequencer - @335
in_agt axis_agent - @264
drv axis_driver - @636
mon axis_monitor - @496
sqr uvm_sequencer - @513
out_agt axis_agent - @272
mon axis_monitor - @674
sb dma_scoreboard - @280Only one class here is UVM's own: uvm_sequencer, used as is through a typedef. Every other type is project policy built on a UVM base class.
The tree itself is mechanism: the names, the parent-child paths and the build order come from UVM, which is why config_db paths such as uvm_test_top.env.apb.* work.
What is missing is the tool's slice: nothing here says which seed ran, where waves went or how coverage merges. Those are simulator and regression-script jobs.
Here is a version of this story that plays out on real teams. Someone spends two weeks on a "UVM bug": a checker that reads a counter kept by the coverage collector starts failing after a harmless-looking rename. The checker had silently relied on the collector receiving each transaction before it did, because both hung off the same analysis port and the collector happened to be called first. The rename moved the checker earlier in the alphabet. An analysis port calls its subscribers in the order of their full names, because its subscriber list is keyed by name, and no UVM document promises any order at all.
UVM was working as designed; the policy ("coverage must see it first") was the team's, and so was the bug. The fix was to remove the dependency: the scoreboard and the collector each processed transactions independently, with no assumption about who heard first. The general rule is the lesson of this page: when a mechanism surprises you, check what it actually promises before calling it broken.
+BURST_MODE command-line switch and asks whether UVM provides one. What do you tell them?UVM provides the mechanism to read plusargs (uvm_cmdline_processor) and to distribute settings (config_db), but a switch called +BURST_MODE and what it means is policy. They define it, read it in the test or env, and pass the result down through a config object.
env.in_agt to env.src_agt, a seed that used to fail now passes. No functional code changed. What happened?The sequencer under the renamed agent has a new full path, so the stream items started on it were reseeded differently, and their random data and idle cycles changed. The bug is still there; the seed no longer hits it. Rerun with more seeds, and record failing scenarios as directed tests rather than relying on seed numbers across refactors.
The simulator and its tools. Your covergroups define what is measured; UVM provides no coverage database. Merging per-run databases into one closure report is a tool job, which is why coverage flows differ between vendors while the covergroup code does not.
Next: the topology above had only two kinds of thing in it, and that split runs through every class you will write. Components vs objects explains the fork.