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
    • Agent Architecture
    • Reusable VIP: Active, Passive and Protocol Agents
    • Error and Fault Injection
      • Sequence-Directed Faults
      • Driver Injection Hooks
      • Callbacks versus Factory Overrides
      • Checking Expected Faults
    • Agent Patterns: Slaves, Reset, Interrupts and Pipelines
  • 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

‹Error and Fault Injection
  1. Topics
  2. UVM Verification
  3. UVM Agents & Protocol VIP
  4. Error and Fault Injection
  5. Driver Injection Hooks

Part 6 · Agents & Protocol IP · Intermediate

Driver Injection Hooks

axis_truncating_driver: a virtual pre_drive hook in the base driver, clone before modify, and a factory type override set before the env is built.

On this page

    The bench's stream sequence library keeps growing: random packets, bursts, back-to-back jobs, the virtual sequence. A reviewer asks for a regression variant in which every packet from every one of them is cut short, so the DMA's error path is hit under all those traffic shapes. Editing each sequence is out of the question; they must keep working for the clean tests. The fault has to be injected below the sequences, where every item passes on its way to the pins. How do you change what a driver does to an item without forking the driver?

    The idea: a socket on the production line

    A hook is a virtual method the driver calls at a fixed point in its loop. The base class's version does nothing interesting, so clean tests behave exactly as before. A derived driver overrides the hook, and a factory override swaps that derived driver in for one test. Think of a production line with a test socket built into one station: normally a blank plug sits in it, and for a fault campaign you plug in a fixture that bends a part on its way past. The line itself is never rebuilt.

    The bench's stream driver has one such socket, pre_drive. It receives the sequence's item and returns the beat to drive: the same item, a modified copy, or null to drop it. Everything else in the driver, including the handshake with the sequencer, stays in the base class, so a fault variant can change what is driven but never forgets to call item_done.

    The picture

    Flowchart of the driver loop: get_next_item, then the pre_drive hook either returns a beat that is driven or null that skips the pins; both paths call item_done and loop.
    The stream driver's loop. The hook decides what reaches the pins; the handshake with the sequencer happens on every path.

    Build it

    The hook in the base driver

    Here is the bench's axis_driver again. Look at two things: pre_drive is declared virtual, and item_done is called whether or not a beat was driven.

    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 fault variant

    axis_truncating_driver overrides only the hook. It counts beats per packet, using the original item's TLAST to know where each packet from the sequence ends. Before the cut it returns the item unchanged; at the cut it returns a copy with TLAST forced; after the cut it returns null, so those beats are never driven. The sequence still sees every one of its items completed, so it finishes normally and never knows its packet was shortened.

    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 clone matters. The item belongs to the sequence. The sequence may read it after finish_item, record it, sample coverage from it, or reuse it for its next beat. Editing it in place would change the sequence's record of what it asked for: its coverage would count a TLAST on beat 3 that it never requested. The rule is the same one monitors follow when they publish a fresh object for each transfer: never modify an object you do not own; copy it first.

    The test: override before the env exists

    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
    // Factory override: the stream driver cuts each packet short, so the DMA must
    // stop early and report ERR. No sequence or env file changes.
    class dma_truncate_test extends dma_base_test;
      `uvm_component_utils(dma_truncate_test)
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        axis_driver::type_id::set_type_override(axis_truncating_driver::get_type());
        super.build_phase(phase);                // the env is created after the override
      endfunction
    
      task run_phase(uvm_phase phase);
        dma_job_cfg job = dma_job_cfg::type_id::create("job");
        bit [31:0] status;
        phase.raise_objection(this);
        if (!job.randomize() with { len == 8; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
        run_job(job, 8, status);
        if (status[2] !== 1'b1) `uvm_error("STATUS", $sformatf("ERR not set after a short packet: STATUS=%08h", status))
        else `uvm_info("TRUNC", "DMA reported ERR for the short packet, as expected", UVM_LOW)
        #100ns;
        phase.drop_objection(this);
      endtask
    endclass

    The order inside build_phase is the whole trick. dma_base_test::build_phase creates the env, and the env's build creates the agents, which create the drivers through type_id::create. The override must be registered before that chain starts, so it comes before super.build_phase. A type override is safe here because the bench has exactly one axis_driver: the output agent is passive and builds none. In a bench with two active stream drivers, such as two DMAs side by side, a test that wanted to cut only one stream would use an instance override with a path instead.

    Knobs: make the cut configurable, carefully

    The cut position is a plain field set to 3. A fault campaign wants it to vary per test, which means a config_db get in the variant's build_phase. Two traps come with it. The type in the get must match the set exactly (int unsigned is not int, and neither is bit [3:0]), or the get silently finds nothing and the default is used. And a growing list of fault knobs is better grouped in one error-configuration object that the test fills and sets once, so a misspelt knob name is a compile error instead of a silent default.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // snippet: a configurable cut in the fault variant
    function void build_phase(uvm_phase phase);
      super.build_phase(phase);                  // still fetches the vif
      if (!uvm_config_db#(int unsigned)::get(this, "", "cut", cut))
        `uvm_info("TRUNC", $sformatf("no cut set, using the default %0d", cut), UVM_MEDIUM)
    endfunction
    // In the test, before super.build_phase:
    //   uvm_config_db#(int unsigned)::set(this, "env.in_agt.drv", "cut", 5);

    Watch it run

    dma_truncate_test, one 8-word job, cut after beat 3 (+UVM_VERBOSITY=UVM_MEDIUM shows the TRUNC line):

    Diagram
    UVM_INFO dma_tb.sv(520) @ 275000: uvm_test_top.env.in_agt.drv [TRUNC] TLAST forced on beat 3
    UVM_INFO dma_tb.sv(761) @ 385000: uvm_test_top [TRUNC] DMA reported ERR for the short packet, as expected
    UVM_INFO dma_tb.sv(411) @ 485000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
    UVM_ERROR :    0
    • The TRUNC line comes from uvm_test_top.env.in_agt.drv: the override really built the variant at the input agent. The topology table at the start shows its type, axis_truncating_driver.

    • The test's own check reads STATUS and finds ERR, the DUT's response to the fault.

    • 4 words matched: beats 0 to 3 went in and came out correctly. Why the data scoreboard is not surprised by a short packet is the subject of Checking expected faults.

    What breaks: the override that built, but never ran

    While the bench was being written, a factory-overridden scoreboard was built (the topology printed its type) but its extended methods were never called, because the base class had not declared them virtual. The same trap waits for every hook. Delete the word virtual from axis_driver::pre_drive and rerun dma_truncate_test: the topology still shows axis_truncating_driver at env.in_agt.drv, but the driver's loop calls pre_drive through code compiled in the base class, so the base version runs. No TRUNC line appears, all eight beats go in, the DMA ends with DONE, and the test fails with "ERR not set after a short packet".

    A factory override replaces the object. Whether the new code runs still depends on virtual dispatch. That is why a VIP's hooks are declared virtual in the base class from the first release, and why a code review of a hook checks the keyword before anything else.

    Common pitfall

    clone() is only as good as the item's copy code. It creates a new object and calls copy(), which calls do_copy(). axis_item implements do_copy; an item with neither field macros nor do_copy would clone into a blank object, and the "truncated" beat would go out with data 0, producing a mismatch that looks like a DUT bug. Copy, compare and print rules are in Copy, compare and print.

    Interview

    "How would you inject a protocol error without changing any sequences?"

    A weak answer: "Add an error flag to the sequence item." That changes every sequence's item, and the existing sequences still need editing to set it.

    A strong answer: "Through a virtual hook in the driver, such as pre_drive, that receives each item and returns the beat to drive. A derived driver overrides the hook, clones the item before changing it so the sequence's own object is untouched, and returns null to drop beats. The handshake stays in the base loop, so item_done is never skipped. A test swaps the variant in with a factory override registered before super.build_phase creates the env: a type override if there is one driver, an instance override if only one of several should change. Then the test checks both that the fault reached the pins and that the DUT responded."

    Follow-ups to expect: What if two tests need two different variants at once? (overrides do not compose; that is the case for callbacks) and Why clone? (the item belongs to the sequence, which may record or reuse it).

    Check yourself A test registers the truncating override *after* super.build_phase(phase). What happens?

    Nothing changes: the env, the agent and its plain axis_driver were already created inside super.build_phase, and a later override affects only objects created afterwards. All eight beats are driven, the DMA reports DONE, and the test's ERR check fails.

    Check yourself The truncating driver returns null for beats 4 to 7. Why does the sequence still finish, and why does it matter?

    Because the base loop calls item_done() for every item, driven or not, so each finish_item in the sequence returns. If dropping a beat also skipped item_done, the sequence would wait forever for its fifth beat and the test would hang instead of testing anything.

    Check yourself Why does the beat counter reset on the original item's last, not on the beat it returns?

    The returned beat at the cut has TLAST forced, but the sequence's packet continues for four more items. Resetting on the returned beat would treat beat 4 as the start of a new packet and drive it. The original item's TLAST marks where the sequence's packet really ends, so counting resumes cleanly at the next packet.

    Next: the reset test overrides axis_driver with axis_driver_ra, and this test overrides it with axis_truncating_driver. A test that wants both runs into the limit of overrides: Callbacks versus factory overrides.

    Related topics

    • Overriding items and sequences

    • Copy, compare and print

    • Callbacks versus factory overrides

    Previous

    108 / 280

    Next: Callbacks versus Factory Overrides

    Up next

    Up nextCallbacks versus Factory Overrides

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Checking Expected Faults