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
    • UVM Factory: Registration, Overrides and Debug
      • The Factory: Swapping Classes Without Editing the Env
      • Registration Macros: What uvm_component_utils Writes for You
      • The type_id::create Pattern
      • Override Resolution: Who Wins
      • Override Timing, Safety and Debug
      • Factory Advanced: Parameterized, Abstract and By-Name
      • Command-Line Overrides
      • Overriding Items and Sequences
    • config_db and Resources
  • 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
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹UVM Factory: Registration, Overrides and Debug
  1. Topics
  2. UVM Verification
  3. Factory & Configuration
  4. UVM Factory: Registration, Overrides and Debug
  5. Override Timing, Safety and Debug

Part 3 · Factory & Configuration · Intermediate

Override Timing, Safety and Debug

When an override must be registered, what makes a derived class safe to substitute, the bench bug where an overridden scoreboard's methods never ran, and the factory debug toolkit.

On this page

    While the DMA bench was being built, dma_drain_test overrode the scoreboard with dma_scoreboard_eot, a subclass that counts the words each job owes and holds the run phase open until they arrive. The topology printed sb dma_scoreboard_eot. The override had worked. And yet the new write_apb and write_out never ran: the owed count stayed at zero, and the run phase ended with a word still inside the DMA. The factory did its job perfectly. So what went wrong?

    The idea: an override replaces the object, not the calls

    Swapping a worker is only half of the job; the people around them must also talk to the new worker. A factory override changes which class is constructed. It does not change how the rest of the testbench calls that object. Every caller holds a handle of the base type (the agent's axis_driver drv, the env's dma_scoreboard sb, an analysis imp's internal handle). A call through a base handle reaches the derived code only if the method is virtual. If it is not, the base method runs, every time, no matter what was built.

    So a safe override needs three things: it is registered before the create runs; the substitute derives from the requested type; and every method it changes is reached through virtual dispatch (a virtual method, or a virtual hook the base class calls).

    The picture

    Sequence diagram: the monitor writes to the imp, the imp calls write_out through a base-type handle; without virtual the base method runs, with virtual the derived method runs.
    War story 2 from the bench. The analysis imp created by uvm_analysis_imp_decl stores a handle of the base scoreboard type and calls write_out through it. Only a virtual write_out reaches the derived class.

    Build it: a safe override, step by step

    Timing: register before the create runs

    Build is top-down. The test's build_phase creates the env object; the env's build_phase runs afterwards and creates the agents; each agent's build_phase runs after that and creates its driver. An override must be in place before the create it should affect. The bench puts every override first, before super.build_phase:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // snippet: from the course DMA bench
    // The test drops its objection as soon as the last input word is accepted, with
    // no settling delay. The end-of-test scoreboard keeps the run phase open until
    // the last output word arrives.
    class dma_drain_test extends dma_base_test;
      `uvm_component_utils(dma_drain_test)
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        dma_scoreboard::type_id::set_type_override(dma_scoreboard_eot::get_type());
        super.build_phase(phase);
      endfunction
    
      task run_phase(uvm_phase phase);
        axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
        phase.raise_objection(this);
        reg_write(KEY, 32'h1234_5678);
        reg_write(LEN, 6);
        reg_write(CTRL, 32'h1);                  // GO, interrupt not needed
        if (!pkt.randomize() with { n == 6; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
        pkt.start(env.in_agt.sqr);
        phase.drop_objection(this);              // the last word is still inside the DMA
      endtask
    endclass

    Strictly, an override for the scoreboard or the driver would still work one line later, after super.build_phase, because those objects are created later in the env's and agent's own build_phase. It would not work for anything the base test creates directly, such as the env itself. Putting overrides first is the habit that is right in every case, including the day someone overrides dma_env.

    Substitution: virtual hooks and virtual methods

    The stream driver was designed to be extended. Its run loop calls a virtual hook for every beat, and the base version returns the beat unchanged:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    // snippet: from the course DMA bench
    class axis_driver extends uvm_driver #(axis_item);
      `uvm_component_utils(axis_driver)
      virtual axis_if vif;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual axis_if)::get(this, "", "vif", vif))
          `uvm_fatal("NOVIF", "axis_if not set for this driver")
      endfunction
    
      task run_phase(uvm_phase phase);
        vif.tvalid <= 1'b0;
        @(posedge vif.rst_n);
        forever begin
          axis_item beat;
          seq_item_port.get_next_item(req);
          beat = pre_drive(req);
          if (beat != null) begin
            repeat (beat.idle) @(posedge vif.clk);
            @(posedge vif.clk);
            vif.tdata  <= beat.data;
            vif.tlast  <= beat.last;
            vif.tvalid <= 1'b1;
            do @(posedge vif.clk); while (!vif.tready);   // the beat moves on TVALID && TREADY
            vif.tvalid <= 1'b0;
          end
          seq_item_port.item_done();
        end
      endtask
    
      // Hook for derived drivers: return the beat to drive (the sequence's item, or a
      // modified copy), or null to drop it. The base driver drives every item as is.
      virtual function axis_item pre_drive(axis_item it);
        return it;
      endfunction
    endclass

    The truncating driver overrides only the hook. Note the clone: the item belongs to the sequence, which may still read it or reuse it, so the driver changes a copy and returns that.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // snippet: from the course DMA bench
    // Fault injection without touching the sequence: cuts every packet after beat
    // `cut` by setting TLAST there and dropping the rest of the packet.
    class axis_truncating_driver extends axis_driver;
      `uvm_component_utils(axis_truncating_driver)
      int unsigned cut = 3;           // index of the beat that gets TLAST
      int unsigned beat_no;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function axis_item pre_drive(axis_item it);
        axis_item b;
        int unsigned i = beat_no;
        beat_no = it.last ? 0 : beat_no + 1;    // count beats per packet
        if (i > cut) return null;                // after the cut: drop the beat
        if (i < cut) return it;
        $cast(b, it.clone());                    // never edit the sequence's own item
        b.last = 1'b1;
        `uvm_info("TRUNC", $sformatf("TLAST forced on beat %0d", i), UVM_MEDIUM)
        return b;
      endfunction
    endclass

    The scoreboard's write_* methods are declared virtual for the same reason, with a comment in the bench that points at this story. The derived scoreboard calls super.write_out first, so the base checking still happens, then adds its owed-word count.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // snippet: from the course DMA bench
    // A scoreboard that owns end of test. It learns how many words each job owes
    // from the observed LEN and GO register writes, and when every objection drops
    // while words are still owed, it raises its own objection until they arrive.
    // (Counting on the expected queue alone is not enough: the last input word may
    // not have been published yet when the test drops its objection.)
    class dma_scoreboard_eot extends dma_scoreboard;
      `uvm_component_utils(dma_scoreboard_eot)
      int unsigned len_reg, owed;
      event        drained;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      virtual function void write_apb(apb_item t);
        super.write_apb(t);
        if (!t.write || t.slverr) return;
        if (t.addr == LEN)                 len_reg = t.data[15:0];
        if (t.addr == CTRL && t.data[0])   owed += len_reg;   // GO: this job owes len_reg words
      endfunction
    
      virtual function void write_out(axis_item t);
        super.write_out(t);
        if (owed > 0) owed--;
        if (owed == 0) -> drained;
      endfunction
    
      function void phase_ready_to_end(uvm_phase phase);
        if (phase.get_imp() != uvm_run_phase::get() || owed == 0) return;
        phase.raise_objection(this, $sformatf("%0d word(s) still owed by the DMA", owed));
        fork begin
          @drained;
          phase.drop_objection(this, "all words out");
        end join_none
      endfunction
    endclass

    The debug toolkit

    Tool

    What it tells you

    Topology (uvm_top.print_topology() / end-of-elaboration print)

    What was actually built, with each component's type name. The first thing to read.

    uvm_factory::get().print(1)

    Registered instance and type overrides, plus user-defined types. print(0) shows overrides only; print(2) adds the UVM library's own types.

    uvm_factory::get().debug_create_by_type(type, parent_path, name)

    Runs the lookup for one path and shows every matching override and the winner. Cross-check it with print(): on the 1800.2-2020 library its debug pass can name the wrong winner (details).

    debug_create_by_name(type_name, parent_path, name)

    The same, starting from a type name.

    +UVM_CONFIG_DB_TRACE

    Config_db sets and gets. It does not trace the factory.

    Common pitfall

    There is no +UVM_FACTORY_TRACE plusarg. Factory debugging is done with print() and debug_create_by_type(), called from the test, usually in end_of_elaboration_phase.

    Watch it run

    With virtual write_* methods, dma_drain_test drops its objection as soon as the last input word is accepted, the derived scoreboard raises its own objection because words are still owed, and the run phase stays open until all six come out:

    Diagram
    +UVM_TESTNAME=dma_drain_test
    UVM_INFO dma_tb.sv(411) @ 375000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched
    UVM_ERROR :    0

    Six of six. The line is printed by the base class's report_phase, but the count is complete only because the derived class's write_out ran and kept the phase alive. How phase_ready_to_end does that is covered in phase_ready_to_end.

    What breaks: the methods that were never virtual

    Here is the bug from the opening, as it happened. The first version of dma_scoreboard declared plain function void write_out(axis_item t). The macro ` uvm_analysis_imp_decl(_out) ` creates an imp class that stores a handle of the scoreboard's declared type, dma_scoreboard, and calls write_out on it. With a non-virtual method, SystemVerilog binds that call at compile time to dma_scoreboard::write_out`. The derived object was there, the topology said so, and its methods were simply unreachable.

    The symptom was quiet: no error, a smaller match count, an end of test that came one word early. The fix was one keyword, virtual, on every write_* method of a class meant to be extended. The broader lesson for interviews: a factory override replaces the object; whether your new code runs still depends on virtual dispatch. When a derived class "does nothing", read the topology first (was it built?), then check that each overridden method is virtual in the base class.

    Interview

    "Your factory override is registered and the topology shows the new class, but its behaviour never appears. What do you check?"

    A weak answer: "Check the override was set before build, and print the factory."

    A strong answer: "If the topology shows the new class, construction worked, so it's dispatch. Callers hold base-type handles: the agent's driver handle, the analysis imp's scoreboard handle. A non-virtual method in the base is bound at compile time, so the base version runs. I make the extended methods virtual, or route behaviour through virtual hooks like pre_drive. If the topology shows the old class, it's construction: new instead of create, or the override registered after the create. I check with print(1) and debug_create_by_type."

    Follow-ups: Why does the truncating driver clone before setting TLAST? (the item belongs to the sequence) and When is setting an override after super.build_phase too late? (for anything the base test creates in its own build_phase, like the env).

    Check yourself A test registers dma_env::type_id::set_type_override(dma_env_dbg::get_type()) after super.build_phase(phase). What is built, and why?

    The original dma_env. The base test's build_phase (called through super.build_phase) already ran dma_env::type_id::create("env", this), so the override arrives after the only create it could have affected. Overrides for the driver or scoreboard would still work after that line, because those are created later, in the env's and agent's own build_phase.

    Check yourself axis_truncating_driver sets last on the item it was given instead of on a clone. What could go wrong?

    The item belongs to the sequence. A sequence that inspects or reuses its item after finish_item (for coverage, logging or the next beat) sees a modified object, and any scoreboard or coverage that stored the same handle sees it too. Cloning keeps the change local to what the driver drives.

    Check yourself Someone "fixes" the derived scoreboard by not calling super.write_out in its write_out. What breaks?

    The base class's compare. dma_scoreboard::write_out pops the prediction and checks the word; skipping it means no word is ever compared, and the base check_phase reports leftover predictions or an empty compare. Extending behaviour means calling super and adding to it.

    Next: the basics are complete. Factory advanced covers the cases senior interviews like: parameterized classes, abstract classes, by-name overrides and the inherited type_id trap.

    Related topics

    • Override resolution: who wins

    • phase_ready_to_end

    • Callbacks vs factory overrides

    • Scoreboard architecture

    Previous

    49 / 280

    Next: Factory Advanced: Parameterized, Abstract and By-Name

    Up next

    Up nextFactory Advanced: Parameterized, Abstract and By-Name

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Factory: Swapping Classes Without Editing the Env
    • Registration Macros: What uvm_component_utils Writes for You
    • The type_id::create Pattern