While the course DMA bench was being built, four bugs cost real debugging time. A stream beat was lost because two drivers waited on different clocking blocks on the same edge. A factory-overridden scoreboard was built but its new methods never ran. A coverage model would not compile because two bins were named after SystemVerilog keywords. And the reference model took a new KEY at once, although the DMA ignores KEY writes while busy. Each of them is now a line on a review checklist.
That is what coding guidelines are: compressed incident reports. This lesson collects the UVM rules that prevent real bugs, says which ones a tool can check and which need a human, and shows the bench's own failures as the evidence.
Every item on an aviation checklist exists because someone once forgot it. Nobody follows the checklist because they enjoy it; they follow it because the item that seems pointless today is the one that caused a crash ten years ago. UVM coding guidelines work the same way. "Make extension points virtual" looks like style until a factory override silently runs the base method. "Never use new for an overridable class" looks like ceremony until a test's override does nothing.
Rules are enforced in three layers. The compiler catches what is illegal. A linter, a static checker that knows UVM, catches what is legal but known to be wrong. Review catches what only a person who knows the design can see. Regressions catch the rest, late and expensively.
Register every class and give it the standard constructor. uvm_component_utils with new(string name, uvm_component parent), uvm_object_utils with new(string name = "..."). A derived class without its own macro inherits its parent's type_id, so child::type_id::create() silently builds the parent.
Create with `type_id::create()`, never `new()`, for anything a test might override: components, items, sequences, config objects. new() is legal and bypasses the factory without a word.
Keep constructors trivial. super.new() first, then ports and analysis exports; nothing that reads configuration or creates children. A scratch run for this course was rejected by Verilator because a declaration with an initializer (json_report_server srv = new();) came before super.new() in a constructor; IEEE 1800-2023 requires super.new to be the first statement, and tools differ in how strictly they enforce it, so code like this can compile on one simulator and fail on the next.
No hierarchical references from classes into the design (top.dut.u_ctrl.state). They tie a class to one hierarchy and break when the block moves one level deeper at SoC. Reach signals through a virtual interface, and internal signals through an interface bound into the module, as the bench's dma_bd_if backdoor does.
Make every extension point virtual: analysis write_* methods, driver hooks, predictor functions. A factory override replaces the object; which method runs is still decided by virtual dispatch (story 2 below).
One config object per agent or env, fetched once in build_phase, with the result of every config_db::get checked and a missing required value reported as uvm_fatal. Scattered per-field gets are slow, untyped across the hierarchy and impossible to document.
No global side effects from reusable code. A VIP or env must not install a report server, set the global timeout, or change verbosity on uvm_root; those belong to the test.
No `#` delays in drivers and monitors. Wait on interface clock events; protocol timing comes from the pins. Delays belong in tests, sized from configuration, or in watchdog polling loops where the delay is the point. Even the bench's tests carry a fixed #100ns drain for the output pipeline, which a stricter review would replace with a scoreboard-owned end of test.
Threads that hand work to each other wait on the same event. Drivers on @(posedge clk) with nonblocking drives; monitors sample through a clocking block with #1step input skew (story 1).
No field macros in hot classes. uvm_field_* automation runs generic, interpreted code on every copy, compare and print. For per-beat items write do_copy, do_compare and convert2string. And write do_compare at all: the bench's axis_item had none, so a comparator calling compare() reported 8 matches for 8 wrong predictions (Built-in comparators).
Stable, specific message IDs, one per check, and `uvm_error` for every check failure. Waivers, verbosity control and regression bucketing all key off IDs; a mismatch reported with uvm_warning never fails a test.
Identifiers are not keywords in any language version you support. small, large, matches and program are all SystemVerilog keywords (story 5).
Component | Questions a reviewer asks |
|---|---|
Driver | Which event does each thread wait on, and is it the same event as the thread it hands off to? Is |
Monitor | Does it sample through a clocking block? Does it create a new item per transaction? Does it raise objections (it should not)? Does it end or abandon recordings and partial transfers on reset? |
Scoreboard and model | For every input the model consumes, does the spec say when it takes effect? Are |
Sequence | Is every |
Config and test | Is every |
Commercial UVM-aware linters, sold by the major EDA vendors and by independent lint vendors, parse the testbench with the UVM library and apply rule sets. Rule names differ per tool, but the classes are consistent:
Rule class | Typical rules | Bench bug it would have caught |
|---|---|---|
Methodology | missing registration macro or constructor, | - |
Inheritance | a method redefined in a derived class but not virtual in the base | Story 2 |
Phasing | objection raised in a function phase, delays in build or connect, objections in monitors | - |
Configuration | unchecked | - |
Reporting | missing or duplicated IDs, | - |
Performance | field macros in items, string formatting outside the report macros, | - |
Language | keywords as identifiers, version-specific constructs, race-prone constructs | Story 5 (and the compiler catches it anyway) |
Free tools cover part of this. A Verilator lint pass (--lint-only) catches width mismatches, the constructor-ordering rule above and many portability problems, and this course compile-checks every complete listing it prints. What no linter catches is meaning: whether a model matches the spec (story 9), or whether two threads on one edge will race in the simulator you use (story 1). Those need the checklist and a reviewer who knows the design.
Story 1 symptom: a random test failed on 2 of 3 jobs; each packet one word short, STATUS.ERR set
cause: stream driver on its own clocking block, same edge as the APB driver's
rule: hand-off threads wait on one event; drivers on @(posedge clk) with NBA
Story 2 symptom: overridden scoreboard built (topology showed its type), its write_out never ran
cause: base write_out not virtual; the analysis imp calls through a base handle
rule: every extension point is virtual
Story 5 symptom: coverage model does not compile
cause: bins named small and large (charge-strength keywords)
rule: no keywords as identifiers in any supported language version
Story 9 symptom: KEY written while busy gives 5 mismatches in both checkers
cause: model takes KEY at once; the DUT ignores it while BUSY
rule: for each input the model consumes, check when the spec says it takes effectThe full stories live with their topics: monitor sampling races for story 1, analysis broadcast for story 2, bins for story 5 and predictor design for story 9. Story 9 is still in the bench as a known limitation; the waiver lesson uses it as ticket DMA-142.
Story 2 is the one every UVM engineer meets eventually. dma_drain_test overrides the scoreboard with dma_scoreboard_eot, which extends write_apb and write_out to count owed words. The topology printout showed the derived type, so the override had worked, yet the extended methods never ran. The analysis imp that uvm_analysis_imp_decl(_out) creates holds a handle of the base scoreboard type and calls write_out through it; with a non-virtual base method, that call binds to the base version at compile time. Nothing warned:
// bad-example: what the base scoreboard first looked like
class dma_scoreboard extends uvm_scoreboard;
// ...
function void write_out(axis_item t); // not virtual: overrides never run
// compare
endfunction
endclassThe fix was one keyword per method. The bench's scoreboard now declares all three write_* methods virtual; the first two:
// snippet: from dma_scoreboard in the course DMA bench (dma_tb.sv)
virtual function void write_apb(apb_item t);
if (t.write && !t.slverr && t.addr == KEY) key = t.data;
endfunction
virtual function void write_in(axis_item t);
expected.push_back(t.data ^ key);
endfunctionThe lasting fix is the rule and the tool: "every method a derived class may redefine is virtual" on the checklist, and the linter's inheritance rule switched on, so the next instance is a lint error, not a lost afternoon.
Story 9. The model's code was clean; it was simply wrong about the DUT: it took a KEY write at once although the DMA ignores KEY while busy. Only a review against the spec, or a directed test of that case, finds it.
apb_item with apb_err_item, but one sequence still sends plain apb_items. What is the most likely cause in that sequence?It constructs items with apb_item::new() (or req = new()) instead of apb_item::type_id::create(). new() bypasses the factory, so the override never applies to those items.
Because two threads waking on different events at the same edge (two clocking blocks, for example) run in an order the standard leaves undefined. It works on most seeds and one simulator, and loses a beat on others, which is exactly story 1.
Next: clean code is the floor. The next problem of scale is stimulus: how to cover combinations of configurations and scenarios that nobody will write by hand. Stimulus at Scale.