Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
    • Callbacks, Command Line, Seeds and Versions
    • Synchronisation and Reporting
    • Testbenches at Scale
      • Testbench Code Quality
      • Stimulus at Scale
      • VIP and IP Reuse at Scale
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Testbenches at Scale
  1. Topics
  2. UVM Verification
  3. Advanced UVM in Practice
  4. Testbenches at Scale
  5. Testbench Code Quality

Part 10 · Advanced Topics · Intermediate

Testbench Code Quality

The UVM coding rules that prevent real bugs, a review checklist per component, what UVM linters check and miss, and the bench's own bugs as evidence.

On this page

    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.

    The idea: a pilot's checklist is a list of old crashes

    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.

    The picture: where each bench bug could have been caught

    Flowchart mapping the bench's war stories to the earliest layer that catches them: keyword bin names to the compiler, a non-virtual write method to UVM lint, two clocking blocks to a review checklist, and a model that ignores a DUT rule to review against the spec, with regressions as the late backstop.
    The earliest layer that can catch each of the bench's four bugs. Only one is a compile error; one is a lint rule; two need a person who knows the timing or the spec.

    Build it: the rules that matter

    Structure and construction

    • 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.

    Extension and configuration

    • 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.

    Timing, cost and messages

    • 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).

    A review checklist per component

    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 item_done called on every path, including reset and error? Does it drive with nonblocking assignments?

    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 write_* virtual? Does it fail when nothing was compared, and when work is left over?

    Sequence

    Is every randomize() checked? Does it rely on starting_phase (deprecated) or on a fixed sequencer path?

    Config and test

    Is every get checked? Are overrides set before the env is built? Does the test set its own timeout? Are waivers narrow, ticketed and counted?

    What UVM linters check

    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, new() on a factory class, missing super.build_phase()

    -

    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 config_db::get, set and get types that differ, unmatched paths

    -

    Reporting

    missing or duplicated IDs, uvm_warning used for a check

    -

    Performance

    field macros in items, string formatting outside the report macros, config_db::get in run-time loops

    -

    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.

    Watch it fail: the bench's evidence

    Diagram
    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 effect

    The 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.

    What breaks: the override that did nothing

    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:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // 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
    endclass

    The fix was one keyword per method. The bench's scoreboard now declares all three write_* methods virtual; the first two:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // 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);
    endfunction

    The 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.

    Interview

    "What do you look for when you review someone's UVM code?"

    A weak answer: "That it follows the coding guidelines and has comments." Which guidelines, and why those?

    A strong answer: "Things that compile and are still wrong. Factory use: create, not new, and every class registered with its constructor. Virtual extension points, because an override replaces the object but not non-virtual calls. Timing: which event each driver thread waits on, item_done on every path. Checking: the model against the spec for when each input takes effect, an error when nothing was compared, mismatches as uvm_error with stable IDs. Config: checked gets, one config object. Reuse: no hierarchical references, no global side effects. A UVM linter takes the mechanical half; I spend review time on timing and spec fidelity, which no tool checks."

    Expect the follow-ups: Give an example of legal code that is wrong. (a non-virtual write_out in a class that gets overridden) and Why not field macros? (slow generic automation in hot paths; explicit do_* methods are faster and say exactly what is compared).

    Check yourself A colleague says "we have a UVM linter, so we can skip the model review". Which of the bench's stories disproves that?

    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.

    Check yourself A test overrides 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.

    Check yourself Why does the checklist ask a driver reviewer which event each thread waits on, when the code compiles and passes most seeds?

    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.

    Related topics

    • Up: Testbenches at Scale

    • The type_id::create pattern

    • Field macros and automation

    • Performance and memory discipline

    Previous

    179 / 280

    Next: Stimulus at Scale

    Up next

    Up nextStimulus at Scale

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • VIP and IP Reuse at Scale